Merge branch '14433_properties_inside_projects'
authorJanicki Artur <artur.janicki@contractors.roche.com>
Mon, 19 Nov 2018 09:56:42 +0000 (10:56 +0100)
committerJanicki Artur <artur.janicki@contractors.roche.com>
Mon, 19 Nov 2018 09:56:42 +0000 (10:56 +0100)
refs #14433

Arvados-DCO-1.1-Signed-off-by: Janicki Artur <artur.janicki@contractors.roche.com>

39 files changed:
src/models/tree.test.ts
src/models/tree.ts
src/store/collections/collection-copy-actions.ts
src/store/collections/collection-move-actions.ts
src/store/collections/collection-partial-copy-actions.ts
src/store/details-panel/details-panel-action.ts
src/store/processes/process-copy-actions.ts
src/store/processes/process-move-actions.ts
src/store/processes/processes-actions.ts
src/store/projects/project-move-actions.ts
src/store/tree-picker/picker-id.tsx [new file with mode: 0644]
src/store/tree-picker/tree-picker-actions.ts
src/store/tree-picker/tree-picker-reducer.ts
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/process-action-set.ts
src/views-components/context-menu/action-sets/process-resource-action-set.ts
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/action-sets/trashed-collection-action-set.ts
src/views-components/details-panel/details-panel.tsx
src/views-components/details-panel/project-details.tsx
src/views-components/dialog-copy/dialog-collection-partial-copy.tsx
src/views-components/dialog-copy/dialog-copy.tsx
src/views-components/dialog-forms/copy-collection-dialog.ts
src/views-components/dialog-forms/copy-process-dialog.ts
src/views-components/dialog-forms/move-collection-dialog.ts
src/views-components/dialog-forms/move-process-dialog.ts
src/views-components/dialog-forms/move-project-dialog.ts
src/views-components/dialog-forms/partial-copy-collection-dialog.ts
src/views-components/dialog-move/dialog-move-to.tsx
src/views-components/form-fields/collection-form-fields.tsx
src/views-components/main-content-bar/main-content-bar.tsx
src/views-components/process-remove-dialog/process-remove-dialog.tsx [new file with mode: 0644]
src/views-components/project-properties-dialog/project-properties-dialog.tsx [new file with mode: 0644]
src/views-components/project-properties-dialog/project-properties-form.tsx [new file with mode: 0644]
src/views-components/project-tree-picker/project-tree-picker.tsx
src/views-components/projects-tree-picker/generic-projects-tree-picker.tsx
src/views-components/projects-tree-picker/projects-tree-picker.tsx
src/views/workbench/workbench.tsx

index 54b11d47aadd2795cc32f8ce1f2ef095270d9b8e..3c7fdca9afdee6357b204ac4f4edad199155d79b 100644 (file)
@@ -18,6 +18,14 @@ describe('Tree', () => {
         expect(Tree.getNode('Node 1')(newTree)).toEqual(initTreeNode({ id: 'Node 1', value: 'Value 1' }));
     });
 
+    it('appends a subtree', () => {
+        const newTree = Tree.setNode(initTreeNode({ id: 'Node 1', value: 'Value 1' }))(tree);
+        const subtree = Tree.setNode(initTreeNode({ id: 'Node 2', value: 'Value 2' }))(Tree.createTree());
+        const mergedTree = Tree.appendSubtree('Node 1', subtree)(newTree);
+        expect(Tree.getNode('Node 1')(mergedTree)).toBeDefined();
+        expect(Tree.getNode('Node 2')(mergedTree)).toBeDefined();
+    });
+
     it('adds new node reference to parent children', () => {
         const newTree = pipe(
             Tree.setNode(initTreeNode({ id: 'Node 1', parent: '', value: 'Value 1' })),
@@ -89,6 +97,6 @@ describe('Tree', () => {
             initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 'Value 2' }),
         ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
         const mappedTree = Tree.mapTreeValues<string, number>(value => parseInt(value.split(' ')[1], 10))(newTree);
-        expect(Tree.getNode('Node 2')(mappedTree)).toEqual(initTreeNode({id: 'Node 2', parent: 'Node 1', value: 2 }));
+        expect(Tree.getNode('Node 2')(mappedTree)).toEqual(initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 2 }));
     });
 });
index 8e18f9fab78cd9e9ee27a2a9345aa1e5b223cd58..fe52a97b0fcdd3806579318d968f2efc8206f364 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { pipe } from 'lodash/fp';
+import { pipe, map, reduce } from 'lodash/fp';
 export type Tree<T> = Record<string, TreeNode<T>>;
 
 export const TREE_ROOT_ID = '';
@@ -34,6 +34,13 @@ export const createTree = <T>(): Tree<T> => ({});
 
 export const getNode = (id: string) => <T>(tree: Tree<T>): TreeNode<T> | undefined => tree[id];
 
+export const appendSubtree = <T>(id: string, subtree: Tree<T>) => (tree: Tree<T>) =>
+    pipe(
+        getNodeDescendants(''),
+        map(node => node.parent === '' ? { ...node, parent: id } : node),
+        reduce((newTree, node) => setNode(node)(newTree), tree)
+    )(subtree) as Tree<T>;
+
 export const setNode = <T>(node: TreeNode<T>) => (tree: Tree<T>): Tree<T> => {
     return pipe(
         (tree: Tree<T>) => getNode(node.id)(tree) === node
index d0387609bd79af99e22630f83b218af2c842afbd..e5a6676c6b4dbc789199f589e3a5315ebe2ed199 100644 (file)
@@ -11,12 +11,14 @@ import { ServiceRepository } from '~/services/services';
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 
 export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
 
 export const openCollectionCopyDialog = (resource: { name: string, uuid: string }) =>
     (dispatch: Dispatch) => {
         dispatch<any>(resetPickerProjectTree());
+        dispatch<any>(initProjectsTreePicker(COLLECTION_COPY_FORM_NAME));
         const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
         dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
         dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
index 54508e139f2b2b35bdfb14fb0bdfe805c1c3847e..770eed1a7872f9f3c30ec65f5f091f4ad2a329fb 100644 (file)
@@ -13,12 +13,14 @@ import { projectPanelActions } from '~/store/project-panel/project-panel-action'
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 
 export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
 
 export const openMoveCollectionDialog = (resource: { name: string, uuid: string }) =>
     (dispatch: Dispatch) => {
         dispatch<any>(resetPickerProjectTree());
+        dispatch<any>(initProjectsTreePicker(COLLECTION_MOVE_FORM_NAME));
         dispatch(initialize(COLLECTION_MOVE_FORM_NAME, resource));
         dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_MOVE_FORM_NAME, data: {} }));
     };
index 4dac9c7d7e5ce55d4246c885dcb7a707b54e7957..b9ada5ee01fa1014bc1ef2fc5605c0c94ade9ec9 100644 (file)
@@ -12,6 +12,7 @@ import { filterCollectionFilesBySelection } from '../collection-panel/collection
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 
 export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
 
