[ARVADOS-WORKBENCH2] created: 1.2.0-646-gf2f7301

Git user git at public.curoverse.com
Sun Oct 14 12:44:53 EDT 2018


        at  f2f7301dab111f2561332c8cc9e7c06df958ea66 (commit)


commit f2f7301dab111f2561332c8cc9e7c06df958ea66
Author: Michal Klobukowski <michal.klobukowski at contractors.roche.com>
Date:   Sun Oct 14 18:44:45 2018 +0200

    Create file array input
    
    Feature #14232
    
    Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski at contractors.roche.com>

diff --git a/src/models/tree.ts b/src/models/tree.ts
index cce27b1..f0b53b4 100644
--- a/src/models/tree.ts
+++ b/src/models/tree.ts
@@ -139,7 +139,30 @@ export const toggleNodeSelection = (id: string) => <T>(tree: Tree<T>) => {
 
 };
 
-export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & {parent?: string}): TreeNode<T> => ({
+export const selectNode = (id: string) => <T>(tree: Tree<T>) => {
+    const node = getNode(id)(tree);
+    return node && node.selected
+        ? tree
+        : toggleNodeSelection(id)(tree);
+};
+
+export const selectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+    const ids = typeof id === 'string' ? [id] : id;
+    return ids.reduce((tree, id) => selectNode(id)(tree), tree);
+};
+export const deselectNode = (id: string) => <T>(tree: Tree<T>) => {
+    const node = getNode(id)(tree);
+    return node && node.selected
+        ? toggleNodeSelection(id)(tree)
+        : tree;
+};
+
+export const deselectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+    const ids = typeof id === 'string' ? [id] : id;
+    return ids.reduce((tree, id) => deselectNode(id)(tree), tree);
+};
+
+export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & { parent?: string }): TreeNode<T> => ({
     children: [],
     active: false,
     selected: false,
diff --git a/src/store/tree-picker/tree-picker-actions.ts b/src/store/tree-picker/tree-picker-actions.ts
index 9ca6184..22a31c1 100644
--- a/src/store/tree-picker/tree-picker-actions.ts
+++ b/src/store/tree-picker/tree-picker-actions.ts
@@ -8,11 +8,11 @@ import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { FilterBuilder } from '~/services/api/filter-builder';
-import { pipe } from 'lodash/fp';
+import { pipe, map, values, mapValues } 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 } from './tree-picker';
+import { getTreePicker, TreePicker } from './tree-picker';
 import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
 
 export const treePickerActions = unionize({
@@ -22,6 +22,8 @@ export const treePickerActions = unionize({
     ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: 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 }>(),
+    DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
     EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
     RESET_TREE_PICKER: ofType<{ pickerId: string }>()
 });
@@ -33,6 +35,27 @@ export const getProjectsTreePickerIds = (pickerId: string) => ({
     shared: `${pickerId}_shared`,
     favorites: `${pickerId}_favorites`,
 });
+
+export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) => {
+    return pipe(
+        () => values(getProjectsTreePickerIds(pickerId)),
+
+        ids => ids
+            .map(id => getTreePicker<Value>(id)(state)),
+
+        trees => trees
+            .map(getNodeDescendants(''))
+            .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
+
+        allNodes => allNodes
+            .reduce((map, node) => node.selected
+                ? map.set(node.id, node)
+                : map, new Map<string, TreeNode<Value>>())
+            .values(),
+
+        uniqueNodes => Array.from(uniqueNodes),
+    )();
+};
 export const initProjectsTreePicker = (pickerId: string) =>
     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
         const { home, shared, favorites } = getProjectsTreePickerIds(pickerId);
diff --git a/src/store/tree-picker/tree-picker-reducer.ts b/src/store/tree-picker/tree-picker-reducer.ts
index 2df567e..846e445 100644
--- a/src/store/tree-picker/tree-picker-reducer.ts
+++ b/src/store/tree-picker/tree-picker-reducer.ts
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode } from '~/models/tree';
+import { createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus, expandNode, deactivateNode, deselectNode, selectNode, selectNodes, deselectNodes } from '~/models/tree';
 import { TreePicker } from "./tree-picker";
 import { treePickerActions, TreePickerAction } from "./tree-picker-actions";
 import { compose } from "redux";
