1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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';
18 export interface Participant {
24 type ParticipantResource = GroupResource | UserResource;
26 interface ParticipantSelectProps {
28 excludedParticipants?: string[];
34 category?: AutocompleteCat;
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;
43 interface ParticipantSelectState {
46 suggestions: ParticipantResource[];
47 cachedSuggestions: ParticipantResource[];
50 const getDisplayName = (item: GroupResource | UserResource, detailed: boolean) => {
52 case ResourceKind.USER:
53 return getUserDisplayName(item, detailed, detailed);
54 case ResourceKind.GROUP:
55 return item.name + `(${`(${(item as Resource).uuid})`})`;
57 return (item as Resource).uuid;
61 const getSharingDisplayName = (item: GroupResource | UserResource, detailed: boolean = false) => {
63 case ResourceKind.USER:
64 return `${getUserDisplayName(item, detailed, detailed)} (${item.email})`;
65 case ResourceKind.GROUP:
68 return (item as Resource).uuid;
72 const getDisplayTooltip = (item: GroupResource | UserResource) => {
74 case ResourceKind.USER:
75 return getUserDetailsString(item);
76 case ResourceKind.GROUP:
77 return item.name + `(${`(${(item as Resource).uuid})`})`;
79 return (item as Resource).uuid;
83 export const ParticipantSelect = connect()(
84 class ParticipantSelect extends React.Component<ParticipantSelectProps & DispatchProp, ParticipantSelectState> {
85 state: ParticipantSelectState = {
89 cachedSuggestions: [],
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 });
99 const { label = 'Add people and groups' } = this.props;
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}
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} />
125 this.setState({ isWorking: true });
126 this.getSuggestions();
130 if (this.props.onBlur) {
131 this.props.onBlur(e);
133 setTimeout(() => this.setState({ value: '', suggestions: [] }), 200);
136 renderChipValue(chipValue: Participant) {
137 const { name, uuid } = chipValue;
141 renderChipTooltip(item: Participant) {
145 renderSuggestion(item: ParticipantResource) {
148 <Typography noWrap>{getDisplayName(item, true)}</Typography>
153 handleDelete = (_: Participant, index: number) => {
154 const { onDelete = noop } = this.props;
158 handleCreate = () => {
159 const { onCreate } = this.props;
161 this.setState({ value: '', suggestions: [] });
165 uuid: this.state.value,
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 });
176 name: this.props.category === AutocompleteCat.SHARING ? getSharingDisplayName(selection) : getDisplayName(selection, false),
177 tooltip: getDisplayTooltip(selection),
182 handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
183 this.setState({ value: event.target.value }, this.getSuggestions);
186 getSuggestions = debounce(() => this.props.dispatch<any>(this.requestSuggestions), 500);
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
194 const filterUsers = new FilterBuilder()
195 .addILike('any', value)
196 .addEqual('is_active', this.props.onlyActive || undefined)
197 .addNotIn('uuid', this.props.excludedParticipants)
199 const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
201 const filterGroups = new FilterBuilder()
202 .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
203 .addNotIn('uuid', this.props.excludedParticipants)
204 .addILike('name', value)
207 const groupItems: ListResults<any> = await groupsService.list({ filters: filterGroups, limit, count: "none" });
209 suggestions: this.props.onlyPeople
211 : userItems.items.concat(groupItems.items),