From 76c359cfa5885fc8fc8c2f363d8a6fbec4a1ac25 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 14 Jun 2022 11:41:15 -0300 Subject: [PATCH 1/2] [NEW] Create Team with an username list --- apps/meteor/app/models/server/raw/Users.js | 9 +++++++++ apps/meteor/server/services/team/service.ts | 7 ++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/models/server/raw/Users.js b/apps/meteor/app/models/server/raw/Users.js index da01f11cbd3c..4c88f56195a5 100644 --- a/apps/meteor/app/models/server/raw/Users.js +++ b/apps/meteor/app/models/server/raw/Users.js @@ -156,6 +156,15 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } + findActiveByIdsOrUsernames(userIds, options = {}) { + const query = { + $or: [{ _id: { $in: userIds } }, { username: { $in: userIds } }], + active: true, + }; + + return this.find(query, options); + } + findByIds(userIds, options = {}) { const query = { _id: { $in: userIds }, diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 7df0e05958e7..cdc21bfdef80 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -73,10 +73,11 @@ export class TeamService extends ServiceClassInternal implements ITeamService { // TODO add validations to `data` and `members` - const membersResult = await this.Users.findActiveByIds(members, { - projection: { username: 1, _id: 0 }, + const membersResult = await this.Users.findActiveByIdsOrUsernames(members, { + projection: { username: 1, _id: 1 }, }).toArray(); const memberUsernames = membersResult.map(({ username }) => username); + const memberIds = membersResult.map(({ _id }) => _id); const teamData = { ...team, @@ -96,7 +97,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { // filter empty strings and falsy values from members list const membersList: Array> = - members + memberIds ?.filter(Boolean) .filter((memberId) => !excludeFromMembers.includes(memberId)) .map((memberId) => ({ From d3a37c7ac2f5fdc857616114945cc60c8d34d58d Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 14 Jun 2022 15:12:07 -0300 Subject: [PATCH 2/2] fix: add members on teams create --- .../teams/CreateTeamModal/CreateTeamModal.tsx | 211 +----------------- .../teams/CreateTeamModal/UsersInput.tsx | 90 -------- .../useCreateTeamModalState.ts | 193 ++++++++++++++++ .../client/views/teams/{index.js => index.ts} | 0 4 files changed, 201 insertions(+), 293 deletions(-) delete mode 100644 apps/meteor/client/views/teams/CreateTeamModal/UsersInput.tsx create mode 100644 apps/meteor/client/views/teams/CreateTeamModal/useCreateTeamModalState.ts rename apps/meteor/client/views/teams/{index.js => index.ts} (100%) diff --git a/apps/meteor/client/views/teams/CreateTeamModal/CreateTeamModal.tsx b/apps/meteor/client/views/teams/CreateTeamModal/CreateTeamModal.tsx index c035fa4a7134..d131b403d925 100644 --- a/apps/meteor/client/views/teams/CreateTeamModal/CreateTeamModal.tsx +++ b/apps/meteor/client/views/teams/CreateTeamModal/CreateTeamModal.tsx @@ -1,207 +1,13 @@ -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[]; - }; - - 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(); - - 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 = ({ onClose }) => { +const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => { const { name, nameError, @@ -226,14 +32,13 @@ const CreateTeamModal: FC = ({ onClose }) => { } = useCreateTeamModalState(onClose); const t = useTranslation(); - const focusRef = useAutoFocus(); return ( {t('Teams_New_Title')} - + @@ -310,7 +115,7 @@ const CreateTeamModal: FC = ({ onClose }) => { ({t('optional')}) - + diff --git a/apps/meteor/client/views/teams/CreateTeamModal/UsersInput.tsx b/apps/meteor/client/views/teams/CreateTeamModal/UsersInput.tsx deleted file mode 100644 index 13126e2c6838..000000000000 --- a/apps/meteor/client/views/teams/CreateTeamModal/UsersInput.tsx +++ /dev/null @@ -1,90 +0,0 @@ -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(() => { - 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 = ({ 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>( - ({ value: selected }) => ( - <> - {selected?.map((value) => ( - - - - {labelData[value]} - - - ))} - - ), - [onClickSelected, props, labelData], - ); - - const renderItem = useCallback>( - ({ value, ...props }) => ( -