1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
8 FileArrayCommandInputParameter,
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, 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 } from 'store/tree-picker/tree-picker-actions';
19 import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
20 import { CollectionFile, CollectionFileType } from 'models/collection-file';
21 import { createSelector, createStructuredSelector } from 'reselect';
22 import { ChipsInput } from 'components/chips-input/chips-input';
23 import { identity, values, noop } from 'lodash';
24 import { InputProps } from '@material-ui/core/Input';
25 import { TreePicker } from 'store/tree-picker/tree-picker';
26 import { RootState } from 'store/store';
27 import { Chips } from 'components/chips/chips';
28 import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles';
30 export interface FileArrayInputProps {
31 input: FileArrayCommandInputParameter;
32 options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
34 export const FileArrayInput = ({ input }: FileArrayInputProps) =>
38 component={FileArrayInputComponent as any}
41 validate={validationSelector(input)} />;
43 const parseFiles = (files: CollectionFile[] | string) =>
44 typeof files === 'string'
48 const parse = (file: CollectionFile): File => ({
51 location: `keep:${file.id}`,
55 const formatFiles = (files: File[] = []) =>
56 files ? files.map(format) : [];
58 const format = (file: File): CollectionFile => ({
60 ? file.location.replace('keep:', '')
62 name: file.basename || '',
63 path: file.path || '',
65 type: CollectionFileType.FILE,
69 const validationSelector = createSelector(
71 isRequired => isRequired
76 const required = (value?: File[]) =>
77 value && value.length > 0
80 interface FileArrayInputComponentState {
82 files: CollectionFile[];
85 interface FileArrayInputComponentProps {
86 treePickerState: TreePicker;
89 const treePickerSelector = (state: RootState) => state.treePicker;
91 const mapStateToProps = createStructuredSelector({
92 treePickerState: treePickerSelector,
95 const FileArrayInputComponent = connect(mapStateToProps)(
96 class FileArrayInputComponent extends React.Component<FileArrayInputComponentProps & GenericInputProps & DispatchProp & {
97 options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
98 }, FileArrayInputComponentState> {
99 state: FileArrayInputComponentState = {
104 fileRefreshTimeout = -1;
106 componentDidMount() {
107 this.props.dispatch<any>(
108 initProjectsTreePicker(this.props.commandInput.id));
119 this.setFilesFromProps(this.props.input.value);
120 this.setState({ open: true });
123 closeDialog = () => {
124 this.setState({ open: false });
129 this.props.input.onChange(this.state.files);
132 setFiles = (files: CollectionFile[]) => {
134 const deletedFiles = this.state.files
135 .reduce((deletedFiles, file) =>
136 files.some(({ id }) => id === file.id)
138 : [...deletedFiles, file]
141 this.setState({ files });
143 const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
144 ids.forEach(pickerId => {
146 treePickerActions.DESELECT_TREE_PICKER_NODE({
148 id: deletedFiles.map(({ id }) => id),
156 setFilesFromProps = (files: CollectionFile[]) => {
158 const addedFiles = files
159 .reduce((addedFiles, file) =>
160 this.state.files.some(({ id }) => id === file.id)
162 : [...addedFiles, file]
165 const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
166 ids.forEach(pickerId => {
168 treePickerActions.SELECT_TREE_PICKER_NODE({
170 id: addedFiles.map(({ id }) => id),
176 this.setFiles(files);
179 refreshFiles = () => {
180 clearTimeout(this.fileRefreshTimeout);
181 this.fileRefreshTimeout = window.setTimeout(this.setSelectedFiles);
184 setSelectedFiles = () => {
185 const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
186 const initialFiles: CollectionFile[] = [];
188 .reduce((files, { value }) =>
189 'type' in value && value.type === CollectionFileType.FILE
190 ? files.concat(value)
191 : files, initialFiles);
193 this.setFiles(files);
197 component={this.chipsInput}
202 values={this.props.input.value}
203 disabled={this.props.commandInput.disabled}
205 createNewValue={identity}
206 getLabel={(file: CollectionFile) => file.name}
207 inputComponent={this.textInput} />
209 textInput = (props: InputProps) =>
212 error={this.props.meta.touched && !!this.props.meta.error}
214 disabled={this.props.commandInput.disabled}
215 onClick={!this.props.commandInput.disabled ? this.openDialog : undefined}
216 onKeyPress={!this.props.commandInput.disabled ? this.openDialog : undefined}
217 onBlur={this.props.input.onBlur} />
219 dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
222 flexDirection: 'column',
226 flexDirection: 'column',
227 flexBasis: `${spacing.unit * 8}vh`,
236 margin: `${spacing.unit}px 0`,
241 padding: `${spacing.unit}px 0`,
247 dialog = withStyles(this.dialogContentStyles)(
248 ({ classes }: WithStyles<DialogContentCssRules>) =>
250 open={this.state.open}
251 onClose={this.closeDialog}
254 <DialogTitle>Choose files</DialogTitle>
255 <DialogContent className={classes.root}>
256 <div className={classes.pickerWrapper}>
257 <div className={classes.tree}>
259 pickerId={this.props.commandInput.id}
264 cascadeSelection={true}
265 options={this.props.options}
266 toggleItemSelection={this.refreshFiles} />
269 <div className={classes.chips}>
270 <Typography variant='subtitle1'>Selected files ({this.state.files.length}):</Typography>
274 values={this.state.files}
275 onChange={this.setFiles}
276 getLabel={(file: CollectionFile) => file.name} />
282 <Button onClick={this.closeDialog}>Cancel</Button>
287 onClick={this.submit}>Ok</Button>
294 type DialogContentCssRules = 'root' | 'pickerWrapper' | 'tree' | 'divider' | 'chips';