19049: Exclude existing users with logins in create login user picker
[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     excludedParticipants?: string[];
28     label?: string;
29     autofocus?: boolean;
30     onlyPeople?: boolean;
31
32     onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
33     onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
34     onCreate?: (person: Participant) => void;
35     onDelete?: (index: number) => void;
36     onSelect?: (person: Participant) => void;
37 }
38
39 interface ParticipantSelectState {
40     value: string;
41     suggestions: ParticipantResource[];
42 }
43
44 const getDisplayName = (item: GroupResource | UserResource) => {
45     switch (item.kind) {
46         case ResourceKind.USER:
47             return getUserDisplayName(item, true, true);
48         case ResourceKind.GROUP:
49             return item.name + `(${`(${(item as Resource).uuid})`})`;
50         default:
51             return (item as Resource).uuid;
52     }
53 };
54
55 export const ParticipantSelect = connect()(
56     class ParticipantSelect extends React.Component<ParticipantSelectProps & DispatchProp, ParticipantSelectState> {
57         state: ParticipantSelectState = {
58             value: '',
59             suggestions: []
60         };
61
62         render() {
63             const { label = 'Share' } = this.props;
64
65             return (
66                 <Autocomplete
67                     label={label}
68                     value={this.state.value}
69                     items={this.props.items}
70                     suggestions={this.state.suggestions}
71                     autofocus={this.props.autofocus}
72                     onChange={this.handleChange}
73                     onCreate={this.handleCreate}
74                     onSelect={this.handleSelect}
75                     onDelete={this.handleDelete}
76                     onFocus={this.props.onFocus}
77                     onBlur={this.props.onBlur}
78                     renderChipValue={this.renderChipValue}
79                     renderSuggestion={this.renderSuggestion} />
80             );
81         }
82
83         renderChipValue(chipValue: Participant) {
84             const { name, uuid } = chipValue;
85             return name || uuid;
86         }
87
88         renderSuggestion(item: ParticipantResource) {
89             return (
90                 <ListItemText>
91                     <Typography noWrap>{getDisplayName(item)}</Typography>
92                 </ListItemText>
93             );
94         }
95
96         handleDelete = (_: Participant, index: number) => {
97             const { onDelete = noop } = this.props;
98             onDelete(index);
99         }
100
101         handleCreate = () => {
102             const { onCreate } = this.props;
103             if (onCreate) {
104                 this.setState({ value: '', suggestions: [] });
105                 onCreate({
106                     name: '',
107                     uuid: this.state.value,
108                 });
109             }
110         }
111
112         handleSelect = (selection: ParticipantResource) => {
113             const { uuid } = selection;
114             const { onSelect = noop } = this.props;
115             this.setState({ value: '', suggestions: [] });
116             onSelect({
117                 name: getDisplayName(selection),
118                 uuid,
119             });
120         }
121
122         handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
123             this.setState({ value: event.target.value }, this.getSuggestions);
124         }
125
126         getSuggestions = debounce(() => this.props.dispatch<any>(this.requestSuggestions), 500);
127
128         requestSuggestions = async (_: void, __: void, { userService, groupsService }: ServiceRepository) => {
129             const { value } = this.state;
130             const limit = 5; // FIXME: Does this provide a good UX?
131
132             const filterUsers = new FilterBuilder()
133                 .addILike('any', value)
134                 .addNotIn('uuid', this.props.excludedParticipants)
135                 .getFilters();
136             const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
137
138             const filterGroups = new FilterBuilder()
139                 .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
140                 .addNotIn('uuid', this.props.excludedParticipants)
141                 .addILike('name', value)
142                 .getFilters();
143
144             const groupItems: ListResults<any> = await groupsService.list({ filters: filterGroups, limit, count: "none" });
145             this.setState({
146                 suggestions: this.props.onlyPeople
147                     ? userItems.items
148                     : userItems.items.concat(groupItems.items)
149             });
150         }
151     });