Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 20 Aug 2018 10:54:29 +0000 (12:54 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 20 Aug 2018 10:54:29 +0000 (12:54 +0200)
Feature #14014

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

28 files changed:
src/common/custom-theme.ts
src/components/details-attribute/details-attribute.tsx
src/components/list-item-text-icon/list-item-text-icon.tsx
src/components/move-to-dialog/move-to-dialog.tsx [new file with mode: 0644]
src/components/tree/tree.tsx
src/index.tsx
src/store/data-explorer/data-explorer-reducer.test.tsx
src/store/project/project-reducer.test.ts
src/store/tree-picker/tree-picker-actions.ts
src/store/tree-picker/tree-picker-reducer.test.ts
src/store/tree-picker/tree-picker-reducer.ts
src/store/tree-picker/tree-picker.ts
src/validators/validators.tsx
src/views-components/context-menu/action-sets/collection-action-set.ts
src/views-components/context-menu/action-sets/collection-resource-action-set.ts
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected.tsx [new file with mode: 0644]
src/views-components/details-panel/collection-details.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/details-panel/process-details.tsx
src/views-components/details-panel/project-details.tsx
src/views-components/dialog-create/dialog-collection-create-selected.tsx [new file with mode: 0644]
src/views-components/move-to-dialog/move-to-dialog.tsx [new file with mode: 0644]
src/views-components/project-tree-picker/project-tree-picker.tsx
src/views-components/project-tree/project-tree.test.tsx
src/views-components/tree-picker/tree-picker.ts
src/views/collection-panel/collection-panel.tsx
src/views/workbench/workbench.tsx

index 098e0090cc90bc896cd795169ddb32e8582fb817..2b0c58918f11270ef786e7d5a99d06f8bd61e001 100644 (file)
@@ -99,6 +99,9 @@ const themeOptions: ArvadosThemeOptions = {
             }
         },
         MuiInput: {
+            root: {
+                fontSize: '0.875rem'
+            },
             underline: {
                 '&:after': {
                     borderBottomColor: purple800
@@ -109,6 +112,9 @@ const themeOptions: ArvadosThemeOptions = {
             }
         },
         MuiFormLabel: {
+            root: {
+                fontSize: '0.875rem'
+            },
             focused: {
                 "&$focused:not($error)": {
                     color: purple800
index 3888b04b67d596ea84f3e11cbfeba30999adbc03..32ab167182c28170660a42e1f0507c40f35256ce 100644 (file)
@@ -8,7 +8,7 @@ import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/st
 import { ArvadosTheme } from '~/common/custom-theme';
 import * as classnames from "classnames";
 
-type CssRules = 'attribute' | 'label' | 'value' | 'link';
+type CssRules = 'attribute' | 'label' | 'value' | 'lowercaseValue' | 'link';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     attribute: {
@@ -26,6 +26,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         alignItems: 'flex-start',
         textTransform: 'capitalize'
     },
+    lowercaseValue: {
+        textTransform: 'lowercase'
+    },
     link: {
         width: '60%',
         color: theme.palette.primary.main,
@@ -39,6 +42,7 @@ interface DetailsAttributeDataProps {
     classLabel?: string;
     value?: string | number;
     classValue?: string;
+    lowercaseValue?: boolean;
     link?: string;
     children?: React.ReactNode;
 }
@@ -46,12 +50,12 @@ interface DetailsAttributeDataProps {
 type DetailsAttributeProps = DetailsAttributeDataProps & WithStyles<CssRules>;
 
 export const DetailsAttribute = withStyles(styles)(
-    ({ label, link, value, children, classes, classLabel, classValue }: DetailsAttributeProps) =>
+    ({ label, link, value, children, classes, classLabel, classValue, lowercaseValue }: DetailsAttributeProps) =>
         <Typography component="div" className={classes.attribute}>
             <Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
             { link
                 ? <a href={link} className={classes.link} target='_blank'>{value}</a>
-                : <Typography component="span" className={classnames([classes.value, classValue])}>
+                : <Typography component="span" className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
                     {value}
                     {children}
                 </Typography> }
index b34c6ab5f9cab8784e525b6356963f4fad4b800c..e7f63eafc0c1f7e9423e148daad623d008580623 100644 (file)
@@ -17,7 +17,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         alignItems: 'center'
     },
     listItemText: {
-        fontWeight: 700
+        fontWeight: 400
     },
     active: {
         color: theme.palette.primary.main,
diff --git a/src/components/move-to-dialog/move-to-dialog.tsx b/src/components/move-to-dialog/move-to-dialog.tsx
new file mode 100644 (file)
index 0000000..2bfc2c3
--- /dev/null
@@ -0,0 +1,48 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Field, InjectedFormProps, WrappedFieldProps } from "redux-form";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, CircularProgress } from "@material-ui/core";
+
+import { WithDialogProps } from "~/store/dialog/with-dialog";
+import { ProjectTreePicker } from "~/views-components/project-tree-picker/project-tree-picker";
+import { MOVE_TO_VALIDATION } from "~/validators/validators";
+
+export const MoveToDialog = (props: WithDialogProps<string> & InjectedFormProps<{ name: string }>) =>
+    <form>
+        <Dialog open={props.open}
+            disableBackdropClick={true}
+            disableEscapeKeyDown={true}>
+            <DialogTitle>Move to</DialogTitle>
+            <DialogContent>
+                <Field
+                    name="projectUuid"
+                    component={Picker}
+                    validate={MOVE_TO_VALIDATION} />
+            </DialogContent>
+            <DialogActions>
+                <Button
+                    variant='flat'
+                    color='primary'
+                    disabled={props.submitting}
+                    onClick={props.closeDialog}>
+                    Cancel
+                    </Button>
+                <Button
+                    variant='contained'
+                    color='primary'
+                    type='submit'
+                    onClick={props.handleSubmit}
+                    disabled={props.pristine || props.invalid || props.submitting}>
+                    {props.submitting ? <CircularProgress size={20} /> : 'Move'}
+                </Button>
+            </DialogActions>
+        </Dialog>
+    </form>;
+
+const Picker = (props: WrappedFieldProps) =>
+    <div style={{ width: '400px', height: '144px', display: 'flex', flexDirection: 'column' }}>
+       <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} /> 
+    </div>;
\ No newline at end of file
index 3e8cf904cfaae2274d1273bb81f1f77048b34c2a..263249588b45f5a5cbcd47caa2b231c2500ca315 100644 (file)
@@ -65,9 +65,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 export enum TreeItemStatus {
-    INITIAL,
-    PENDING,
-    LOADED
+    INITIAL = 'initial',
+    PENDING = 'pending',
+    LOADED = 'loaded'
 }
 
 export interface TreeItem<T> {
@@ -110,7 +110,7 @@ export const Tree = withStyles(styles)(
                             <i onClick={() => this.props.toggleItemOpen(it.id, it.status)}
                                 className={toggableIconContainer}>
                                 <ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
-                                    {it.status !== TreeItemStatus.INITIAL && it.items && it.items.length === 0 ? <span /> : <SidePanelRightArrowIcon />}
+                                    {this.getProperArrowAnimation(it.status, it.items!)}
                                 </ListItemIcon>
                             </i>
                             {this.props.showSelection &&
@@ -140,6 +140,10 @@ export const Tree = withStyles(styles)(
             </List>;
         }
 
+        getProperArrowAnimation = (status: string, items: Array<TreeItem<T>>) => {
+            return status === TreeItemStatus.PENDING || (status === TreeItemStatus.LOADED && !items) ? <span /> : <SidePanelRightArrowIcon />;
+        }
+
         getToggableIconClassNames = (isOpen?: boolean, isActive?: boolean) => {
             const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
             return classnames(toggableIcon, {
index 443e76f3e62a42597d804aefd779b32b3f65ffa9..fcc02f1e73088f18b2af30d74da1c7c1647ab400 100644 (file)
@@ -27,6 +27,7 @@ import { collectionFilesActionSet } from './views-components/context-menu/action
 import { collectionFilesItemActionSet } from './views-components/context-menu/action-sets/collection-files-item-action-set';
 import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
 import { collectionResourceActionSet } from './views-components/context-menu/action-sets/collection-resource-action-set';
+import { initPickerProjectTree } from './views-components/project-tree-picker/project-tree-picker';
 
 const getBuildNumber = () => "BN-" + (process.env.BUILD_NUMBER || "dev");
 const getGitCommit = () => "GIT-" + (process.env.GIT_COMMIT || "latest").substr(0, 7);
@@ -53,6 +54,7 @@ fetchConfig()
 
         store.dispatch(initAuth());
         store.dispatch(getProjectList(services.authService.getUuid()));
+        store.dispatch(initPickerProjectTree());    
 
         const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props}/>;
         const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props}/>;
index 0bc44ba85bf06195bb34ef88500ec807c4b99e54..d26d768a0ecd089447d587a08190ef4ebe24a45f 100644 (file)
@@ -29,8 +29,8 @@ describe('data-explorer-reducer', () => {
             filters: [],
             render: jest.fn(),
             selected: true,
-            configurable: true,
-            sortDirection: SortDirection.ASC
+            sortDirection: SortDirection.ASC,
+            configurable: true
         }, {
             name: "Column 2",
             filters: [],
index bb60e396946a588f8d93a1c1f7e6803461ba82eb..8cd3121eecb871c8d9fa538bcfdb6983cf1322f6 100644 (file)
@@ -21,7 +21,7 @@ describe('project-reducer', () => {
                 id: "1",
                 items: [],
                 data: mockProjectResource({ uuid: "1" }),
-                status: 0
+                status: TreeItemStatus.INITIAL
             }, {
                 active: false,
                 open: false,
index e3bebe1c858f6e9a6ef7017de35b6162e72a745d..e1e8d5c620325212df2426f57e4a1776e1be6b6f 100644 (file)
@@ -3,13 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { default as unionize, ofType, UnionOf } from "unionize";
+
 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 }>()
+    LOAD_TREE_PICKER_NODE: ofType<{ nodeId: string, pickerId: string }>(),
+    LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ nodeId: string, nodes: Array<TreePickerNode>, pickerId: string }>(),
+    TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ nodeId: string, pickerId: string }>(),
+    TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ nodeId: string, pickerId: string }>()
 }, {
         tag: 'type',
         value: 'payload'
index b092d5ac2a89c80ed29b0d525bacee3790e24821..e09d12d777a485199325da74c146d76f6da375e6 100644 (file)
@@ -11,89 +11,95 @@ 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);
+        const newState = treePickerReducer({}, treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: '1', pickerId: "projects" }));
+        expect(newState).toEqual({ 'projects': 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' }),
+        const node = createTreePickerNode({ nodeId: '1', value: '1' });
+        const [newState] = [{
+            projects: createTree<TreePickerNode>()
+        }]
+            .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+            .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: '1', pickerId: "projects" })));
+
+        expect(getNodeValue('1')(newState.projects)).toEqual({
+            ...createTreePickerNode({ nodeId: '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(getNodeChildrenIds('')(newTree)).toEqual(['1.1']);
+        const subNode = createTreePickerNode({ nodeId: '1.1', value: '1.1' });
+        const newState = treePickerReducer({}, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [subNode], pickerId: "projects" }));
+        expect(getNodeChildrenIds('')(newState.projects)).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(getNodeChildrenIds('1')(newTree)).toEqual(['1.1']);
-        expect(getNodeValue('1')(newTree)).toEqual({
-            ...createTreePickerNode({ id: '1', value: '1' }),
+        const node = createTreePickerNode({ nodeId: '1', value: '1' });
+        const subNode = createTreePickerNode({ nodeId: '1.1', value: '1.1' });
+        const [newState] = [{
+            projects: createTree<TreePickerNode>()
+        }]
+            .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+            .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '1', nodes: [subNode], pickerId: "projects" })));
+        expect(getNodeChildrenIds('1')(newState.projects)).toEqual(['1.1']);
+        expect(getNodeValue('1')(newState.projects)).toEqual({
+            ...createTreePickerNode({ nodeId: '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' }),
+        const node = createTreePickerNode({ nodeId: '1', value: '1' });
+        const [newState] = [{
+            projects: createTree<TreePickerNode>()
+        }]
+            .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+            .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" })));
+        expect(getNodeValue('1')(newState.projects)).toEqual({
+            ...createTreePickerNode({ nodeId: '1', value: '1' }),
             collapsed: false
         });
     });
 
     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' }),
+        const node = createTreePickerNode({ nodeId: '1', value: '1' });
+        const [newState] = [{
+            projects: createTree<TreePickerNode>()
+        }]
+            .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+            .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" })))
+            .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId: '1', pickerId: "projects" })));
+        expect(getNodeValue('1')(newState.projects)).toEqual({
+            ...createTreePickerNode({ nodeId: '1', value: '1' }),
             collapsed: true
         });
     });
 
     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' }),
