21842: changed sharing dialog width
[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     isWorking: boolean;
45     value: string;
46     suggestions: ParticipantResource[];
47     cachedSuggestions: ParticipantResource[];
48 }
49
50 const getDisplayName = (item: GroupResource | UserResource, detailed: boolean) => {
51     switch (item.kind) {
52         case ResourceKind.USER:
53             return getUserDisplayName(item, detailed, detailed);
54         case ResourceKind.GROUP:
55             return item.name + `(${`(${(item as Resource).uuid})`})`;
56         default:
57             return (item as Resource).uuid;
58     }
59 };
60
61 const getSharingDisplayName = (item: GroupResource | UserResource, detailed: boolean = false) => {
62     switch (item.kind) {
63         case ResourceKind.USER:
64             return `${getUserDisplayName(item, detailed, detailed)} (${item.email})`;
65         case ResourceKind.GROUP:
66             return item.name;
67         default:
68             return (item as Resource).uuid;
69     }
70 };
71
72 const getDisplayTooltip = (item: GroupResource | UserResource) => {
73     switch (item.kind) {
74         case ResourceKind.USER:
75             return getUserDetailsString(item);
76         case ResourceKind.GROUP:
77             return item.name + `(${`(${(item as Resource).uuid})`})`;
78         default:
79             return (item as Resource).uuid;
80     }
81 };
82
83 export const ParticipantSelect = connect()(
84     class ParticipantSelect extends React.Component<ParticipantSelectProps & DispatchProp, ParticipantSelectState> {
85         state: ParticipantSelectState = {
86             isWorking: false,
87             value: '',
88             suggestions: [],
89             cachedSuggestions: [],
90         };
91
92         componentDidUpdate(prevProps: ParticipantSelectProps & DispatchProp, prevState: ParticipantSelectState) {
93             if (prevState.suggestions.length === 0 && this.state.suggestions.length > 0 && this.state.value.length === 0) {
94                 this.setState({ cachedSuggestions: this.state.suggestions });
95             }
96         }
97
98         render() {
99             const { label = 'Add people and groups' } = this.props;
100
101             return (
102                 <Autocomplete
103                     label={label}
104                     value={this.state.value}
105                     items={this.props.items}
106                     suggestions={this.state.suggestions}
107                     autofocus={this.props.autofocus}
108                     onChange={this.handleChange}
109                     onCreate={this.handleCreate}
110                     onSelect={this.handleSelect}
111                     onDelete={this.props.onDelete && !this.props.disabled ? this.handleDelete : undefined}
112                     onFocus={this.props.onFocus || this.onFocus}
113                     onBlur={this.onBlur}
114                     renderChipValue={this.renderChipValue}
115                     renderChipTooltip={this.renderChipTooltip}
116                     renderSuggestion={this.renderSuggestion}
117                     category={this.props.category}
118                     isWorking={this.state.isWorking}
119                     maxLength={this.props.category === AutocompleteCat.SHARING ? 10 : undefined}
120                     disabled={this.props.disabled} />
121             );
122         }
123
124         onFocus = (e) => {
125             this.setState({ isWorking: true });
126             this.getSuggestions();
127         }
128
129         onBlur = (e) => {
130             if (this.props.onBlur) {
131                 this.props.onBlur(e);
132             }
133             setTimeout(() => this.setState({ value: '', suggestions: [] }), 200);
134         }
135
136         renderChipValue(chipValue: Participant) {
137             const { name, uuid } = chipValue;
138             return name || uuid;
139         }
140
141         renderChipTooltip(item: Participant) {
142             return item.tooltip;
143         }
144
145         renderSuggestion(item: ParticipantResource) {
146             return (
147                 <ListItemText>
148                     <Typography noWrap>{getDisplayName(item, true)}</Typography>
149                 </ListItemText>
150             );
151         }
152
153         handleDelete = (_: Participant, index: number) => {
154             const { onDelete = noop } = this.props;
155             onDelete(index);
156         }
157
158         handleCreate = () => {
159             const { onCreate } = this.props;
160             if (onCreate) {
161                 this.setState({ value: '', suggestions: [] });
162                 onCreate({
163                     name: '',
164                     tooltip: '',
165                     uuid: this.state.value,
166                 });
167             }
168         }
169
170         handleSelect = (selection: ParticipantResource) => {
171             if (!selection) return;
172             const { uuid } = selection;
173             const { onSelect = noop } = this.props;
174             this.setState({ value: '', suggestions: this.state.cachedSuggestions });
175             onSelect({
176                 name: this.props.category === AutocompleteCat.SHARING ? getSharingDisplayName(selection) : getDisplayName(selection, false),
177                 tooltip: getDisplayTooltip(selection),
178                 uuid,
179             });
180         }
181
182         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
183             this.setState({ value: event.target.value }, this.getSuggestions);
184         }
185
186         getSuggestions = debounce(() => this.props.dispatch<any>(this.requestSuggestions), 500);
187
188         requestSuggestions = async (_: void, __: void, { userService, groupsService }: ServiceRepository) => {
189             this.setState({ isWorking: true });
190             const { value } = this.state;
191             // +1 to see if there are more than 10 results
192             const limit = 11;
193
194             const filterUsers = new FilterBuilder()
195                 .addILike('any', value)
196                 .addEqual('is_active', this.props.onlyActive || undefined)
197                 .addNotIn('uuid', this.props.excludedParticipants)
198                 .getFilters();
199             const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
200
201             const filterGroups = new FilterBuilder()
202                 .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
203                 .addNotIn('uuid', this.props.excludedParticipants)
204                 .addILike('name', value)
205                 .getFilters();
206
207             const groupItems: ListResults<any> = await groupsService.list({ filters: filterGroups, limit, count: "none" });
208             this.setState({
209                 suggestions: this.props.onlyPeople
210                     ? userItems.items
211                     : userItems.items.concat(groupItems.items),
212                 isWorking: false,
213             });
214         }
215     });