Skip to content

Commit fbefb23

Browse files
juliajforestigaolin1
authored andcommitted
fix: UserAutoCompleteMultiple not removing selected items (RocketChat#37823)
1 parent 84ddb2c commit fbefb23

File tree

17 files changed

+196
-234
lines changed

17 files changed

+196
-234
lines changed

.changeset/cuddly-eels-perform.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/meteor': patch
3+
---
4+
5+
Fixes members tab > add members not removing selected items

apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateChannelModal.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { useId, useEffect, useMemo } from 'react';
3434
import { useForm, Controller } from 'react-hook-form';
3535

3636
import { useEncryptedRoomDescription } from './useEncryptedRoomDescription';
37-
import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated';
37+
import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple';
3838
import { useCreateChannelTypePermission } from '../../../hooks/useCreateChannelTypePermission';
3939
import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule';
4040
import { useIsFederationEnabled } from '../../../hooks/useIsFederationEnabled';
@@ -260,7 +260,13 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
260260
!federated && hasExternalMembers(members) ? t('You_cannot_add_external_users_to_non_federated_room') : true,
261261
}}
262262
render={({ field: { onChange, value } }): ReactElement => (
263-
<UserAutoCompleteMultipleFederated id={addMembersId} value={value} onChange={onChange} placeholder={t('Add_people')} />
263+
<UserAutoCompleteMultiple
264+
id={addMembersId}
265+
value={value}
266+
onChange={onChange}
267+
federated={federated}
268+
placeholder={t('Add_people')}
269+
/>
264270
)}
265271
/>
266272
{errors.members && (

apps/meteor/client/NavBarV2/NavBarPagesGroup/actions/CreateDirectMessage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { useMutation } from '@tanstack/react-query';
2020
import { useId, memo } from 'react';
2121
import { useForm, Controller } from 'react-hook-form';
2222

23-
import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated';
23+
import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple';
2424
import { goToRoomById } from '../../../lib/utils/goToRoomById';
2525

2626
type CreateDirectMessageProps = { onClose: () => void };
@@ -78,11 +78,12 @@ const CreateDirectMessage = ({ onClose }: CreateDirectMessageProps) => {
7878
}}
7979
control={control}
8080
render={({ field: { name, onChange, value, onBlur } }) => (
81-
<UserAutoCompleteMultipleFederated
81+
<UserAutoCompleteMultiple
8282
name={name}
8383
onChange={onChange}
8484
value={value}
8585
onBlur={onBlur}
86+
federated
8687
id={membersFieldId}
8788
aria-describedby={`${membersFieldId}-hint ${membersFieldId}-error`}
8889
aria-required='true'

apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useState } from 'react';
66
import { useTranslation } from 'react-i18next';
77

88
import type { IGame } from './GameCenter';
9-
import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated';
9+
import UserAutoCompleteMultiple from '../../components/UserAutoCompleteMultiple';
1010
import { useOpenedRoom } from '../../lib/RoomManager';
1111
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
1212
import { callWithErrorHandling } from '../../lib/utils/callWithErrorHandling';
@@ -57,7 +57,7 @@ const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayer
5757
<GenericModal onClose={onClose} onCancel={onClose} onConfirm={sendInvite} title={t('Apps_Game_Center_Invite_Friends')}>
5858
<Box mbe={16}>{t('Invite_Users')}</Box>
5959
<Box mbe={16} display='flex' justifyContent='stretch'>
60-
<UserAutoCompleteMultipleFederated value={users} onChange={setUsers} />
60+
<UserAutoCompleteMultiple value={users} onChange={setUsers} federated />
6161
</Box>
6262
</GenericModal>
6363
</>

apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.tsx

Lines changed: 108 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,124 @@
1-
import { AutoComplete, OptionAvatar, Option, OptionContent, OptionDescription } from '@rocket.chat/fuselage';
1+
import { MultiSelectFiltered } from '@rocket.chat/fuselage';
22
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
3-
import { UserAvatar } from '@rocket.chat/ui-avatar';
43
import { useEndpoint } from '@rocket.chat/ui-contexts';
5-
import { useQuery } from '@tanstack/react-query';
6-
import type { ComponentProps, ReactElement } from 'react';
7-
import { memo, useMemo, useState } from 'react';
4+
import { keepPreviousData, useQuery } from '@tanstack/react-query';
5+
import type { ReactElement, AllHTMLAttributes } from 'react';
6+
import { memo, useState, useCallback, useMemo } from 'react';
87

8+
import AutocompleteOptions, { OptionsContext } from './UserAutoCompleteMultipleOptions';
99
import UserAvatarChip from './UserAvatarChip';
10+
import { usersQueryKeys } from '../../lib/queryKeys';
1011

11-
const query = (
12-
term = '',
13-
): {
14-
selector: string;
15-
} => ({ selector: JSON.stringify({ term }) });
12+
type UserAutoCompleteMultipleProps = {
13+
onChange: (value: Array<string>) => void;
14+
value: Array<string> | undefined;
15+
placeholder?: string;
16+
federated?: boolean;
17+
error?: string;
18+
} & Omit<AllHTMLAttributes<HTMLInputElement>, 'is' | 'onChange' | 'value'>;
1619

17-
type UserAutoCompleteMultipleProps = Omit<ComponentProps<typeof AutoComplete>, 'filter'>;
20+
type UserAutoCompleteOptionType = {
21+
name: string;
22+
username: string;
23+
_federated?: boolean;
24+
};
25+
26+
type UserAutoCompleteOptions = {
27+
[k: string]: UserAutoCompleteOptionType;
28+
};
1829

19-
// TODO: useDisplayUsername
20-
const UserAutoCompleteMultiple = ({ onChange, ...props }: UserAutoCompleteMultipleProps): ReactElement => {
30+
const matrixRegex = new RegExp('@(.*:.*)');
31+
32+
const UserAutoCompleteMultiple = ({ onChange, value, placeholder, federated, ...props }: UserAutoCompleteMultipleProps): ReactElement => {
2133
const [filter, setFilter] = useState('');
22-
const debouncedFilter = useDebouncedValue(filter, 1000);
23-
const usersAutoCompleteEndpoint = useEndpoint('GET', '/v1/users.autocomplete');
34+
const [selectedCache, setSelectedCache] = useState<UserAutoCompleteOptions>({});
35+
36+
const debouncedFilter = useDebouncedValue(filter, 500);
37+
const getUsers = useEndpoint('GET', '/v1/users.autocomplete');
38+
2439
const { data } = useQuery({
25-
queryKey: ['usersAutoComplete', debouncedFilter],
26-
queryFn: async () => usersAutoCompleteEndpoint(query(debouncedFilter)),
40+
queryKey: usersQueryKeys.userAutoComplete(debouncedFilter, federated ?? false),
41+
42+
queryFn: async () => {
43+
const users = await getUsers({ selector: JSON.stringify({ term: debouncedFilter }) });
44+
const options = users.items.map((item): [string, UserAutoCompleteOptionType] => [item.username, item]);
45+
46+
// Add extra option if filter text matches `username:server`
47+
// Used to add federated users that do not exist yet
48+
if (federated && matrixRegex.test(debouncedFilter)) {
49+
options.unshift([debouncedFilter, { name: debouncedFilter, username: debouncedFilter, _federated: true }]);
50+
}
51+
52+
return options;
53+
},
54+
55+
placeholderData: keepPreviousData,
2756
});
2857

29-
const options = useMemo(() => data?.items.map((user) => ({ value: user.username, label: user.name })) || [], [data]);
58+
const options = useMemo(() => data || [], [data]);
59+
60+
const onAddUser = useCallback(
61+
(username: string): void => {
62+
const user = options?.find(([val]) => val === username)?.[1];
63+
if (!user) {
64+
throw new Error('UserAutoCompleteMultiple - onAddSelected - failed to cache option');
65+
}
66+
setSelectedCache((selectedCache) => ({ ...selectedCache, [username]: user }));
67+
},
68+
[setSelectedCache, options],
69+
);
70+
71+
const onRemoveUser = useCallback(
72+
(username: string): void =>
73+
setSelectedCache((selectedCache) => {
74+
const users = { ...selectedCache };
75+
delete users[username];
76+
return users;
77+
}),
78+
[setSelectedCache],
79+
);
80+
81+
const handleOnChange = useCallback(
82+
(usernames: string[]) => {
83+
onChange(usernames);
84+
const newAddedUsername = usernames.filter((username) => !value?.includes(username))[0];
85+
const removedUsername = value?.filter((username) => !usernames.includes(username))[0];
86+
setFilter('');
87+
newAddedUsername && onAddUser(newAddedUsername);
88+
removedUsername && onRemoveUser(removedUsername);
89+
},
90+
[onChange, setFilter, onAddUser, onRemoveUser, value],
91+
);
3092

3193
return (
32-
<AutoComplete
33-
{...props}
34-
filter={filter}
35-
setFilter={setFilter}
36-
onChange={onChange}
37-
multiple
38-
renderSelected={({ selected: { value: username, label }, onRemove, ...props }): ReactElement => (
39-
<UserAvatarChip {...props} username={username} name={label} mie={4} onClick={onRemove} />
40-
)}
41-
renderItem={({ value, label, ...props }): ReactElement => (
42-
<Option data-qa-type='autocomplete-user-option' key={value} {...props}>
43-
<OptionAvatar>
44-
<UserAvatar username={value} size='x20' />
45-
</OptionAvatar>
46-
<OptionContent>
47-
{label} <OptionDescription>({value})</OptionDescription>
48-
</OptionContent>
49-
</Option>
50-
)}
51-
options={options}
52-
/>
94+
<OptionsContext.Provider value={{ options }}>
95+
<MultiSelectFiltered
96+
{...props}
97+
data-qa-type='user-auto-complete-input'
98+
placeholder={placeholder}
99+
value={value}
100+
onChange={handleOnChange}
101+
filter={filter}
102+
setFilter={setFilter}
103+
renderSelected={({ value: username, onMouseDown }: { value: string; onMouseDown: () => void }) => {
104+
const currentCachedOption = selectedCache[username] || {};
105+
106+
return (
107+
<UserAvatarChip
108+
mie={4}
109+
mb={2}
110+
key={username}
111+
federated={currentCachedOption._federated}
112+
name={currentCachedOption.name}
113+
username={currentCachedOption.username || username}
114+
onMouseDown={onMouseDown}
115+
/>
116+
);
117+
}}
118+
renderOptions={AutocompleteOptions}
119+
options={options.concat(Object.entries(selectedCache)).map(([, item]) => [item.username, item.name || item.username])}
120+
/>
121+
</OptionsContext.Provider>
53122
);
54123
};
55124

apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx

Lines changed: 0 additions & 128 deletions
This file was deleted.

apps/meteor/client/lib/queryKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export const usersQueryKeys = {
123123
all: ['users'] as const,
124124
userInfo: ({ uid, username }: { uid?: IUser['_id']; username?: IUser['username'] }) =>
125125
[...usersQueryKeys.all, 'info', { uid, username }] as const,
126+
userAutoComplete: (filter: string, federated: boolean) => [...usersQueryKeys.all, 'autocomplete', filter, federated] as const,
126127
};
127128

128129
export const teamsQueryKeys = {

0 commit comments

Comments
 (0)