+        const node = createTreePickerNode({ nodeId: '1', value: '1' });
+        const [newState] = [{
+            projects: createTree<TreePickerNode>()
+        }]
+            .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+            .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" })));
+        expect(getNodeValue('1')(newState.projects)).toEqual({
+            ...createTreePickerNode({ nodeId: '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' }),
+        const node = createTreePickerNode({ nodeId: '1', value: '1' });
+        const [newState] = [{
+            projects: createTree<TreePickerNode>()
+        }]
+            .map(state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ nodeId: '', nodes: [node], pickerId: "projects" })))
+            .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" })))
+            .map(state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '1', pickerId: "projects" })));
+        expect(getNodeValue('1')(newState.projects)).toEqual({
+            ...createTreePickerNode({ nodeId: '1', value: '1' }),
             selected: false
         });
     });
index 8d61714cc9f744929587dd7f96f613ba18120f67..6a87fb4c77b17ac3d1ad8c2e898c3468e38c7e85 100644 (file)
@@ -2,28 +2,31 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { createTree, setNodeValueWith, TreeNode, setNode, mapTreeValues } from "~/models/tree";
+import { createTree, setNodeValueWith, TreeNode, setNode, mapTreeValues, Tree } from "~/models/tree";
 import { TreePicker, TreePickerNode } from "./tree-picker";
 import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
 import { TreeItemStatus } from "~/components/tree/tree";
