22141: Improve selectors used in tests
[arvados.git] / services / workbench2 / src / views / run-process-panel / inputs / directory-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 { connect, DispatchProp } from 'react-redux';
7 import { memoize } from 'lodash/fp';
8 import { Field } from 'redux-form';
9 import { CustomStyleRulesCallback } from 'common/custom-theme';
10 import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
11 import { WithStyles } from '@mui/styles';
12 import withStyles from '@mui/styles/withStyles';
13 import {
14     isRequiredInput,
15     DirectoryCommandInputParameter,
16     CWLType,
17     Directory
18 } from 'models/workflow';
19 import { GenericInputProps, GenericInput } from './generic-input';
20 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
21 import { FileOperationLocation, getFileOperationLocation, initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
22 import { TreeItem } from 'components/tree/tree';
23 import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
24 import { ERROR_MESSAGE } from 'validators/require';
25 import { Dispatch } from 'redux';
26
27 export interface DirectoryInputProps {
28     input: DirectoryCommandInputParameter;
29     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
30 }
31
32 type DialogContentCssRules = 'root' | 'pickerWrapper';
33
34 export const DirectoryInput = ({ input, options }: DirectoryInputProps) =>
35     <Field
36         name={input.id}
37         commandInput={input}
38         component={DirectoryInputComponent as any}
39         format={format}
40         parse={parse}
41         {...{
42             options
43         }}
44         validate={getValidation(input)} />;
45
46 const format = (value?: Directory) => value ? value.basename : '';
47
48 const parse = (directory: FileOperationLocation): Directory => ({
49     class: CWLType.DIRECTORY,
50     location: `keep:${directory.pdh}${directory.subpath}`,
51     basename: directory.name,
52 });
53
54 const getValidation = memoize(
55     (input: DirectoryCommandInputParameter) => ([
56         isRequiredInput(input)
57             ? (directory?: Directory) => directory ? undefined : ERROR_MESSAGE
58             : () => undefined,
59     ])
60 );
61
62 interface DirectoryInputComponentState {
63     open: boolean;
64     directory?: FileOperationLocation;
65 }
66
67 interface DirectoryInputActionProps {
68     initProjectsTreePicker: (pickerId: string) => void;
69     getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
70 }
71
72 const mapDispatchToProps = (dispatch: Dispatch): DirectoryInputActionProps => ({
73     initProjectsTreePicker: (pickerId: string) => dispatch<any>(initProjectsTreePicker(pickerId)),
74     getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
75 });
76
77 const DirectoryInputComponent = connect(null, mapDispatchToProps)(
78     class FileInputComponent extends React.Component<GenericInputProps & DirectoryInputActionProps & DispatchProp & {
79         options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
80     }, DirectoryInputComponentState> {
81         state: DirectoryInputComponentState = {
82             open: false,
83         };
84
85         componentDidMount() {
86             this.props.initProjectsTreePicker(this.props.commandInput.id);
87         }
88
89         render() {
90             return <>
91                 {this.renderInput()}
92                 <this.dialog />
93             </>;
94         }
95
96         openDialog = () => {
97             this.setState({ open: true });
98         }
99
100         closeDialog = () => {
101             this.setState({ open: false });
102         }
103
104         submit = () => {
105             this.closeDialog();
106             this.props.input.onChange(this.state.directory);
107         }
108
109         setDirectory = async (_: {}, { data: item }: TreeItem<ProjectsTreePickerItem>) => {
110             const location = await this.props.getFileOperationLocation(item);
111             this.setState({ directory: location });
112         }
113
114         renderInput() {
115             return <GenericInput
116                 component={props =>
117                     <Input
118                         readOnly
119                         fullWidth
120                         value={props.input.value}
121                         error={props.meta.touched && !!props.meta.error}
122                         disabled={props.commandInput.disabled}
123                         onClick={!this.props.commandInput.disabled ? this.openDialog : undefined}
124                         onKeyPress={!this.props.commandInput.disabled ? this.openDialog : undefined} />}
125                 {...this.props} />;
126         }
127
128         dialogContentStyles: CustomStyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
129             root: {
130                 display: 'flex',
131                 flexDirection: 'column',
132                 height: "80vh",
133             },
134             pickerWrapper: {
135                 display: 'flex',
136                 flexDirection: 'column',
137                 height: "100%",
138             },
139         });
140
141         dialog = withStyles(this.dialogContentStyles)(
142             ({ classes }: WithStyles<DialogContentCssRules>) =>
143                 <Dialog
144                     open={this.state.open}
145                     onClose={this.closeDialog}
146                     fullWidth
147                     data-cy="choose-a-directory-dialog"
148                     maxWidth='md'>
149                     <DialogTitle>Choose a directory</DialogTitle>
150                     <DialogContent className={classes.root}>
151                         <div className={classes.pickerWrapper}>
152                             <ProjectsTreePicker
153                                 pickerId={this.props.commandInput.id}
154                                 includeCollections
155                                 includeDirectories
156                                 cascadeSelection={false}
157                                 options={this.props.options}
158                                 toggleItemActive={this.setDirectory} />
159                         </div>
160                     </DialogContent>
161                     <DialogActions>
162                         <Button onClick={this.closeDialog}>Cancel</Button>
163                         <Button
164                             disabled={!this.state.directory}
165                             variant='contained'
166                             color='primary'
167                             onClick={this.submit}>Ok</Button>
168                     </DialogActions>
169                 </Dialog>
170         );
171
172     });