@@ -22,6 +22,10 @@ export const treePickerReducer = (state: TreePicker = {}, action: TreePickerActi
             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 }) =>
diff --git a/src/views/run-process-panel/inputs/file-array-input.tsx b/src/views/run-process-panel/inputs/file-array-input.tsx
new file mode 100644
index 0000000..39884a6
--- /dev/null
+++ b/src/views/run-process-panel/inputs/file-array-input.tsx
@@ -0,0 +1,282 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+    isRequiredInput,
+    FileArrayCommandInputParameter,
+    File,
+    CWLType
+} from '~/models/workflow';
+import { Field } from 'redux-form';
+import { ERROR_MESSAGE } from '~/validators/require';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divider, Grid, WithStyles, Typography } from '@material-ui/core';
+import { GenericInputProps, GenericInput } from './generic-input';
+import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
+import { connect, DispatchProp } from 'react-redux';
+import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds } from '~/store/tree-picker/tree-picker-actions';
+import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
+import { CollectionFile, CollectionFileType } from '~/models/collection-file';
+import { createSelector, createStructuredSelector } from 'reselect';
+import { ChipsInput } from '~/components/chips-input/chips-input';
+import { identity, values, noop } from 'lodash';
+import { InputProps } from '@material-ui/core/Input';
+import { TreePicker } from '~/store/tree-picker/tree-picker';
+import { RootState } from '~/store/store';
+import { Chips } from '~/components/chips/chips';
+import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles';
+
+export interface FileArrayInputProps {
+    input: FileArrayCommandInputParameter;
+}
+export const FileArrayInput = ({ input }: FileArrayInputProps) =>
+    <Field
+        name={input.id}
+        commandInput={input}
+        component={FileArrayInputComponent}
+        parse={parseFiles}
+        format={formatFiles}
+        validate={validationSelector(input)} />;
+
+const parseFiles = (files: CollectionFile[]) =>
+    files.length > 0
+        ? files.map(parse)
+        : undefined;
+
+const parse = (file: CollectionFile): File => ({
+    class: CWLType.FILE,
+    basename: file.name,
+    location: `keep:${file.id}`,
+    path: file.path,
+});
+
+const formatFiles = (files: File[] = []) => files.map(format);
+
+const format = (file: File): CollectionFile => ({
+    id: file.location
+        ? file.location.replace('keep:', '')
+        : '',
+    name: file.basename || '',
+    path: file.path || '',
+    size: 0,
+    type: CollectionFileType.FILE,
+    url: '',
+});
+
+const validationSelector = createSelector(
+    isRequiredInput,
+    isRequired => isRequired
+        ? [required]
+        : undefined
+);
+
+const required = (value?: File[]) =>
+    value && value.length > 0
+        ? undefined
+        : ERROR_MESSAGE;
+interface FileArrayInputComponentState {
+    open: boolean;
+    files: CollectionFile[];
+}
+
+interface FileArrayInputComponentProps {
+    treePickerState: TreePicker;
+}
+
+const treePickerSelector = (state: RootState) => state.treePicker;
+
+const mapStateToProps = createStructuredSelector({
+    treePickerState: treePickerSelector,
+});
+
+const FileArrayInputComponent = connect(mapStateToProps)(
+    class FileArrayInputComponent extends React.Component<FileArrayInputComponentProps & GenericInputProps & DispatchProp, FileArrayInputComponentState> {
+        state: FileArrayInputComponentState = {
+            open: false,
+            files: [],
+        };
+
+        fileRefreshTimeout = -1;
+
+        componentDidMount() {
+            this.props.dispatch<any>(
+                initProjectsTreePicker(this.props.commandInput.id));
+        }
+
+        render() {
+            return <>
+                <this.input />
+                <this.dialog />
+            </>;
+        }
+
+        openDialog = () => {
+            this.setFilesFromProps(this.props.input.value);
+            this.setState({ open: true });
+        }
+
+
+        closeDialog = () => {
+            this.setState({ open: false });
+        }
+
+        submit = () => {
+            this.closeDialog();
+            this.props.input.onChange(this.state.files);
+        }
+
+        setFiles = (files: CollectionFile[]) => {
+
+            const deletedFiles = this.state.files
+                .reduce((deletedFiles, file) =>
+                    files.some(({ id }) => id === file.id)
+                        ? deletedFiles
+                        : [...deletedFiles, file]
+                    , []);
+
+            this.setState({ files });
+
+            const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
+            deletedFiles.forEach(({ id }) => {
+                ids.forEach(pickerId => {
+                    this.props.dispatch(
+                        treePickerActions.DESELECT_TREE_PICKER_NODE({
+                            pickerId, id,
+                        })
+                    );
+                });
+            });
+
+        }
+
+        setFilesFromProps = (files: CollectionFile[]) => {
+
+            const addedFiles = files
+                .reduce((addedFiles, file) =>
+                    this.state.files.some(({ id }) => id === file.id)
+                        ? addedFiles
+                        : [...addedFiles, file]
+                    , []);
+
+            this.setState({ files });
+
+            const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
+            addedFiles.forEach(({ id }) => {
+                ids.forEach(pickerId => {
+                    this.props.dispatch(
+                        treePickerActions.SELECT_TREE_PICKER_NODE({
+                            pickerId, id,
+                        })
+                    );
+                });
+            });
+
+        }
+
+        refreshFiles = () => {
+            clearTimeout(this.fileRefreshTimeout);
+            this.fileRefreshTimeout = setTimeout(this.setSelectedFiles);
+        }
+
+        setSelectedFiles = () => {
+            const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
+            const initialFiles: CollectionFile[] = [];
+            const files = nodes
+                .reduce((files, { value }) =>
+                    'type' in value && value.type === CollectionFileType.FILE
+                        ? files.concat(value)
+                        : files, initialFiles);
+
+            this.setFiles(files);
+        }
+        input = () =>
+            <GenericInput
+                component={this.chipsInput}
+                {...this.props} />
+
+        chipsInput = () =>
+            <ChipsInput
+                value={this.props.input.value}
+                onChange={noop}
+                createNewValue={identity}
+                getLabel={(file: CollectionFile) => file.name}
+                inputComponent={this.textInput} />
+
+        textInput = (props: InputProps) =>
+            <Input
+                {...props}
+                error={this.props.meta.touched && !!this.props.meta.error}
+                readOnly
+                onClick={this.openDialog}
+                onKeyPress={this.openDialog}
+                onBlur={this.props.input.onBlur} />
+
+        dialog = () =>
+            <Dialog
+                open={this.state.open}
+                onClose={this.closeDialog}
+                fullWidth
+                maxWidth='md' >
+                <DialogTitle>Choose files</DialogTitle>
+                <DialogContent>
+                    <this.dialogContent />
+                </DialogContent>
+                <DialogActions>
+                    <Button onClick={this.closeDialog}>Cancel</Button>
+                    <Button
+                        variant='contained'
+                        color='primary'
+                        onClick={this.submit}>Ok</Button>
+                </DialogActions>
+            </Dialog>
+
+        dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
+            root: {
+                display: 'flex',
+                flexDirection: 'column',
+                height: `${spacing.unit * 8}vh`,
+            },
+            tree: {
+                flex: 3,
+                overflow: 'auto',
+            },
+            divider: {
+                margin: `${spacing.unit}px 0`,
+            },
+            chips: {
+                flex: 1,
+                overflow: 'auto',
+                padding: `${spacing.unit}px 0`,
+                overflowX: 'hidden',
+            },
+        })
+
+        dialogContent = withStyles(this.dialogContentStyles)(
+            ({ classes }: WithStyles<DialogContentCssRules>) =>
+                <div className={classes.root}>
+                    <div className={classes.tree}>
+                        <ProjectsTreePicker
+                            pickerId={this.props.commandInput.id}
+                            includeCollections
+                            includeFiles
+                            showSelection
+                            toggleItemSelection={this.refreshFiles} />
+                    </div>
+                    <Divider />
+                    <div className={classes.chips}>
+                        <Typography variant='subheading'>Selected files ({this.state.files.length}):</Typography>
+                        <Chips
+                            values={this.state.files}
+                            onChange={this.setFiles}
+                            getLabel={(file: CollectionFile) => file.name} />
+                    </div>
+                </div>
+        );
+
+    });
+
+type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';
+
+
+
diff --git a/src/views/run-process-panel/inputs/file-input.tsx b/src/views/run-process-panel/inputs/file-input.tsx
index 838aa51..f5d3d93 100644
--- a/src/views/run-process-panel/inputs/file-input.tsx
+++ b/src/views/run-process-panel/inputs/file-input.tsx
@@ -19,7 +19,6 @@ import { initProjectsTreePicker } from '~/store/tree-picker/tree-picker-actions'
 import { TreeItem } from '~/components/tree/tree';
 import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
 import { CollectionFile, CollectionFileType } from '~/models/collection-file';