+import { compose } from "redux";
 
-export const treePickerReducer = (state: TreePicker = createTree(), action: TreePickerAction) =>
+export const treePickerReducer = (state: TreePicker = {}, 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),
+        LOAD_TREE_PICKER_NODE: ({ nodeId, pickerId }) =>
+            updateOrCreatePicker(state, pickerId, setNodeValueWith(setPending)(nodeId)),
+        LOAD_TREE_PICKER_NODE_SUCCESS: ({ nodeId, nodes, pickerId }) => 
+            updateOrCreatePicker(state, pickerId, compose(receiveNodes(nodes)(nodeId),setNodeValueWith(setLoaded)(nodeId))),
+        TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ nodeId, pickerId }) => 
+            updateOrCreatePicker(state, pickerId, setNodeValueWith(toggleCollapse)(nodeId)),
+        TOGGLE_TREE_PICKER_NODE_SELECT: ({ nodeId, pickerId }) => 
+            updateOrCreatePicker(state, pickerId, mapTreeValues(toggleSelect(nodeId))),
         default: () => state
     });
 
+const updateOrCreatePicker = (state: TreePicker, pickerId: string, func: (value: Tree<TreePickerNode>) => Tree<TreePickerNode>) => {
+    const picker = state[pickerId] || createTree();
+    const updatedPicker = func(picker);
+    return { ...state, [pickerId]: updatedPicker };
+};
+
 const setPending = (value: TreePickerNode): TreePickerNode =>
     ({ ...value, status: TreeItemStatus.PENDING });
 
@@ -33,12 +36,12 @@ const setLoaded = (value: TreePickerNode): TreePickerNode =>
 const toggleCollapse = (value: TreePickerNode): TreePickerNode =>
     ({ ...value, collapsed: !value.collapsed });
 
-const toggleSelect = (id: string) => (value: TreePickerNode): TreePickerNode =>
-    value.id === id
+const toggleSelect = (nodeId: string) => (value: TreePickerNode): TreePickerNode =>
+    value.nodeId === nodeId
         ? ({ ...value, selected: !value.selected })
         : ({ ...value, selected: false });
 
