Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 7 Aug 2018 12:20:35 +0000 (14:20 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 7 Aug 2018 12:20:35 +0000 (14:20 +0200)
Feature #13952

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

25 files changed:
src/components/confirmation-dialog/confirmation-dialog.tsx [new file with mode: 0644]
src/components/rename-dialog/rename-dialog.tsx [new file with mode: 0644]
src/components/text-field/text-field.tsx [new file with mode: 0644]
src/components/tree/tree.tsx
src/store/dialog/dialog-reducer.ts
src/store/dialog/with-dialog.ts
src/store/store.ts
src/store/tree-picker/tree-picker-actions.ts [new file with mode: 0644]
src/store/tree-picker/tree-picker-reducer.test.ts [new file with mode: 0644]
src/store/tree-picker/tree-picker-reducer.ts [new file with mode: 0644]
src/store/tree-picker/tree-picker.ts [new file with mode: 0644]
src/validators/create-project/create-project-validator.tsx
src/views-components/context-menu/action-sets/collection-files-action-set.ts
src/views-components/context-menu/action-sets/collection-files-item-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/dialog-create/dialog-collection-create-selected.tsx [new file with mode: 0644]
src/views-components/dialog-create/dialog-collection-create.tsx
src/views-components/dialog-create/dialog-project-create.tsx
src/views-components/file-remove-dialog/file-remove-dialog.ts [new file with mode: 0644]
src/views-components/file-remove-dialog/multiple-files-remove-dialog.ts [new file with mode: 0644]
src/views-components/project-tree-picker/project-tree-picker.tsx [new file with mode: 0644]
src/views-components/rename-dialog/rename-dialog.tsx [deleted file]
src/views-components/rename-file-dialog/rename-file-dialog.tsx [new file with mode: 0644]
src/views-components/tree-picker/tree-picker.ts [new file with mode: 0644]
src/views/workbench/workbench.tsx

diff --git a/src/components/confirmation-dialog/confirmation-dialog.tsx b/src/components/confirmation-dialog/confirmation-dialog.tsx
new file mode 100644 (file)
index 0000000..6e87416
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { defaultTo, property } from 'lodash';
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, DialogContentText, CircularProgress } from "@material-ui/core";
+import { WithDialogProps } from "../../store/dialog/with-dialog";
+import { TextField } from "../text-field/text-field";
+
+export interface ConfirmationDialogDataProps {
+    title: string;
+    text: string;
+    cancelButtonLabel?: string;
+    confirmButtonLabel?: string;
+}
+
+export interface ConfirmationDialogProps {
+    onConfirm: () => void;
+}
+
+export const ConfirmationDialog = (props: ConfirmationDialogProps & WithDialogProps<ConfirmationDialogDataProps>) =>
+    <Dialog open={props.open}>
+        <DialogTitle>{props.data.title}</DialogTitle>
+        <DialogContent>
+            <DialogContentText>
+                {props.data.text}
+            </DialogContentText>
+        </DialogContent>
+        <DialogActions>
+            <Button
+                variant='flat'
+                color='primary'
+                onClick={props.closeDialog}>
+                {props.data.cancelButtonLabel || 'Cancel'}
+            </Button>
+            <Button
+                variant='contained'
+                color='primary'
+                type='submit'
+                onClick={props.onConfirm}>
+                {props.data.confirmButtonLabel || 'Ok'}
+            </Button>
+        </DialogActions>
+    </Dialog>;
diff --git a/src/components/rename-dialog/rename-dialog.tsx b/src/components/rename-dialog/rename-dialog.tsx
new file mode 100644 (file)
index 0000000..f70d857
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { InjectedFormProps, Field } from "redux-form";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, DialogContentText, CircularProgress } from "@material-ui/core";
+import { WithDialogProps } from "../../store/dialog/with-dialog";
+import { TextField } from "../text-field/text-field";
+
+export const RenameDialog = (props: WithDialogProps<string> & InjectedFormProps<{ name: string }>) =>
+    <form>
+        <Dialog open={props.open}>
+            <DialogTitle>{`Rename`}</DialogTitle>
+            <DialogContent>
+                <DialogContentText>
+                    {`Please, enter a new name for ${props.data}`}
+                </DialogContentText>
+                <Field
+                    name='name'
+                    component={TextField}
+                />
+            </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} />
+                        : 'Ok'}
+                </Button>
+            </DialogActions>
+        </Dialog>
+    </form>;
diff --git a/src/components/text-field/text-field.tsx b/src/components/text-field/text-field.tsx
new file mode 100644 (file)
index 0000000..f7564f6
--- /dev/null
@@ -0,0 +1,28 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WrappedFieldProps } from 'redux-form';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { TextField as MaterialTextField, StyleRulesCallback, WithStyles, withStyles } from '../../../node_modules/@material-ui/core';
+
+type CssRules = 'textField';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    textField: {
+        marginBottom: theme.spacing.unit * 3
+    },
+});
+
+export const TextField = withStyles(styles)((props: WrappedFieldProps & WithStyles<CssRules> & { label?: string }) =>
+    <MaterialTextField
+        helperText={props.meta.touched && props.meta.error}
+        className={props.classes.textField}
+        label={props.label}
+        disabled={props.meta.submitting}
+        error={props.meta.touched && !!props.meta.error}
+        autoComplete='off'
+        fullWidth={true}
+        {...props.input}
+    />);
\ No newline at end of file
index ea15b6b1bd8df4a8b830bbdc6aec48f6c56e7ea2..669b70c0e855c1d551bc4a4dedfeacbe35000814 100644 (file)
@@ -80,7 +80,7 @@ export interface TreeItem<T> {
     items?: Array<TreeItem<T>>;
 }
 