-import { getFileFullPath } from '~/services/collection-service/collection-service-files-response';
 
 export interface FileInputProps {
     input: FileCommandInputParameter;
diff --git a/src/views/run-process-panel/run-process-inputs-form.tsx b/src/views/run-process-panel/run-process-inputs-form.tsx
index 912be0d..f8c6c1b 100644
--- a/src/views/run-process-panel/run-process-inputs-form.tsx
+++ b/src/views/run-process-panel/run-process-inputs-form.tsx
@@ -7,7 +7,7 @@ import { reduxForm, InjectedFormProps } from 'redux-form';
 import { CommandInputParameter, CWLType, IntCommandInputParameter, BooleanCommandInputParameter, FileCommandInputParameter, DirectoryCommandInputParameter } from '~/models/workflow';
 import { IntInput } from '~/views/run-process-panel/inputs/int-input';
 import { StringInput } from '~/views/run-process-panel/inputs/string-input';
-import { StringCommandInputParameter, FloatCommandInputParameter, isPrimitiveOfType, File, Directory, WorkflowInputsData, EnumCommandInputParameter, isArrayOfType, StringArrayCommandInputParameter } from '../../models/workflow';
+import { StringCommandInputParameter, FloatCommandInputParameter, isPrimitiveOfType, File, Directory, WorkflowInputsData, EnumCommandInputParameter, isArrayOfType, StringArrayCommandInputParameter, FileArrayCommandInputParameter } from '../../models/workflow';
 import { FloatInput } from '~/views/run-process-panel/inputs/float-input';
 import { BooleanInput } from './inputs/boolean-input';
 import { FileInput } from './inputs/file-input';
@@ -18,6 +18,7 @@ import { EnumInput } from './inputs/enum-input';
 import { DirectoryInput } from './inputs/directory-input';
 import { StringArrayInput } from './inputs/string-array-input';
 import { createStructuredSelector, createSelector } from 'reselect';
+import { FileArrayInput } from './inputs/file-array-input';
 
 export const RUN_PROCESS_INPUTS_FORM = 'runProcessInputsForm';
 
@@ -98,6 +99,9 @@ const getInputComponent = (input: CommandInputParameter) => {
         case isArrayOfType(input, CWLType.STRING):
             return <StringArrayInput input={input as StringArrayCommandInputParameter} />;
 
+        case isArrayOfType(input, CWLType.FILE):
+            return <FileArrayInput input={input as FileArrayCommandInputParameter} />;
+
         default:
             return null;
     }

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list