[ARVADOS-WORKBENCH2] updated: 1.1.4-528-g2396268

Git user git at public.curoverse.com
Tue Aug 7 07:24:20 EDT 2018


Summary of changes:
 src/components/tree/tree.tsx                       |   2 +-
 src/store/store.ts                                 |   6 +-
 src/store/tree-picker/tree-picker-actions.ts       |  19 ++++
 src/store/tree-picker/tree-picker-reducer.test.ts  | 101 +++++++++++++++++++++
 src/store/tree-picker/tree-picker-reducer.ts       |  53 +++++++++++
 src/store/tree-picker/tree-picker.ts               |  23 +++++
 .../project-tree-picker/project-tree-picker.tsx    |  72 +++++++++++++++
 src/views-components/tree-picker/tree-picker.ts    |  47 ++++++++++
 8 files changed, 321 insertions(+), 2 deletions(-)
 create mode 100644 src/store/tree-picker/tree-picker-actions.ts
 create mode 100644 src/store/tree-picker/tree-picker-reducer.test.ts
 create mode 100644 src/store/tree-picker/tree-picker-reducer.ts
 create mode 100644 src/store/tree-picker/tree-picker.ts
 create mode 100644 src/views-components/project-tree-picker/project-tree-picker.tsx
 create mode 100644 src/views-components/tree-picker/tree-picker.ts

       via  23962688961f81c0fbdb20870096024ded86a0b5 (commit)
       via  4b259f71b418af769c0edad5d36ad519c9a22864 (commit)
      from  3b76fca64bbbcbce0fae0b1acaa5813b782dc2cb (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 23962688961f81c0fbdb20870096024ded86a0b5
Merge: 4b259f7 3b76fca
Author: Michal Klobukowski <michal.klobukowski at contractors.roche.com>
Date:   Tue Aug 7 13:24:09 2018 +0200

    Merge branch '13952-file-context-menu-actions-implementation' of git.curoverse.com:arvados-workbench2 into 13952-file-context-menu-actions-implementation
    
    refs #13952
    2
    13952
    
    Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski at contractors.roche.com>


commit 4b259f71b418af769c0edad5d36ad519c9a22864
Author: Michal Klobukowski <michal.klobukowski at contractors.roche.com>
Date:   Tue Aug 7 13:23:50 2018 +0200

    Create ProjectTreePicker
    
    Feature #13952
    
    Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski at contractors.roche.com>

diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx
index ea15b6b..669b70c 100644
--- a/src/components/tree/tree.tsx
+++ b/src/components/tree/tree.tsx
@@ -80,7 +80,7 @@ export interface TreeItem<T> {
     items?: Array<TreeItem<T>>;
 }
 
-interface TreeProps<T> {
+export interface TreeProps<T> {
     items?: Array<TreeItem<T>>;
     render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
     toggleItemOpen: (id: string, status: TreeItemStatus) => void;
diff --git a/src/store/store.ts b/src/store/store.ts
index aeb6a09..0002a6d 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -27,6 +27,8 @@ import { CollectionPanelState, collectionPanelReducer } from './collection-panel
 import { DialogState, dialogReducer } from './dialog/dialog-reducer';
 import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
 import { ServiceRepository } from "../services/services";
+import { treePickerReducer } from './tree-picker/tree-picker-reducer';
+import { TreePicker } from './tree-picker/tree-picker';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -47,6 +49,7 @@ export interface RootState {
     snackbar: SnackbarState;
     collectionPanelFiles: CollectionPanelFilesState;
     dialog: DialogState;
+    treePicker: TreePicker;
 }
 
 export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
@@ -66,7 +69,8 @@ export function configureStore(history: History, services: ServiceRepository): R
         favorites: favoritesReducer,
         snackbar: snackbarReducer,
         collectionPanelFiles: collectionPanelFilesReducer,
-        dialog: dialogReducer
+        dialog: dialogReducer,
+        treePicker: treePickerReducer,
     });
 
     const projectPanelMiddleware = dataExplorerMiddleware(
diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
new file mode 100644
index 0000000..772d89d
--- /dev/null
+++ b/src/store/tree-picker/tree-picker-actions.ts
@@ -0,0 +1,19 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { TreeNode } from "../../models/tree";
+import { TreePickerNode } from "./tree-picker";
+
+export const treePickerActions = unionize({
+    LOAD_TREE_PICKER_NODE: ofType<{ id: string }>(),
+    LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreePickerNode> }>(),
+    TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string }>(),
+    TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ id: string }>()
+}, {
+        tag: 'type',
+        value: 'payload'
+    });
+
+export type TreePickerAction = UnionOf<typeof treePickerActions>;
diff --git a/src/store/tree-picker/tree-picker-reducer.test.ts b/src/store/tree-picker/tree-picker-reducer.test.ts
new file mode 100644
index 0000000..443da37
--- /dev/null
+++ b/src/store/tree-picker/tree-picker-reducer.test.ts
@@ -0,0 +1,101 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { createTree, getNodeValue, getNodeChildren } from "../../models/tree";
+import { TreePickerNode, createTreePickerNode } from "./tree-picker";
+import { treePickerReducer } from "./tree-picker-reducer";
+import { treePickerActions } from "./tree-picker-actions";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+
+describe('TreePickerReducer', () => {
+    it('LOAD_TREE_PICKER_NODE - initial state', () => {
+        const tree = createTree<TreePickerNode>();
+        const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1' }));
+        expect(newTree).toEqual(tree);
+    });
+
+    it('LOAD_TREE_PICKER_NODE', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            status: TreeItemStatus.PENDING
+        });
+    });
+
+    it('LOAD_TREE_PICKER_NODE_SUCCESS - initial state', () => {
+        const tree = createTree<TreePickerNode>();
+        const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
+        const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [subNode] }));
+        expect(getNodeChildren('')(newTree)).toEqual(['1.1']);
+    });
+
+    it('LOAD_TREE_PICKER_NODE_SUCCESS', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode] })));
+        expect(getNodeChildren('1')(newTree)).toEqual(['1.1']);
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            status: TreeItemStatus.LOADED
+        });
+    });
+
+    it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - collapsed', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            collapsed: true
+        });
+    });
+
+    it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - expanded', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            collapsed: false
+        });
+    });
+
+    it('TOGGLE_TREE_PICKER_NODE_SELECT - selected', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            selected: true
+        });
+    });
+
+    it('TOGGLE_TREE_PICKER_NODE_SELECT - not selected', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            selected: false
+        });
+    });
+});
diff --git a/src/store/tree-picker/tree-picker-reducer.ts b/src/store/tree-picker/tree-picker-reducer.ts
new file mode 100644
index 0000000..d195a98
--- /dev/null
+++ b/src/store/tree-picker/tree-picker-reducer.ts
@@ -0,0 +1,53 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { createTree, setNodeValueWith, TreeNode, setNode, mapTree, mapTreeValues } from "../../models/tree";
+import { TreePicker, TreePickerNode } from "./tree-picker";
+import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+
+export const treePickerReducer = (state: TreePicker = createTree(), action: TreePickerAction) =>
+    treePickerActions.match(action, {
+        LOAD_TREE_PICKER_NODE: ({ id }) =>
+            setNodeValueWith(setPending)(id)(state),
+        LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes }) => {
+            const [newState] = [state]
+                .map(receiveNodes(nodes)(id))
+                .map(setNodeValueWith(setLoaded)(id));
+            return newState;
+        },
+        TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id }) =>
+            setNodeValueWith(toggleCollapse)(id)(state),
+        TOGGLE_TREE_PICKER_NODE_SELECT: ({ id }) =>
+            mapTreeValues(toggleSelect(id))(state),
+        default: () => state
+    });
+
+const setPending = (value: TreePickerNode): TreePickerNode =>
+    ({ ...value, status: TreeItemStatus.PENDING });
+
+const setLoaded = (value: TreePickerNode): TreePickerNode =>
+    ({ ...value, status: TreeItemStatus.LOADED });
+
+const toggleCollapse = (value: TreePickerNode): TreePickerNode =>
+    ({ ...value, collapsed: !value.collapsed });
+
+const toggleSelect = (id: string) => (value: TreePickerNode): TreePickerNode =>
+    value.id === id
+        ? ({ ...value, selected: !value.selected })
+        : ({ ...value, selected: false });
+
+const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: TreePicker) =>
+    nodes.reduce((tree, node) =>
+        setNode(
+            createTreeNode(parent)(node)
+        )(tree), state);
+
+const createTreeNode = (parent: string) => (node: TreePickerNode): TreeNode<TreePickerNode> => ({
+    children: [],
+    id: node.id,
+    parent,
+    value: node
+});
\ No newline at end of file
diff --git a/src/store/tree-picker/tree-picker.ts b/src/store/tree-picker/tree-picker.ts
new file mode 100644
index 0000000..ee45bec
--- /dev/null
+++ b/src/store/tree-picker/tree-picker.ts
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Tree } from "../../models/tree";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+export type TreePicker = Tree<TreePickerNode>;
+
+export interface TreePickerNode {
+    id: string;
+    value: any;
+    selected: boolean;
+    collapsed: boolean;
+    status: TreeItemStatus;
+}
+
+export const createTreePickerNode = (data: {id: string, value: any}) => ({
+    ...data,
+    selected: false,
+    collapsed: true,
+    status: TreeItemStatus.INITIAL
+});
\ No newline at end of file
diff --git a/src/views-components/project-tree-picker/project-tree-picker.tsx b/src/views-components/project-tree-picker/project-tree-picker.tsx
new file mode 100644
index 0000000..c4795e1
--- /dev/null
+++ b/src/views-components/project-tree-picker/project-tree-picker.tsx
@@ -0,0 +1,72 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { Typography } from "@material-ui/core";
+import { TreePicker } from "../tree-picker/tree-picker";
+import { TreeProps, TreeItem, TreeItemStatus } from "../../components/tree/tree";
+import { ProjectResource } from "../../models/project";
+import { treePickerActions } from "../../store/tree-picker/tree-picker-actions";
+import { ListItemTextIcon } from "../../components/list-item-text-icon/list-item-text-icon";
+import { ProjectIcon } from "../../components/icon/icon";
+import { createTreePickerNode } from "../../store/tree-picker/tree-picker";
+import { RootState } from "../../store/store";
+import { ServiceRepository } from "../../services/services";
+import { FilterBuilder } from "../../common/api/filter-builder";
+
+type ProjectTreePickerProps = Pick<TreeProps<ProjectResource>, 'toggleItemActive' | 'toggleItemOpen'>;
+
+const mapDispatchToProps = (dispatch: Dispatch): ProjectTreePickerProps => ({
+    toggleItemActive: id => {
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id }));
+    },
+    toggleItemOpen: (id, status) => {
+        status === TreeItemStatus.INITIAL
+            ? dispatch<any>(loadProjectTreePickerProjects(id))
+            : dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+    }
+});
+
+export const ProjectTreePicker = connect(undefined, mapDispatchToProps)((props: ProjectTreePickerProps) =>
+    <div>
+        <Typography variant='caption'>
+            Select a project
+        </Typography>
+        <TreePicker {...props} render={renderTreeItem} />
+    </div>);
+
+// TODO: move action creator to store directory
+export const loadProjectTreePickerProjects = (id: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id }));
+
+        const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
+
+        const filters = FilterBuilder
+            .create<ProjectResource>()
+            .addEqual('ownerUuid', ownerUuid);
+
+        const { items } = await services.projectService.list({ filters });
+
+        dispatch<any>(receiveProjectTreePickerData(id, items));
+    };
+
+const renderTreeItem = (item: TreeItem<ProjectResource>) =>
+    <ListItemTextIcon
+        icon={ProjectIcon}
+        name={item.data.name}
+        isActive={item.active}
+        hasMargin={true} />;
+
+// TODO: move action creator to store directory
+const receiveProjectTreePickerData = (id: string, projects: ProjectResource[]) =>
+    (dispatch: Dispatch) => {
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+            id,
+            nodes: projects.map(project => createTreePickerNode({ id: project.uuid, value: project }))
+        }));
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+    };
\ No newline at end of file
diff --git a/src/views-components/tree-picker/tree-picker.ts b/src/views-components/tree-picker/tree-picker.ts
new file mode 100644
index 0000000..3e0fc6e
--- /dev/null
+++ b/src/views-components/tree-picker/tree-picker.ts
@@ -0,0 +1,47 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Tree, TreeProps, TreeItem } from "../../components/tree/tree";
+import { RootState } from "../../store/store";
+import { TreePicker as TTreePicker, TreePickerNode, createTreePickerNode } from "../../store/tree-picker/tree-picker";
+import { getNodeValue, getNodeChildren } from "../../models/tree";
+
+const memoizedMapStateToProps = () => {
+    let prevState: TTreePicker;
+    let prevTree: Array<TreeItem<any>>;
+
+    return (state: RootState): Pick<TreeProps<any>, 'items'> => {
+        if (prevState !== state.treePicker) {
+            prevState = state.treePicker;
+            prevTree = getNodeChildren('')(state.treePicker)
+                .map(treePickerToTreeItems(state.treePicker));
+        }
+        return {
+            items: prevTree
+        };
+    };
+};
+
+const mapDispatchToProps = (): Pick<TreeProps<any>, 'onContextMenu'> => ({
+    onContextMenu: () => { return; },
+});
+
+export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
+
+const treePickerToTreeItems = (tree: TTreePicker) =>
+    (id: string): TreeItem<any> => {
+        const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ id: '', value: 'InvalidNode' });
+        const items = getNodeChildren(node.id)(tree)
+            .map(treePickerToTreeItems(tree));
+        return {
+            active: node.selected,
+            data: node.value,
+            id: node.id,
+            items: items.length > 0 ? items : undefined,
+            open: !node.collapsed,
+            status: node.status
+        };
+    };
+

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list