-interface TreeProps<T> {
+export interface TreeProps<T> {
     items?: Array<TreeItem<T>>;
     render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
     toggleItemOpen: (id: string, status: TreeItemStatus) => void;
index e49f65debc70c37dbfaeaa3f960cbf60442a96fe..34d38fdf4ea6e2d39dc28814a2f2a1dcee5e9733 100644 (file)
@@ -8,7 +8,7 @@ export type DialogState = Record<string, Dialog>;
 
 export interface Dialog {
     open: boolean;
-    data?: any;
+    data: any;
 }
 
 export const dialogReducer = (state: DialogState = {}, action: DialogAction) =>
@@ -16,7 +16,7 @@ export const dialogReducer = (state: DialogState = {}, action: DialogAction) =>
         OPEN_DIALOG: ({ id, data }) => ({ ...state, [id]: { open: true, data } }),
         CLOSE_DIALOG: ({ id }) => ({ 
             ...state, 
-            [id]: state[id] ? { ...state[id], open: false } : { open: false } }),
+            [id]: state[id] ? { ...state[id], open: false } : { open: false, data: {} } }),
         default: () => state,
     });
 
index 42ae73e4a46ddabfe3959bd6b7db99ce725969de..d53a79d6efe19f5ccde771f05d1590bd77199f0e 100644 (file)
@@ -8,25 +8,27 @@ import { DialogState } from './dialog-reducer';
 import { Dispatch } from 'redux';
 import { dialogActions } from './dialog-actions';
 
