[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