-const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: TreePicker) =>
+const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: Tree<TreePickerNode>) =>
     nodes.reduce((tree, node) =>
         setNode(
             createTreeNode(parent)(node)
@@ -46,7 +49,7 @@ const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (stat
 
 const createTreeNode = (parent: string) => (node: TreePickerNode): TreeNode<TreePickerNode> => ({
     children: [],
-    id: node.id,
+    id: node.nodeId,
     parent,
     value: node
 });
index e19ce3a7a48c0d162c1fb462c00ddb26d8b30c57..c815ad4f900468ed74f2bd0b33d062e5e377f219 100644 (file)
@@ -5,17 +5,17 @@
 import { Tree } from "~/models/tree";
 import { TreeItemStatus } from "~/components/tree/tree";
 
-export type TreePicker = Tree<TreePickerNode>;
+export type TreePicker = { [key: string]: Tree<TreePickerNode> };
 
 export interface TreePickerNode {
-    id: string;
+    nodeId: string;
     value: any;
     selected: boolean;
     collapsed: boolean;
     status: TreeItemStatus;
 }
 
-export const createTreePickerNode = (data: {id: string, value: any}) => ({
+export const createTreePickerNode = (data: { nodeId: string, value: any }) => ({
     ...data,
     selected: false,
     collapsed: true,
index edd07822942ace10ac40ae68e79b9222422dbe55..389e8cdfd140cd32b6e372a5bee3c0352680e8f4 100644 (file)
@@ -13,4 +13,6 @@ export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
 
 export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
 export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_PROJECT_VALIDATION = [require];
\ No newline at end of file
+export const COLLECTION_PROJECT_VALIDATION = [require];
+
+export const MOVE_TO_VALIDATION = [require];
\ No newline at end of file
index 4561f9d308879b981c8a9a72dc32c29dc701ddcc..9c07fb0551eab71995d7f488f74d1a6292af9128 100644 (file)
@@ -8,6 +8,7 @@ import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "~/components/icon/icon";
 import { openUpdater } from "~/store/collections/updater/collection-updater-action";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { openMoveToDialog } from "../../move-to-dialog/move-to-dialog";
 
 export const collectionActionSet: ContextMenuActionSet = [[
     {
@@ -27,9 +28,7 @@ export const collectionActionSet: ContextMenuActionSet = [[
     {
         icon: MoveToIcon,
         name: "Move to",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: dispatch => dispatch<any>(openMoveToDialog())
     },
     {
         component: ToggleFavoriteAction,
index 7d8364bd70ea22b4c29bb69c5c11b95699f6765b..337ca2ff95fd261864476a8da72239442e4f718b 100644 (file)
@@ -8,6 +8,7 @@ import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
 import { openUpdater } from "~/store/collections/updater/collection-updater-action";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { openMoveToDialog } from "../../move-to-dialog/move-to-dialog";
 
 export const collectionResourceActionSet: ContextMenuActionSet = [[
     {
@@ -27,9 +28,7 @@ export const collectionResourceActionSet: ContextMenuActionSet = [[
     {
         icon: MoveToIcon,
         name: "Move to",
-        execute: (dispatch, resource) => {
-            // add code
-        }
+        execute: dispatch => dispatch<any>(openMoveToDialog())
     },
     {
         component: ToggleFavoriteAction,
index 1b000c88fcee77ec2c39a844d3476001fca725a7..efba457821db1621e1dbe51426a49cc68c155bcc 100644 (file)
@@ -6,10 +6,11 @@ import { reset, initialize } from "redux-form";
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { projectActions, PROJECT_FORM_NAME } from "~/store/project/project-action";
-import { NewProjectIcon, RenameIcon } from "~/components/icon/icon";
+import { NewProjectIcon, MoveToIcon, RenameIcon } from "~/components/icon/icon";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { openMoveToDialog } from "../../move-to-dialog/move-to-dialog";
 import { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create";
 
 export const projectActionSet: ContextMenuActionSet = [[
@@ -36,5 +37,10 @@ export const projectActionSet: ContextMenuActionSet = [[
                 dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
             });
         }
-    }
+    },
+    {
+        icon: MoveToIcon,
+        name: "Move to",
+        execute: dispatch => dispatch<any>(openMoveToDialog())
+    },
 ]];
diff --git a/src/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected.tsx b/src/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected.tsx
new file mode 100644 (file)
index 0000000..60ce05c
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { reduxForm, reset, startSubmit, stopSubmit } from "redux-form";
+import { withDialog } from "~/store/dialog/with-dialog";
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { DialogCollectionCreateWithSelected } from "../dialog-create/dialog-collection-create-selected";
+
+export const DIALOG_COLLECTION_CREATE_WITH_SELECTED = 'dialogCollectionCreateWithSelected';
+
+export const createCollectionWithSelected = () =>
+    (dispatch: Dispatch) => {
+        dispatch(reset(DIALOG_COLLECTION_CREATE_WITH_SELECTED));
+        dispatch(dialogActions.OPEN_DIALOG({ id: DIALOG_COLLECTION_CREATE_WITH_SELECTED, data: {} }));
+    };
+
+export const [DialogCollectionCreateWithSelectedFile] = [DialogCollectionCreateWithSelected]
+    .map(withDialog(DIALOG_COLLECTION_CREATE_WITH_SELECTED))
+    .map(reduxForm({
+        form: DIALOG_COLLECTION_CREATE_WITH_SELECTED,
+        onSubmit: (data, dispatch) => {
+            dispatch(startSubmit(DIALOG_COLLECTION_CREATE_WITH_SELECTED));
+            setTimeout(() => dispatch(stopSubmit(DIALOG_COLLECTION_CREATE_WITH_SELECTED, { name: 'Invalid name' })), 2000);
+        }
+    }));
index c41e0b80ee1616af2b97f20d3d5c71e2311b0288..721418ef5463c5f043cae43c4a71b4b94db4242d 100644 (file)
@@ -21,7 +21,7 @@ export class CollectionDetails extends DetailsData<CollectionResource> {
         return <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.COLLECTION)} />
             <DetailsAttribute label='Size' value='---' />
-            <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+            <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
             <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
             <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
             {/* Links but we dont have view */}
index a298d670ee70de01e14ab58b03c9410aaccd4e69..70c026d3d3385440dad0035ac6d657b2387c1f4e 100644 (file)
@@ -21,7 +21,7 @@ import { EmptyDetails } from "./empty-details";
 import { DetailsData } from "./details-data";
 import { DetailsResource } from "~/models/details";
 
-type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
+type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
 
 const drawerWidth = 320;
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
@@ -45,7 +45,10 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         textAlign: 'center'
     },
     headerIcon: {
-        fontSize: "34px"
+        fontSize: '2.125rem'
+    },
+    headerTitle: {
+        wordBreak: 'break-all'
     },
     tabContainer: {
         padding: theme.spacing.unit * 3
@@ -114,7 +117,7 @@ export const DetailsPanel = withStyles(styles)(
                                         {item.getIcon(classes.headerIcon)}
                                     </Grid>
                                     <Grid item xs={8}>
-                                        <Typography variant="title">
+                                        <Typography variant="title" className={classes.headerTitle}>
                                             {item.getTitle()}
                                         </Typography>
                                     </Grid>
index dee6e8b0b9a20db979ca64cddda58b268c2535d1..cec01b966d91ecd5eb26dcda42779c579915dc49 100644 (file)
@@ -21,7 +21,7 @@ export class ProcessDetails extends DetailsData<ProcessResource> {
         return <div>
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
             <DetailsAttribute label='Size' value='---' />
-            <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+            <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
 
             {/* Missing attr */}
             <DetailsAttribute label='Status' value={this.item.state} />
index 154f0a2c906e908660d05d5d437c65ad7890911a..1e65ec834bf8e48f75230bea8505d843eda82a63 100644 (file)
@@ -22,7 +22,7 @@ export class ProjectDetails extends DetailsData<ProjectResource> {
             <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
             {/* Missing attr */}
             <DetailsAttribute label='Size' value='---' />
-            <DetailsAttribute label='Owner' value={this.item.ownerUuid} />
+            <DetailsAttribute label='Owner' value={this.item.ownerUuid} lowercaseValue={true} />
             <DetailsAttribute label='Last modified' value={formatDate(this.item.modifiedAt)} />
             <DetailsAttribute label='Created at' value={formatDate(this.item.createdAt)} />
             {/* Missing attr */}
diff --git a/src/views-components/dialog-create/dialog-collection-create-selected.tsx b/src/views-components/dialog-create/dialog-collection-create-selected.tsx
new file mode 100644 (file)
index 0000000..ad684d7
--- /dev/null
@@ -0,0 +1,60 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { InjectedFormProps, Field, WrappedFieldProps } from "redux-form";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, CircularProgress } from "@material-ui/core";
+import { WithDialogProps } from "~/store/dialog/with-dialog";
+import { TextField } from "~/components/text-field/text-field";
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
+import { ProjectTreePicker } from "../project-tree-picker/project-tree-picker";
+
+export const DialogCollectionCreateWithSelected = (props: WithDialogProps<string> & InjectedFormProps<{ name: string }>) =>
+    <form>
+        <Dialog open={props.open}
+            disableBackdropClick={true}
+            disableEscapeKeyDown={true}>
+            <DialogTitle>Create a collection</DialogTitle>
+            <DialogContent style={{ display: 'flex' }}>
+                <div>
+                    <Field
+                        name='name'
+                        component={TextField}
+                        validate={COLLECTION_NAME_VALIDATION}
+                        label="Collection Name" />
+                    <Field
+                        name='description'
+                        component={TextField}
+                        validate={COLLECTION_DESCRIPTION_VALIDATION}
+                        label="Description - optional" />
+                </div>
+                <Field
+                    name="projectUuid"
+                    component={Picker}
+                    validate={COLLECTION_PROJECT_VALIDATION} />
+            </DialogContent>
+            <DialogActions>
+                <Button
+                    variant='flat'
+                    color='primary'
+                    disabled={props.submitting}
+                    onClick={props.closeDialog}>
+                    Cancel
+                    </Button>
+                <Button
+                    variant='contained'
+                    color='primary'
+                    type='submit'
+                    onClick={props.handleSubmit}
+                    disabled={props.pristine || props.invalid || props.submitting}>
+                    {props.submitting ? <CircularProgress size={20} /> : 'Create a collection'}
+                </Button>
+            </DialogActions>
+        </Dialog>
+    </form>;
+
+const Picker = (props: WrappedFieldProps) =>
+    <div style={{ width: '400px', height: '144px', display: 'flex', flexDirection: 'column' }}>
+        <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
+    </div>;
diff --git a/src/views-components/move-to-dialog/move-to-dialog.tsx b/src/views-components/move-to-dialog/move-to-dialog.tsx
new file mode 100644 (file)
index 0000000..5939662
--- /dev/null
@@ -0,0 +1,27 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from "redux";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+import { MoveToDialog } from "../../components/move-to-dialog/move-to-dialog";
+import { reduxForm, startSubmit, stopSubmit } from "redux-form";
+
+export const MOVE_TO_DIALOG = 'moveToDialog';
+
+export const openMoveToDialog = () =>
+    (dispatch: Dispatch) => {
+        dispatch(dialogActions.OPEN_DIALOG({ id: MOVE_TO_DIALOG, data: {} }));
+    };
+
+export const MoveToProjectDialog = compose(
+    withDialog(MOVE_TO_DIALOG),
+    reduxForm({
+        form: MOVE_TO_DIALOG,
+        onSubmit: (data, dispatch) => {
+            dispatch(startSubmit(MOVE_TO_DIALOG));
+            setTimeout(() => dispatch(stopSubmit(MOVE_TO_DIALOG, { name: 'Invalid path' })), 2000);
+        }
+    })
+)(MoveToDialog);
index 9143c47a2da5efe25b4d983015946706a8487c7f..30acf2a731da415362fc783cc157879e91f73df9 100644 (file)
@@ -6,47 +6,77 @@ 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 { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
+import { 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 { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon } 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";
+import { mockProjectResource } from "~/models/test-utils";
 
-type ProjectTreePickerProps = Pick<TreeProps<ProjectResource>, 'toggleItemActive' | 'toggleItemOpen'>;
+type ProjectTreePickerProps = Pick<TreePickerProps, 'toggleItemActive' | 'toggleItemOpen'>;
 
-const mapDispatchToProps = (dispatch: Dispatch, props: {onChange: (projectUuid: string) => void}): ProjectTreePickerProps => ({
-    toggleItemActive: id => {
-        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id }));
-        props.onChange(id);
+const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({
+    toggleItemActive: (nodeId, status, pickerId) => {
+        getNotSelectedTreePickerKind(pickerId)
+            .forEach(pickerId => dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '', pickerId })));
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId }));
+
+        props.onChange(nodeId);
     },
-    toggleItemOpen: (id, status) => {
-        status === TreeItemStatus.INITIAL
-            ? dispatch<any>(loadProjectTreePickerProjects(id))
-            : dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+    toggleItemOpen: (nodeId, status, pickerId) => {
+        dispatch<any>(toggleItemOpen(nodeId, status, pickerId));
     }
 });
 
+const toggleItemOpen = (nodeId: string, status: TreeItemStatus, pickerId: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        if (status === TreeItemStatus.INITIAL) {
+            if (pickerId === TreePickerId.PROJECTS) {
+                dispatch<any>(loadProjectTreePickerProjects(nodeId));
+            } else if (pickerId === TreePickerId.FAVORITES) {
+                dispatch<any>(loadFavoriteTreePickerProjects(nodeId === services.authService.getUuid() ? '' : nodeId));
+            } else {
+                // TODO: load sharedWithMe
+            }
+        } else {
+            dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId }));
+        }
+    };
+
+const getNotSelectedTreePickerKind = (pickerId: string) => {
+    return [TreePickerId.PROJECTS, TreePickerId.FAVORITES, TreePickerId.SHARED_WITH_ME].filter(nodeId => nodeId !== pickerId);
+};
+
+export enum TreePickerId {
+    PROJECTS = 'Projects',
+    SHARED_WITH_ME = 'Shared with me',
+    FAVORITES = 'Favorites'
+}
+
 export const ProjectTreePicker = connect(undefined, mapDispatchToProps)((props: ProjectTreePickerProps) =>
-    <div style={{display: 'flex', flexDirection: 'column'}}>
-        <Typography variant='caption' style={{flexShrink: 0}}>
+    <div style={{ display: 'flex', flexDirection: 'column' }}>
+        <Typography variant='caption' style={{ flexShrink: 0 }}>
             Select a project
         </Typography>
-        <div style={{flexGrow: 1, overflow: 'auto'}}>
-            <TreePicker {...props} render={renderTreeItem} />
+        <div style={{ flexGrow: 1, overflow: 'auto' }}>
+            <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.PROJECTS} />
+            <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.SHARED_WITH_ME} />
+            <TreePicker {...props} render={renderTreeItem} pickerId={TreePickerId.FAVORITES} />
         </div>
     </div>);
 
+
 // TODO: move action creator to store directory
-export const loadProjectTreePickerProjects = (id: string) =>
+export const loadProjectTreePickerProjects = (nodeId: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id }));
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.PROJECTS }));
 
-        const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
+        const ownerUuid = nodeId.length === 0 ? services.authService.getUuid() || '' : nodeId;
 
         const filters = new FilterBuilder()
             .addEqual('ownerUuid', ownerUuid)
@@ -54,22 +84,89 @@ export const loadProjectTreePickerProjects = (id: string) =>
 
         const { items } = await services.projectService.list({ filters });
 
-        dispatch<any>(receiveProjectTreePickerData(id, items));
+        dispatch<any>(receiveTreePickerData(nodeId, items, TreePickerId.PROJECTS));
     };
 
+export const loadFavoriteTreePickerProjects = (nodeId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const parentId = services.authService.getUuid() || '';
+
+        if (nodeId === '') {
+            dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: parentId, pickerId: TreePickerId.FAVORITES }));
+            const { items } = await services.favoriteService.list(parentId);
+
+            dispatch<any>(receiveTreePickerData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
+        } else {
+            dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId, pickerId: TreePickerId.FAVORITES }));
+            const filters = new FilterBuilder()
+                .addEqual('ownerUuid', nodeId)
+                .getFilters();
+
+            const { items } = await services.projectService.list({ filters });
+
+            dispatch<any>(receiveTreePickerData(nodeId, items, TreePickerId.FAVORITES));
+        }
+
+    };
+
+const getProjectPickerIcon = (item: TreeItem<ProjectResource>) => {
+    switch (item.data.name) {
+        case TreePickerId.FAVORITES:
+            return FavoriteIcon;
+        case TreePickerId.PROJECTS:
+            return ProjectsIcon;
+        case TreePickerId.SHARED_WITH_ME:
+            return ShareMeIcon;
+        default:
+            return ProjectIcon;
+    }
+};
+
 const renderTreeItem = (item: TreeItem<ProjectResource>) =>
     <ListItemTextIcon