-export type WithDialog<T> = {
+export type WithDialogStateProps<T> = {
     open: boolean;
-    data?: T;
+    data: T;
 };
 
-export type WithDialogActions = {
+export type WithDialogDispatchProps = {
     closeDialog: () => void;
 };
 
+export type WithDialogProps<T> = WithDialogStateProps<T> & WithDialogDispatchProps;
+
 export const withDialog = (id: string) =>
-    <T>(component: React.ComponentType<WithDialog<T> & WithDialogActions>) =>
+    <T, P>(component: React.ComponentType<WithDialogProps<T> & P>) =>
         connect(mapStateToProps(id), mapDispatchToProps(id))(component);
 
-export const mapStateToProps = (id: string) => <T>(state: { dialog: DialogState }): WithDialog<T> => {
+export const mapStateToProps = (id: string) => <T>(state: { dialog: DialogState }): WithDialogStateProps<T> => {
     const dialog = state.dialog[id];
-    return dialog ? dialog : { open: false };
+    return dialog ? dialog : { open: false, data: {} };
 };
 
-export const mapDispatchToProps = (id: string) => (dispatch: Dispatch): WithDialogActions => ({
+export const mapDispatchToProps = (id: string) => (dispatch: Dispatch): WithDialogDispatchProps => ({
     closeDialog: () => {
         dispatch(dialogActions.CLOSE_DIALOG({ id }));
     }
index aeb6a09cd388af3e559a41142590adcfc31234c1..0002a6d2fa00c7956fc83c8cf367c35d3649bca4 100644 (file)
@@ -27,6 +27,8 @@ import { CollectionPanelState, collectionPanelReducer } from './collection-panel
 import { DialogState, dialogReducer } from './dialog/dialog-reducer';
 import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
 import { ServiceRepository } from "../services/services";
+import { treePickerReducer } from './tree-picker/tree-picker-reducer';
+import { TreePicker } from './tree-picker/tree-picker';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -47,6 +49,7 @@ export interface RootState {
     snackbar: SnackbarState;
     collectionPanelFiles: CollectionPanelFilesState;
     dialog: DialogState;
+    treePicker: TreePicker;
 }
 
 export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
@@ -66,7 +69,8 @@ export function configureStore(history: History, services: ServiceRepository): R
         favorites: favoritesReducer,
         snackbar: snackbarReducer,
         collectionPanelFiles: collectionPanelFilesReducer,
-        dialog: dialogReducer
+        dialog: dialogReducer,
+        treePicker: treePickerReducer,
     });
 
     const projectPanelMiddleware = dataExplorerMiddleware(
diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
new file mode 100644 (file)
index 0000000..772d89d
--- /dev/null
@@ -0,0 +1,19 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { TreeNode } from "../../models/tree";
+import { TreePickerNode } from "./tree-picker";
+
+export const treePickerActions = unionize({
+    LOAD_TREE_PICKER_NODE: ofType<{ id: string }>(),
+    LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreePickerNode> }>(),
+    TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string }>(),
+    TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ id: string }>()
+}, {
+        tag: 'type',
+        value: 'payload'
+    });
+
+export type TreePickerAction = UnionOf<typeof treePickerActions>;
diff --git a/src/store/tree-picker/tree-picker-reducer.test.ts b/src/store/tree-picker/tree-picker-reducer.test.ts
new file mode 100644 (file)
index 0000000..443da37
--- /dev/null
@@ -0,0 +1,101 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { createTree, getNodeValue, getNodeChildren } from "../../models/tree";
+import { TreePickerNode, createTreePickerNode } from "./tree-picker";
+import { treePickerReducer } from "./tree-picker-reducer";
+import { treePickerActions } from "./tree-picker-actions";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+
+describe('TreePickerReducer', () => {
+    it('LOAD_TREE_PICKER_NODE - initial state', () => {
+        const tree = createTree<TreePickerNode>();
+        const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1' }));
+        expect(newTree).toEqual(tree);
+    });
+
+    it('LOAD_TREE_PICKER_NODE', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            status: TreeItemStatus.PENDING
+        });
+    });
+
+    it('LOAD_TREE_PICKER_NODE_SUCCESS - initial state', () => {
+        const tree = createTree<TreePickerNode>();
+        const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
+        const newTree = treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [subNode] }));
+        expect(getNodeChildren('')(newTree)).toEqual(['1.1']);
+    });
+
+    it('LOAD_TREE_PICKER_NODE_SUCCESS', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const subNode = createTreePickerNode({ id: '1.1', value: '1.1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode] })));
+        expect(getNodeChildren('1')(newTree)).toEqual(['1.1']);
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            status: TreeItemStatus.LOADED
+        });
+    });
+
+    it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - collapsed', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            collapsed: true
+        });
+    });
+
+    it('TOGGLE_TREE_PICKER_NODE_COLLAPSE - expanded', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            collapsed: false
+        });
+    });
+
+    it('TOGGLE_TREE_PICKER_NODE_SELECT - selected', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            selected: true
+        });
+    });
+
+    it('TOGGLE_TREE_PICKER_NODE_SELECT - not selected', () => {
+        const tree = createTree<TreePickerNode>();
+        const node = createTreePickerNode({ id: '1', value: '1' });
+        const [newTree] = [tree]
+            .map(tree => treePickerReducer(tree, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node] })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })))
+            .map(tree => treePickerReducer(tree, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id: '1' })));
+        expect(getNodeValue('1')(newTree)).toEqual({
+            ...createTreePickerNode({ id: '1', value: '1' }),
+            selected: false
+        });
+    });
+});
diff --git a/src/store/tree-picker/tree-picker-reducer.ts b/src/store/tree-picker/tree-picker-reducer.ts
new file mode 100644 (file)
index 0000000..d195a98
--- /dev/null
@@ -0,0 +1,53 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { createTree, setNodeValueWith, TreeNode, setNode, mapTree, mapTreeValues } from "../../models/tree";
+import { TreePicker, TreePickerNode } from "./tree-picker";
+import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+
+export const treePickerReducer = (state: TreePicker = createTree(), action: TreePickerAction) =>
+    treePickerActions.match(action, {
+        LOAD_TREE_PICKER_NODE: ({ id }) =>
+            setNodeValueWith(setPending)(id)(state),
+        LOAD_TREE_PICKER_NODE_SUCCESS: ({ id, nodes }) => {
+            const [newState] = [state]
+                .map(receiveNodes(nodes)(id))
+                .map(setNodeValueWith(setLoaded)(id));
+            return newState;
+        },
+        TOGGLE_TREE_PICKER_NODE_COLLAPSE: ({ id }) =>
+            setNodeValueWith(toggleCollapse)(id)(state),
+        TOGGLE_TREE_PICKER_NODE_SELECT: ({ id }) =>
+            mapTreeValues(toggleSelect(id))(state),
+        default: () => state
+    });
+
+const setPending = (value: TreePickerNode): TreePickerNode =>
+    ({ ...value, status: TreeItemStatus.PENDING });
+
+const setLoaded = (value: TreePickerNode): TreePickerNode =>
+    ({ ...value, status: TreeItemStatus.LOADED });
+
+const toggleCollapse = (value: TreePickerNode): TreePickerNode =>
+    ({ ...value, collapsed: !value.collapsed });
+
+const toggleSelect = (id: string) => (value: TreePickerNode): TreePickerNode =>
+    value.id === id
+        ? ({ ...value, selected: !value.selected })
+        : ({ ...value, selected: false });
+
+const receiveNodes = (nodes: Array<TreePickerNode>) => (parent: string) => (state: TreePicker) =>
+    nodes.reduce((tree, node) =>
+        setNode(
+            createTreeNode(parent)(node)
+        )(tree), state);
+
+const createTreeNode = (parent: string) => (node: TreePickerNode): TreeNode<TreePickerNode> => ({
+    children: [],
+    id: node.id,
+    parent,
+    value: node
+});
\ No newline at end of file
diff --git a/src/store/tree-picker/tree-picker.ts b/src/store/tree-picker/tree-picker.ts
new file mode 100644 (file)
index 0000000..ee45bec
--- /dev/null
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Tree } from "../../models/tree";
+import { TreeItemStatus } from "../../components/tree/tree";
+
+export type TreePicker = Tree<TreePickerNode>;
+
+export interface TreePickerNode {
+    id: string;
+    value: any;
+    selected: boolean;
+    collapsed: boolean;
+    status: TreeItemStatus;
+}
+
+export const createTreePickerNode = (data: {id: string, value: any}) => ({
+    ...data,
+    selected: false,
+    collapsed: true,
+    status: TreeItemStatus.INITIAL
+});
\ No newline at end of file
index 928efdd2205e439c82fbd936e0e476e13f046139..ddea8be97055a0c0f5ded7a3e48386c0a4794660 100644 (file)
@@ -7,3 +7,6 @@ import { maxLength } from '../max-length';
 
 export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
 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];
