RocketChat/Rocket.Chat

View on GitHub
apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx

Summary

Maintainability
F
5 days
Test Coverage
import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings';
import { isRoomFederated } from '@rocket.chat/core-typings';
import type { SelectOption } from '@rocket.chat/fuselage';
import {
    FieldError,
    Field,
    FieldRow,
    FieldLabel,
    FieldHint,
    TextInput,
    PasswordInput,
    ToggleSwitch,
    MultiSelect,
    Accordion,
    Callout,
    NumberInput,
    FieldGroup,
    Button,
    ButtonGroup,
    Box,
    TextAreaInput,
} from '@rocket.chat/fuselage';
import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useSetting, useTranslation, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
import React, { useMemo } from 'react';
import { useForm, Controller } from 'react-hook-form';

import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes';
import {
    ContextualbarHeader,
    ContextualbarBack,
    ContextualbarTitle,
    ContextualbarClose,
    ContextualbarScrollableContent,
    ContextualbarFooter,
} from '../../../../../components/Contextualbar';
import RawText from '../../../../../components/RawText';
import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor';
import { getDirtyFields } from '../../../../../lib/getDirtyFields';
import { useArchiveRoom } from '../../../../hooks/roomActions/useArchiveRoom';
import { useEditRoomInitialValues } from './useEditRoomInitialValues';
import { useEditRoomPermissions } from './useEditRoomPermissions';

type EditRoomInfoProps = {
    room: IRoomWithRetentionPolicy;
    onClickClose: () => void;
    onClickBack: () => void;
};

const title = {
    team: 'Edit_team' as TranslationKey,
    channel: 'Edit_channel' as TranslationKey,
    discussion: 'Edit_discussion' as TranslationKey,
};

