21842: set getSuggestions to trigger on input focus
[arvados.git] / services / workbench2 / 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, AutocompleteCat } 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 '@mui/material';
12 import { noop } from 'lodash/fp';
13 import { GroupClass, GroupResource } from 'models/group';
14 import { getUserDetailsString, 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     tooltip: string;
21     uuid: string;
22 }
23
24 type ParticipantResource = GroupResource | UserResource;
25
26 interface ParticipantSelectProps {
27     items: Participant[];
28     excludedParticipants?: string[];
29     label?: string;
30     autofocus?: boolean;
31     onlyPeople?: boolean;
32     onlyActive?: boolean;
33     disabled?: boolean;
34     category?: AutocompleteCat;
35
36     onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
37     onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
38     onCreate?: (person: Participant) => void;
39     onDelete?: (index: number) => void;
40     onSelect?: (person: Participant) => void;
41 }
42
43 interface ParticipantSelectState {
44     value: string;
45     suggestions: ParticipantResource[];
46 }
47
48 const getDisplayName = (item: GroupResource | UserResource, detailed: boolean) => {
49     switch (item.kind) {
50         case ResourceKind.USER:
51             return getUserDisplayName(item, detailed, detailed);
52         case ResourceKind.GROUP:
53             return item.name + `(${`(${(item as Resource).uuid})`})`;
54         default:
55             return (item as Resource).uuid;
56     }
57 };
58
59 const getDisplayTooltip = (item: GroupResource | UserResource) => {
60     switch (item.kind) {
61         case ResourceKind.USER:
62             return getUserDetailsString(item);
63         case ResourceKind.GROUP:
64             return item.name + `(${`(${(item as Resource).uuid})`})`;
65         default:
66             return (item as Resource).uuid;
67     }
68 };
69
70 export const ParticipantSelect = connect()(
71     class ParticipantSelect extends React.Component<ParticipantSelectProps & DispatchProp, ParticipantSelectState> {
72         state: ParticipantSelectState = {
73             value: '',
74             suggestions: []
75         };
76
77         render() {
78             const { label = 'Add people and groups' } = this.props;
79
80             return (
81                 <Autocomplete
82                     label={label}
83                     value={this.state.value}
84                     items={this.props.items}
85                     suggestions={this.state.suggestions}
86                     autofocus={this.props.autofocus}
87                     onChange={this.handleChange}
88                     onCreate={this.handleCreate}
89                     onSelect={this.handleSelect}
90                     onDelete={this.props.onDelete && !this.props.disabled ? this.handleDelete : undefined}
91                     onFocus={this.props.onFocus || this.onFocus}
92                     onBlur={this.onBlur}
93                     renderChipValue={this.renderChipValue}
94                     renderChipTooltip={this.renderChipTooltip}
95                     renderSuggestion={this.renderSuggestion}
96                     category={this.props.category}
97                     disabled={this.props.disabled} />
98             );
99         }
100
101         onFocus = (e) => {
102             this.getSuggestions();
103         }
104
105         onBlur = (e) => {
106             if (this.props.onBlur) {
107                 this.props.onBlur(e);
108             }
109             setTimeout(() => this.setState({ value: '', suggestions: [] }), 200);
110         }
111
112         renderChipValue(chipValue: Participant) {
113             const { name, uuid } = chipValue;
114             return name || uuid;
115         }
116
117         renderChipTooltip(item: Participant) {
118             return item.tooltip;
119         }
120
121         renderSuggestion(item: ParticipantResource) {
122             return (
123                 <ListItemText>
124                     <Typography noWrap>{getDisplayName(item, true)}</Typography>
125                 </ListItemText>
126             );
127         }
128
129         handleDelete = (_: Participant, index: number) => {
130             const { onDelete = noop } = this.props;
131             onDelete(index);
132         }
133
134         handleCreate = () => {
135             const { onCreate } = this.props;
136             if (onCreate) {
137                 this.setState({ value: '', suggestions: [] });
138                 onCreate({
139                     name: '',
140                     tooltip: '',
141                     uuid: this.state.value,
142                 });
143             }
144         }
145
146         handleSelect = (selection: ParticipantResource) => {
147             if (!selection) return;
148             const { uuid } = selection;
149             const { onSelect = noop } = this.props;
150             this.setState({ value: '', suggestions: [] });
151             onSelect({
152                 name: getDisplayName(selection, false),
153                 tooltip: getDisplayTooltip(selection),
154                 uuid,
155             });
156         }
157
158         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
159             this.setState({ value: event.target.value }, this.getSuggestions);
160         }
161
162         getSuggestions = debounce(() => this.props.dispatch<any>(this.requestSuggestions), 500);
163
164         requestSuggestions = async (_: void, __: void, { userService, groupsService }: ServiceRepository) => {
165             const { value } = this.state;
166             const limit = 10; // FIXME: Does this provide a good UX?
167
168             const filterUsers = new FilterBuilder()
169                 .addILike('any', value)
170                 .addEqual('is_active', this.props.onlyActive || undefined)
171                 .addNotIn('uuid', this.props.excludedParticipants)
172                 .getFilters();
173             const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
174
175             const filterGroups = new FilterBuilder()
176                 .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
177                 .addNotIn('uuid', this.props.excludedParticipants)
178                 .addILike('name', value)
179                 .getFilters();
180
181             const groupItems: ListResults<any> = await groupsService.list({ filters: filterGroups, limit, count: "none" });
182             this.setState({
183                 suggestions: this.props.onlyPeople
184                     ? userItems.items
185                     : userItems.items.concat(groupItems.items)
186             });
187         }
188     });