index 9396b9e472f2e287919370f821dbb94235ae6f2f..91fa2b00f6d8e3f7e9ca2d4d3bbd1a7ffd933966 100644 (file)
@@ -4,7 +4,8 @@
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { collectionPanelFilesAction } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
-import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
+import { openMultipleFilesRemoveDialog } from "../../file-remove-dialog/multiple-files-remove-dialog";
+import { createCollectionWithSelected } from "../../create-collection-dialog-with-selected/create-collection-dialog-with-selected";
 
 
 export const collectionFilesActionSet: ContextMenuActionSet = [[{
@@ -12,24 +13,24 @@ export const collectionFilesActionSet: ContextMenuActionSet = [[{
     execute: (dispatch) => {
         dispatch(collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES());
     }
-},{
+}, {
     name: "Unselect all",
     execute: (dispatch) => {
         dispatch(collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES());
     }
-},{
+}, {
     name: "Remove selected",
     execute: (dispatch, resource) => {
-        dispatch(openRemoveDialog('selected files'));
+        dispatch(openMultipleFilesRemoveDialog());
     }
-},{
+}, {
     name: "Download selected",
     execute: (dispatch, resource) => {
         return;
     }
-},{
+}, {
     name: "Create a new collection with selected",
-    execute: (dispatch, resource) => {
-        return;
+    execute: (dispatch) => {
+        dispatch<any>(createCollectionWithSelected());
     }
 }]];
index 0b623eca4c9fd24fc1c666cbfde0b01bc27f8e2f..e24108f4d88754fa30e22760e2d993cde0ae6b7f 100644 (file)
@@ -4,26 +4,26 @@
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { RenameIcon, DownloadIcon, RemoveIcon } from "../../../components/icon/icon";
-import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
-import { openRenameDialog } from "../../rename-dialog/rename-dialog";
+import { openRenameFileDialog } from "../../rename-file-dialog/rename-file-dialog";
+import { openFileRemoveDialog } from "../../file-remove-dialog/file-remove-dialog";
 
 
 export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
     name: "Rename",
     icon: RenameIcon,
     execute: (dispatch, resource) => {
-        dispatch(openRenameDialog('the item'));
+        dispatch<any>(openRenameFileDialog(resource.name));
     }
-},{
+}, {
     name: "Download",
     icon: DownloadIcon,
     execute: (dispatch, resource) => {
         return;
     }
-},{
+}, {
     name: "Remove",
     icon: RemoveIcon,
     execute: (dispatch, resource) => {
-        dispatch(openRemoveDialog('selected file'));
+        dispatch(openFileRemoveDialog(resource.uuid));
     }
 }]];
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..8a2efca
--- /dev/null
@@ -0,0 +1,29 @@
+// 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";
+import { loadProjectTreePickerProjects } from "../project-tree-picker/project-tree-picker";
+
+export const DIALOG_COLLECTION_CREATE_WITH_SELECTED = 'dialogCollectionCreateWithSelected';
+
+export const createCollectionWithSelected = () =>
+    (dispatch: Dispatch) => {
+        dispatch(reset(DIALOG_COLLECTION_CREATE_WITH_SELECTED));
+        dispatch<any>(loadProjectTreePickerProjects(''));
+        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);
+        }
+    }));
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..5069db9
--- /dev/null
@@ -0,0 +1,62 @@
+// 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, FormHelperText } 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/create-project/create-project-validator";
+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>;
index 804aae11f72f3e85f69eab01660b121d8a789b5f..0686904ab3403aba2a60ae1f5599a129acbeb77d 100644 (file)
@@ -5,11 +5,8 @@
 import * as React from 'react';
 import { reduxForm, Field } from 'redux-form';
 import { compose } from 'redux';
