Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-eels-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes members tab > add members not removing selected items
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { useId, useEffect, useMemo } from 'react';
import { useForm, Controller } from 'react-hook-form';

import { useEncryptedRoomDescription } from './useEncryptedRoomDescription';
import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated';
import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple';
import { useCreateChannelTypePermission } from '../../../hooks/useCreateChannelTypePermission';
import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule';
import { useIsFederationEnabled } from '../../../hooks/useIsFederationEnabled';
Expand Down Expand Up @@ -260,7 +260,13 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal
!federated && hasExternalMembers(members) ? t('You_cannot_add_external_users_to_non_federated_room') : true,
}}
render={({ field: { onChange, value } }): ReactElement => (
<UserAutoCompleteMultipleFederated id={addMembersId} value={value} onChange={onChange} placeholder={t('Add_people')} />
<UserAutoCompleteMultiple
id={addMembersId}
value={value}
onChange={onChange}
federated={federated}
placeholder={t('Add_people')}
/>
)}
/>
{errors.members && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useMutation } from '@tanstack/react-query';
import { useId, memo } from 'react';
import { useForm, Controller } from 'react-hook-form';

import UserAutoCompleteMultipleFederated from '../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated';
import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple';
import { goToRoomById } from '../../../lib/utils/goToRoomById';