const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => {
    const t = useTranslation();
    const dispatchToastMessage = useToastMessageDispatch();
    const isFederated = useMemo(() => isRoomFederated(room), [room]);
    // eslint-disable-next-line no-nested-ternary
    const roomType = 'prid' in room ? 'discussion' : room.teamId ? 'team' : 'channel';

    const retentionPolicy = useSetting<boolean>('RetentionPolicy_Enabled');
    const defaultValues = useEditRoomInitialValues(room);
    const namesValidation = useSetting('UTF8_Channel_Names_Validation');
    const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars');
    const checkTeamNameExists = useEndpoint('GET', '/v1/rooms.nameExists');

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

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

    const {
        watch,
        reset,
        control,
        handleSubmit,
        getFieldState,
        formState: { isDirty, dirtyFields, errors, isSubmitting },
    } = useForm({ mode: 'onBlur', defaultValues });

    const sysMesOptions: SelectOption[] = useMemo(
        () => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel as TranslationKey)]),
        [t],
    );

    const { isDirty: isRoomNameDirty } = getFieldState('roomName');

    const {
        readOnly,
        archived,
        joinCodeRequired,
        hideSysMes,
        retentionEnabled,
        retentionMaxAge,
        retentionOverrideGlobal,
        roomType: roomTypeP,
        reactWhenReadOnly,
    } = watch();

    const {
        canChangeType,
        canSetReadOnly,
        canSetReactWhenReadOnly,
        canEditRoomRetentionPolicy,
        canArchiveOrUnarchive,
        canToggleEncryption,
        canViewName,
        canViewTopic,
        canViewAnnouncement,
        canViewArchived,
        canViewDescription,
        canViewType,
        canViewReadOnly,
        canViewHideSysMes,
        canViewJoinCode,
        canViewEncrypted,
    } = useEditRoomPermissions(room);

    const changeArchiving = archived !== !!room.archived;

    const saveAction = useEndpoint('POST', '/v1/rooms.saveRoomSettings');

    const handleArchive = useArchiveRoom(room);

    const handleUpdateRoomData = useEffectEvent(async ({ hideSysMes, joinCodeRequired, ...formData }) => {
        const data = getDirtyFields(formData, dirtyFields);
        delete data.archived;

        try {
            await saveAction({
                rid: room._id,
                ...data,
                ...((data.joinCode || 'joinCodeRequired' in data) && { joinCode: joinCodeRequired ? data.joinCode : '' }),
                ...((data.systemMessages || !hideSysMes) && {
                    systemMessages: hideSysMes && data.systemMessages,
                }),
            });

            dispatchToastMessage({ type: 'success', message: t('Room_updated_successfully') });
            onClickClose();
        } catch (error) {
            dispatchToastMessage({ type: 'error', message: error });
        }
    });

    const handleSave = useEffectEvent((data) =>
        Promise.all([isDirty && handleUpdateRoomData(data), changeArchiving && handleArchive()].filter(Boolean)),
    );

    const validateName = async (name: string): Promise<string | undefined> => {
        if (!name || !isRoomNameDirty) return;
        if (roomType === 'discussion') 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 formId = useUniqueId();
    const roomNameField = useUniqueId();
    const roomDescriptionField = useUniqueId();
    const roomAnnouncementField = useUniqueId();
    const roomTopicField = useUniqueId();
    const roomTypeField = useUniqueId();
    const readOnlyField = useUniqueId();
    const reactWhenReadOnlyField = useUniqueId();
    const archivedField = useUniqueId();
    const joinCodeRequiredField = useUniqueId();
    const hideSysMesField = useUniqueId();
    const encryptedField = useUniqueId();
    const retentionEnabledField = useUniqueId();
    const retentionOverrideGlobalField = useUniqueId();
    const retentionMaxAgeField = useUniqueId();
    const retentionExcludePinnedField = useUniqueId();
    const retentionFilesOnlyField = useUniqueId();

    return (
        <>
            <ContextualbarHeader>
                {onClickBack && <ContextualbarBack onClick={onClickBack} />}
                <ContextualbarTitle>{t(`${title[roomType]}`)}</ContextualbarTitle>
                {onClickClose && <ContextualbarClose onClick={onClickClose} />}
            </ContextualbarHeader>
            <ContextualbarScrollableContent p={24}>
                <form id={formId} onSubmit={handleSubmit(handleSave)}>
                    <Box display='flex' justifyContent='center'>
                        <Controller
                            control={control}
                            name='roomAvatar'
                            render={({ field: { onChange, value } }) => <RoomAvatarEditor room={room} roomAvatar={value} onChangeAvatar={onChange} />}
                        />
                    </Box>
                    <FieldGroup>
                        <Field>
                            <FieldLabel htmlFor={roomNameField} required>
                                {t('Name')}
                            </FieldLabel>
                            <FieldRow>
                                <Controller
                                    name='roomName'
                                    control={control}
                                    rules={{
                                        required: t('error-the-field-is-required', { field: t('Name') }),
                                        validate: (value) => validateName(value),
                                    }}
                                    render={({ field }) => (
                                        <TextInput
                                            id={roomNameField}
                                            {...field}
                                            disabled={!canViewName}
                                            aria-invalid={errors.roomName ? 'true' : 'false'}
                                            aria-describedby={`${roomNameField}-error`}
                                            aria-required='true'
                                        />
                                    )}
                                />
                            </FieldRow>
                            {errors.roomName && <FieldError id={`${roomNameField}-error`}>{errors.roomName.message}</FieldError>}
                        </Field>
                        {canViewTopic && (
                            <Field>
                                <FieldLabel htmlFor={roomTopicField}>{t('Topic')}</FieldLabel>
                                <FieldRow>
                                    <Controller
                                        name='roomTopic'
                                        control={control}
                                        render={({ field }) => <TextInput id={roomTopicField} aria-describedby={`${roomTopicField}-hint`} {...field} />}
                                    />
                                </FieldRow>
                                <FieldRow>
                                    <FieldHint id={`${roomTopicField}-hint`}>{t('Displayed_next_to_name')}</FieldHint>
                                </FieldRow>
                            </Field>
                        )}
                        {canViewAnnouncement && (
                            <Field>
                                <FieldLabel htmlFor={roomAnnouncementField}>{t('Announcement')}</FieldLabel>
                                <FieldRow>
                                    <Controller
                                        name='roomAnnouncement'
                                        control={control}
                                        render={({ field }) => (
                                            <TextInput
                                                id={roomAnnouncementField}
                                                aria-describedby={`${roomAnnouncementField}-hint`}
                                                {...field}
                                                disabled={isFederated}
                                            />
                                        )}
                                    />
                                </FieldRow>
                                <FieldRow>
                                    <FieldHint id={`${roomAnnouncementField}-hint`}>{t('Information_to_keep_top_of_mind')}</FieldHint>
                                </FieldRow>
                            </Field>
                        )}
                        {canViewDescription && (
                            <Field>
                                <FieldLabel htmlFor={roomDescriptionField}>{t('Description')}</FieldLabel>
                                <FieldRow>
                                    <Controller
                                        name='roomDescription'
                                        control={control}
                                        render={({ field }) => <TextAreaInput id={roomDescriptionField} {...field} disabled={isFederated} rows={4} />}
                                    />
                                </FieldRow>
                            </Field>
                        )}

                        {canViewType && (
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={roomTypeField}>{t('Private')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='roomType'
                                        render={({ field: { name, onBlur, onChange, value, ref } }) => (
                                            <ToggleSwitch
                                                id={roomTypeField}
                                                ref={ref}
                                                name={name}
                                                onBlur={onBlur}
                                                disabled={!canChangeType || isFederated}
                                                checked={value === 'p'}
                                                onChange={() => onChange(value === 'p' ? 'c' : 'p')}
                                                aria-describedby={`${roomTypeField}-hint`}
                                            />
                                        )}
                                    />
                                </FieldRow>
                                <FieldRow>
                                    <FieldHint id={`${roomTypeField}-hint`}>
                                        {roomTypeP === 'p' ? t('Only_invited_people') : t('Anyone_can_access')}
                                    </FieldHint>
                                </FieldRow>
                            </Field>
                        )}
                        {canViewReadOnly && (
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={readOnlyField}>{t('Read_only')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='readOnly'
                                        render={({ field: { value, ...field } }) => (
                                            <ToggleSwitch
                                                id={readOnlyField}
                                                {...field}
                                                checked={value}
                                                disabled={!canSetReadOnly || isFederated}
                                                aria-describedby={`${readOnlyField}-hint`}
                                            />
                                        )}
                                    />
                                </FieldRow>
                                <FieldHint id={`${readOnlyField}-hint`}>
                                    {readOnly ? t('Read_only_field_hint_enabled', { roomType }) : t('Read_only_field_hint_disabled')}
                                </FieldHint>
                            </Field>
                        )}
                        {readOnly && (
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={reactWhenReadOnlyField}>{t('React_when_read_only')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='reactWhenReadOnly'
                                        render={({ field: { value, ...field } }) => (
                                            <ToggleSwitch
                                                id={reactWhenReadOnlyField}
                                                {...field}
                                                disabled={!canSetReactWhenReadOnly}
                                                checked={value}
                                                aria-describedby={`${reactWhenReadOnlyField}-hint`}
                                            />
                                        )}
                                    />
                                </FieldRow>
                                <FieldRow>
                                    <FieldHint id={`${reactWhenReadOnlyField}-hint`}>
                                        {reactWhenReadOnly ? t('Anyone_can_react_to_messages') : t('Only_authorized_users_can_react_to_messages')}
                                    </FieldHint>
                                </FieldRow>
                            </Field>
                        )}
                        {canViewArchived && (
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={archivedField}>{t('Room_archivation_state_true')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='archived'
                                        render={({ field: { value, ...field } }) => (
                                            <ToggleSwitch
                                                id={archivedField}
                                                aria-describedby={`${archivedField}-hint`}
                                                {...field}
                                                disabled={!canArchiveOrUnarchive}
                                                checked={value}
                                            />
                                        )}
                                    />
                                </FieldRow>
                                {archived && (
                                    <FieldRow>
                                        <FieldHint id={`${archivedField}-hint`}>{t('New_messages_cannot_be_sent')}</FieldHint>
                                    </FieldRow>
                                )}
                            </Field>
                        )}
                        {canViewJoinCode && (
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={joinCodeRequiredField}>{t('Password_to_access')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='joinCodeRequired'
                                        render={({ field: { value, ...field } }) => (
                                            <ToggleSwitch id={joinCodeRequiredField} {...field} disabled={isFederated} checked={value} />
                                        )}
                                    />
                                </FieldRow>
                                {joinCodeRequired && (
                                    <FieldRow>
                                        <Controller
                                            name='joinCode'
                                            control={control}
                                            render={({ field }) => <PasswordInput {...field} placeholder={t('Reset_password')} disabled={!joinCodeRequired} />}
                                        />
                                    </FieldRow>
                                )}
                            </Field>
                        )}
                        {canViewHideSysMes && (
                            <Field>
                                <FieldRow>
                                    <FieldLabel htmlFor={hideSysMesField}>{t('Hide_System_Messages')}</FieldLabel>
                                    <Controller
                                        control={control}
                                        name='hideSysMes'
                                        render={({ field: { value, ...field } }) => (
                                            <ToggleSwitch id={hideSysMesField} {...field} checked={value} disabled={isFederated} />
                                        )}
                                    />
                                </FieldRow>
                                <FieldRow>
                                    <Controller
                                        control={control}
                                        name='systemMessages'
                                        render={({ field }) => (
                                            <MultiSelect
                                                {...field}
                                                options={sysMesOptions}
                                                disabled={!hideSysMes || isFederated}
                                                placeholder={t('Select_messages_to_hide')}
                                            />
                                        )}
                                    />
                                </FieldRow>
                            </Field>
                        )}
                    </FieldGroup>
                    {retentionPolicy && (
                        <Accordion>
                            <Accordion.Item title={t('Prune')}>
                                <FieldGroup>
                                    <Field>
                                        <FieldRow>
                                            <FieldLabel htmlFor={retentionEnabledField}>{t('RetentionPolicyRoom_Enabled')}</FieldLabel>
                                            <Controller
                                                control={control}
                                                name='retentionEnabled'
                                                render={({ field: { value, ...field } }) => <ToggleSwitch id={retentionEnabledField} {...field} checked={value} />}
                                            />
                                        </FieldRow>
                                    </Field>
                                    <Field>
                                        <FieldRow>
                                            <FieldLabel htmlFor={retentionOverrideGlobalField}>{t('RetentionPolicyRoom_OverrideGlobal')}</FieldLabel>
                                            <Controller
                                                control={control}
                                                name='retentionOverrideGlobal'
                                                render={({ field: { value, ...field } }) => (
                                                    <ToggleSwitch
                                                        id={retentionOverrideGlobalField}
                                                        {...field}
                                                        disabled={!retentionEnabled || !canEditRoomRetentionPolicy}
                                                        checked={value}
                                                    />
                                                )}
                                            />
                                        </FieldRow>
                                    </Field>
                                    {retentionOverrideGlobal && (
                                        <>
                                            <Callout type='danger'>
                                                <RawText>{t('RetentionPolicyRoom_ReadTheDocs')}</RawText>
                                            </Callout>
                                            <Field>
                                                <FieldLabel htmlFor={retentionMaxAgeField}>{t('RetentionPolicyRoom_MaxAge', { max: retentionMaxAge })}</FieldLabel>
                                                <FieldRow>
                                                    <Controller
                                                        control={control}
                                                        name='retentionMaxAge'
                                                        render={({ field: { onChange, ...field } }) => (
                                                            <NumberInput
                                                                id={retentionMaxAgeField}
                                                                {...field}
                                                                onChange={(currentValue) => onChange(Math.max(1, Number(currentValue)))}
                                                            />
                                                        )}
                                                    />
                                                </FieldRow>
                                            </Field>
                                            <Field>
                                                <FieldRow>
                                                    <FieldLabel htmlFor={retentionExcludePinnedField}>{t('RetentionPolicyRoom_ExcludePinned')}</FieldLabel>
                                                    <Controller
                                                        control={control}
                                                        name='retentionExcludePinned'
                                                        render={({ field: { value, ...field } }) => (
                                                            <ToggleSwitch id={retentionExcludePinnedField} {...field} checked={value} />
                                                        )}
                                                    />
                                                </FieldRow>
                                            </Field>
                                            <Field>
                                                <FieldRow>
                                                    <FieldLabel htmlFor={retentionFilesOnlyField}>{t('RetentionPolicyRoom_FilesOnly')}</FieldLabel>
                                                    <Controller
                                                        control={control}
                                                        name='retentionFilesOnly'
                                                        render={({ field: { value, ...field } }) => (
                                                            <ToggleSwitch id={retentionFilesOnlyField} {...field} checked={value} />
                                                        )}
                                                    />
                                                </FieldRow>
                                            </Field>
                                            {canViewEncrypted && (
                                                <Field>
                                                    <FieldRow>
                                                        <FieldLabel htmlFor={encryptedField}>{t('Encrypted')}</FieldLabel>
                                                        <Controller
                                                            control={control}
                                                            name='encrypted'
                                                            render={({ field: { value, ...field } }) => (
                                                                <ToggleSwitch
                                                                    id={encryptedField}
                                                                    aria-describedby={`${encryptedField}-hint`}
                                                                    {...field}
                                                                    disabled={!canToggleEncryption || isFederated}
                                                                    checked={value}
                                                                />
                                                            )}
                                                        />
                                                    </FieldRow>
                                                    <FieldRow>
                                                        <FieldHint id={`${encryptedField}-hint`}>{t('Encrypted_field_hint')}</FieldHint>
                                                    </FieldRow>
                                                </Field>
                                            )}
                                        </>
                                    )}
                                </FieldGroup>
                            </Accordion.Item>
                        </Accordion>
                    )}
                </form>
            </ContextualbarScrollableContent>
            <ContextualbarFooter>
                <ButtonGroup stretch>
                    <Button type='reset' disabled={!isDirty || isSubmitting} onClick={() => reset(defaultValues)}>
                        {t('Reset')}
                    </Button>
                    <Button form={formId} type='submit' primary loading={isSubmitting} disabled={!isDirty}>
                        {t('Save')}
                    </Button>
                </ButtonGroup>
            </ContextualbarFooter>
        </>
    );
};

export default EditRoomInfo;