-import TextField from '@material-ui/core/TextField';
-import Dialog from '@material-ui/core/Dialog';
-import DialogActions from '@material-ui/core/DialogActions';
-import DialogContent from '@material-ui/core/DialogContent';
-import DialogTitle from '@material-ui/core/DialogTitle';
+import { TextField } from '../../components/text-field/text-field';
+import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
 import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
@@ -56,83 +53,61 @@ interface DialogCollectionCreateProps {
     files: UploadFile[];
 }
 
-interface TextFieldProps {
-    label: string;
-    floatinglabeltext: string;
-    className?: string;
-    input?: string;
-    meta?: any;
-}
-
 export const DialogCollectionCreate = compose(
     connect((state: RootState) => ({
         files: state.collections.uploader
     })),
     reduxForm({ form: 'collectionCreateDialog' }),
     withStyles(styles))(
-    class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & DispatchProp & WithStyles<CssRules>> {
-        render() {
-            const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine, files } = this.props;
-            const busy = submitting || files.reduce(
-                (prev, curr) => prev + (curr.loaded > 0 && curr.loaded < curr.total ? 1 : 0), 0
-            ) > 0;
-            return (
-                <Dialog
-                    open={open}
-                    onClose={handleClose}
-                    fullWidth={true}
-                    maxWidth='sm'
-                    disableBackdropClick={true}
-                    disableEscapeKeyDown={true}>
-                    <form onSubmit={handleSubmit((data: any) => onSubmit(data, files))}>
-                        <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
-                        <DialogContent className={classes.formContainer}>
-                            <Field name="name"
+        class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & DispatchProp & WithStyles<CssRules>> {
+            render() {
+                const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine, files } = this.props;
+                const busy = submitting || files.reduce(
+                    (prev, curr) => prev + (curr.loaded > 0 && curr.loaded < curr.total ? 1 : 0), 0
+                ) > 0;
+                return (
+                    <Dialog
+                        open={open}
+                        onClose={handleClose}
+                        fullWidth={true}
+                        maxWidth='sm'
+                        disableBackdropClick={true}
+                        disableEscapeKeyDown={true}>
+                        <form onSubmit={handleSubmit((data: any) => onSubmit(data, files))}>
+                            <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
+                            <DialogContent className={classes.formContainer}>
+                                <Field name="name"
                                     disabled={submitting}
-                                    component={this.renderTextField}
-                                    floatinglabeltext="Collection Name"
+                                    component={TextField}
                                     validate={COLLECTION_NAME_VALIDATION}
                                     className={classes.textField}
-                                    label="Collection Name"/>
-                            <Field name="description"
+                                    label="Collection Name" />
+                                <Field name="description"
                                     disabled={submitting}
-                                    component={this.renderTextField}
-                                    floatinglabeltext="Description - optional"
+                                    component={TextField}
                                     validate={COLLECTION_DESCRIPTION_VALIDATION}
                                     className={classes.textField}
-                                    label="Description - optional"/>
-                            <FileUpload
-                                files={files}
-                                disabled={busy}
-                                onDrop={files => this.props.dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files))}/>
-                        </DialogContent>
-                        <DialogActions className={classes.dialogActions}>
-                            <Button onClick={handleClose} className={classes.button} color="primary"
+                                    label="Description - optional" />
+                                <FileUpload
+                                    files={files}
+                                    disabled={busy}
+                                    onDrop={files => this.props.dispatch(collectionUploaderActions.SET_UPLOAD_FILES(files))} />
+                            </DialogContent>
+                            <DialogActions className={classes.dialogActions}>
+                                <Button onClick={handleClose} className={classes.button} color="primary"
                                     disabled={busy}>CANCEL</Button>
-                            <Button type="submit"
+                                <Button type="submit"
                                     className={classes.lastButton}
                                     color="primary"
                                     disabled={invalid || busy || pristine}
                                     variant="contained">
-                                CREATE A COLLECTION
+                                    CREATE A COLLECTION
                             </Button>
-                            {busy && <CircularProgress size={20} className={classes.createProgress}/>}
-                        </DialogActions>
-                    </form>
-                </Dialog>
-            );
+                                {busy && <CircularProgress size={20} className={classes.createProgress} />}
+                            </DialogActions>
+                        </form>
+                    </Dialog>
+                );
+            }
         }
