Merge branch '17782-react-scripts-ts-migration' into main. Closes #17782
[arvados-workbench2.git] / src / views-components / sharing-dialog / participant-select.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 { Autocomplete } from 'components/autocomplete/autocomplete';
7 import { connect, DispatchProp } from 'react-redux';
8 import { ServiceRepository } from 'services/services';
9 import { FilterBuilder } from '../../services/api/filter-builder';
10 import { debounce } from 'debounce';
11 import { ListItemText, Typography } from '@material-ui/core';
12 import { noop } from 'lodash/fp';
13 import { GroupClass, GroupResource } from 'models/group';
14 import { getUserDisplayName, UserResource } from 'models/user';
15 import { Resource, ResourceKind } from 'models/resource';
16 import { ListResults } from 'services/common-service/common-service';
17
18 export interface Participant {
19     name: string;
20     uuid: string;
21 }
22
23 type ParticipantResource = GroupResource | UserResource;
24
25 interface ParticipantSelectProps {
26     items: Participant[];
27     label?: string;
28     autofocus?: boolean;
29     onlyPeople?: boolean;
30
31     onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
32     onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
33     onCreate?: (person: Participant) => void;
34     onDelete?: (index: number) => void;
35     onSelect?: (person: Participant) => void;
36 }
37
38 interface ParticipantSelectState {
39     value: string;
40     suggestions: ParticipantResource[];
41 }
42
43 const getDisplayName = (item: GroupResource | UserResource) => {
44     switch (item.kind) {
45         case ResourceKind.USER:
46             return getUserDisplayName(item, true);
47         case ResourceKind.GROUP:
48             return item.name;
49         default:
50             return (item as Resource).uuid;
51     }
52 };
53
54 export const ParticipantSelect = connect()(
55     class ParticipantSelect extends React.Component<ParticipantSelectProps & DispatchProp, ParticipantSelectState> {
56         state: ParticipantSelectState = {
57             value: '',
58             suggestions: []
59         };
60
61         render() {
62             const { label = 'Share' } = this.props;
63
64             return (
65                 <Autocomplete
66                     label={label}
67                     value={this.state.value}
68                     items={this.props.items}
69                     suggestions={this.state.suggestions}
70                     autofocus={this.props.autofocus}
71                     onChange={this.handleChange}
72                     onCreate={this.handleCreate}
73                     onSelect={this.handleSelect}
74                     onDelete={this.handleDelete}
75                     onFocus={this.props.onFocus}
76                     onBlur={this.props.onBlur}
77                     renderChipValue={this.renderChipValue}
78                     renderSuggestion={this.renderSuggestion} />
79             );
80         }
81
82         renderChipValue(chipValue: Participant) {
83             const { name, uuid } = chipValue;
84             return name || uuid;
85         }
86
87         renderSuggestion(item: ParticipantResource) {
88             return (
89                 <ListItemText>
90                     <Typography noWrap>{getDisplayName(item)}</Typography>
91                 </ListItemText>
92             );
93         }
94
95         handleDelete = (_: Participant, index: number) => {
96             const { onDelete = noop } = this.props;
97             onDelete(index);
98         }
99
100         handleCreate = () => {
101             const { onCreate } = this.props;
102             if (onCreate) {
103                 this.setState({ value: '', suggestions: [] });
104                 onCreate({
105                     name: '',
106                     uuid: this.state.value,
107                 });
108             }
109         }
110
111         handleSelect = (selection: ParticipantResource) => {
112             const { uuid } = selection;
113             const { onSelect = noop } = this.props;
114             this.setState({ value: '', suggestions: [] });
115             onSelect({
116                 name: getDisplayName(selection),
117                 uuid,
118             });
119         }
120
121         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
122             this.setState({ value: event.target.value }, this.getSuggestions);
123         }
124
125         getSuggestions = debounce(() => this.props.dispatch<any>(this.requestSuggestions), 500);
126
127         requestSuggestions = async (_: void, __: void, { userService, groupsService }: ServiceRepository) => {
128             const { value } = this.state;
129             const limit = 5; // FIXME: Does this provide a good UX?
130
131             const filterUsers = new FilterBuilder()
132                 .addILike('any', value)
133                 .getFilters();
134             const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
135
136             const filterGroups = new FilterBuilder()
137                 .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
138                 .addILike('name', value)
139                 .getFilters();
140
141             const groupItems: ListResults<any> = await groupsService.list({ filters: filterGroups, limit, count: "none" });
142             this.setState({
143                 suggestions: this.props.onlyPeople
144                     ? userItems.items
145                     : userItems.items.concat(groupItems.items)
146             });
147         }
148     });