1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import * as 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, 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 } from '~/store/tree-picker/tree-picker-actions';
19 import { ProjectsTreePickerItem } from '~/views-components/projects-tree-picker/generic-projects-tree-picker';
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;
33 export const FileArrayInput = ({ input }: FileArrayInputProps) =>
37 component={FileArrayInputComponent}
40 validate={validationSelector(input)} />;
42 const parseFiles = (files: CollectionFile[]) =>
47 const parse = (file: CollectionFile): File => ({
50 location: `keep:${file.id}`,
54 const formatFiles = (files: File[] = []) => files.map(format);
56 const format = (file: File): CollectionFile => ({
58 ? file.location.replace('keep:', '')
60 name: file.basename || '',
61 path: file.path || '',
63 type: CollectionFileType.FILE,
67 const validationSelector = createSelector(
69 isRequired => isRequired
74 const required = (value?: File[]) =>
75 value && value.length > 0
78 interface FileArrayInputComponentState {
80 files: CollectionFile[];
83 interface FileArrayInputComponentProps {
84 treePickerState: TreePicker;
87 const treePickerSelector = (state: RootState) => state.treePicker;
89 const mapStateToProps = createStructuredSelector({
90 treePickerState: treePickerSelector,
93 const FileArrayInputComponent = connect(mapStateToProps)(
94 class FileArrayInputComponent extends React.Component<FileArrayInputComponentProps & GenericInputProps & DispatchProp, FileArrayInputComponentState> {
95 state: FileArrayInputComponentState = {
100 fileRefreshTimeout = -1;
102 componentDidMount() {
103 this.props.dispatch<any>(
104 initProjectsTreePicker(this.props.commandInput.id));
115 this.setFilesFromProps(this.props.input.value);
116 this.setState({ open: true });
120 closeDialog = () => {
121 this.setState({ open: false });
126 this.props.input.onChange(this.state.files);
129 setFiles = (files: CollectionFile[]) => {
131 const deletedFiles = this.state.files
132 .reduce((deletedFiles, file) =>
133 files.some(({ id }) => id === file.id)
135 : [...deletedFiles, file]
138 this.setState({ files });
140 const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
141 ids.forEach(pickerId => {
143 treePickerActions.DESELECT_TREE_PICKER_NODE({
144 pickerId, id: deletedFiles.map(({ id }) => id),
151 setFilesFromProps = (files: CollectionFile[]) => {
153 const addedFiles = files
154 .reduce((addedFiles, file) =>
155 this.state.files.some(({ id }) => id === file.id)
157 : [...addedFiles, file]
160 const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
161 ids.forEach(pickerId => {
163 treePickerActions.SELECT_TREE_PICKER_NODE({
164 pickerId, id: addedFiles.map(({ id }) => id),
169 this.setFiles(files);
173 refreshFiles = () => {
174 clearTimeout(this.fileRefreshTimeout);
175 this.fileRefreshTimeout = setTimeout(this.setSelectedFiles);
178 setSelectedFiles = () => {
179 const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
180 const initialFiles: CollectionFile[] = [];
182 .reduce((files, { value }) =>
183 'type' in value && value.type === CollectionFileType.FILE
184 ? files.concat(value)
185 : files, initialFiles);
187 this.setFiles(files);
191 component={this.chipsInput}
196 value={this.props.input.value}
198 createNewValue={identity}
199 getLabel={(file: CollectionFile) => file.name}
200 inputComponent={this.textInput} />
202 textInput = (props: InputProps) =>
205 error={this.props.meta.touched && !!this.props.meta.error}
207 onClick={this.openDialog}
208 onKeyPress={this.openDialog}
209 onBlur={this.props.input.onBlur} />
213 open={this.state.open}
214 onClose={this.closeDialog}
217 <DialogTitle>Choose files</DialogTitle>
219 <this.dialogContent />
222 <Button onClick={this.closeDialog}>Cancel</Button>
226 onClick={this.submit}>Ok</Button>
230 dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
233 flexDirection: 'column',
234 height: `${spacing.unit * 8}vh`,
241 margin: `${spacing.unit}px 0`,
246 padding: `${spacing.unit}px 0`,
251 dialogContent = withStyles(this.dialogContentStyles)(
252 ({ classes }: WithStyles<DialogContentCssRules>) =>
253 <div className={classes.root}>
254 <div className={classes.tree}>
256 pickerId={this.props.commandInput.id}
260 toggleItemSelection={this.refreshFiles} />
263 <div className={classes.chips}>
264 <Typography variant='subheading'>Selected files ({this.state.files.length}):</Typography>
268 values={this.state.files}
269 onChange={this.setFiles}
270 getLabel={(file: CollectionFile) => file.name} />
277 type DialogContentCssRules = 'root' | 'tree' | 'divider' | 'chips';