@@ -32,6 +33,7 @@ export const openCollectionPartialCopyDialog = () =>
             };
             dispatch(initialize(COLLECTION_PARTIAL_COPY_FORM_NAME, initialData));
             dispatch<any>(resetPickerProjectTree());
+            dispatch<any>(initProjectsTreePicker(COLLECTION_PARTIAL_COPY_FORM_NAME));
             dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME, data: {} }));
         }
     };
index 2724a3e3465dbbac374a029f1f68c321dce2a9b1..2c742a1f38a3f4e63698019c092dec34935f1079 100644 (file)
@@ -3,6 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from '~/common/unionize';
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { getResource } from '~/store/resources/resources';
+import { ProjectResource } from "~/models/project";
+import { ServiceRepository } from '~/services/services';
+import { TagProperty } from '~/models/tag';
+import { startSubmit, stopSubmit } from 'redux-form';
+import { resourcesActions } from '~/store/resources/resources-actions';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+
+export const SLIDE_TIMEOUT = 500;
 
 export const detailsPanelActions = unionize({
     TOGGLE_DETAILS_PANEL: ofType<{}>(),
@@ -11,8 +23,57 @@ export const detailsPanelActions = unionize({
 
 export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
 
-export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+export const PROJECT_PROPERTIES_FORM_NAME = 'projectPropertiesFormName';
+export const PROJECT_PROPERTIES_DIALOG_NAME = 'projectPropertiesDialogName';
 
+export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
 
+export const openProjectPropertiesDialog = () =>
+    (dispatch: Dispatch) => {
+        dispatch<any>(dialogActions.OPEN_DIALOG({ id: PROJECT_PROPERTIES_DIALOG_NAME, data: { } }));
+    };
 
+export const deleteProjectProperty = (key: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { detailsPanel, resources } = getState();
+        const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource;
+        try {
+            if (project) {
+                delete project.properties[key];
+                const updatedProject = await services.projectService.update(project.uuid, project);
+                dispatch(resourcesActions.SET_RESOURCES([updatedProject]));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully deleted.", hideDuration: 2000 }));
+            }
+        } catch (e) {
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_PROPERTIES_FORM_NAME }));
+            throw new Error('Could not remove property from the project.');
+        }
+    };
 
+export const createProjectProperty = (data: TagProperty) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { detailsPanel, resources } = getState();
+        const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource;
+        dispatch(startSubmit(PROJECT_PROPERTIES_FORM_NAME));
+        try {
+            if (project) {
+                project.properties[data.key] = data.value;
+                const updatedProject = await services.projectService.update(project.uuid, project);
+                dispatch(resourcesActions.SET_RESOURCES([updatedProject]));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Property has been successfully added.", hideDuration: 2000 }));
+                dispatch(stopSubmit(PROJECT_PROPERTIES_FORM_NAME));
+            }
+            return;
+        } catch (e) {
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_PROPERTIES_FORM_NAME }));
+            throw new Error('Could not add property to the project.');
+        }
+    };
+export const toggleDetailsPanel = () => (dispatch: Dispatch) => {
+    // because of material-ui issue resizing details panel breaks tabs.
+    // triggering window resize event fixes that.
+    setTimeout(() => {
+        window.dispatchEvent(new Event('resize'));
+    }, SLIDE_TIMEOUT);
+    dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+};
index bb8d8f5aeca95d9fd1c27de94817e99cc7251cc3..cd3fe21c28abb97d96334aa2c562202ba8202560 100644 (file)
@@ -11,6 +11,7 @@ import { ServiceRepository } from '~/services/services';
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 import { getProcess, ProcessStatus, getProcessStatus } from '~/store/processes/process';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 
 export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
 
@@ -21,6 +22,7 @@ export const openCopyProcessDialog = (resource: { name: string, uuid: string })
             const processStatus = getProcessStatus(process);
             if (processStatus === ProcessStatus.DRAFT) {
                 dispatch<any>(resetPickerProjectTree());
+                dispatch<any>(initProjectsTreePicker(PROCESS_COPY_FORM_NAME));
                 const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, uuid: resource.uuid, ownerUuid: '' };
                 dispatch<any>(initialize(PROCESS_COPY_FORM_NAME, initialData));
                 dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COPY_FORM_NAME, data: {} }));
index 6df826992861cc965abef5c430bd780a22d11164..edba5a8574e16814c6a23988d85a1d4657d431b5 100644 (file)
@@ -13,6 +13,7 @@ import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
 import { projectPanelActions } from '~/store/project-panel/project-panel-action';
 import { getProcess, getProcessStatus, ProcessStatus } from '~/store/processes/process';
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 
 export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName';
 
@@ -23,6 +24,7 @@ export const openMoveProcessDialog = (resource: { name: string, uuid: string })
             const processStatus = getProcessStatus(process);
             if (processStatus === ProcessStatus.DRAFT) {
                 dispatch<any>(resetPickerProjectTree());
+                dispatch<any>(initProjectsTreePicker(PROCESS_MOVE_FORM_NAME));
                 dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource));
                 dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} }));
             } else {
index 031683a7e8af5a48fbf6067de0455c9ee31f2dd3..f9f5ef728beff7659ff2b56e96aed93ba6d6736a 100644 (file)
@@ -7,8 +7,11 @@ import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { updateResources } from '~/store/resources/resources-actions';
 import { FilterBuilder } from '~/services/api/filter-builder';
-import { ContainerRequestResource } from '../../models/container-request';
+import { ContainerRequestResource } from '~/models/container-request';
 import { Process } from './process';
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { projectPanelActions } from '~/store/project-panel/project-panel-action';
 
 export const loadProcess = (containerRequestUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
@@ -54,3 +57,28 @@ export const loadContainers = (filters: string) =>
         dispatch<any>(updateResources(items));
         return items;
     };
+
+export const openRemoveProcessDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: REMOVE_PROCESS_DIALOG,
+            data: {
+                title: 'Remove process permanently',
+                text: 'Are you sure you want to remove this process?',
+                confirmButtonLabel: 'Remove',
+                uuid
+            }
+        }));
+    };
+
+export const REMOVE_PROCESS_DIALOG = 'removeProcessDialog';
+
+export const removeProcessPermanently = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) =>{
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+        await services.containerRequestService.delete(uuid);
+        dispatch(projectPanelActions.REQUEST_ITEMS());
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 }));
+    };
+        
+
index c251bdf8f8724d3a6fd7969dce4aae0b011d1ab2..cacd49e68f8f8d5699d807bf1c2df286863719c6 100644 (file)
@@ -10,12 +10,14 @@ import { RootState } from '~/store/store';
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions';
 
 export const PROJECT_MOVE_FORM_NAME = 'projectMoveFormName';
 
 export const openMoveProjectDialog = (resource: { name: string, uuid: string }) =>
     (dispatch: Dispatch) => {
         dispatch<any>(resetPickerProjectTree());
+        dispatch<any>(initProjectsTreePicker(PROJECT_MOVE_FORM_NAME));
         dispatch(initialize(PROJECT_MOVE_FORM_NAME, resource));
         dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_MOVE_FORM_NAME, data: {} }));
     };
