refs #13610 Merge branch '13610-projects-hierarchy'
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 12 Jun 2018 21:30:03 +0000 (23:30 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 12 Jun 2018 21:30:38 +0000 (23:30 +0200)
Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

26 files changed:
package.json
public/index.html
src/common/api/filter-builder.ts [new file with mode: 0644]
src/common/api/server-api.ts [moved from src/common/server-api.ts with 100% similarity]
src/common/api/url-builder.ts [new file with mode: 0644]
src/components/api-token/api-token.tsx
src/components/project-list/project-list.tsx
src/components/project-tree/project-tree.test.tsx [new file with mode: 0644]
src/components/project-tree/project-tree.tsx [new file with mode: 0644]
src/components/tree/tree.test.tsx [new file with mode: 0644]
src/components/tree/tree.tsx
src/index.tsx
src/models/project.ts
src/services/auth-service/auth-service.ts
src/services/project-service/project-service.ts
src/store/auth/auth-reducer.test.ts
src/store/auth/auth-reducer.ts
src/store/project/project-action.ts
src/store/project/project-reducer.test.ts
src/store/project/project-reducer.ts
src/views/workbench/workbench.test.tsx
src/views/workbench/workbench.tsx
tsconfig.json
tsconfig.test.json
tslint.json
yarn.lock

index ac890d8d89460f5e42f70ab33b4f43c8f67c67da..967faf77823e761ce636b40c81b9726a0fd5c70a 100644 (file)
@@ -5,7 +5,9 @@
   "dependencies": {
     "@material-ui/core": "1.2.0",
     "@material-ui/icons": "^1.1.0",
+    "@types/lodash": "^4.14.109",
     "axios": "0.18.0",
+    "lodash": "4.17.10",
     "react": "16.4.0",
     "react-dom": "16.4.0",
     "react-redux": "5.0.7",
@@ -25,6 +27,8 @@
     "lint": "tslint src/** -t verbose"
   },
   "devDependencies": {
+    "@types/enzyme": "^3.1.10",
+    "@types/enzyme-adapter-react-16": "^1.0.2",
     "@types/jest": "23.0.0",
     "@types/node": "10.3.0",
     "@types/react": "16.3.16",
@@ -34,6 +38,8 @@
     "@types/react-router-dom": "4.2.7",
     "@types/react-router-redux": "5.0.15",
     "@types/redux-devtools": "3.0.44",
+    "enzyme": "^3.3.0",
+    "enzyme-adapter-react-16": "^1.1.1",
     "jest-localstorage-mock": "2.2.0",
     "redux-devtools": "3.4.1",
     "typescript": "2.9.1"
index f6111b91895db5aad0429e757fb378ec2ba66c3a..a8d655e5872080571e26bf3f5a830046b3ffcffe 100644 (file)
@@ -10,6 +10,7 @@
     -->
     <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
     <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
+    <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet">
     <!--
       Notice the use of %PUBLIC_URL% in the tags above.
       It will be replaced with the URL of the `public` folder during the build.
@@ -20,6 +21,8 @@
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
     <title>Arvados Workbench 2</title>
+    <script>FontAwesomeConfig = { autoReplaceSvg: 'nest' }</script>
+    <script defer src="https://use.fontawesome.com/releases/v5.0.13/js/all.js" integrity="sha384-xymdQtn1n3lH2wcu0qhcdaOpQwyoarkgLVxC/wZ5q7h9gHtxICrpcaSUfygqZGOe" crossorigin="anonymous"></script>
   </head>
   <body>
     <noscript>
diff --git a/src/common/api/filter-builder.ts b/src/common/api/filter-builder.ts
new file mode 100644 (file)
index 0000000..3f8e323
--- /dev/null
@@ -0,0 +1,35 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export enum FilterField {
+    UUID = "uuid",
+    OWNER_UUID = "owner_uuid"
+}
+
+export default class FilterBuilder {
+    private filters = "";
+
+    private addCondition(field: FilterField, cond: string, value?: string) {
+        if (value) {
+            this.filters += `["${field}","${cond}","${value}"]`;
+        }
+        return this;
+    }
+
+    public addEqual(field: FilterField, value?: string) {
+        return this.addCondition(field, "=", value);
+    }
+
+    public addLike(field: FilterField, value?: string) {
+        return this.addCondition(field, "like", value);
+    }
+
+    public addILike(field: FilterField, value?: string) {
+        return this.addCondition(field, "ilike", value);
+    }
+
+    public get() {
+        return "[" + this.filters + "]";
+    }
+}
diff --git a/src/common/api/url-builder.ts b/src/common/api/url-builder.ts
new file mode 100644 (file)
index 0000000..e5786a2
--- /dev/null
@@ -0,0 +1,26 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export default class UrlBuilder {
+       private url: string = "";
+       private query: string = "";
+
+       constructor(host: string) {
+               this.url = host;
+       }
+
+       public addParam(param: string, value: string) {
+               if (this.query.length === 0) {
+                       this.query += "?";
+               } else {
+                       this.query += "&";
+               }
+               this.query += `${param}=${value}`;
+               return this;
+       }
+
+       public get() {
+               return this.url + this.query;
+       }
+}
index 34e2d64c110ca43a55cba2c7f0f2e11e361c2446..87da39b025e9706c05ebdade6e600f936fae1a89 100644 (file)
@@ -24,7 +24,7 @@ class ApiToken extends React.Component<ApiTokenProps & RouteProps & DispatchProp
         const apiToken = ApiToken.getUrlParameter(search, 'api_token');
         this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
         this.props.dispatch(authService.getUserDetails());
-        this.props.dispatch(projectService.getTopProjectList());
+        this.props.dispatch(projectService.getProjectList());
     }
     render() {
         return <Redirect to="/"/>
index 3526da391cd6d2585140b9c993d31029f676a674..ec16a677f45a83926f7ec79d15c17913ba10d10a 100644 (file)
@@ -4,9 +4,8 @@
 
 import * as React from 'react';
 import { Theme } from "@material-ui/core";
-import { StyleRulesCallback, WithStyles } from "@material-ui/core/styles";
+import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
 import Paper from "@material-ui/core/Paper/Paper";
-import withStyles from "@material-ui/core/es/styles/withStyles";
 import Table from "@material-ui/core/Table/Table";
 import TableHead from "@material-ui/core/TableHead/TableHead";
 import TableRow from "@material-ui/core/TableRow/TableRow";
diff --git a/src/components/project-tree/project-tree.test.tsx b/src/components/project-tree/project-tree.test.tsx
new file mode 100644 (file)
index 0000000..d42df08
--- /dev/null
@@ -0,0 +1,106 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { mount } from 'enzyme';
+import * as Enzyme from 'enzyme';
+import * as Adapter from 'enzyme-adapter-react-16';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import { Collapse } from '@material-ui/core';
+
+import ProjectTree from './project-tree';
+import { TreeItem } from '../tree/tree';
+import { Project } from '../../models/project';
+Enzyme.configure({ adapter: new Adapter() });
+
+describe("ProjectTree component", () => {
+
+    it("checks is there ListItemIcon in the ProjectTree component", () => {
+        const project: TreeItem<Project> = {
+            data: {
+                name: "sample name",
+                createdAt: "2018-06-12",
+                modifiedAt: "2018-06-13",
+                uuid: "uuid",
+                ownerUuid: "ownerUuid",
+                href: "href",
+            },
+            id: "3",
+            open: true,
+            active: true
+        };
+        const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
+
+        expect(wrapper.find(ListItemIcon).length).toEqual(1);
+    });
+
+    it("checks are there two ListItemIcon's in the ProjectTree component", () => {
+        const project: Array<TreeItem<Project>> = [
+            {
+                data: {
+                    name: "sample name",
+                    createdAt: "2018-06-12",
+                    modifiedAt: "2018-06-13",
+                    uuid: "uuid",
+                    ownerUuid: "ownerUuid",
+                    href: "href",
+                },
+                id: "3",
+                open: false,
+                active: true
+            },
+            {
+                data: {
+                    name: "sample name",
+                    createdAt: "2018-06-12",
+                    modifiedAt: "2018-06-13",
+                    uuid: "uuid",
+                    ownerUuid: "ownerUuid",
+                    href: "href",
+                },
+                id: "3",
+                open: false,
+                active: true
+            }
+        ];
+        const wrapper = mount(<ProjectTree projects={project} toggleProjectTreeItem={() => { }} />);
+
+        expect(wrapper.find(ListItemIcon).length).toEqual(2);
+    });
+
+    it("check ProjectTree, when open is changed", () => {
+        const project: TreeItem<Project> = {
+            data: {
+                name: "sample name",
+                createdAt: "2018-06-12",
+                modifiedAt: "2018-06-13",
+                uuid: "uuid",
+                ownerUuid: "ownerUuid",
+                href: "href",
+            },
+            id: "3",
+            open: true,
+            active: true,
+            items: [
+                {
+                    data: {
+                        name: "sample name",
+                        createdAt: "2018-06-12",
+                        modifiedAt: "2018-06-13",
+                        uuid: "uuid",
+                        ownerUuid: "ownerUuid",
+                        href: "href",
+                    },
+                    id: "4",
+                    open: false,
+                    active: true
+                }
+            ]
+        };
+        const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
+        wrapper.setState({open: true });
+
+        expect(wrapper.find(Collapse).length).toEqual(1);
+    });
+});
diff --git a/src/components/project-tree/project-tree.tsx b/src/components/project-tree/project-tree.tsx
new file mode 100644 (file)
index 0000000..5243b5e
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { ReactElement } from 'react';
+import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles';
+import ListItemText from "@material-ui/core/ListItemText/ListItemText";
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import Typography from '@material-ui/core/Typography';
+
+import Tree, { TreeItem } from '../tree/tree';
+import { Project } from '../../models/project';
+
+type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer';
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+    active: {
+        color: '#4285F6',
+    },
+    listItemText: {
+        padding: '0px',
+    },
+    row: {
+        display: 'flex',
+        alignItems: 'center',
+        marginLeft: '20px',
+    },
+    treeContainer: {
+        position: 'absolute',
+        overflowX: 'visible',
+        marginTop: '80px',
+        minWidth: '240px',
+        whiteSpace: 'nowrap',
+    }
+});
+
+export interface ProjectTreeProps {
+    projects: Array<TreeItem<Project>>;
+    toggleProjectTreeItem: (id: string) => void;
+}
+
+class ProjectTree<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
+    render(): ReactElement<any> {
+        const {classes, projects} = this.props;
+        const {active, listItemText, row, treeContainer} = classes;
+        return (
+            <div className={treeContainer}>
+                <Tree items={projects}
+                    toggleItem={this.props.toggleProjectTreeItem}
+                    render={(project: TreeItem<Project>, level: number) =>
+                        <span className={row}>
+                            <ListItemIcon className={project.active ? active : ''}>
+                                {level === 0 ? <i className="fas fa-th"/> : <i className="fas fa-folder"/>}
+                            </ListItemIcon>
+                            <ListItemText className={listItemText} primary={
+                                <Typography className={project.active ? active : ''}>
+                                    {project.data.name}
+                                </Typography>
+                            }/>
+                        </span>
+                    }/>
+            </div>
+        );
+    }
+}
+
+export default withStyles(styles)(ProjectTree)
diff --git a/src/components/tree/tree.test.tsx b/src/components/tree/tree.test.tsx
new file mode 100644 (file)
index 0000000..ffdc74f
--- /dev/null
@@ -0,0 +1,7 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+it("should render the tree", () => {
+       expect(true).toBe(true);
+});
\ No newline at end of file
index 12369d57e12b1415624839a8244dbe95bb7442d6..d8397d6673a7a9f2c30a95ee7e7f1c090f1a24d1 100644 (file)
@@ -5,23 +5,71 @@
 import * as React from 'react';
 import List from "@material-ui/core/List/List";
 import ListItem from "@material-ui/core/ListItem/ListItem";
