22141: Improve selectors used in tests
[arvados.git] / services / workbench2 / src / views / run-process-panel / inputs / project-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 { Field } from 'redux-form';
8 import { CustomStyleRulesCallback } from 'common/custom-theme';
9 import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
10 import { WithStyles } from '@mui/styles';
11 import withStyles from '@mui/styles/withStyles';
12 import {
13     GenericCommandInputParameter
14 } from 'models/workflow';
15 import { GenericInput, GenericInputProps } from './generic-input';
16 import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
17 import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
18 import { TreeItem } from 'components/tree/tree';
19 import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
20 import { ProjectResource } from 'models/project';
21 import { ResourceKind } from 'models/resource';
22 import { RootState } from 'store/store';
23 import { getUserUuid } from 'common/getuser';
24
25 export type ProjectCommandInputParameter = GenericCommandInputParameter<ProjectResource, ProjectResource>;
26
27 const isUndefined: any = (value?: ProjectResource) => (value === undefined);
28
29 export interface ProjectInputProps {
30     required: boolean;
31     input: ProjectCommandInputParameter;
32     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
33 }
34
35 type DialogContentCssRules = 'root' | 'pickerWrapper';
36
37 export const ProjectInput = ({ required, input, options }: ProjectInputProps) =>
38     <Field
39         name={input.id}
40         commandInput={input}
41         component={ProjectInputComponent as any}
42         format={format}
43         validate={required ? isUndefined : undefined}
44         {...{
45             options,
46             required
47         }} />;
48
49 const format = (value?: ProjectResource) => value ? value.name : '';
50
51 interface ProjectInputComponentState {
52     open: boolean;
53     project?: ProjectResource;
54 }
55
56 interface HasUserUuid {
57     userUuid: string;
58 }
59
60 const mapStateToProps = (state: RootState) => ({ userUuid: getUserUuid(state) });
61
62 export const ProjectInputComponent = connect(mapStateToProps)(
63     class ProjectInputComponent extends React.Component<GenericInputProps & DispatchProp & HasUserUuid & {
64         options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
65         required?: boolean;
66     }, ProjectInputComponentState> {
67         state: ProjectInputComponentState = {
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.project);
95         }
96
97         setProject = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
98             if ('kind' in data && data.kind === ResourceKind.PROJECT) {
99                 this.setState({ project: data });
100             } else {
101                 this.setState({ project: undefined });
102             }
103         }
104
105         invalid = () => (!this.state.project || !this.state.project.canWrite);
106
107         renderInput() {
108             return <GenericInput
109                 component={props =>
110                     <Input
111                         readOnly
112                         fullWidth
113                         value={props.input.value}
114                         error={props.meta.touched && !!props.meta.error}
115                         disabled={props.commandInput.disabled}
116                         onClick={!this.props.commandInput.disabled ? this.openDialog : undefined}
117                         onKeyPress={!this.props.commandInput.disabled ? this.openDialog : undefined} />}
118                 {...this.props} />;
119         }
120
121         dialogContentStyles: CustomStyleRulesCallback<DialogContentCssRules> = ({ spacing }) => ({
122             root: {
123                 display: 'flex',
124                 flexDirection: 'column',
125                 height: "80vh",
126             },
127             pickerWrapper: {
128                 display: 'flex',
129                 flexDirection: 'column',
130                 height: "100%",
131             },
132         });
133
134         dialog = withStyles(this.dialogContentStyles)(
135             ({ classes }: WithStyles<DialogContentCssRules>) =>
136                 this.state.open ? <Dialog
137                                       open={this.state.open}
138                                       onClose={this.closeDialog}
139                                       fullWidth
140                                       data-cy="choose-a-project-dialog"
141                                       maxWidth='md'>
142                     <DialogTitle>Choose a project</DialogTitle>
143                     <DialogContent className={classes.root}>
144                         <div className={classes.pickerWrapper}>
145                             <ProjectsTreePicker
146                                 pickerId={this.props.commandInput.id}
147                                 cascadeSelection={false}
148                                 options={this.props.options}
149                                 toggleItemActive={this.setProject} />
150                         </div>
151                     </DialogContent>
152                     <DialogActions>
153                         <Button onClick={this.closeDialog}>Cancel</Button>
154                         <Button
155                             disabled={this.invalid()}
156                             variant='contained'
157                             color='primary'
158                             onClick={this.submit}>Ok</Button>
159                     </DialogActions>
160                 </Dialog> : null
161         );
162
163     });