diff --git a/src/store/tree-picker/picker-id.tsx b/src/store/tree-picker/picker-id.tsx
new file mode 100644 (file)
index 0000000..3907ba8
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+
+export interface PickerIdProp {
+    pickerId: string;
+}
+
+export const pickerId =
+    (id: string) =>
+        <P extends PickerIdProp>(Component: React.ComponentType<P>) =>
+            (props: P) =>
+                <Component {...props} pickerId={id} />;
+                
\ No newline at end of file
index f51dcac7ecb63eff4d9a47b501a64ed8c67ca78c..657d65b75f71aea7217bf488d51a76a85765ec1e 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from "~/common/unionize";
-import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId } from '~/models/tree';
+import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree } from '~/models/tree';
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
@@ -11,17 +11,18 @@ import { FilterBuilder } from '~/services/api/filter-builder';
 import { pipe, values } from 'lodash/fp';
 import { ResourceKind } from '~/models/resource';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
-import { CollectionDirectory, CollectionFile } from '~/models/collection-file';
 import { getTreePicker, TreePicker } from './tree-picker';
 import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
 import { OrderBuilder } from '~/services/api/order-builder';
 import { ProjectResource } from '~/models/project';
+import { mapTree } from '../../models/tree';
 
 export const treePickerActions = unionize({
     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
     LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
+    APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
     TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
-    ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
+    ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
     DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
     TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
     SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
@@ -60,7 +61,7 @@ export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Va
     )();
 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
     getAllNodes<Value>(pickerId, node => node.selected)(state);
-    
+
 export const initProjectsTreePicker = (pickerId: string) =>
     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
         const { home, shared, favorites } = getProjectsTreePickerIds(pickerId);
@@ -135,19 +136,16 @@ export const loadCollection = (id: string, pickerId: string) =>
             const node = getNode(id)(picker);
             if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
 
-                const files = await services.collectionService.files(node.value.portableDataHash);
-                const data = getNodeDescendants('')(files).map(node => node.value);
-
-                dispatch<any>(receiveTreePickerData<CollectionDirectory | CollectionFile>({
-                    id,
-                    pickerId,
-                    data,
-                    extractNodeData: value => ({
-                        id: value.id,
-                        status: TreeNodeStatus.LOADED,
-                        value,
-                    }),
-                }));
+                const filesTree = await services.collectionService.files(node.value.portableDataHash);
+
+                dispatch(
+                    treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
+                        id,
+                        pickerId,
+                        subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
+                    }));
+
+                dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
             }
         }
     };
@@ -177,13 +175,13 @@ export const loadUserProject = (pickerId: string, includeCollections = false, in
         }
     };
 