-        icon={ProjectIcon}
+        icon={getProjectPickerIcon(item)}
         name={item.data.name}
         isActive={item.active}
         hasMargin={true} />;
 
+
 // TODO: move action creator to store directory
-const receiveProjectTreePickerData = (id: string, projects: ProjectResource[]) =>
+export const receiveTreePickerData = (nodeId: string, projects: ProjectResource[], pickerId: string) =>
     (dispatch: Dispatch) => {
         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
-            id,
-            nodes: projects.map(project => createTreePickerNode({ id: project.uuid, value: project }))
+            nodeId,
+            nodes: projects.map(project => createTreePickerNode({ nodeId: project.uuid, value: project })),
+            pickerId,
         }));
-        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId }));
     };
+
+export const initPickerProjectTree = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    const uuid = services.authService.getUuid();
+
+    dispatch<any>(getPickerTreeProjects(uuid));
+    dispatch<any>(getSharedWithMeProjectsPickerTree(uuid));
+    dispatch<any>(getFavoritesProjectsPickerTree(uuid));
+};
+
+const getPickerTreeProjects = (uuid: string = '') => {
+    return getProjectsPickerTree(uuid, TreePickerId.PROJECTS);
+};
+
+const getSharedWithMeProjectsPickerTree = (uuid: string = '') => {
+    return getProjectsPickerTree(uuid, TreePickerId.SHARED_WITH_ME);
+};
+
+const getFavoritesProjectsPickerTree = (uuid: string = '') => {
+    return getProjectsPickerTree(uuid, TreePickerId.FAVORITES);
+};
+
+const getProjectsPickerTree = (uuid: string, kind: string) => {
+    return receiveTreePickerData(
+        '',
+        [mockProjectResource({ uuid, name: kind })],
+        kind
+    );
+};
+
index 140119e17e6849ad57d397fa02f362b2f1d7111f..18efdaf88d6d235d66a9547ec9fd6afe18805b9d 100644 (file)
@@ -11,9 +11,9 @@ import { Collapse } from '@material-ui/core';
 import CircularProgress from '@material-ui/core/CircularProgress';
 
 import { ProjectTree } from './project-tree';
