RocketChat/Rocket.Chat

View on GitHub
apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx

Summary

Maintainability
F
2 wks
Test Coverage
import {
    Box,
    Button,
    Field,
    Icon,
    Modal,
    TextInput,
    ToggleSwitch,
    FieldGroup,
    FieldLabel,
    FieldRow,
    FieldError,
    FieldDescription,
    FieldHint,
    Accordion,
    AccordionItem,
} from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import {
    useEndpoint,
    usePermission,
    usePermissionWithScopedRoles,
    useSetting,
    useToastMessageDispatch,
    useTranslation,
} from '@rocket.chat/ui-contexts';
import type { ComponentProps, ReactElement } from 'react';
import React, { memo, useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';

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

type CreateTeamModalInputs = {
    name: string;
    topic: string;
    isPrivate: boolean;
    readOnly: boolean;
    encrypted: boolean;
    broadcast: boolean;
    members?: string[];
};

type CreateTeamModalProps = { onClose: () => void };

const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => {
    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 dispatchToastMessage = useToastMessageDispatch();
    const canCreateTeam = usePermission('create-team');
    const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']);

    const checkTeamNameExists = useEndpoint('GET', '/v1/rooms.nameExists');
    const createTeamAction = useEndpoint('POST', '/v1/teams.create');

    const teamNameRegex = useMemo(() => {
        if (allowSpecialNames) {
            return null;
        }

        return new RegExp(`^${namesValidation}$`);
    }, [allowSpecialNames, namesValidation]);

    const validateTeamName = async (name: string): Promise<string | undefined> => {
        if (!name) {
            return;
        }

        if (teamNameRegex && !teamNameRegex?.test(name)) {
            return t('Name_cannot_have_special_characters');
        }

        const { exists } = await checkTeamNameExists({ roomName: name });
        if (exists) {
            return t('Teams_Errors_Already_exists', { name });
        }
    };

    const {
        register,
        control,
        handleSubmit,
        setValue,
        watch,
        formState: { errors, isSubmitting },
    } = useForm<CreateTeamModalInputs>({
        defaultValues: {
            isPrivate: true,
            readOnly: false,
            encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false,
            broadcast: false,
            members: [],
        },
    });

    const { isPrivate, broadcast, readOnly, encrypted } = watch();

    useEffect(() => {
        if (!isPrivate) {
            setValue('encrypted', false);
        }

        if (broadcast) {
            setValue('encrypted', false);
        }

        setValue('readOnly', broadcast);
    }, [watch, setValue, broadcast, isPrivate]);

    const canChangeReadOnly = !broadcast;
    const canChangeEncrypted = isPrivate && !broadcast && e2eEnabled && !e2eEnabledForPrivateByDefault;
    const getEncryptedHint = useEncryptedRoomDescription('team');

    const handleCreateTeam = async ({
        name,
        members,
        isPrivate,
        readOnly,
        topic,
        broadcast,
        encrypted,
    }: CreateTeamModalInputs): Promise<void> => {
        const params = {
            name,
            members,
            type: isPrivate ? 1 : 0,
            room: {
                readOnly,
                extraData: {
                    topic,
                    broadcast,
                    encrypted,
                },
            },
        };

        try {
            const { team } = await createTeamAction(params);
            dispatchToastMessage({ type: 'success', message: t('Team_has_been_created') });
            goToRoomById(team.roomId);
        } catch (error) {
            dispatchToastMessage({ type: 'error', message: error });
        } finally {
            onClose();
        }
    };

    const createTeamFormId = useUniqueId();
    const nameId = useUniqueId();
    const topicId = useUniqueId();
    const privateId = useUniqueId();
    const readOnlyId = useUniqueId();
    const encryptedId = useUniqueId();
    const broadcastId = useUniqueId();
    const addMembersId = useUniqueId();

    return (
        <Modal
            aria-labelledby={`${createTeamFormId}-title`}
            wrapperFunction={(props: ComponentProps<typeof Box>) => (
                <Box is='form' id={createTeamFormId} onSubmit={handleSubmit(handleCreateTeam)} {...props} />
            )}
        >
            <Modal.Header>
                <Modal.Title id={`${createTeamFormId}-title`}>{t('Teams_New_Title')}</Modal.Title>
                <Modal.Close title={t('Close')} onClick={onClose} tabIndex={-1} />
            </Modal.Header>
            <Modal.Content mbe={2}>
                <Box fontScale='p2' mbe={16}>
                    {t('Teams_new_description')}
                </Box>
                <FieldGroup mbe={24}>
                    <Field>
                        <FieldLabel required htmlFor={nameId}>
                            {t('Teams_New_Name_Label')}
                        </FieldLabel>
                        <FieldRow>
                            <TextInput
                                id={nameId}
                                aria-invalid={errors.name ? 'true' : 'false'}
                                {...register('name', {
                                    required: t('error-the-field-is-required', { field: t('Name') }),
                                    validate: (value) => validateTeamName(value),
                                })}
                                addon={<Icon size='x20' name={isPrivate ? 'team-lock' : 'team'} />}
                                error={errors.name?.message}
                                aria-describedby={`${nameId}-error ${nameId}-hint`}
                                aria-required='true'
                            />
                        </FieldRow>
                        {errors?.name && (
                            <FieldError aria-live='assertive' id={`${nameId}-error`}>
                                {errors.name.message}
                            </FieldError>
                        )}
                        {!allowSpecialNames && <FieldHint id={`${nameId}-hint`}>{t('No_spaces')}</FieldHint>}
                    </Field>
                    <Field>
                        <FieldLabel htmlFor={topicId}>{t('Topic')}</FieldLabel>
                        <FieldRow>
                            <TextInput id={topicId} aria-describedby={`${topicId}-hint`} {...register('topic')} />
                        </FieldRow>
                        <FieldRow>
                            <FieldHint id={`${topicId}-hint`}>{t('Displayed_next_to_name')}</FieldHint>
                        </FieldRow>
                    </Field>
                    <Field>
                        <FieldLabel htmlFor={addMembersId}>{t('Teams_New_Add_members_Label')}</FieldLabel>
                        <Controller
                            control={control}
                            name='members'
                            render={({ field: { onChange, value } }): ReactElement => (
                                <UserAutoCompleteMultiple id={addMembersId} value={value} onChange={onChange} placeholder={t('Add_people')} />
                            )}
                        />
                    </Field>
                    <Field>
                        <FieldRow>
                            <FieldLabel htmlFor={privateId}>{t('Teams_New_Private_Label')}</FieldLabel>
                            <Controller
                                control={control}
                                name='isPrivate'
                                render={({ field: { onChange, value, ref } }): ReactElement => (
                                    <ToggleSwitch id={privateId} aria-describedby={`${privateId}-hint`} onChange={onChange} checked={value} ref={ref} />
                                )}
                            />
                        </FieldRow>
                        <FieldDescription id={`${privateId}-hint`}>
                            {isPrivate ? t('People_can_only_join_by_being_invited') : t('Anyone_can_access')}
                        </FieldDescription>
                    </Field>
                </FieldGroup>
                <Accordion>
                    <AccordionItem title={t('Advanced_settings')}>
                        <FieldGroup>
                            <Box is='h5' fontScale='h5' color='titles-labels'>
                                {t('Security_and_permissions')}
                            </Box>
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={encryptedId}>{t('Teams_New_Encrypted_Label')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='encrypted'
                                        render={({ field: { onChange, value, ref } }): ReactElement => (
                                            <ToggleSwitch
                                                id={encryptedId}
                                                disabled={!canSetReadOnly || !canChangeEncrypted}
                                                onChange={onChange}
                                                aria-describedby={`${encryptedId}-hint`}
                                                checked={value}
                                                ref={ref}
                                            />
                                        )}
                                    />
                                </FieldRow>
                                <FieldDescription id={`${encryptedId}-hint`}>{getEncryptedHint({ isPrivate, broadcast, encrypted })}</FieldDescription>
                            </Field>
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={readOnlyId}>{t('Teams_New_Read_only_Label')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='readOnly'
                                        render={({ field: { onChange, value, ref } }): ReactElement => (
                                            <ToggleSwitch
                                                id={readOnlyId}
                                                aria-describedby={`${readOnlyId}-hint`}
                                                disabled={!canChangeReadOnly}
                                                onChange={onChange}
                                                checked={value}
                                                ref={ref}
                                            />
                                        )}
                                    />
                                </FieldRow>
                                <FieldDescription id={`${readOnlyId}-hint`}>
                                    {readOnly ? t('Read_only_field_hint_enabled', { roomType: 'team' }) : t('Anyone_can_send_new_messages')}
                                </FieldDescription>
                            </Field>
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={broadcastId}>{t('Teams_New_Broadcast_Label')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='broadcast'
                                        render={({ field: { onChange, value, ref } }): ReactElement => (
                                            <ToggleSwitch
                                                aria-describedby={`${broadcastId}-hint`}
                                                id={broadcastId}
                                                onChange={onChange}
                                                checked={value}
                                                ref={ref}
                                            />
                                        )}
                                    />
                                </FieldRow>
                                {broadcast && <FieldDescription id={`${broadcastId}-hint`}>{t('Teams_New_Broadcast_Description')}</FieldDescription>}
                            </Field>
                        </FieldGroup>
                    </AccordionItem>
                </Accordion>
            </Modal.Content>
            <Modal.Footer>
                <Modal.FooterControllers>
                    <Button onClick={onClose}>{t('Cancel')}</Button>
                    <Button disabled={!canCreateTeam} loading={isSubmitting} type='submit' primary>
                        {t('Create')}
                    </Button>
                </Modal.FooterControllers>
            </Modal.Footer>
        </Modal>
    );
};

export default memo(CreateTeamModal);