-
+export const SHARED_PROJECT_ID = 'Shared with me';
 export const initSharedProject = (pickerId: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(receiveTreePickerData({
             id: '',
             pickerId,
-            data: [{ uuid: 'Shared with me', name: 'Shared with me' }],
+            data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
             extractNodeData: value => ({
                 id: value.uuid,
                 status: TreeNodeStatus.INITIAL,
@@ -192,12 +190,13 @@ export const initSharedProject = (pickerId: string) =>
         }));
     };
 
+export const FAVORITES_PROJECT_ID = 'Favorites';
 export const initFavoritesProject = (pickerId: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(receiveTreePickerData({
             id: '',
             pickerId,
-            data: [{ uuid: 'Favorites', name: 'Favorites' }],
+            data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
             extractNodeData: value => ({
                 id: value.uuid,
                 status: TreeNodeStatus.INITIAL,
index 846e445633328256d66b3e4e07e32beb4411d689..fb9bc50c7fbc8202399812cf26241065616a3045 100644 (file)
@@ -7,29 +7,50 @@ import { TreePicker } from "./tree-picker";
 import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
 import { compose } from "redux";
 import { activateNode, getNode, toggleNodeCollapse, toggleNodeSelection } from '~/models/tree';
+import { pipe } from 'lodash/fp';
+import { appendSubtree } from '~/models/tree';
 
 export const treePickerReducer = (state: TreePicker = {}, action: TreePickerAction) =>
     treePickerActions.match(action, {
         LOAD_TREE_PICKER_NODE: ({ id, pickerId }) =>
             updateOrCreatePicker(state, pickerId, setNodeStatus(id)(TreeNodeStatus.PENDING)),
+
         LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes, pickerId }) =>
             updateOrCreatePicker(state, pickerId, compose(receiveNodes(nodes)(id), setNodeStatus(id)(TreeNodeStatus.LOADED))),
+
+        APPEND_TREE_PICKER_NODE_SUBTREE: ({ id, subtree, pickerId}) =>
+            updateOrCreatePicker(state, pickerId, compose(appendSubtree(id, subtree), setNodeStatus(id)(TreeNodeStatus.LOADED))),
+
         TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id, pickerId }) =>
             updateOrCreatePicker(state, pickerId, toggleNodeCollapse(id)),
-        ACTIVATE_TREE_PICKER_NODE: ({ id, pickerId }) =>
-            updateOrCreatePicker(state, pickerId, activateNode(id)),
+
+        ACTIVATE_TREE_PICKER_NODE: ({ id, pickerId, relatedTreePickers = [] }) =>
+            pipe(
+                () => relatedTreePickers.reduce(
+                    (state, relatedPickerId) => updateOrCreatePicker(state, relatedPickerId, deactivateNode),
+                    state
+                ),
+                state => updateOrCreatePicker(state, pickerId, activateNode(id))
+            )(),
+
         DEACTIVATE_TREE_PICKER_NODE: ({ pickerId }) =>
             updateOrCreatePicker(state, pickerId, deactivateNode),
+
         TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId }) =>
             updateOrCreatePicker(state, pickerId, toggleNodeSelection(id)),
+
         SELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
             updateOrCreatePicker(state, pickerId, selectNodes(id)),
+
         DESELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
             updateOrCreatePicker(state, pickerId, deselectNodes(id)),
+
         RESET_TREE_PICKER: ({ pickerId }) =>
             updateOrCreatePicker(state, pickerId, createTree),
+
         EXPAND_TREE_PICKER_NODES: ({ pickerId, ids }) =>
             updateOrCreatePicker(state, pickerId, expandNode(...ids)),
+
         default: () => state
     });
 
index a33f78d12d5c922005eb70db8c08f5ee243d5d60..9d26fad2ff86a7659b90d1dfdd21b2e679576208 100644 (file)
@@ -15,6 +15,7 @@ import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
 import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
 import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 export const collectionActionSet: ContextMenuActionSet = [[
     {
@@ -62,7 +63,7 @@ export const collectionActionSet: ContextMenuActionSet = [[
         icon: DetailsIcon,
         name: "View details",
         execute: dispatch => {
-            dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+            dispatch<any>(toggleDetailsPanel());
         }
     },
     // {
index c398a0a2c0a904f2e017720fbd12cfeaf7a29185..7730b1453812f730aab765254275298ab14e5bda 100644 (file)
@@ -15,6 +15,7 @@ import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
 import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
 import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
 import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 export const collectionResourceActionSet: ContextMenuActionSet = [[
     {
@@ -63,7 +64,7 @@ export const collectionResourceActionSet: ContextMenuActionSet = [[
         icon: DetailsIcon,
         name: "View details",
         execute: dispatch => {
-            dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+            dispatch<any>(toggleDetailsPanel());
         }
     },
     {
index 5db50dd50d85a86f61cdc94d9b05c0b65415c2da..2d152543caa248eb9e5b19e1edc35993aab396a4 100644 (file)
@@ -19,6 +19,7 @@ import { detailsPanelActions } from '~/store/details-panel/details-panel-action'
 import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
 import { openProcessInputDialog } from "~/store/processes/process-input-actions";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 export const processActionSet: ContextMenuActionSet = [[
     {
@@ -96,7 +97,7 @@ export const processActionSet: ContextMenuActionSet = [[
         icon: DetailsIcon,
         name: "View details",
         execute: dispatch => {
-            dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+            dispatch<any>(toggleDetailsPanel());
         }
     },
     // {
index 4a0a83b91d69130d9ccc0a274fd323aca9b4140c..8cab9bfd5171b39f1171def4376cfa2e9dd15df5 100644 (file)
@@ -10,8 +10,9 @@ import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-acti
 import { openMoveProcessDialog } from '~/store/processes/process-move-actions';
 import { openProcessUpdateDialog } from "~/store/processes/process-update-actions";
 import { openCopyProcessDialog } from '~/store/processes/process-copy-actions';
-import { detailsPanelActions } from '~/store/details-panel/details-panel-action';
 import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
+import { openRemoveProcessDialog } from "~/store/processes/processes-actions";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 export const processResourceActionSet: ContextMenuActionSet = [[
     {
@@ -54,14 +55,14 @@ export const processResourceActionSet: ContextMenuActionSet = [[
         icon: DetailsIcon,
         name: "View details",
         execute: dispatch => {
-            dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+            dispatch<any>(toggleDetailsPanel());
+        }
+    },
+    {
+        name: "Remove",
+        icon: RemoveIcon,
+        execute: (dispatch, resource) => {
+            dispatch<any>(openRemoveProcessDialog(resource.uuid));
         }
     }
-    // {
-    //     icon: RemoveIcon,
-    //     name: "Remove",
-    //     execute: (dispatch, resource) => {
-    //         // add code
-    //     }
-    // }
 ]];
index aa82c7fa2864de5174a2f1779a8c00ba06b127d2..9b8ced5663037596646b1d1598ebd8eb6bc74d80 100644 (file)
@@ -16,6 +16,7 @@ import { detailsPanelActions } from '~/store/details-panel/details-panel-action'
 import { ShareIcon } from '~/components/icon/icon';
 import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions";
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 export const projectActionSet: ContextMenuActionSet = [[
     {
@@ -71,7 +72,7 @@ export const projectActionSet: ContextMenuActionSet = [[
         icon: DetailsIcon,
         name: "View details",
         execute: dispatch => {
-            dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+            dispatch<any>(toggleDetailsPanel());
         }
     },
     {
index 1f91d7e54361258ca45048284bf2033a51d78e45..cefef345f0a0df2b842a764a0ae37ce0b1a5d25e 100644 (file)
@@ -7,13 +7,14 @@ import { DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RestoreFromTrashIcon }
 import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
 import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 export const trashedCollectionActionSet: ContextMenuActionSet = [[
     {
         icon: DetailsIcon,
         name: "View details",
         execute: dispatch => {
-            dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+            dispatch<any>(toggleDetailsPanel());
         }
     },
     {
index 5e5ccefcd37fd6e9df165fa2ea4d7d4ec0c240d2..fe434b6c731aef10539945b662cb36bec2c8b9dd 100644 (file)
@@ -10,7 +10,6 @@ import { ArvadosTheme } from '~/common/custom-theme';
 import * as classnames from "classnames";
 import { connect } from 'react-redux';
 import { RootState } from '~/store/store';
-import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
 import { CloseIcon } from '~/components/icon/icon';
 import { EmptyResource } from '~/models/empty';
 import { Dispatch } from "redux";
@@ -24,11 +23,11 @@ import { DetailsResource } from "~/models/details";
 import { getResource } from '~/store/resources/resources';
 import { ResourceData } from "~/store/resources-data/resources-data-reducer";
 import { getResourceData } from "~/store/resources-data/resources-data";
+import { toggleDetailsPanel, SLIDE_TIMEOUT } from '~/store/details-panel/details-panel-action';
 
 type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer';
 
 const DRAWER_WIDTH = 320;
-const SLIDE_TIMEOUT = 500;
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         background: theme.palette.background.paper,
@@ -84,7 +83,7 @@ const mapStateToProps = ({ detailsPanel, resources, resourcesData }: RootState)
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     onCloseDrawer: () => {
-        dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
+        dispatch<any>(toggleDetailsPanel());
     }
 });
 
index 18affbacfd0698e6ea3f5eef3b071f6eede3827f..91c5e027ba61cb9a68deb9f4b8f214145d76561b 100644 (file)
@@ -3,7 +3,10 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { ProjectIcon } from '~/components/icon/icon';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import { openProjectPropertiesDialog } from '~/store/details-panel/details-panel-action';
+import { ProjectIcon, RenameIcon } from '~/components/icon/icon';
 import { ProjectResource } from '~/models/project';
 import { formatDate } from '~/common/formatters';
 import { ResourceKind } from '~/models/resource';
@@ -11,32 +14,74 @@ import { resourceLabel } from '~/common/labels';
 import { DetailsData } from "./details-data";
 import { DetailsAttribute } from "~/components/details-attribute/details-attribute";
 import { RichTextEditorLink } from '~/components/rich-text-editor-link/rich-text-editor-link';
+import { withStyles, StyleRulesCallback, Chip, WithStyles } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
 
 export class ProjectDetails extends DetailsData<ProjectResource> {
-
     getIcon(className?: string) {
         return <ProjectIcon className={className} />;
     }
 
     getDetails() {
-        return <div>
-            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
-            {/* Missing attr */}
-            <DetailsAttribute label='Size' value='---' />
-            <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 */}
-            {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
-            <DetailsAttribute label='Description'>
-                {this.item.description ?
-                    <RichTextEditorLink
-                        title={`Description of ${this.item.name}`}
-                        content={this.item.description}
-                        label='Show full description' />
-                    : '---'
-                }
-            </DetailsAttribute>
-        </div>;
+        return <ProjectDetailsComponent project={this.item} />;
+    }
+}
+
+type CssRules = 'tag' | 'editIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    tag: {
+        marginRight: theme.spacing.unit,
+        marginBottom: theme.spacing.unit
+    },
+    editIcon: {
+        fontSize: '1.125rem',
+        cursor: 'pointer'
     }
+});
+
+
+interface ProjectDetailsComponentDataProps {
+    project: ProjectResource;
+}
+
+interface ProjectDetailsComponentActionProps {
+    onClick: () => void;
 }
+
+const mapDispatchToProps = ({ onClick: openProjectPropertiesDialog });
+
+type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDetailsComponentActionProps & WithStyles<CssRules>;
+
+const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
+    withStyles(styles)(
+        ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
+            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
+                {/* Missing attr */}
+                <DetailsAttribute label='Size' value='---' />
+                <DetailsAttribute label='Owner' value={project.ownerUuid} lowercaseValue={true} />
+                <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
+                <DetailsAttribute label='Created at' value={formatDate(project.createdAt)} />
+                {/* Missing attr */}
+                {/*<DetailsAttribute label='File size' value='1.4 GB' />*/}
+                <DetailsAttribute label='Description'>
+                    {project.description ?
+                        <RichTextEditorLink
+                            title={`Description of ${project.name}`}
+                            content={project.description}
+                            label='Show full description' />
+                        : '---'
+                    }
+                </DetailsAttribute>
+                <DetailsAttribute label='Properties'>
+                    <div onClick={onClick}>
+                        <RenameIcon className={classes.editIcon} />
+                    </div>
+                </DetailsAttribute>
+                {
+                    Object.keys(project.properties).map(k => {
+                        return <Chip key={k} className={classes.tag} label={`${k}: ${project.properties[k]}`} />;
+                    })
+                }
+        </div>
+));
\ No newline at end of file
index 7c335a358c9048cff8af1b136143252a009aad3b..095c2b9ca97cec2943d183094c94ffb09f61651e 100644 (file)
@@ -3,24 +3,29 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
+import { memoize } from "lodash/fp";
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { CollectionNameField, CollectionDescriptionField, CollectionProjectPickerField } from '~/views-components/form-fields/collection-form-fields';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { InjectedFormProps } from 'redux-form';
 import { CollectionPartialCopyFormData } from '~/store/collections/collection-partial-copy-actions';
+import { PickerIdProp } from "~/store/tree-picker/picker-id";
 
 type DialogCollectionPartialCopyProps = WithDialogProps<string> & InjectedFormProps<CollectionPartialCopyFormData>;
 
-export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyProps) =>
+export const DialogCollectionPartialCopy = (props: DialogCollectionPartialCopyProps & PickerIdProp) =>
     <FormDialog
         dialogTitle='Create a collection'
-        formFields={CollectionPartialCopyFields}
+        formFields={CollectionPartialCopyFields(props.pickerId)}
         submitLabel='Create a collection'
         {...props}
     />;
 
-export const CollectionPartialCopyFields = () => <div>
-    <CollectionNameField />
-    <CollectionDescriptionField />
-    <CollectionProjectPickerField />
-</div>;
+export const CollectionPartialCopyFields = memoize(
+    (pickerId: string) =>
+        () =>
+            <div>
+                <CollectionNameField />
+                <CollectionDescriptionField />
+                <CollectionProjectPickerField {...{ pickerId }} />
+            </div>);
index 415541595c564ff1d3b672062852af92aaf25461..de8a321cf695183ef17b435887469c183bce12f2 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
+import { memoize } from 'lodash/fp';
 import { InjectedFormProps, Field } from 'redux-form';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
@@ -10,25 +11,29 @@ import { ProjectTreePickerField } from '~/views-components/project-tree-picker/p
 import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from '~/validators/validators';
 import { TextField } from "~/components/text-field/text-field";
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { PickerIdProp } from '~/store/tree-picker/picker-id';
 
 type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
 
-export const DialogCopy = (props: CopyFormDialogProps) =>
+export const DialogCopy = (props: CopyFormDialogProps & PickerIdProp) =>
     <FormDialog
         dialogTitle='Make a copy'
-        formFields={CopyDialogFields}
+        formFields={CopyDialogFields(props.pickerId)}
         submitLabel='Copy'
         {...props}
     />;
 
-const CopyDialogFields = () => <span>
-    <Field
-        name='name'
-        component={TextField}
-        validate={COPY_NAME_VALIDATION}
-        label="Enter a new name for the copy" />
-    <Field
-        name="ownerUuid"
-        component={ProjectTreePickerField}
-        validate={COPY_FILE_VALIDATION} />
-</span>;
+const CopyDialogFields = memoize((pickerId: string) =>
+    () =>
+        <span>
+            <Field
+                name='name'
+                component={TextField}
+                validate={COPY_NAME_VALIDATION}
+                label="Enter a new name for the copy" />
+            <Field
+                name="ownerUuid"
+                component={ProjectTreePickerField}
+                validate={COPY_FILE_VALIDATION} 
+                pickerId={pickerId}/>
+        </span>);
index 41309fdff6952762ed9ae9d9c2922f53dc3e5082..3c8f7ebf537f4ed755ba2f380e9251a3a4f4d768 100644 (file)
@@ -9,6 +9,7 @@ import { COLLECTION_COPY_FORM_NAME } from '~/store/collections/collection-copy-a
 import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
 import { copyCollection } from '~/store/workbench/workbench-actions';
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { pickerId } from '~/store/tree-picker/picker-id';
 
 export const CopyCollectionDialog = compose(
     withDialog(COLLECTION_COPY_FORM_NAME),
@@ -17,5 +18,6 @@ export const CopyCollectionDialog = compose(
         onSubmit: (data, dispatch) => {
             dispatch(copyCollection(data));
         }
-    })
+    }),
+    pickerId(COLLECTION_COPY_FORM_NAME),
 )(DialogCopy);
\ No newline at end of file
index 4ec17c65da870a95b4a0b5255652c66b141179e7..89d38f83388dcb712ebfe3b2bd8b961801925114 100644 (file)
@@ -9,6 +9,7 @@ import { PROCESS_COPY_FORM_NAME } from '~/store/processes/process-copy-actions';
 import { DialogCopy } from "~/views-components/dialog-copy/dialog-copy";
 import { copyProcess } from '~/store/workbench/workbench-actions';
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { pickerId } from "~/store/tree-picker/picker-id";
 
 export const CopyProcessDialog = compose(
     withDialog(PROCESS_COPY_FORM_NAME),
@@ -17,5 +18,6 @@ export const CopyProcessDialog = compose(
         onSubmit: (data, dispatch) => {
             dispatch(copyProcess(data));
         }
-    })
+    }),
+    pickerId(PROCESS_COPY_FORM_NAME),
 )(DialogCopy);
\ No newline at end of file
index fcdd999393ba7e765ad283110dd3a5ab1b7b7be3..b817b6a0fe435c80f510a7bc61eaa2063c3d3ea8 100644 (file)
@@ -9,6 +9,7 @@ import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
 import { COLLECTION_MOVE_FORM_NAME } from '~/store/collections/collection-move-actions';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { moveCollection } from '~/store/workbench/workbench-actions';
+import { pickerId } from '~/store/tree-picker/picker-id';
 
 export const MoveCollectionDialog = compose(
     withDialog(COLLECTION_MOVE_FORM_NAME),
@@ -17,5 +18,6 @@ export const MoveCollectionDialog = compose(
         onSubmit: (data, dispatch) => {
             dispatch(moveCollection(data));
         }
-    })
+    }),
+    pickerId(COLLECTION_MOVE_FORM_NAME),
 )(DialogMoveTo);
index baea34bc71c63ba2a1c7634a34101f0974f0d669..ce854ef251a04eb0e49b7a65a54f8f1ea4f3cfc2 100644 (file)
@@ -9,6 +9,7 @@ import { PROCESS_MOVE_FORM_NAME } from '~/store/processes/process-move-actions';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
 import { moveProcess } from '~/store/workbench/workbench-actions';
+import { pickerId } from '~/store/tree-picker/picker-id';
 
 export const MoveProcessDialog = compose(
     withDialog(PROCESS_MOVE_FORM_NAME),
@@ -17,5 +18,6 @@ export const MoveProcessDialog = compose(
         onSubmit: (data, dispatch) => {
             dispatch(moveProcess(data));
         }
-    })
+    }),
+    pickerId(PROCESS_MOVE_FORM_NAME),
 )(DialogMoveTo);
\ No newline at end of file
index c1fbb76ebc5e973d70962ecea56c6f1359aaffa1..03e474b1778074668cfedcc41a44c4f3e9335d09 100644 (file)
@@ -9,6 +9,7 @@ import { PROJECT_MOVE_FORM_NAME } from '~/store/projects/project-move-actions';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { DialogMoveTo } from '~/views-components/dialog-move/dialog-move-to';
 import { moveProject } from '~/store/workbench/workbench-actions';
+import { pickerId } from '~/store/tree-picker/picker-id';
 
 export const MoveProjectDialog = compose(
     withDialog(PROJECT_MOVE_FORM_NAME),
@@ -17,6 +18,7 @@ export const MoveProjectDialog = compose(
         onSubmit: (data, dispatch) => {
             dispatch(moveProject(data));
         }
-    })
+    }),
+    pickerId(PROJECT_MOVE_FORM_NAME),
 )(DialogMoveTo);
 
index 16f8275e8fb57821c8ed2f4b8065c4b8c6c43eb9..37d928be1c18c0348c06a0e9133ab4b6d7d806d7 100644 (file)
@@ -7,6 +7,7 @@ import { reduxForm } from 'redux-form';
 import { withDialog, } from '~/store/dialog/with-dialog';
 import { CollectionPartialCopyFormData, copyCollectionPartial, COLLECTION_PARTIAL_COPY_FORM_NAME } from '~/store/collections/collection-partial-copy-actions';
 import { DialogCollectionPartialCopy } from "~/views-components/dialog-copy/dialog-collection-partial-copy";
+import { pickerId } from "~/store/tree-picker/picker-id";
 
 
 export const PartialCopyCollectionDialog = compose(
@@ -16,4 +17,6 @@ export const PartialCopyCollectionDialog = compose(
         onSubmit: (data, dispatch) => {
             dispatch(copyCollectionPartial(data));
         }
-    }))(DialogCollectionPartialCopy);
\ No newline at end of file
+    }),
+    pickerId(COLLECTION_PARTIAL_COPY_FORM_NAME),
+)(DialogCollectionPartialCopy);
\ No newline at end of file
index 425b9e462a5439b47f3eb82a26fbe4eefe5481e0..c962522f3cf8292853bd743d08cfe92f55c18d7d 100644 (file)
@@ -3,24 +3,28 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
+import { memoize } from 'lodash/fp';
 import { InjectedFormProps, Field } from 'redux-form';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { FormDialog } from '~/components/form-dialog/form-dialog';
 import { ProjectTreePickerField } from '~/views-components/project-tree-picker/project-tree-picker';
 import { MOVE_TO_VALIDATION } from '~/validators/validators';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