-import { TreeItem } from '~/components/tree/tree';
-import { ProjectResource } from '~/models/project';
-import { mockProjectResource } from '~/models/test-utils';
+import { TreeItem, TreeItemStatus } from '../../components/tree/tree';
+import { ProjectResource } from '../../models/project';
+import { mockProjectResource } from '../../models/test-utils';
 
 Enzyme.configure({ adapter: new Adapter() });
 
@@ -25,7 +25,7 @@ describe("ProjectTree component", () => {
             id: "3",
             open: true,
             active: true,
-            status: 1
+            status: TreeItemStatus.PENDING
         };
         const wrapper = mount(<ProjectTree
             projects={[project]}
@@ -43,14 +43,14 @@ describe("ProjectTree component", () => {
                 id: "3",
                 open: true,
                 active: true,
-                status: 2,
+                status: TreeItemStatus.LOADED,
                 items: [
                     {
                         data: mockProjectResource(),
                         id: "3",
                         open: true,
                         active: true,
-                        status: 1
+                        status: TreeItemStatus.PENDING
                     }
                 ]
             }
@@ -70,7 +70,7 @@ describe("ProjectTree component", () => {
             id: "3",
             open: false,
             active: true,
-            status: 1
+            status: TreeItemStatus.PENDING
         };
         const wrapper = mount(<ProjectTree
             projects={[project]}
index ba9ccb916efd38d955badedae7e6a7418871d354..b90f2e420656dd1365b37869c65779830726a589 100644 (file)
@@ -3,42 +3,43 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { connect } from "react-redux";
-import { Tree, TreeProps, TreeItem } from "~/components/tree/tree";
+import { Tree, TreeProps, TreeItem, TreeItemStatus } from "~/components/tree/tree";
 import { RootState } from "~/store/store";
-import { TreePicker as TTreePicker, TreePickerNode, createTreePickerNode } from "~/store/tree-picker/tree-picker";
-import { getNodeValue, getNodeChildrenIds } from "~/models/tree";
+import { createTreePickerNode, TreePickerNode } from "~/store/tree-picker/tree-picker";
+import { getNodeValue, getNodeChildrenIds, Tree as Ttree, createTree } from "~/models/tree";
+import { Dispatch } from "redux";
 
-const memoizedMapStateToProps = () => {
-    let prevState: TTreePicker;
-    let prevTree: Array<TreeItem<any>>;
+export interface TreePickerProps {
+    pickerId: string;
+    toggleItemOpen: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
+    toggleItemActive: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
+}
 
-    return (state: RootState): Pick<TreeProps<any>, 'items'> => {
-        if (prevState !== state.treePicker) {
-            prevState = state.treePicker;
-            prevTree = getNodeChildrenIds('')(state.treePicker)
-                .map(treePickerToTreeItems(state.treePicker));
-        }
-        return {
-            items: prevTree
-        };
+const mapStateToProps = (state: RootState, props: TreePickerProps): Pick<TreeProps<any>, 'items'> => {
+    const tree = state.treePicker[props.pickerId] || createTree();
+    return {
+        items: getNodeChildrenIds('')(tree)
+            .map(treePickerToTreeItems(tree))
     };
 };
 
-const mapDispatchToProps = (): Pick<TreeProps<any>, 'onContextMenu'> => ({
+const mapDispatchToProps = (dispatch: Dispatch, props: TreePickerProps): Pick<TreeProps<any>, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive'> => ({
     onContextMenu: () => { return; },
+    toggleItemActive: (id, status) => props.toggleItemActive(id, status, props.pickerId),
+    toggleItemOpen: (id, status) => props.toggleItemOpen(id, status, props.pickerId)
 });
 
-export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
+export const TreePicker = connect(mapStateToProps, mapDispatchToProps)(Tree);
 
-const treePickerToTreeItems = (tree: TTreePicker) =>
+const treePickerToTreeItems = (tree: Ttree<TreePickerNode>) =>
     (id: string): TreeItem<any> => {
-        const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ id: '', value: 'InvalidNode' });
-        const items = getNodeChildrenIds(node.id)(tree)
+        const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ nodeId: '', value: 'InvalidNode' });
+        const items = getNodeChildrenIds(node.nodeId)(tree)
             .map(treePickerToTreeItems(tree));
         return {
             active: node.selected,
             data: node.value,
-            id: node.id,
+            id: node.nodeId,
             items: items.length > 0 ? items : undefined,
             open: !node.collapsed,
             status: node.status
index 374cb95159483f5d4896fcbd539fddfc61931ea3..f476c9397c5f0c7d7cff49759e6d041e25c15af8 100644 (file)
@@ -20,7 +20,7 @@ import { TagResource } from '~/models/tag';
 import { CollectionTagForm } from './collection-tag-form';
 import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
 
-type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'value';
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     card: {
@@ -40,8 +40,12 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         color: theme.palette.grey["500"],
         cursor: 'pointer'
     },
+    label: {
+        fontSize: '0.875rem'
+    },
     value: {
-        textTransform: 'none'
+        textTransform: 'none',
+        fontSize: '0.875rem'
     }
 });
 
@@ -84,16 +88,19 @@ export const CollectionPanel = withStyles(styles)(
                             <CardContent>
                                 <Grid container direction="column">
                                     <Grid item xs={6}>
-                                    <DetailsAttribute classValue={classes.value}
-                                            label='Collection UUID'
-                                            value={item && item.uuid}>
-                                        <CopyToClipboard text={item && item.uuid}>
-                                            <CopyIcon className={classes.copyIcon} />
-                                        </CopyToClipboard>
-                                    </DetailsAttribute>
-                                    <DetailsAttribute label='Number of files' value='14' />
-                                    <DetailsAttribute label='Content size' value='54 MB' />
-                                    <DetailsAttribute classValue={classes.value} label='Owner' value={item && item.ownerUuid} />
+                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                                label='Collection UUID'
+                                                value={item && item.uuid}>
+                                            <CopyToClipboard text={item && item.uuid}>
+                                                <CopyIcon className={classes.copyIcon} />
+                                            </CopyToClipboard>
+                                        </DetailsAttribute>
+                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
+                                            label='Number of files' value='14' />
+                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
+                                            label='Content size' value='54 MB' />
+                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
+                                            label='Owner' value={item && item.ownerUuid} />
                                     </Grid>
                                 </Grid>
                             </CardContent>
index 4949b6a0b60e8194c04c0bdce967c5b83ce9af9d..4d0adf15184ccff1e9d210e9e5dc15bb00832eca 100644 (file)
@@ -46,6 +46,8 @@ import { AuthService } from "~/services/auth-service/auth-service";
 import { RenameFileDialog } from '~/views-components/rename-file-dialog/rename-file-dialog';
 import { FileRemoveDialog } from '~/views-components/file-remove-dialog/file-remove-dialog';
 import { MultipleFilesRemoveDialog } from '~/views-components/file-remove-dialog/multiple-files-remove-dialog';
+import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
+import { MoveToProjectDialog } from '../../views-components/move-to-dialog/move-to-dialog';
 import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
 import { PROJECT_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-project-create';
 import { UploadCollectionFilesDialog } from '~/views-components/upload-collection-files-dialog/upload-collection-files-dialog';
@@ -244,6 +246,8 @@ export const Workbench = withStyles(styles)(
                         <CreateCollectionDialog />
                         <RenameFileDialog />
                         <CollectionPartialCopyDialog />
+                        <MoveToProjectDialog />
+                        <DialogCollectionCreateWithSelectedFile />
                         <FileRemoveDialog />
                         <MultipleFilesRemoveDialog />
                         <UpdateCollectionDialog />