19783: Add dialog height to picker dialogs that previously depended on picker height...
[arvados.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';
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.renderDialog()}
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                 height: `${spacing.unit * 8}vh`,
122             },
123         });
124
125         renderDialog() {
126             return <Dialog
127                 open={this.state.open}
128                 onClose={this.closeDialog}
129                 fullWidth
130                 data-cy="choose-a-file-dialog"
131                 maxWidth='md'>
132                 <DialogTitle>Choose a file</DialogTitle>
133                 <DialogContent>
134                     <this.dialogContent />
135                 </DialogContent>
136                 <DialogActions>
137                     <Button onClick={this.closeDialog}>Cancel</Button>
138                     <Button
139                         disabled={!this.state.file}
140                         variant='contained'
141                         color='primary'
142                         onClick={this.submit}>Ok</Button>
143                 </DialogActions>
144             </Dialog >;
145         }
146
147         dialogContent = withStyles(this.dialogContentStyles)(
148             ({ classes }: WithStyles<DialogContentCssRules>) =>
149                 <div className={classes.root}>
150                     <ProjectsTreePicker
151                         pickerId={this.props.commandInput.id}
152                         includeCollections
153                         includeFiles
154                         options={this.props.options}
155                         toggleItemActive={this.setFile} />
156                 </div>
157         );
158
159
160     });