+import { PickerIdProp } from "~/store/tree-picker/picker-id";
 
-export const DialogMoveTo = (props: WithDialogProps<string> & InjectedFormProps<MoveToFormDialogData>) =>
+export const DialogMoveTo = (props: WithDialogProps<string> & InjectedFormProps<MoveToFormDialogData> & PickerIdProp) =>
     <FormDialog
         dialogTitle='Move to'
-        formFields={MoveToDialogFields}
+        formFields={MoveToDialogFields(props.pickerId)}
         submitLabel='Move'
         {...props}
     />;
 
-const MoveToDialogFields = () =>
-    <Field
-        name="ownerUuid"
-        component={ProjectTreePickerField}
-        validate={MOVE_TO_VALIDATION} />;
+const MoveToDialogFields = memoize(
+    (pickerId: string) => () =>
+        <Field
+            name="ownerUuid"
+            pickerId={pickerId}
+            component={ProjectTreePickerField}
+            validate={MOVE_TO_VALIDATION} />);
 
index be5f93df6a52b401177f0fa12f27594e46abe2c4..2d2a7c80880b0fef31e428c46150fd504d506f95 100644 (file)
@@ -6,7 +6,8 @@ import * as React from "react";
 import { Field, WrappedFieldProps } from "redux-form";
 import { TextField } from "~/components/text-field/text-field";
 import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