-
-        renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
-            <TextField
-                helperText={touched && error}
-                label={label}
-                className={this.props.classes.textField}
-                error={touched && !!error}
-                autoComplete='off'
-                {...input}
-                {...custom}
-            />
-        )
-    }
-);
+    );
index acfe3973b2d0dfdb69826f586fb568795c0f65a0..35fdca9f25b5a0db5466f831e6e4606492db4087 100644 (file)
@@ -5,11 +5,8 @@
 import * as React from 'react';
 import { reduxForm, Field } from 'redux-form';
 import { compose } from 'redux';
-import TextField from '@material-ui/core/TextField';
-import Dialog from '@material-ui/core/Dialog';
-import DialogActions from '@material-ui/core/DialogActions';
-import DialogContent from '@material-ui/core/DialogContent';
-import DialogTitle from '@material-ui/core/DialogTitle';
+import { TextField } from '../../components/text-field/text-field';
+import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
 import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
@@ -58,14 +55,6 @@ interface DialogProjectProps {
     pristine: boolean;
 }
 
-interface TextFieldProps {
-    label: string;
-    floatinglabeltext: string;
-    className?: string;
-    input?: string;
-    meta?: any;
-}
-
 export const DialogProjectCreate = compose(
     reduxForm({ form: 'projectCreateDialog' }),
     withStyles(styles))(
@@ -85,14 +74,12 @@ export const DialogProjectCreate = compose(
                                 project</DialogTitle>
                             <DialogContent className={classes.formContainer}>
                                 <Field name="name"
-                                       component={this.renderTextField}
-                                       floatinglabeltext="Project Name"
+                                       component={TextField}
                                        validate={PROJECT_NAME_VALIDATION}
                                        className={classes.textField}
                                        label="Project Name"/>
                                 <Field name="description"
-                                       component={this.renderTextField}
-                                       floatinglabeltext="Description - optional"
+                                       component={TextField}
                                        validate={PROJECT_DESCRIPTION_VALIDATION}
                                        className={classes.textField}
                                        label="Description - optional"/>
@@ -114,17 +101,5 @@ export const DialogProjectCreate = compose(
                 </Dialog>
             );
         }
-
-        renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
-            <TextField
-                helperText={touched && error}
-                label={label}
-                className={this.props.classes.textField}
-                error={touched && !!error}
-                autoComplete='off'
-                {...input}
-                {...custom}
-            />
-        )
     }
 );
