1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import * as React from 'react';
8 DirectoryArrayCommandInputParameter,
11 } from '~/models/workflow';
12 import { Field } from 'redux-form';
13 import { ERROR_MESSAGE } from '~/validators/require';
14 import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, Divider, Grid, WithStyles, Typography } from '@material-ui/core';
15 import { GenericInputProps, GenericInput } from './generic-input';
16 import { ProjectsTreePicker } from '~/views-components/projects-tree-picker/projects-tree-picker';
17 import { connect, DispatchProp } from 'react-redux';
18 import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, getAllNodes } from '~/store/tree-picker/tree-picker-actions';
19 import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
20 import { createSelector, createStructuredSelector } from 'reselect';
21 import { ChipsInput } from '~/components/chips-input/chips-input';
22 import { identity, values, noop } from 'lodash';
23 import { InputProps } from '@material-ui/core/Input';
24 import { TreePicker } from '~/store/tree-picker/tree-picker';
25 import { RootState } from '~/store/store';
26 import { Chips } from '~/components/chips/chips';
27 import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles';
28 import { CollectionResource } from '~/models/collection';
29 import { ResourceKind } from '~/models/resource';
31 export interface DirectoryArrayInputProps {
32 input: DirectoryArrayCommandInputParameter;
35 export const DirectoryArrayInput = ({ input }: DirectoryArrayInputProps) =>
39 component={DirectoryArrayInputComponent}
40 parse={parseDirectories}
41 format={formatDirectories}
42 validate={validationSelector(input)} />;
44 interface FormattedDirectory {
46 portableDataHash: string;
49 const parseDirectories = (directories: CollectionResource[] | string) =>
50 typeof directories === 'string'
52 : directories.map(parse);
54 const parse = (directory: CollectionResource): Directory => ({
55 class: CWLType.DIRECTORY,
56 basename: directory.name,
57 location: `keep:${directory.portableDataHash}`,
60 const formatDirectories = (directories: Directory[] = []) =>
61 directories.map(format);
63 const format = ({ location = '', basename = '' }: Directory): FormattedDirectory => ({
64 portableDataHash: location.replace('keep:', ''),
68 const validationSelector = createSelector(
70 isRequired => isRequired
75 const required = (value?: Directory[]) =>
76 value && value.length > 0
79 interface DirectoryArrayInputComponentState {
81 directories: CollectionResource[];
82 prevDirectories: CollectionResource[];
85 interface DirectoryArrayInputComponentProps {
86 treePickerState: TreePicker;
89 const treePickerSelector = (state: RootState) => state.treePicker;
91 const mapStateToProps = createStructuredSelector({
92 treePickerState: treePickerSelector,
95 const DirectoryArrayInputComponent = connect(mapStateToProps)(
96 class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp, DirectoryArrayInputComponentState> {
97 state: DirectoryArrayInputComponentState = {
103 directoryRefreshTimeout = -1;
105 componentDidMount() {
106 this.props.dispatch<any>(
107 initProjectsTreePicker(this.props.commandInput.id));
118 this.setDirectoriesFromProps(this.props.input.value);
119 this.setState({ open: true });
122 closeDialog = () => {
123 this.setState({ open: false });
128 this.props.input.onChange(this.state.directories);
131 setDirectories = (directories: CollectionResource[]) => {
133 const deletedDirectories = this.state.directories
134 .reduce((deletedDirectories, directory) =>
135 directories.some(({ uuid }) => uuid === directory.uuid)
137 : [...deletedDirectories, directory]
140 this.setState({ directories });
142 const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
143 ids.forEach(pickerId => {
145 treePickerActions.DESELECT_TREE_PICKER_NODE({
146 pickerId, id: deletedDirectories.map(({ uuid }) => uuid),
153 setDirectoriesFromProps = (formattedDirectories: FormattedDirectory[]) => {
154 const nodes = getAllNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
155 const initialDirectories: CollectionResource[] = [];
156 const directories = nodes
157 .reduce((directories, { value }) =>
159 value.kind === ResourceKind.COLLECTION &&
160 formattedDirectories.find(({ portableDataHash }) => value.portableDataHash === portableDataHash)
161 ? directories.concat(value)
162 : directories, initialDirectories);
164 const addedDirectories = directories
165 .reduce((addedDirectories, directory) =>
166 this.state.directories.find(({ uuid }) =>
167 uuid === directory.uuid)
169 : [...addedDirectories, directory]
172 const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
173 ids.forEach(pickerId => {
175 treePickerActions.SELECT_TREE_PICKER_NODE({
176 pickerId, id: addedDirectories.map(({ uuid }) => uuid),
181 const orderedDirectories = formattedDirectories.reduce((dirs, formattedDir) => {
182 const dir = directories.find(({ portableDataHash }) => portableDataHash === formattedDir.portableDataHash);
188 this.setDirectories(orderedDirectories);
192 refreshDirectories = () => {
193 clearTimeout(this.directoryRefreshTimeout);
194 this.directoryRefreshTimeout = setTimeout(this.setSelectedFiles);
197 setSelectedFiles = () => {
198 const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
199 const initialDirectories: CollectionResource[] = [];
200 const directories = nodes
201 .reduce((directories, { value }) =>
202 'kind' in value && value.kind === ResourceKind.COLLECTION
203 ? directories.concat(value)
204 : directories, initialDirectories);
205 this.setDirectories(directories);
209 component={this.chipsInput}
214 value={this.props.input.value}
216 disabled={this.props.commandInput.disabled}
217 createNewValue={identity}
218 getLabel={(data: FormattedDirectory) => data.name}
219 inputComponent={this.textInput} />
221 textInput = (props: InputProps) =>
224 error={this.props.meta.touched && !!this.props.meta.error}
226 onClick={!this.props.commandInput.disabled ? this.openDialog : undefined}
227 onKeyPress={!this.props.commandInput.disabled ? this.openDialog : undefined}
228 onBlur={this.props.input.onBlur}
229 disabled={this.props.commandInput.disabled} />
233 open={this.state.open}
234 onClose={this.closeDialog}
237 <DialogTitle>Choose collections</DialogTitle>
239 <this.dialogContent />
242 <Button onClick={this.closeDialog}>Cancel</Button>
246 onClick={this.submit}>Ok</Button>
250 dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
253 flexDirection: 'column',
254 height: `${spacing.unit * 8}vh`,
261 margin: `${spacing.unit}px 0`,
266 padding: `${spacing.unit}px 0`,
271 dialogContent = withStyles(this.dialogContentStyles)(
272 ({ classes }: WithStyles<DialogContentCssRules>) =>
273 <div className={classes.root}>
274 <div className={classes.tree}>
276 pickerId={this.props.commandInput.id}
279 toggleItemSelection={this.refreshDirectories} />
282 <div className={classes.chips}>
283 <Typography variant='subheading'>Selected collections ({this.state.directories.length}):</Typography>
287 values={this.state.directories}
288 onChange={this.setDirectories}
289 getLabel={(directory: CollectionResource) => directory.name} />
296 type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';