-import { ProjectTreePicker } from "~/views-components/project-tree-picker/project-tree-picker";
+import { ProjectTreePicker, ProjectTreePickerField } from "~/views-components/project-tree-picker/project-tree-picker";
+import { PickerIdProp } from '../../store/tree-picker/picker-id';
 
 export const CollectionNameField = () =>
     <Field
@@ -23,13 +24,9 @@ export const CollectionDescriptionField = () =>
         validate={COLLECTION_DESCRIPTION_VALIDATION}
         label="Description - optional" />;
 
-export const CollectionProjectPickerField = () =>
+export const CollectionProjectPickerField = (props: PickerIdProp) =>
     <Field
         name="projectUuid"
-        component={ProjectPicker}
+        pickerId={props.pickerId}
+        component={ProjectTreePickerField}
         validate={COLLECTION_PROJECT_VALIDATION} />;
-
-const ProjectPicker = (props: WrappedFieldProps) =>
-    <div style={{ height: '144px', display: 'flex', flexDirection: 'column' }}>
-        <ProjectTreePicker onChange={projectUuid => props.input.onChange(projectUuid)} />
-    </div>;
index 6fb419e36710aa187e0f28a79b46fba82ed5f0d7..b0478377ba1a8f05490a662966a0cd7755d4bfc8 100644 (file)
@@ -10,6 +10,7 @@ import { detailsPanelActions } from "~/store/details-panel/details-panel-action"
 import { connect } from 'react-redux';
 import { RootState } from '~/store/store';
 import { matchWorkflowRoute } from '~/routes/routes';