diff --git a/src/views-components/file-remove-dialog/file-remove-dialog.ts b/src/views-components/file-remove-dialog/file-remove-dialog.ts
new file mode 100644 (file)
index 0000000..3678e53
--- /dev/null
@@ -0,0 +1,38 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "../../components/confirmation-dialog/confirmation-dialog";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+
+const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    onConfirm: () => {
+        // TODO: dispatch action that removes single file
+        dispatch(dialogActions.CLOSE_DIALOG({ id: FILE_REMOVE_DIALOG }));
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing file...', hideDuration: 2000 }));
+        setTimeout(() => {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'File removed.', hideDuration: 2000 }));
+        }, 1000);
+    }
+});
+
+export const openFileRemoveDialog = (fileId: string) =>
+    dialogActions.OPEN_DIALOG({
+        id: FILE_REMOVE_DIALOG,
+        data: {
+            title: 'Removing file',
+            text: 'Are you sure you want to remove this file?',
+            confirmButtonLabel: 'Remove',
+            fileId
+        }
+    });
+
+export const [FileRemoveDialog] = [ConfirmationDialog]
+    .map(withDialog(FILE_REMOVE_DIALOG))
+    .map(connect(undefined, mapDispatchToProps));
\ No newline at end of file
diff --git a/src/views-components/file-remove-dialog/multiple-files-remove-dialog.ts b/src/views-components/file-remove-dialog/multiple-files-remove-dialog.ts
new file mode 100644 (file)
index 0000000..8810e23
--- /dev/null
@@ -0,0 +1,37 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "../../components/confirmation-dialog/confirmation-dialog";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+
+const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    onConfirm: () => {
+        // TODO: dispatch action that removes multiple files
+        dispatch(dialogActions.CLOSE_DIALOG({ id: MULTIPLE_FILES_REMOVE_DIALOG }));
+        dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Removing files...', hideDuration: 2000}));
+        setTimeout(() => {
+            dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Files removed.', hideDuration: 2000}));
+        }, 1000);
+    }
+});
+
+export const openMultipleFilesRemoveDialog = () =>
+    dialogActions.OPEN_DIALOG({
+        id: MULTIPLE_FILES_REMOVE_DIALOG,
+        data: {
+            title: 'Removing files',
+            text: 'Are you sure you want to remove selected files?',
+            confirmButtonLabel: 'Remove'
+        }
+    });
+
+export const [MultipleFilesRemoveDialog] = [ConfirmationDialog]
+    .map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG))
+    .map(connect(undefined, mapDispatchToProps));
\ No newline at end of file
diff --git a/src/views-components/project-tree-picker/project-tree-picker.tsx b/src/views-components/project-tree-picker/project-tree-picker.tsx
new file mode 100644 (file)
index 0000000..1c343a2
--- /dev/null
@@ -0,0 +1,75 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { Typography } from "@material-ui/core";
+import { TreePicker } from "../tree-picker/tree-picker";
+import { TreeProps, TreeItem, TreeItemStatus } from "../../components/tree/tree";
+import { ProjectResource } from "../../models/project";
+import { treePickerActions } from "../../store/tree-picker/tree-picker-actions";
+import { ListItemTextIcon } from "../../components/list-item-text-icon/list-item-text-icon";
+import { ProjectIcon } from "../../components/icon/icon";
+import { createTreePickerNode } from "../../store/tree-picker/tree-picker";
+import { RootState } from "../../store/store";
+import { ServiceRepository } from "../../services/services";
+import { FilterBuilder } from "../../common/api/filter-builder";
+
+type ProjectTreePickerProps = Pick<TreeProps<ProjectResource>, 'toggleItemActive' | 'toggleItemOpen'>;
+
+const mapDispatchToProps = (dispatch: Dispatch, props: {onChange: (projectUuid: string) => void}): ProjectTreePickerProps => ({
+    toggleItemActive: id => {
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ id }));
+        props.onChange(id);
+    },
+    toggleItemOpen: (id, status) => {
+        status === TreeItemStatus.INITIAL
+            ? dispatch<any>(loadProjectTreePickerProjects(id))
+            : dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+    }
+});
+
+export const ProjectTreePicker = connect(undefined, mapDispatchToProps)((props: ProjectTreePickerProps) =>
+    <div 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>
+    </div>);
+
+// TODO: move action creator to store directory
+export const loadProjectTreePickerProjects = (id: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id }));
+
+        const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
+
+        const filters = FilterBuilder
+            .create()
+            .addEqual('ownerUuid', ownerUuid);
+
+        const { items } = await services.projectService.list({ filters });
+
+        dispatch<any>(receiveProjectTreePickerData(id, items));
+    };
+
+const renderTreeItem = (item: TreeItem<ProjectResource>) =>
+    <ListItemTextIcon
+        icon={ProjectIcon}
+        name={item.data.name}
+        isActive={item.active}
+        hasMargin={true} />;
+
+// TODO: move action creator to store directory
+const receiveProjectTreePickerData = (id: string, projects: ProjectResource[]) =>
+    (dispatch: Dispatch) => {
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+            id,
+            nodes: projects.map(project => createTreePickerNode({ id: project.uuid, value: project }))
+        }));
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id }));
+    };
\ No newline at end of file
diff --git a/src/views-components/rename-dialog/rename-dialog.tsx b/src/views-components/rename-dialog/rename-dialog.tsx
deleted file mode 100644 (file)
index 8fe38cb..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from "react";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography } from "@material-ui/core";
-import { withDialog } from "../../store/dialog/with-dialog";
-import { dialogActions } from "../../store/dialog/dialog-actions";
-
-export const RENAME_DIALOG = 'nameDialog';
-
-export const RenameDialog = withDialog(RENAME_DIALOG)(
-    (props) =>
-        <Dialog open={props.open}>
-            <DialogTitle>{`Rename`}</DialogTitle>
-            <DialogContent>
-                <Typography variant='body1' gutterBottom>
-                    {`Please, enter a new name for ${props.data}`}
-                </Typography>
-                <TextField fullWidth={true} placeholder='New name' />
-            </DialogContent>
-            <DialogActions>
-                <Button
-                    variant='flat'
-                    color='primary'
-                    onClick={props.closeDialog}>
-                    Cancel
-                </Button>
-                <Button variant='raised' color='primary'>
-                    Ok
-                </Button>
-            </DialogActions>
-        </Dialog>
-);
-
-export const openRenameDialog = (originalName: string, ) =>
-    dialogActions.OPEN_DIALOG({ id: RENAME_DIALOG, data: originalName });
diff --git a/src/views-components/rename-file-dialog/rename-file-dialog.tsx b/src/views-components/rename-file-dialog/rename-file-dialog.tsx
new file mode 100644 (file)
index 0000000..6c880cc
--- /dev/null
@@ -0,0 +1,28 @@
+// 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 { RenameDialog } from "../../components/rename-dialog/rename-dialog";
+
+export const RENAME_FILE_DIALOG = 'renameFileDialog';
+
+export const openRenameFileDialog = (originalName: string, ) =>
+    (dispatch: Dispatch) => {
+        dispatch(reset(RENAME_FILE_DIALOG));
+        dispatch(dialogActions.OPEN_DIALOG({ id: RENAME_FILE_DIALOG, data: originalName }));
+    };
+
+export const [RenameFileDialog] = [RenameDialog]
+    .map(withDialog(RENAME_FILE_DIALOG))
+    .map(reduxForm({
+        form: RENAME_FILE_DIALOG,
+        onSubmit: (data, dispatch) => {
+            dispatch(startSubmit(RENAME_FILE_DIALOG));
+            // TODO: call collection file renaming action here
+            setTimeout(() => dispatch(stopSubmit(RENAME_FILE_DIALOG, { name: 'Invalid name' })), 2000);
+        }
+    }));
diff --git a/src/views-components/tree-picker/tree-picker.ts b/src/views-components/tree-picker/tree-picker.ts
new file mode 100644 (file)
index 0000000..3e0fc6e
--- /dev/null
@@ -0,0 +1,47 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Tree, TreeProps, TreeItem } from "../../components/tree/tree";
+import { RootState } from "../../store/store";
+import { TreePicker as TTreePicker, TreePickerNode, createTreePickerNode } from "../../store/tree-picker/tree-picker";
+import { getNodeValue, getNodeChildren } from "../../models/tree";
+
+const memoizedMapStateToProps = () => {
+    let prevState: TTreePicker;
+    let prevTree: Array<TreeItem<any>>;
+
+    return (state: RootState): Pick<TreeProps<any>, 'items'> => {
+        if (prevState !== state.treePicker) {
+            prevState = state.treePicker;
+            prevTree = getNodeChildren('')(state.treePicker)
+                .map(treePickerToTreeItems(state.treePicker));
+        }
+        return {
+            items: prevTree
+        };
+    };
+};
+
+const mapDispatchToProps = (): Pick<TreeProps<any>, 'onContextMenu'> => ({
+    onContextMenu: () => { return; },
+});
+
+export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
+
+const treePickerToTreeItems = (tree: TTreePicker) =>
+    (id: string): TreeItem<any> => {
+        const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ id: '', value: 'InvalidNode' });
+        const items = getNodeChildren(node.id)(tree)
+            .map(treePickerToTreeItems(tree));
+        return {
+            active: node.selected,
+            data: node.value,
+            id: node.id,
+            items: items.length > 0 ? items : undefined,
+            open: !node.collapsed,
+            status: node.status
+        };
+    };
+
index 69d809869995d3c3344a043b0901076ec9633add..a8552eef824053bf7c197bc85d18758045a77bdd 100644 (file)
@@ -40,10 +40,12 @@ import { CreateCollectionDialog } from '../../views-components/create-collection
 import { CollectionPanel } from '../collection-panel/collection-panel';
 import { loadCollection, loadCollectionTags } from '../../store/collection-panel/collection-panel-action';
 import { getCollectionUrl } from '../../models/collection';
-import { RemoveDialog } from '../../views-components/remove-dialog/remove-dialog';
-import { RenameDialog } from '../../views-components/rename-dialog/rename-dialog';
 import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
 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';
 
 const DRAWER_WITDH = 240;
 const APP_BAR_HEIGHT = 100;
@@ -233,8 +235,10 @@ export const Workbench = withStyles(styles)(
                         <Snackbar />
                         <CreateProjectDialog />
                         <CreateCollectionDialog />
-                        <RemoveDialog />
-                        <RenameDialog />
+                        <RenameFileDialog />
+                        <DialogCollectionCreateWithSelectedFile />
+                        <FileRemoveDialog />
+                        <MultipleFilesRemoveDialog />
                         <UpdateCollectionDialog />
                         <CurrentTokenDialog
                             currentToken={this.props.currentToken}