Merge branch '21128-toolbar-context-menu'
[arvados-workbench2.git] / src / views / run-process-panel / inputs / file-input.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { memoize } from 'lodash/fp';
7 import {
8     isRequiredInput,
9     FileCommandInputParameter,
10     File,
11     CWLType
12 } from 'models/workflow';
13 import { Field } from 'redux-form';
14 import { ERROR_MESSAGE } from 'validators/require';
15 import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
16 import { GenericInputProps, GenericInput } from './generic-input';
17 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
18 import { connect, DispatchProp } from 'react-redux';
19 import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
20 import { TreeItem } from 'components/tree/tree';
21 import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
22 import { CollectionFile, CollectionFileType } from 'models/collection-file';
23
24 export interface FileInputProps {
25     input: FileCommandInputParameter;
26     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
27 }
28
29 type DialogContentCssRules = 'root' | 'pickerWrapper';
30
31 export const FileInput = ({ input, options }: FileInputProps) =>
32     <Field
33         name={input.id}
34         commandInput={input}
35         component={FileInputComponent as any}
36         format={format}
37         parse={parse}
38         {...{
39             options
40         }}
41         validate={getValidation(input)} />;
42
43 const format = (value?: File) => value ? value.basename : '';
44
45 const parse = (file: CollectionFile): File => ({
46     class: CWLType.FILE,
47     location: `keep:${file.id}`,
48     basename: file.name,
49 });
50
51 const getValidation = memoize(
52     (input: FileCommandInputParameter) => ([
53         isRequiredInput(input)
54             ? (file?: File) => file ? undefined : ERROR_MESSAGE
55             : () => undefined,
56     ]));
57
58 interface FileInputComponentState {
59     open: boolean;
60     file?: CollectionFile;
61 }
62
63 const FileInputComponent = connect()(
64     class FileInputComponent extends React.Component<GenericInputProps & DispatchProp & {
65         options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
66     }, FileInputComponentState> {
67         state: FileInputComponentState = {
68             open: false,
69         };
70
71         componentDidMount() {
72             this.props.dispatch<any>(
73                 initProjectsTreePicker(this.props.commandInput.id));
74         }
75
76         render() {
77             return <>
78                 {this.renderInput()}
79                 <this.dialog />
80             </>;
81         }
82
83         openDialog = () => {
84             this.componentDidMount();
85             this.setState({ open: true });
86         }
87
88         closeDialog = () => {
89             this.setState({ open: false });
90         }
91
92         submit = () => {
93             this.closeDialog();
94             this.props.input.onChange(this.state.file);
95         }
96
97         setFile = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
98             if ('type' in data && data.type === CollectionFileType.FILE) {
99                 this.setState({ file: data });
100             } else {
101                 this.setState({ file: undefined });
102             }
103         }
104
105         renderInput() {
106             return <GenericInput
107                 component={props =>
108                     <Input
109                         readOnly
110                         fullWidth
111                         disabled={props.commandInput.disabled}
112                         value={props.input.value}
113                         error={props.meta.touched && !!props.meta.error}
114                         onClick={!props.commandInput.disabled ? this.openDialog : undefined}
115                         onKeyPress={!props.commandInput.disabled ? this.openDialog : undefined} />}
116                 {...this.props} />;
117         }
118
119         dialogContentStyles: StyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
120             root: {
121                 display: 'flex',
122                 flexDirection: 'column',
123             },
124             pickerWrapper: {
125                 flexBasis: `${spacing.unit * 8}vh`,
126                 flexShrink: 1,
127                 minHeight: 0,
128             },
129         });
130
131         dialog = withStyles(this.dialogContentStyles)(
132             ({ classes }: WithStyles<DialogContentCssRules>) =>
133                 <Dialog
134                     open={this.state.open}
135                     onClose={this.closeDialog}
136                     fullWidth
137                     data-cy="choose-a-file-dialog"
138                     maxWidth='md'>
139                     <DialogTitle>Choose a file</DialogTitle>
140                     <DialogContent className={classes.root}>
141                         <div className={classes.pickerWrapper}>
142                             <ProjectsTreePicker
143                                 pickerId={this.props.commandInput.id}
144                                 includeCollections
145                                 includeDirectories
146                                 includeFiles
147                                 cascadeSelection={false}
148                                 options={this.props.options}
149                                 toggleItemActive={this.setFile} />
150                         </div>
151                     </DialogContent>
152                     <DialogActions>
153                         <Button onClick={this.closeDialog}>Cancel</Button>
154                         <Button
155                             disabled={!this.state.file}
156                             variant='contained'
157                             color='primary'
158                             onClick={this.submit}>Ok</Button>
159                     </DialogActions>
160                 </Dialog >
161         );
162     });