+import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 interface MainContentBarProps {
     onDetailsPanelToggle: () => void;
@@ -25,7 +26,7 @@ const isWorkflowPath = ({ router }: RootState) => {
 export const MainContentBar = connect((state: RootState) => ({
     buttonVisible: !isWorkflowPath(state)
 }), {
-        onDetailsPanelToggle: detailsPanelActions.TOGGLE_DETAILS_PANEL
+        onDetailsPanelToggle: toggleDetailsPanel
     })((props: MainContentBarProps) =>
         <Toolbar>
             <Grid container>
diff --git a/src/views-components/process-remove-dialog/process-remove-dialog.tsx b/src/views-components/process-remove-dialog/process-remove-dialog.tsx
new file mode 100644 (file)
index 0000000..b0db3f9
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { removeProcessPermanently, REMOVE_PROCESS_DIALOG } from '~/store/processes/processes-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(removeProcessPermanently(props.data.uuid));
+    }
+});
+
+export const RemoveProcessDialog = compose(
+    withDialog(REMOVE_PROCESS_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
diff --git a/src/views-components/project-properties-dialog/project-properties-dialog.tsx b/src/views-components/project-properties-dialog/project-properties-dialog.tsx
new file mode 100644 (file)
index 0000000..d165f98
--- /dev/null
@@ -0,0 +1,73 @@
+// 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 { RootState } from '~/store/store';
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
+import { ProjectResource } from '~/models/project';
+import { PROJECT_PROPERTIES_DIALOG_NAME, deleteProjectProperty } from '~/store/details-panel/details-panel-action';
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Chip, withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { ProjectPropertiesForm } from '~/views-components/project-properties-dialog/project-properties-form';
+import { getResource } from '~/store/resources/resources';
+
+type CssRules = 'tag';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    tag: {
+        marginRight: theme.spacing.unit,
+        marginBottom: theme.spacing.unit
+    }
+});
+
+interface ProjectPropertiesDialogDataProps {
+    project: ProjectResource;
+}
+
+interface ProjectPropertiesDialogActionProps {
+    handleDelete: (key: string) => void;
+}
+
+const mapStateToProps = ({ detailsPanel, resources }: RootState): ProjectPropertiesDialogDataProps => {
+    const project = getResource(detailsPanel.resourceUuid)(resources) as ProjectResource;
+    return { project };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionProps => ({
+    handleDelete: (key: string) => dispatch<any>(deleteProjectProperty(key))
+});
+
+type ProjectPropertiesDialogProps =  ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
+
+export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToProps)(
+    withStyles(styles)(
+    withDialog(PROJECT_PROPERTIES_DIALOG_NAME)(
+        ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) =>
+            <Dialog open={open}
+                onClose={closeDialog}
+                fullWidth
+                maxWidth='sm'>
+                <DialogTitle>Properties</DialogTitle>
+                <DialogContent>
+                    <ProjectPropertiesForm />
+                    {project && project.properties && 
+                        Object.keys(project.properties).map(k => {
+                            return <Chip key={k} className={classes.tag}
+                                onDelete={() => handleDelete(k)}
+                                label={`${k}: ${project.properties[k]}`} />;
+                        })
+                    }
+                </DialogContent>
+                <DialogActions>
+                    <Button
+                        variant='flat'
+                        color='primary'
+                        onClick={closeDialog}>
+                        Close
+                    </Button>
+                </DialogActions>
+            </Dialog>
+)));
\ No newline at end of file
diff --git a/src/views-components/project-properties-dialog/project-properties-form.tsx b/src/views-components/project-properties-dialog/project-properties-form.tsx
new file mode 100644 (file)
index 0000000..82ae040
--- /dev/null
@@ -0,0 +1,95 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field, reset } from 'redux-form';
+import { compose, Dispatch } from 'redux';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core';
+import { TagProperty } from '~/models/tag';
+import { TextField } from '~/components/text-field/text-field';
+import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators';
+import { PROJECT_PROPERTIES_FORM_NAME, createProjectProperty } from '~/store/details-panel/details-panel-action';
+
+type CssRules = 'root' | 'keyField' | 'valueField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+        display: 'flex'
+    },
+    keyField: {
+        width: '40%',
+        marginRight: theme.spacing.unit * 3
+    },
+    valueField: {
+        width: '40%',
+        marginRight: theme.spacing.unit * 3
+    },
+    buttonWrapper: {
+        paddingTop: '14px',
+        position: 'relative',
+    },
+    saveButton: {
+        boxShadow: 'none'
+    },
+    circularProgress: {
+        position: 'absolute',
+        top: -9,
+        bottom: 0,
+        left: 0,
+        right: 0,
+        margin: 'auto'
+    }
+});
+
+interface ProjectPropertiesFormDataProps {
+    submitting: boolean;
+    invalid: boolean;
+    pristine: boolean;
+}
+
+interface ProjectPropertiesFormActionProps {
+    handleSubmit: any;
+}
+
+type ProjectPropertiesFormProps = ProjectPropertiesFormDataProps & ProjectPropertiesFormActionProps & WithStyles<CssRules>;
+
+export const ProjectPropertiesForm = compose(
+    reduxForm({
+        form: PROJECT_PROPERTIES_FORM_NAME,
+        onSubmit: (data: TagProperty, dispatch: Dispatch) => {
+            dispatch<any>(createProjectProperty(data));
+            dispatch(reset(PROJECT_PROPERTIES_FORM_NAME));
+        }
+    }),
+    withStyles(styles))(
+        ({ classes, submitting, pristine, invalid, handleSubmit }: ProjectPropertiesFormProps) => 
+            <form onSubmit={handleSubmit} className={classes.root}>
+                <div className={classes.keyField}>
+                    <Field name="key"
+                        disabled={submitting}
+                        component={TextField}
+                        validate={TAG_KEY_VALIDATION}
+                        label="Key" />
+                </div>
+                <div className={classes.valueField}>
+                    <Field name="value"
+                        disabled={submitting}
+                        component={TextField}
+                        validate={TAG_VALUE_VALIDATION}
+                        label="Value" />
+                </div>
+                <div className={classes.buttonWrapper}>
+                    <Button type="submit" className={classes.saveButton}
+                        color="primary"
+                        size='small'
+                        disabled={invalid || submitting || pristine}
+                        variant="contained">
+                        ADD
+                    </Button>
+                    {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+                </div>
+            </form>
+        );
index a4e4c4062631e6881de807a19970ec9e8b6bd582..bae5d59f07dd10a699e9aaaf51f21817202f8106 100644 (file)
@@ -16,6 +16,9 @@ import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
 import { WrappedFieldProps } from 'redux-form';
 import { TreePickerId } from '~/models/tree';
+import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
+import { PickerIdProp } from '~/store/tree-picker/picker-id';
 
 type ProjectTreePickerProps = Pick<TreePickerProps<ProjectResource>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
 
@@ -87,17 +90,17 @@ const renderTreeItem = (item: TreeItem<ProjectResource>) =>
         isActive={item.active}
         hasMargin={true} />;
 
-export const ProjectTreePickerField = (props: WrappedFieldProps) =>
+export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
     <div style={{ height: '200px', display: 'flex', flexDirection: 'column' }}>
-        <ProjectTreePicker onChange={handleChange(props)} />
+        <ProjectsTreePicker
+            pickerId={props.pickerId}
+            toggleItemActive={handleChange(props)} />
         {props.meta.dirty && props.meta.error &&
             <Typography variant='caption' color='error'>
                 {props.meta.error}
             </Typography>}
     </div>;
 
-const handleChange = (props: WrappedFieldProps) => (value: string) =>
-    props.input.value === value
-        ? props.input.onChange('')
-        : props.input.onChange(value);
-
+const handleChange = (props: WrappedFieldProps) =>
+    (_: any, { id }: TreeItem<ProjectsTreePickerItem>) =>
+        props.input.onChange(id);
index 77f831fafd10432fc1ec86513b1f9fa23cbdb646..fafb05056ca712b86b841bb221da6258129ae080 100644 (file)
@@ -5,6 +5,7 @@
 import * as React from "react";
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
+import { isEqual } from 'lodash/fp';
 import { TreeItem, TreeItemStatus } from '~/components/tree/tree';
 import { ProjectResource } from "~/models/project";
 import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
@@ -29,6 +30,8 @@ export interface ProjectsTreePickerDataProps {
     includeFiles?: boolean;
     rootItemIcon: IconType;
     showSelection?: boolean;
+    relatedTreePickers?: string[];
+    disableActivation?: string[];
     loadRootItem: (item: TreeItem<ProjectsTreePickerRootItem>, pickerId: string, includeCollections?: boolean, inlcudeFiles?: boolean) => void;
 }
 
@@ -39,10 +42,16 @@ const mapStateToProps = (_: any, { rootItemIcon, showSelection }: ProjectsTreePi
     showSelection: isSelectionVisible(showSelection),
 });
 