+import { StyleRulesCallback, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
 import { ReactElement } from "react";
+import Collapse from "@material-ui/core/Collapse/Collapse";
+
+type CssRules = 'list' | 'activeArrow' | 'arrow' | 'arrowRotate';
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+    list: {
+        paddingBottom: '3px',
+        paddingTop: '3px',
+    },
+    activeArrow: {
+        color: '#4285F6',
+        position: 'absolute',
+    },
+    arrow: {
+        position: 'absolute',
+    },
+    arrowRotate: {
+        transform: 'rotate(-90deg)',
+    }
+});
+
+export interface TreeItem<T> {
+    data: T;
+    id: string;
+    open: boolean;
+    active: boolean;
+    items?: Array<TreeItem<T>>;
+}
 
 interface TreeProps<T> {
-    items: T[],
-    render: (item: T) => ReactElement<{}>
+    items?: Array<TreeItem<T>>;
+    render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
+    toggleItem: (id: string) => any;
+    level?: number;
 }
 
-class Tree<T> extends React.Component<TreeProps<T>, {}> {
-    render() {
-        return <List>
-            {this.props.items && this.props.items.map((it: T, idx: number) =>
-                <ListItem key={`item/${idx}`} button>
-                    {this.props.render(it)}
+class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
+    renderArrow (items: boolean, arrowClass: string, open: boolean){
+        return <i className={`${arrowClass} ${open ? "fas fa-caret-down" : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />
+    }
+    render(): ReactElement<any> {
+        const level = this.props.level ? this.props.level : 0;
+        const {classes, render, toggleItem, items} = this.props;
+        const {list, arrow, activeArrow} = classes;
+        return <List component="div" className={list}>
+            {items && items.map((it: TreeItem<T>, idx: number) =>
+             <div key={`item/${level}/${idx}`}>
+                <ListItem button onClick={() => toggleItem(it.id)} className={list} style={{paddingLeft: (level + 1) * 20}}>
+                    {this.renderArrow(true, it.active ? activeArrow : arrow, it.open)}
+                    {render(it, level)}
                 </ListItem>
-            )}
+                {it.items && it.items.length > 0 &&
+                <Collapse in={it.open} timeout="auto" unmountOnExit>
+                    <StyledTree
+                        items={it.items}
+                        render={render}
+                        toggleItem={toggleItem}
+                        level={level + 1}/>
+                </Collapse>}
+             </div>)}
         </List>
     }
 }
 
-export default Tree;
+const StyledTree = withStyles(styles)(Tree);
+export default StyledTree
index 67de95fb3af108699e853dee79f84ee623678b6a..351ed2e04b7df755e29fc8131fac218bee9921e9 100644 (file)
@@ -17,6 +17,7 @@ import authActions from "./store/auth/auth-action";
 import { projectService } from "./services/services";
 
 const history = createBrowserHistory();
+
 const store = configureStore({
     projects: [
     ],
@@ -29,7 +30,7 @@ const store = configureStore({
 }, history);
 
 store.dispatch(authActions.INIT());
-store.dispatch<any>(projectService.getTopProjectList());
+store.dispatch<any>(projectService.getProjectList());
 
 const App = () =>
     <Provider store={store}>
index 83862c94001a575f55136faa8b1d93653a44f415..83fb59bd3eb0b4f77854848a3637488ce9894eb2 100644 (file)
@@ -8,5 +8,5 @@ export interface Project {
     modifiedAt: string;
     uuid: string;
     ownerUuid: string;
-    href: string
+    href: string;
 }
index 80d13e3c64676b3be8ef21604420f01b34abe1c7..da593c2dfb7a44fad7d88b29ef8ebff70febffc7 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { API_HOST, serverApi } from "../../common/server-api";
+import { API_HOST, serverApi } from "../../common/api/server-api";
 import { User } from "../../models/user";
 import { Dispatch } from "redux";
 import actions from "../../store/auth/auth-action";
index f35ca9cd4102e7d5a97b515b910ab4c737301430..9350dabdc4894f7ef4ebecb3105da204a0a98f14 100644 (file)
@@ -2,10 +2,12 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { serverApi } from "../../common/server-api";
+import { serverApi } from "../../common/api/server-api";
 import { Dispatch } from "redux";
 import actions from "../../store/project/project-action";
 import { Project } from "../../models/project";
+import UrlBuilder from "../../common/api/url-builder";
+import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
 
 interface GroupsResponse {
     offset: number;
@@ -31,9 +33,15 @@ interface GroupsResponse {
 }
 
 export default class ProjectService {
-    public getTopProjectList = () => (dispatch: Dispatch) => {
-        dispatch(actions.TOP_PROJECTS_REQUEST());
-        serverApi.get<GroupsResponse>('/groups').then(groups => {
+    public getProjectList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Project[]> => {
+        dispatch(actions.PROJECTS_REQUEST());
+
+        const ub = new UrlBuilder('/groups');
+        const fb = new FilterBuilder();
+        fb.addEqual(FilterField.OWNER_UUID, parentUuid);
+        const url = ub.addParam('filters', fb.get()).get();
+
+        return serverApi.get<GroupsResponse>(url).then(groups => {
             const projects = groups.data.items.map(g => ({
                 name: g.name,
                 createdAt: g.created_at,
@@ -42,7 +50,8 @@ export default class ProjectService {
                 uuid: g.uuid,
                 ownerUuid: g.owner_uuid
             } as Project));
-            dispatch(actions.TOP_PROJECTS_SUCCESS(projects));
+            dispatch(actions.PROJECTS_SUCCESS({projects, parentItemId: parentUuid}));
+            return projects;
         });
-    }
+    };
 }
index f2e20baa8971c93f2b036f2a762697e4f7fc7161..17bd42175f8df6647a98d847f6c0a48d79ee4b1d 100644 (file)
@@ -10,7 +10,7 @@ import {
     USER_FIRST_NAME_KEY,
     USER_LAST_NAME_KEY
 } from "../../services/auth-service/auth-service";
-import { API_HOST } from "../../common/server-api";
+import { API_HOST } from "../../common/api/server-api";
 
 import 'jest-localstorage-mock';
 
index 3fad4cf7569019b3ecbc1ed0d7b8d047e5f2c219..57a17ae53cfbb45f9c69529ad34d866771b0f215 100644 (file)
@@ -5,7 +5,7 @@
 import actions, { AuthAction } from "./auth-action";
 import { User } from "../../models/user";
 import { authService } from "../../services/services";
-import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/server-api";
+import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
 import { UserDetailsResponse } from "../../services/auth-service/auth-service";
 
 export interface AuthState {
index 7c91cc551a521fdb97db5879ab7f089fb6f85250..87ecbda9b56fbe1850d202d5cbb824373554d758 100644 (file)
@@ -8,8 +8,9 @@ import { default as unionize, ofType, UnionOf } from "unionize";
 const actions = unionize({
     CREATE_PROJECT: ofType<Project>(),
     REMOVE_PROJECT: ofType<string>(),
-    TOP_PROJECTS_REQUEST: {},
-    TOP_PROJECTS_SUCCESS: ofType<Project[]>()
+    PROJECTS_REQUEST: {},
+    PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(),
+    TOGGLE_PROJECT_TREE_ITEM: ofType<string>()
 }, {
     tag: 'type',
     value: 'payload'
index e5f5c27532ebb71a02039aa961cd125968d769ec..9c1ed3b4ded0db64082dbc3879dcafc9f98eae91 100644 (file)
@@ -32,8 +32,21 @@ describe('project-reducer', () => {
             uuid: 'test123'
         };
 
-        const topProjects = [project, project];
-        const state = projectsReducer(initialState, actions.TOP_PROJECTS_SUCCESS(topProjects));
-        expect(state).toEqual(topProjects);
+        const projects = [project, project];
+        const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({projects, parentItemId: undefined}));
+        expect(state).toEqual([{
+                active: false,
+                open: false,
+                id: "test123",
+                items: [],
+                data: project
+            }, {
+                active: false,
+                open: false,
+                id: "test123",
+                items: [],
+                data: project
+            }
+        ]);
     });
 });
index 64e7925522614ca08c80d4a3912dee646cb2c3cb..887cf89b334fea055334b6871c2e3dff00ca2271 100644 (file)
@@ -4,16 +4,70 @@
 
 import { Project } from "../../models/project";
 import actions, { ProjectAction } from "./project-action";
+import { TreeItem } from "../../components/tree/tree";
+import * as _ from "lodash";
+
+export type ProjectState = Array<TreeItem<Project>>;
+
+function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
+    let item;
+    for (const t of tree) {
+        item = t.id === itemId
+            ? t
+            : findTreeItem(t.items ? t.items : [], itemId);
+        if (item) {
+            break;
+        }
+    }
+    return item;
+}
+
+function resetTreeActivity<T>(tree: Array<TreeItem<T>>) {
+    for (const t of tree) {
+        t.active = false;
+        resetTreeActivity(t.items ? t.items : []);
+    }
+}
+
+function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[], parentItemId?: string): Array<TreeItem<Project>> {
+    let treeItem;
+    if (parentItemId) {
+        treeItem = findTreeItem(tree, parentItemId);
+    }
+    const items = projects.map((p, idx) => ({
+        id: p.uuid,
+        open: false,
+        active: false,
+        data: p,
+        items: []
+    } as TreeItem<Project>));
+
+    if (treeItem) {
+        treeItem.items = items;
+        return tree;
+    }
+
+    return items;
+}
 
-export type ProjectState = Project[];
 
 const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
     return actions.match(action, {
         CREATE_PROJECT: project => [...state, project],
         REMOVE_PROJECT: () => state,
-        TOP_PROJECTS_REQUEST: () => state,
-        TOP_PROJECTS_SUCCESS: projects => {
-            return projects;
+        PROJECTS_REQUEST: () => state,
+        PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
+            return updateProjectTree(state, projects, parentItemId);
+        },
+        TOGGLE_PROJECT_TREE_ITEM: itemId => {
+            const tree = _.cloneDeep(state);
+            resetTreeActivity(tree);
+            const item = findTreeItem(tree, itemId);
+            if (item) {
+                item.open = !item.open;
+                item.active = true;
+            }
+            return tree;
         },
         default: () => state
     });
index fc89609acc43b45767fcebb0cec2f0518953423f..7b9b74d095c65a8a1ad760a2c4c87f360cb13836 100644 (file)
@@ -8,10 +8,19 @@ import Workbench from '../../views/workbench/workbench';
 import { Provider } from "react-redux";
 import configureStore from "../../store/store";
 import createBrowserHistory from "history/createBrowserHistory";
+import { ConnectedRouter } from "react-router-redux";
+
+const history = createBrowserHistory();
 
 it('renders without crashing', () => {
     const div = document.createElement('div');
     const store = configureStore({ projects: [], router: { location: null }, auth: {} }, createBrowserHistory());
-    ReactDOM.render(<Provider store={store}><Workbench/></Provider>, div);
+    ReactDOM.render(
+        <Provider store={store}>
+            <ConnectedRouter history={history}>
+                <Workbench/>
+            </ConnectedRouter>
+        </Provider>,
+    div);
     ReactDOM.unmountComponentAtNode(div);
 });
index 8c3145b823e1950a58aa2bca49129ab23c0e8023..d18d113bcbca66ced17a91bfc920e04a7d1be65d 100644 (file)
@@ -10,8 +10,6 @@ import AppBar from '@material-ui/core/AppBar';
 import Toolbar from '@material-ui/core/Toolbar';
 import Typography from '@material-ui/core/Typography';
 import { connect, DispatchProp } from "react-redux";
-import Tree from "../../components/tree/tree";
-import { Project } from "../../models/project";
 import ProjectList from "../../components/project-list/project-list";
 import { Route, Switch } from "react-router";
 import { Link } from "react-router-dom";
@@ -24,6 +22,12 @@ import { AccountCircle } from "@material-ui/icons";
 import { User } from "../../models/user";
 import Grid from "@material-ui/core/Grid/Grid";
 import { RootState } from "../../store/store";
+import projectActions from "../../store/project/project-action"
+
+import ProjectTree from '../../components/project-tree/project-tree';
+import { TreeItem } from "../../components/tree/tree";
+import { Project } from "../../models/project";
+import { projectService } from '../../services/services';
 
 const drawerWidth = 240;
 
@@ -58,7 +62,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
 });
 
 interface WorkbenchDataProps {
-    projects: Project[];
+    projects: Array<TreeItem<Project>>;
     user?: User;
 }
 
@@ -100,6 +104,12 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
         });
     };
 
+    toggleProjectTreeItem = (itemId: string) => {
+        this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => {
+            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
+        });
+    };
+
     render() {
         const {classes, user} = this.props;
         return (
@@ -154,9 +164,9 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                         paper: classes.drawerPaper,
                     }}>
                     <div className={classes.toolbar}/>
-                    <Tree items={this.props.projects} render={(p: Project) =>
-                        <Link to={`/project/${p.name}`}>{p.name}</Link>
-                    }/>
+                    <ProjectTree
+                        projects={this.props.projects}
+                        toggleProjectTreeItem={this.toggleProjectTreeItem}/>
                 </Drawer>}
                 <main className={classes.content}>
                     <div className={classes.toolbar}/>
index 98d5d9151c7b8f706dcfd3944f0bdee6a573f2c8..af933d9fa9a09755a64da1f8f3f8c4c5b54c1d68 100644 (file)
@@ -31,6 +31,7 @@
     "acceptance-tests",
     "webpack",
     "jest",
-    "src/setupTests.ts"
+    "src/setupTests.ts",
+    "**/*.test.tsx"
   ]
 }
index 65ffdd493929cf996f7f185609fb9f3f7f14184b..2c7b284162f4cafdbef8875c7ae7cb517c8e7abd 100644 (file)
@@ -3,4 +3,4 @@
   "compilerOptions": {
     "module": "commonjs"
   }
-}
\ No newline at end of file
+}
index ccb194f75b7577c150c09125d6b0d8ef6b0edc0d..1b26ab5f0f629330f31409d8b463422f1ee668d1 100644 (file)
@@ -10,7 +10,8 @@
     "jsx-boolean-value": false,
     "jsx-no-lambda": false,
     "no-debugger": false,
-    "no-console": false
+    "no-console": false,
+    "no-shadowed-variable": false
   },
   "linterOptions": {
     "exclude": [
index 7b9820bb5807c253e3aa0f5c2c5539f9d0053d6d..309773f01a2af6e57d7aecc169a7b80d1e076620 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   dependencies:
     recompose "^0.26.0 || ^0.27.0"
 
+"@types/cheerio@*":
+  version "0.22.7"
+  resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.7.tgz#4a92eafedfb2b9f4437d3a4410006d81114c66ce"
+
+"@types/enzyme-adapter-react-16@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz#15ae37c64d6221a6f4b3a4aacc357cf773859de4"
+  dependencies:
+    "@types/enzyme" "*"
+
+"@types/enzyme@*", "@types/enzyme@^3.1.10":
+  version "3.1.10"
+  resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.10.tgz#28108a9864e65699751469551a803a35d2e26160"
+  dependencies:
+    "@types/cheerio" "*"
+    "@types/react" "*"
+
 "@types/history@*":
   version "4.6.2"
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
     csstype "^2.0.0"
     indefinite-observable "^1.0.1"
 
+"@types/lodash@^4.14.109":
+  version "4.14.109"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.109.tgz#b1c4442239730bf35cabaf493c772b18c045886d"
+
 "@types/node@*", "@types/node@10.3.0":
   version "10.3.0"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.3.0.tgz#078516315a84d56216b5d4fed8f75d59d3b16cac"
@@ -1464,6 +1485,17 @@ chardet@^0.4.0:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
 
+cheerio@^1.0.0-rc.2:
+  version "1.0.0-rc.2"
+  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
+  dependencies:
+    css-select "~1.2.0"
+    dom-serializer "~0.1.0"
+    entities "~1.1.1"
+    htmlparser2 "^3.9.1"
+    lodash "^4.15.0"
+    parse5 "^3.0.1"
+
 chokidar@^1.6.0, chokidar@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@@ -1632,6 +1664,10 @@ colormin@^1.0.5:
     css-color-names "0.0.4"
     has "^1.0.1"
 
+colors@0.5.x:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774"
+
 colors@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
@@ -1882,7 +1918,7 @@ css-loader@0.28.7:
     postcss-value-parser "^3.3.0"
     source-list-map "^2.0.0"
 
-css-select@^1.1.0:
+css-select@^1.1.0, css-select@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
   dependencies:
@@ -2168,6 +2204,10 @@ diffie-hellman@^5.0.0:
     miller-rabin "^4.0.0"
     randombytes "^2.0.0"
 
+discontinuous-range@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+
 dns-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
@@ -2195,7 +2235,7 @@ dom-helpers@^3.2.1, dom-helpers@^3.3.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
 
-dom-serializer@0:
+dom-serializer@0, dom-serializer@~0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
   dependencies:
@@ -2216,7 +2256,7 @@ domain-browser@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
 
-domelementtype@1:
+domelementtype@1, domelementtype@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
 
@@ -2236,6 +2276,12 @@ domhandler@2.1:
   dependencies:
     domelementtype "1"
 
+domhandler@^2.3.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+  dependencies:
+    domelementtype "1"
+
 domutils@1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
@@ -2249,6 +2295,13 @@ domutils@1.5.1:
     dom-serializer "0"
     domelementtype "1"
 
+domutils@^1.5.1:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+  dependencies:
+    dom-serializer "0"
+    domelementtype "1"
+
 dot-prop@^4.1.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
@@ -2335,10 +2388,51 @@ enhanced-resolve@^3.0.0, enhanced-resolve@^3.4.0:
     object-assign "^4.0.1"
     tapable "^0.2.7"
 
-entities@~1.1.1:
+entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
 
+enzyme-adapter-react-16@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4"
+  dependencies:
+    enzyme-adapter-utils "^1.3.0"
+    lodash "^4.17.4"
+    object.assign "^4.0.4"
+    object.values "^1.0.4"
+    prop-types "^15.6.0"
+    react-reconciler "^0.7.0"
+    react-test-renderer "^16.0.0-0"
+
+enzyme-adapter-utils@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7"
+  dependencies:
+    lodash "^4.17.4"
+    object.assign "^4.0.4"
+    prop-types "^15.6.0"
+
+enzyme@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
+  dependencies:
+    cheerio "^1.0.0-rc.2"
+    function.prototype.name "^1.0.3"
+    has "^1.0.1"
+    is-boolean-object "^1.0.0"
+    is-callable "^1.1.3"
+    is-number-object "^1.0.3"
+    is-string "^1.0.4"
+    is-subset "^0.1.1"
+    lodash "^4.17.4"
+    object-inspect "^1.5.0"
+    object-is "^1.0.1"
+    object.assign "^4.1.0"
+    object.entries "^1.0.4"
+    object.values "^1.0.4"
+    raf "^3.4.0"
+    rst-selector-parser "^2.2.3"
+
 errno@^0.1.3, errno@~0.1.7:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@@ -2351,7 +2445,7 @@ error-ex@^1.2.0:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.5.1, es-abstract@^1.7.0:
+es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
   version "1.12.0"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
   dependencies:
@@ -2933,10 +3027,18 @@ fsevents@^1.0.0, fsevents@^1.1.2, fsevents@^1.1.3, fsevents@^1.2.3:
     nan "^2.9.2"
     node-pre-gyp "^0.10.0"
 
-function-bind@^1.1.1:
+function-bind@^1.1.0, function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
 
+function.prototype.name@^1.0.3:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
+  dependencies:
+    define-properties "^1.1.2"
+    function-bind "^1.1.1"
+    is-callable "^1.1.3"
+
 gauge@~2.7.3:
   version "2.7.4"
   resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -3138,6 +3240,10 @@ has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
 
+has-symbols@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
 has-unicode@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -3278,6 +3384,17 @@ html-webpack-plugin@2.29.0:
     pretty-error "^2.0.2"
     toposort "^1.0.0"
 
+htmlparser2@^3.9.1:
+  version "3.9.2"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
+  dependencies:
+    domelementtype "^1.3.0"
+    domhandler "^2.3.0"
+    domutils "^1.5.1"
+    entities "^1.1.1"
+    inherits "^2.0.1"
+    readable-stream "^2.0.2"
+
 htmlparser2@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
@@ -3514,6 +3631,10 @@ is-binary-path@^1.0.0:
   dependencies:
     binary-extensions "^1.0.0"
 
+is-boolean-object@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
+
 is-buffer@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -3655,6 +3776,10 @@ is-npm@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
 
+is-number-object@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
+
 is-number@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@@ -3741,6 +3866,14 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
 
+is-string@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
+
+is-subset@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+
 is-svg@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9"
@@ -4470,6 +4603,10 @@ lodash.endswith@^4.2.1:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09"
 
+lodash.flattendeep@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+
 lodash.isfunction@^3.0.8:
   version "3.0.9"
   resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
@@ -4507,7 +4644,7 @@ lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 
-"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
+lodash@4.17.10, "lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
@@ -4841,6 +4978,15 @@ natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
 
+nearley@^2.7.10:
+  version "2.13.0"
+  resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.13.0.tgz#6e7b0f4e68bfc3e74c99eaef2eda39e513143439"
+  dependencies:
+    nomnom "~1.6.2"
+    railroad-diagrams "^1.0.0"
+    randexp "0.4.6"
+    semver "^5.4.1"
+
 needle@^2.2.0:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
@@ -4934,6 +5080,13 @@ node-pre-gyp@^0.10.0:
     semver "^5.3.0"
     tar "^4"
 
+nomnom@~1.6.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971"
+  dependencies:
+    colors "0.5.x"
+    underscore "~1.4.4"
+
 nopt@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@@ -5033,7 +5186,15 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-keys@^1.0.8:
+object-inspect@^1.5.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
+
+object-is@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
+
+object-keys@^1.0.11, object-keys@^1.0.8:
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
 
@@ -5043,6 +5204,24 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
+object.assign@^4.0.4, object.assign@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+  dependencies:
+    define-properties "^1.1.2"
+    function-bind "^1.1.1"
+    has-symbols "^1.0.0"
+    object-keys "^1.0.11"
+
+object.entries@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f"
+  dependencies:
+    define-properties "^1.1.2"
+    es-abstract "^1.6.1"
+    function-bind "^1.1.0"
+    has "^1.0.1"
+
 object.getownpropertydescriptors@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
@@ -5063,6 +5242,15 @@ object.pick@^1.3.0:
   dependencies:
     isobject "^3.0.1"
 
+object.values@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a"
+  dependencies:
+    define-properties "^1.1.2"
+    es-abstract "^1.6.1"
+    function-bind "^1.1.0"
+    has "^1.0.1"
+
 obuf@^1.0.0, obuf@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
@@ -5242,6 +5430,12 @@ parse5@4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
 
+parse5@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+  dependencies:
+    "@types/node" "*"
+
 parseurl@~1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
@@ -5802,7 +5996,7 @@ querystringify@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755"
 
-raf@3.4.0:
+raf@3.4.0, raf@^3.4.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
   dependencies:
@@ -5814,6 +6008,17 @@ rafl@~1.2.1:
   dependencies:
     global "~4.3.0"
 
+railroad-diagrams@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+
+randexp@0.4.6:
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+  dependencies:
+    discontinuous-range "1.0.0"
+    ret "~0.1.10"
+
 randomatic@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.0.0.tgz#d35490030eb4f7578de292ce6dfb04a91a128923"
@@ -5901,6 +6106,10 @@ react-event-listener@^0.6.0:
     prop-types "^15.6.0"
     warning "^3.0.0"
 
+react-is@^16.4.0:
+  version "16.4.0"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf"
+
 react-jss@^8.1.0:
   version "8.4.0"
   resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.4.0.tgz#7cb43d85dea56afafc8f0fd072ae27fcc0518950"
@@ -5922,6 +6131,15 @@ react-popper@^0.10.0:
     popper.js "^1.14.1"
     prop-types "^15.6.1"
 
+react-reconciler@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d"
+  dependencies:
+    fbjs "^0.8.16"
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+    prop-types "^15.6.0"
+
 react-redux@5.0.7:
   version "5.0.7"
   resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
@@ -6008,6 +6226,15 @@ react-scripts-ts@2.16.0:
   optionalDependencies:
     fsevents "^1.1.3"
 
+react-test-renderer@^16.0.0-0:
+  version "16.4.0"
+  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.0.tgz#0dbe0e24263e94e1830c7afb1f403707fad313a3"
+  dependencies:
+    fbjs "^0.8.16"
+    object-assign "^4.1.1"
+    prop-types "^15.6.0"
+    react-is "^16.4.0"
+
 react-transition-group@^2.2.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.3.1.tgz#31d611b33e143a5e0f2d94c348e026a0f3b474b6"
@@ -6390,6 +6617,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
     hash-base "^3.0.0"
     inherits "^2.0.1"
 
+rst-selector-parser@^2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+  dependencies:
+    lodash.flattendeep "^4.4.0"
+    nearley "^2.7.10"
+
 rsvp@^3.3.3:
   version "3.6.2"
   resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
@@ -7319,6 +7553,10 @@ uglifyjs-webpack-plugin@^1.1.8:
     webpack-sources "^1.1.0"
     worker-farm "^1.5.2"
 
+underscore@~1.4.4:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
+
 union-value@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"