type CreateDirectMessageProps = { onClose: () => void };
Expand Down Expand Up @@ -78,11 +78,12 @@ const CreateDirectMessage = ({ onClose }: CreateDirectMessageProps) => {
}}
control={control}
render={({ field: { name, onChange, value, onBlur } }) => (
<UserAutoCompleteMultipleFederated
<UserAutoCompleteMultiple
name={name}
onChange={onChange}
value={value}
onBlur={onBlur}
federated
id={membersFieldId}
aria-describedby={`${membersFieldId}-hint ${membersFieldId}-error`}
aria-required='true'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';

import type { IGame } from './GameCenter';
import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated';
import UserAutoCompleteMultiple from '../../components/UserAutoCompleteMultiple';
import { useOpenedRoom } from '../../lib/RoomManager';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { callWithErrorHandling } from '../../lib/utils/callWithErrorHandling';
Expand Down Expand Up @@ -57,7 +57,7 @@ const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayer
<GenericModal onClose={onClose} onCancel={onClose} onConfirm={sendInvite} title={t('Apps_Game_Center_Invite_Friends')}>
<Box mbe={16}>{t('Invite_Users')}</Box>
<Box mbe={16} display='flex' justifyContent='stretch'>
<UserAutoCompleteMultipleFederated value={users} onChange={setUsers} />
<UserAutoCompleteMultiple value={users} onChange={setUsers} federated />
</Box>
</GenericModal>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,124 @@
import { AutoComplete, OptionAvatar, Option, OptionContent, OptionDescription } from '@rocket.chat/fuselage';
import { MultiSelectFiltered } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ComponentProps, ReactElement } from 'react';
import { memo, useMemo, useState } from 'react';
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import type { ReactElement, AllHTMLAttributes } from 'react';
import { memo, useState, useCallback, useMemo } from 'react';

import AutocompleteOptions, { OptionsContext } from './UserAutoCompleteMultipleOptions';
import UserAvatarChip from './UserAvatarChip';
import { usersQueryKeys } from '../../lib/queryKeys';

const query = (
term = '',
): {
selector: string;
} => ({ selector: JSON.stringify({ term }) });
type UserAutoCompleteMultipleProps = {
onChange: (value: Array<string>) => void;
value: Array<string> | undefined;
placeholder?: string;
federated?: boolean;
error?: string;
} & Omit<AllHTMLAttributes<HTMLInputElement>, 'is' | 'onChange' | 'value'>;

type UserAutoCompleteMultipleProps = Omit<ComponentProps<typeof AutoComplete>, 'filter'>;
type UserAutoCompleteOptionType = {
name: string;
username: string;
_federated?: boolean;
};

type UserAutoCompleteOptions = {
[k: string]: UserAutoCompleteOptionType;
};

// TODO: useDisplayUsername
const UserAutoCompleteMultiple = ({ onChange, ...props }: UserAutoCompleteMultipleProps): ReactElement => {
const matrixRegex = new RegExp('@(.*:.*)');

const UserAutoCompleteMultiple = ({ onChange, value, placeholder, federated, ...props }: UserAutoCompleteMultipleProps): ReactElement => {
const [filter, setFilter] = useState('');
const debouncedFilter = useDebouncedValue(filter, 1000);
const usersAutoCompleteEndpoint = useEndpoint('GET', '/v1/users.autocomplete');
const [selectedCache, setSelectedCache] = useState<UserAutoCompleteOptions>({});

const debouncedFilter = useDebouncedValue(filter, 500);
const getUsers = useEndpoint('GET', '/v1/users.autocomplete');

const { data } = useQuery({
queryKey: ['usersAutoComplete', debouncedFilter],
queryFn: async () => usersAutoCompleteEndpoint(query(debouncedFilter)),
queryKey: usersQueryKeys.userAutoComplete(debouncedFilter, federated ?? false),

queryFn: async () => {
const users = await getUsers({ selector: JSON.stringify({ term: debouncedFilter }) });
const options = users.items.map((item): [string, UserAutoCompleteOptionType] => [item.username, item]);

// Add extra option if filter text matches `username:server`
// Used to add federated users that do not exist yet
if (federated && matrixRegex.test(debouncedFilter)) {
options.unshift([debouncedFilter, { name: debouncedFilter, username: debouncedFilter, _federated: true }]);
}

return options;
},

placeholderData: keepPreviousData,
});

const options = useMemo(() => data?.items.map((user) => ({ value: user.username, label: user.name })) || [], [data]);
const options = useMemo(() => data || [], [data]);

const onAddUser = useCallback(
(username: string): void => {
const user = options?.find(([val]) => val === username)?.[1];
if (!user) {
throw new Error('UserAutoCompleteMultiple - onAddSelected - failed to cache option');
}
setSelectedCache((selectedCache) => ({ ...selectedCache, [username]: user }));
},
[setSelectedCache, options],
);

const onRemoveUser = useCallback(
(username: string): void =>
setSelectedCache((selectedCache) => {
const users = { ...selectedCache };
delete users[username];
return users;
}),
[setSelectedCache],
);

const handleOnChange = useCallback(
(usernames: string[]) => {
onChange(usernames);
const newAddedUsername = usernames.filter((username) => !value?.includes(username))[0];
const removedUsername = value?.filter((username) => !usernames.includes(username))[0];
setFilter('');
newAddedUsername && onAddUser(newAddedUsername);
removedUsername && onRemoveUser(removedUsername);
},
[onChange, setFilter, onAddUser, onRemoveUser, value],
);

return (
<AutoComplete
{...props}
filter={filter}
setFilter={setFilter}
onChange={onChange}
multiple
renderSelected={({ selected: { value: username, label }, onRemove, ...props }): ReactElement => (
<UserAvatarChip {...props} username={username} name={label} mie={4} onClick={onRemove} />
)}
renderItem={({ value, label, ...props }): ReactElement => (
<Option data-qa-type='autocomplete-user-option' key={value} {...props}>
<OptionAvatar>
<UserAvatar username={value} size='x20' />
</OptionAvatar>
<OptionContent>
{label} <OptionDescription>({value})</OptionDescription>
</OptionContent>
</Option>
)}
options={options}
/>
<OptionsContext.Provider value={{ options }}>
<MultiSelectFiltered
{...props}
data-qa-type='user-auto-complete-input'
placeholder={placeholder}
value={value}
onChange={handleOnChange}
filter={filter}
setFilter={setFilter}
renderSelected={({ value: username, onMouseDown }: { value: string; onMouseDown: () => void }) => {
const currentCachedOption = selectedCache[username] || {};

return (
<UserAvatarChip
mie={4}
mb={2}
key={username}
federated={currentCachedOption._federated}
name={currentCachedOption.name}
username={currentCachedOption.username || username}
onMouseDown={onMouseDown}
/>
);
}}
renderOptions={AutocompleteOptions}
options={options.concat(Object.entries(selectedCache)).map(([, item]) => [item.username, item.name || item.username])}
/>
</OptionsContext.Provider>
);
};

Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions apps/meteor/client/lib/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export const usersQueryKeys = {
all: ['users'] as const,
userInfo: ({ uid, username }: { uid?: IUser['_id']; username?: IUser['username'] }) =>
[...usersQueryKeys.all, 'info', { uid, username }] as const,
userAutoComplete: (filter: string, federated: boolean) => [...usersQueryKeys.all, 'autocomplete', filter, federated] as const,
};

export const teamsQueryKeys = {
Expand Down
Loading
Loading