-const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeFiles, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
+const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeFiles, relatedTreePickers, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
     onContextMenu: () => { return; },
     toggleItemActive: (event, item, pickerId) => {
-        dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: item.id, pickerId }));
+        
+        const { disableActivation = [] } = props;
+        if(disableActivation.some(isEqual(item.id))){
+            return;
+        }
+
+        dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: item.id, pickerId, relatedTreePickers }));
         if (props.toggleItemActive) {
             props.toggleItemActive(event, item, pickerId);
         }
@@ -65,7 +74,7 @@ const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollectio
     },
     toggleItemSelection: (event, item, pickerId) => {
         dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: item.id, pickerId }));
-        if(props.toggleItemSelection){
+        if (props.toggleItemSelection) {
             props.toggleItemSelection(event, item, pickerId);
         }
     },
index ba29be82c275cd37573f0056e33f9462f9bf86f1..ae98cf00896a3978c137a5593fdadc977c7978e0 100644 (file)
@@ -3,10 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { values, memoize, pipe, pick } from 'lodash/fp';
 import { HomeTreePicker } from '~/views-components/projects-tree-picker/home-tree-picker';
 import { SharedTreePicker } from '~/views-components/projects-tree-picker/shared-tree-picker';
 import { FavoritesTreePicker } from '~/views-components/projects-tree-picker/favorites-tree-picker';
-import { getProjectsTreePickerIds } from '~/store/tree-picker/tree-picker-actions';
+import { getProjectsTreePickerIds, SHARED_PROJECT_ID, FAVORITES_PROJECT_ID } from '~/store/tree-picker/tree-picker-actions';
 import { TreeItem } from '~/components/tree/tree';
 import { ProjectsTreePickerItem } from './generic-projects-tree-picker';
 
@@ -21,9 +22,18 @@ export interface ProjectsTreePickerProps {
 
 export const ProjectsTreePicker = ({ pickerId, ...props }: ProjectsTreePickerProps) => {
     const { home, shared, favorites } = getProjectsTreePickerIds(pickerId);
+    const relatedTreePickers = getRelatedTreePickers(pickerId);
+    const p = {
+        ...props,
+        relatedTreePickers,
+        disableActivation
+    };
     return <div>
-        <HomeTreePicker pickerId={home} {...props} />
-        <SharedTreePicker pickerId={shared} {...props} />
-        <FavoritesTreePicker pickerId={favorites} {...props} />
+        <HomeTreePicker pickerId={home} {...p} />
+        <SharedTreePicker pickerId={shared} {...p} />
+        <FavoritesTreePicker pickerId={favorites} {...p} />
     </div>;
 };
+
+const getRelatedTreePickers = memoize(pipe(getProjectsTreePickerIds, values));
+const disableActivation = [SHARED_PROJECT_ID, FAVORITES_PROJECT_ID];
index 92b2b5b7622d772743bc8e83bf3ee503df79fbf0..8d1fb6700dc308c6709d2374299706f51e75adda 100644 (file)
@@ -35,6 +35,7 @@ import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-colle
 import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
 import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
 import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
+import { RemoveProcessDialog } from '~/views-components/process-remove-dialog/process-remove-dialog';
 import { MainContentBar } from '~/views-components/main-content-bar/main-content-bar';
 import { Grid } from '@material-ui/core';
 import { TrashPanel } from "~/views/trash-panel/trash-panel";
@@ -46,6 +47,7 @@ import { SearchResultsPanel } from '~/views/search-results-panel/search-results-
 import { SharingDialog } from '~/views-components/sharing-dialog/sharing-dialog';
 import { AdvancedTabDialog } from '~/views-components/advanced-tab-dialog/advanced-tab-dialog';
 import { ProcessInputDialog } from '~/views-components/process-input-dialog/process-input-dialog';
+import { ProjectPropertiesDialog } from '~/views-components/project-properties-dialog/project-properties-dialog';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -140,6 +142,8 @@ export const WorkbenchPanel =
             <PartialCopyCollectionDialog />
             <ProcessCommandDialog />
             <ProcessInputDialog />
+            <ProjectPropertiesDialog />
+            <RemoveProcessDialog />
             <RenameFileDialog />
             <RichTextEditorDialog />
             <SharingDialog />