Skip to content
Snippets Groups Projects
Unverified Commit 9a41c4fa authored by Douglas Fabris's avatar Douglas Fabris Committed by GitHub
Browse files

[FIX] Members selection field on creating team modal (#25871)

<!-- This is a pull request template, you do not need to uncomment or remove the comments, they won't show up in the PR text. -->

<!-- Your Pull Request name should start with one of the following tags
  [NEW] For new features
  [IMPROVE] For an improvement (performance or little improvements) in existing features
  [FIX] For bug fixes that affect the end-user
  [BREAK] For pull requests including breaking changes
  Chore: For small tasks
  Doc: For documentation
-->

<!-- Checklist!!! If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. 
  - I have read the Contributing Guide - https://github.com/RocketChat/Rocket.Chat/blob/develop/.github/CONTRIBUTING.md#contributing-to-rocketchat doc
  - I have signed the CLA - https://cla-assistant.io/RocketChat/Rocket.Chat
  - Lint and unit tests pass locally with my changes
  - I have added tests that prove my fix is effective or that my feature works (if applicable)
  - I have added necessary documentation (if applicable)
  - Any dependent changes have been merged and published in downstream modules
-->

## Proposed changes (including videos or screenshots)
<!-- CHANGELOG -->
<!--
  Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
  If it fixes a bug or resolves a feature request, be sure to link to that issue below.
  This description will appear in the release notes if we accept the contribution.
-->
- Fix: add members breaking when searching users

![image](https://user-images.githubusercontent.com/27704687/121788070-b792f700-cba0-11eb-92b9-5833e1213c74.png

)

<!-- END CHANGELOG -->

## Issue(s)
<!-- Link the issues being closed by or related to this PR. For example, you can use #594 if this PR closes issue number 594 -->

## Steps to test or reproduce
<!-- Mention how you would reproduce the bug if not mentioned on the issue page already. Also mention which screens are going to have the changes if applicable -->

## Further comments
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->


Co-authored-by: default avatarPierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>
parent acb74f1c
No related branches found
No related tags found
No related merge requests found
import type { IUser } from '@rocket.chat/core-typings';
import { Box, Modal, ButtonGroup, Button, TextInput, Field, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage';
import { useMutableCallback, useDebouncedCallback, useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { useSetting, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { memo, ReactElement } from 'react';
import { useEndpointActionExperimental } from '../../../hooks/useEndpointActionExperimental';
import { useForm } from '../../../hooks/useForm';
import { goToRoomById } from '../../../lib/utils/goToRoomById';
import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple';
import TeamNameInput from './TeamNameInput';
import UsersInput from './UsersInput';
import { useCreateTeamModalState } from './useCreateTeamModalState';
type CreateTeamModalState = {
name: any;
nameError: any;
onChangeName: any;
description: any;
onChangeDescription: any;
type: any;
onChangeType: any;
readOnly: any;
canChangeReadOnly: any;
onChangeReadOnly: any;
encrypted: any;
canChangeEncrypted: any;
onChangeEncrypted: any;
broadcast: any;
onChangeBroadcast: any;
members: any;
onChangeMembers: any;
hasUnsavedChanges: any;
isCreateButtonEnabled: any;
onCreate: any;
};
const useCreateTeamModalState = (onClose: () => void): CreateTeamModalState => {
const e2eEnabled = useSetting('E2E_Enable');
const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms');
const namesValidation = useSetting('UTF8_Channel_Names_Validation');
const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars');
const { values, handlers, hasUnsavedChanges } = useForm({
members: [],
name: '',
description: '',
type: true,
readOnly: false,
encrypted: e2eEnabledForPrivateByDefault ?? false,
broadcast: false,
});
const { name, description, type, readOnly, broadcast, encrypted, members } = values as {
name: string;
description: string;
type: boolean;
readOnly: boolean;
broadcast: boolean;
encrypted: boolean;
members: Exclude<IUser['username'], undefined>[];
};
const { handleMembers, handleEncrypted, handleType, handleBroadcast, handleReadOnly } = handlers;
const t = useTranslation();
const teamNameRegex = useMemo(() => {
if (allowSpecialNames) {
return null;
}
return new RegExp(`^${namesValidation}$`);
}, [allowSpecialNames, namesValidation]);
const [nameError, setNameError] = useState<string>();
const teamNameExists = useMethod('roomNameExists');
const checkName = useDebouncedCallback(
async (name: string) => {
setNameError(undefined);
if (!hasUnsavedChanges) {
return;
}
if (!name || name.length === 0) {
setNameError(t('Field_required'));
return;
}
if (teamNameRegex && !teamNameRegex.test(name)) {
setNameError(t('error-invalid-name'));
return;
}
const isNotAvailable = await teamNameExists(name);
if (isNotAvailable) {
setNameError(t('Teams_Errors_team_name', { name }));
}
},
230,
[name],
);
useEffect(() => {
checkName(name);
}, [checkName, name]);
const canChangeReadOnly = !broadcast;
const canChangeEncrypted = type && !broadcast && e2eEnabled && !e2eEnabledForPrivateByDefault;
const onChangeName = handlers.handleName;
const onChangeDescription = handlers.handleDescription;
const onChangeType = useMutableCallback((value) => {
handleEncrypted(!value);
return handleType(value);
});
const onChangeReadOnly = handlers.handleReadOnly;
const onChangeEncrypted = handlers.handleEncrypted;
const onChangeBroadcast = useCallback(
(value) => {
handleEncrypted(!value);
handleReadOnly(value);
return handleBroadcast(value);
},
[handleBroadcast, handleEncrypted, handleReadOnly],
);
const onChangeMembers = useCallback(
(value, action) => {
if (!action) {
if (members.includes(value)) {
return;
}
return handleMembers([...members, value]);
}
handleMembers(members.filter((current) => current !== value));
},
[handleMembers, members],
);
const canSave = hasUnsavedChanges && !nameError;
const canCreateTeam = usePermission('create-team');
const isCreateButtonEnabled = canSave && canCreateTeam;
const createTeam = useEndpointActionExperimental('POST', '/v1/teams.create');
const onCreate = useCallback(async () => {
const params = {
name,
members,
type: type ? 1 : 0,
room: {
readOnly,
extraData: {
description,
broadcast,
encrypted,
},
},
};
const data = await createTeam(params);
goToRoomById(data.team.roomId);
onClose();
}, [name, members, type, readOnly, description, broadcast, encrypted, createTeam, onClose]);
return {
name,
nameError,
onChangeName,
description,
onChangeDescription,
type,
onChangeType,
readOnly,
canChangeReadOnly,
onChangeReadOnly,
encrypted,
canChangeEncrypted,
onChangeEncrypted,
broadcast,
onChangeBroadcast,
members,
onChangeMembers,
hasUnsavedChanges,
isCreateButtonEnabled,
onCreate,
};
};
type CreateTeamModalProps = {
onClose: () => void;
};
const CreateTeamModal: FC<CreateTeamModalProps> = ({ onClose }) => {
const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => {
const {
name,
nameError,
......@@ -226,14 +32,13 @@ const CreateTeamModal: FC<CreateTeamModalProps> = ({ onClose }) => {
} = useCreateTeamModalState(onClose);
const t = useTranslation();
const focusRef = useAutoFocus<HTMLInputElement>();
return (
<Modal>
<Modal.Header>
<Modal.Title>{t('Teams_New_Title')}</Modal.Title>
<Modal.Close onClick={onClose} />
<Modal.Close title={t('Close')} onClick={onClose} />
</Modal.Header>
<Modal.Content>
<FieldGroup>
......@@ -310,7 +115,7 @@ const CreateTeamModal: FC<CreateTeamModalProps> = ({ onClose }) => {
({t('optional')})
</Box>
</Field.Label>
<UsersInput value={members} onChange={onChangeMembers} />
<UserAutoCompleteMultiple value={members} onChange={onChangeMembers} />
</Field>
</FieldGroup>
</Modal.Content>
......
import { AutoComplete, Box, Option, Options, Chip, AutoCompleteProps } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { FC, memo, useCallback, useMemo, useState } from 'react';
import UserAvatar from '../../../components/avatar/UserAvatar';
import { useEndpointData } from '../../../hooks/useEndpointData';
type UsersInputProps = {
value: unknown[];
onChange: (value: unknown, action: 'remove' | undefined) => void;
};
type AutocompleteData = [AutoCompleteProps['options'], { [key: string]: string | undefined }];
const useUsersAutoComplete = (term: string): AutocompleteData => {
const params = useMemo(
() => ({
selector: JSON.stringify({ term }),
}),
[term],
);
const { value: data } = useEndpointData('/v1/users.autocomplete', params);
return useMemo<AutocompleteData>(() => {
if (!data) {
return [[], {}];
}
const options =
data.items.map((user) => ({
label: user.name ?? '',
value: user._id ?? '',
})) || [];
const labelData = Object.fromEntries(data.items.map((user) => [user._id, user.username]) || []);
return [options, labelData];
}, [data]);
};
const UsersInput: FC<UsersInputProps> = ({ onChange, ...props }) => {
const [filter, setFilter] = useState('');
const [options, labelData] = useUsersAutoComplete(useDebouncedValue(filter, 1000));
const onClickSelected = useCallback(
(e) => {
e.stopPropagation();
e.preventDefault();
onChange(e.currentTarget.value, 'remove');
},
[onChange],
);
const renderSelected = useCallback<FC<{ value?: string[] }>>(
({ value: selected }) => (
<>
{selected?.map((value) => (
<Chip key={value} {...props} height='x20' value={value} onClick={onClickSelected} mie='x4'>
<UserAvatar size='x20' username={labelData[value] as string} />
<Box is='span' margin='none' mis='x4'>
{labelData[value]}
</Box>
</Chip>
))}
</>
),
[onClickSelected, props, labelData],
);
const renderItem = useCallback<FC<{ value: string }>>(
({ value, ...props }) => (
<Option key={value} {...props} avatar={<UserAvatar size={Options.AvatarSize} username={labelData[value] as string} />} />
),
[labelData],
);
return (
<AutoComplete
{...props}
filter={filter}
options={options}
renderSelected={renderSelected}
renderItem={renderItem}
setFilter={setFilter}
onChange={onChange}
/>
);
};
export default memo(UsersInput);
import type { IUser } from '@rocket.chat/core-typings';
import { useMutableCallback, useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import { useSetting, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEndpointActionExperimental } from '../../../hooks/useEndpointActionExperimental';
import { useForm } from '../../../hooks/useForm';
import { goToRoomById } from '../../../lib/utils/goToRoomById';
// TODO: Type it correctly when rewrit it using react-hook-form
type CreateTeamModalState = {
name: any;
nameError: any;
onChangeName: any;
description: any;
onChangeDescription: any;
type: any;
onChangeType: any;
readOnly: any;
canChangeReadOnly: any;
onChangeReadOnly: any;
encrypted: any;
canChangeEncrypted: any;
onChangeEncrypted: any;
broadcast: any;
onChangeBroadcast: any;
members: any;
onChangeMembers: any;
hasUnsavedChanges: any;
isCreateButtonEnabled: any;
onCreate: any;
};
export const useCreateTeamModalState = (onClose: () => void): CreateTeamModalState => {
const t = useTranslation();
const e2eEnabled = useSetting('E2E_Enable');
const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms');
const namesValidation = useSetting('UTF8_Channel_Names_Validation');
const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars');
const { values, handlers, hasUnsavedChanges } = useForm({
members: [],
name: '',
description: '',
type: true,
readOnly: false,
encrypted: e2eEnabledForPrivateByDefault ?? false,
broadcast: false,
});
const { name, description, type, readOnly, broadcast, encrypted, members } = values as {
name: string;
description: string;
type: boolean;
readOnly: boolean;
broadcast: boolean;
encrypted: boolean;
members: Exclude<IUser['username'], undefined>[];
};
const { handleMembers, handleEncrypted, handleType, handleBroadcast, handleReadOnly } = handlers;
const teamNameRegex = useMemo(() => {
if (allowSpecialNames) {
return null;
}
return new RegExp(`^${namesValidation}$`);
}, [allowSpecialNames, namesValidation]);
const [nameError, setNameError] = useState<string>();
const teamNameExists = useMethod('roomNameExists');
const checkName = useDebouncedCallback(
async (name: string) => {
setNameError(undefined);
if (!hasUnsavedChanges) {
return;
}
if (!name || name.length === 0) {
setNameError(t('Field_required'));
return;
}
if (teamNameRegex && !teamNameRegex.test(name)) {
setNameError(t('error-invalid-name'));
return;
}
const isNotAvailable = await teamNameExists(name);
if (isNotAvailable) {
setNameError(t('Teams_Errors_team_name', { name }));
}
},
230,
[name],
);
useEffect(() => {
checkName(name);
}, [checkName, name]);
const canChangeReadOnly = !broadcast;
const canChangeEncrypted = type && !broadcast && e2eEnabled && !e2eEnabledForPrivateByDefault;
const onChangeName = handlers.handleName;
const onChangeDescription = handlers.handleDescription;
const onChangeType = useMutableCallback((value) => {
handleEncrypted(!value);
return handleType(value);
});
const onChangeReadOnly = handlers.handleReadOnly;
const onChangeEncrypted = handlers.handleEncrypted;
const onChangeBroadcast = useCallback(
(value) => {
handleEncrypted(!value);
handleReadOnly(value);
return handleBroadcast(value);
},
[handleBroadcast, handleEncrypted, handleReadOnly],
);
const onChangeMembers = useCallback(
(value, action) => {
if (!action) {
if (members.includes(value)) {
return;
}
return handleMembers([...members, value]);
}
handleMembers(members.filter((current) => current !== value));
},
[handleMembers, members],
);
const canSave = hasUnsavedChanges && !nameError;
const canCreateTeam = usePermission('create-team');
const isCreateButtonEnabled = canSave && canCreateTeam;
const createTeam = useEndpointActionExperimental('POST', '/v1/teams.create');
const onCreate = useCallback(async () => {
const params = {
name,
members,
type: type ? 1 : 0,
room: {
readOnly,
extraData: {
description,
broadcast,
encrypted,
},
},
};
const data = await createTeam(params);
goToRoomById(data.team.roomId);
onClose();
}, [name, members, type, readOnly, description, broadcast, encrypted, createTeam, onClose]);
return {
name,
nameError,
onChangeName,
description,
onChangeDescription,
type,
onChangeType,
readOnly,
canChangeReadOnly,
onChangeReadOnly,
encrypted,
canChangeEncrypted,
onChangeEncrypted,
broadcast,
onChangeBroadcast,
members,
onChangeMembers,
hasUnsavedChanges,
isCreateButtonEnabled,
onCreate,
};
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment