snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings';
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import EditRoomInfo from './EditRoomInfo';
|
||||
import { createFakeRoom } from '../../../../../../tests/mocks/data';
|
||||
|
||||
jest.mock('../../../../../lib/rooms/roomCoordinator', () => ({
|
||||
roomCoordinator: {
|
||||
getRoomDirectives: () => ({
|
||||
allowRoomSettingChange: () => true,
|
||||
}),
|
||||
getRoomName: (_t: string, room: { name?: string }) => room.name ?? 'test-room',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../components/avatar/RoomAvatarEditor', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
const noop = () => undefined;
|
||||
|
||||
const createRoom = (sysMes?: string[]) =>
|
||||
createFakeRoom<IRoomWithRetentionPolicy>({
|
||||
t: 'c',
|
||||
name: 'test-room',
|
||||
...(sysMes && { sysMes: sysMes as IRoomWithRetentionPolicy['sysMes'] }),
|
||||
});
|
||||
|
||||
const buildAppRoot = (saveFn: jest.Mock, room: IRoomWithRetentionPolicy) =>
|
||||
mockAppRoot()
|
||||
.withJohnDoe()
|
||||
.withPermission('edit-room')
|
||||
.withEndpoint('POST', '/v1/rooms.saveRoomSettings', saveFn as never)
|
||||
.withEndpoint('POST', '/v1/rooms.changeArchivationState', () => ({ success: true }) as never)
|
||||
.withEndpoint('GET', '/v1/rooms.nameExists', () => ({ exists: false }) as never)
|
||||
.withSetting('UTF8_Channel_Names_Validation', '[0-9a-zA-Z-_.]+')
|
||||
.withSetting('UI_Allow_room_names_with_special_chars', true)
|
||||
.withSetting('RetentionPolicy_Enabled', false)
|
||||
.withTranslations('en', 'core', {
|
||||
Save: 'Save',
|
||||
Hide_System_Messages: 'Hide system messages',
|
||||
Advanced_settings: 'Advanced settings',
|
||||
Select_messages_to_hide: 'Select messages to hide',
|
||||
})
|
||||
.withRoom(room)
|
||||
.build();
|
||||
|
||||
const openAdvancedSettings = async () => {
|
||||
await userEvent.click(await screen.findByRole('button', { name: /Advanced settings/i }));
|
||||
};
|
||||
|
||||
const findHideSysMesToggle = () => screen.findByLabelText(/Hide system messages/i);
|
||||
|
||||
const clickSave = async () => {
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
};
|
||||
|
||||
const removeSystemMessageChip = async (translationKey: string) => {
|
||||
const chip = screen.getByRole('button', { name: new RegExp(translationKey, 'i') });
|
||||
await userEvent.click(chip);
|
||||
};
|
||||
|
||||
const selectSystemMessageOption = async (translationKey: string) => {
|
||||
const trigger = screen.getByRole('combobox', { name: /Select messages to hide/i });
|
||||
await userEvent.click(trigger);
|
||||
|
||||
const listboxes = await screen.findAllByRole('listbox', { hidden: true });
|
||||
const listbox = listboxes.find((el) => within(el).queryAllByRole('option', { hidden: true }).length > 0);
|
||||
|
||||
if (!listbox) {
|
||||
throw new Error('Could not find a listbox containing options');
|
||||
}
|
||||
|
||||
const optionEl = within(listbox).getByText(translationKey).closest('li[role="option"]');
|
||||
|
||||
if (!optionEl) {
|
||||
throw new Error(`Could not find option with text "${translationKey}"`);
|
||||
}
|
||||
|
||||
await userEvent.click(optionEl);
|
||||
};
|
||||
|
||||
it('should send systemMessages: [] when hideSysMes is toggled OFF', async () => {
|
||||
const saveFn = jest.fn((_data: Record<string, unknown>) => ({ rid: 'room-id', success: true }));
|
||||
const room = createRoom(['au', 'ru']);
|
||||
|
||||
render(<EditRoomInfo room={room} onClickClose={noop} onClickBack={noop} />, { wrapper: buildAppRoot(saveFn, room) });
|
||||
await openAdvancedSettings();
|
||||
|
||||
const toggle = await findHideSysMesToggle();
|
||||
expect(toggle).toBeChecked();
|
||||
|
||||
await userEvent.click(toggle);
|
||||
expect(toggle).not.toBeChecked();
|
||||
|
||||
await clickSave();
|
||||
await waitFor(() => expect(saveFn).toHaveBeenCalled());
|
||||
|
||||
expect(saveFn.mock.calls[0][0]).toEqual(expect.objectContaining({ systemMessages: [] }));
|
||||
});
|
||||
|
||||
it('should send selected systemMessages when user selects message types to hide', async () => {
|
||||
const saveFn = jest.fn((_data: Record<string, unknown>) => ({ rid: 'room-id', success: true }));
|
||||
const room = createRoom();
|
||||
|
||||
render(<EditRoomInfo room={room} onClickClose={noop} onClickBack={noop} />, { wrapper: buildAppRoot(saveFn, room) });
|
||||
await openAdvancedSettings();
|
||||
|
||||
const toggle = await findHideSysMesToggle();
|
||||
await userEvent.click(toggle);
|
||||
expect(toggle).toBeChecked();
|
||||
|
||||
await selectSystemMessageOption('Message_HideType_au');
|
||||
|
||||
await clickSave();
|
||||
await waitFor(() => expect(saveFn).toHaveBeenCalled());
|
||||
|
||||
expect(saveFn.mock.calls[0][0]).toEqual(expect.objectContaining({ systemMessages: expect.arrayContaining(['au']) }));
|
||||
});
|
||||
|
||||
it('should send existing systemMessages when toggle is turned ON without changing selection', async () => {
|
||||
const saveFn = jest.fn((_data: Record<string, unknown>) => ({ rid: 'room-id', success: true }));
|
||||
const room = createRoom();
|
||||
|
||||
render(<EditRoomInfo room={room} onClickClose={noop} onClickBack={noop} />, { wrapper: buildAppRoot(saveFn, room) });
|
||||
await openAdvancedSettings();
|
||||
|
||||
const toggle = await findHideSysMesToggle();
|
||||
expect(toggle).not.toBeChecked();
|
||||
|
||||
await userEvent.click(toggle);
|
||||
expect(toggle).toBeChecked();
|
||||
|
||||
await clickSave();
|
||||
await waitFor(() => expect(saveFn).toHaveBeenCalled());
|
||||
|
||||
expect(saveFn.mock.calls[0][0]).toEqual(expect.objectContaining({ systemMessages: [] }));
|
||||
});
|
||||
|
||||
it('should send updated systemMessages when only the multi-select is changed', async () => {
|
||||
const saveFn = jest.fn((_data: Record<string, unknown>) => ({ rid: 'room-id', success: true }));
|
||||
const room = createRoom(['ru']);
|
||||
|
||||
render(<EditRoomInfo room={room} onClickClose={noop} onClickBack={noop} />, { wrapper: buildAppRoot(saveFn, room) });
|
||||
await openAdvancedSettings();
|
||||
|
||||
const toggle = await findHideSysMesToggle();
|
||||
expect(toggle).toBeChecked();
|
||||
|
||||
await removeSystemMessageChip('Message_HideType_ru');
|
||||
await selectSystemMessageOption('Message_HideType_au');
|
||||
|
||||
await clickSave();
|
||||
await waitFor(() => expect(saveFn).toHaveBeenCalled());
|
||||
|
||||
expect(saveFn.mock.calls[0][0]).toEqual(expect.objectContaining({ systemMessages: expect.arrayContaining(['au']) }));
|
||||
});
|
||||
|
||||
it('should NOT send systemMessages when something else changed', async () => {
|
||||
const saveFn = jest.fn((_data: Record<string, unknown>) => ({ rid: 'room-id', success: true }));
|
||||
const room = createRoom(['au']);
|
||||
|
||||
render(<EditRoomInfo room={room} onClickClose={noop} onClickBack={noop} />, { wrapper: buildAppRoot(saveFn, room) });
|
||||
|
||||
await userEvent.type(await screen.findByRole('textbox', { name: 'Topic' }), 'new topic');
|
||||
|
||||
await clickSave();
|
||||
await waitFor(() => expect(saveFn).toHaveBeenCalled());
|
||||
|
||||
expect(saveFn.mock.calls[0][0]).not.toHaveProperty('systemMessages');
|
||||
});
|
||||
@@ -0,0 +1,621 @@
|
||||
/* eslint-disable complexity */
|
||||
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,
|
||||
AccordionItem,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import {
|
||||
ContextualbarHeader,
|
||||
ContextualbarBack,
|
||||
ContextualbarTitle,
|
||||
ContextualbarClose,
|
||||
ContextualbarScrollableContent,
|
||||
ContextualbarFooter,
|
||||
ContextualbarDialog,
|
||||
} from '@rocket.chat/ui-client';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { useSetting, useTranslation, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
|
||||
import type { EditRoomInfoFormData } from './useEditRoomInitialValues';
|
||||
import { useEditRoomInitialValues } from './useEditRoomInitialValues';
|
||||
import { useEditRoomPermissions } from './useEditRoomPermissions';
|
||||
import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes';
|
||||
import RawText from '../../../../../components/RawText';
|
||||
import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor';
|
||||
import { msToTimeUnit, TIMEUNIT } from '../../../../../lib/convertTimeUnit';
|
||||
import { getDirtyFields } from '../../../../../lib/getDirtyFields';
|
||||
import { links } from '../../../../../lib/links';
|
||||
import { roomsQueryKeys } from '../../../../../lib/queryKeys';
|
||||
import { useIsABACManagedRoom } from '../../../../admin/ABAC/hooks/useIsABACManagedRoom';
|
||||
import { useArchiveRoom } from '../../../../hooks/roomActions/useArchiveRoom';
|
||||
import { useRetentionPolicy } from '../../../hooks/useRetentionPolicy';
|
||||
|
||||
export type EditRoomInfoProps = {
|
||||
room: IRoomWithRetentionPolicy;
|
||||
onClickClose: () => void;
|
||||
onClickBack: () => void;
|
||||
};
|
||||
|
||||
const title = {
|
||||
team: 'Edit_team',
|
||||
channel: 'Edit_channel',
|
||||
discussion: 'Edit_discussion',
|
||||
} as const;
|
||||
|
||||
const getRetentionSetting = (roomType: IRoomWithRetentionPolicy['t']): string => {
|
||||
switch (roomType) {
|
||||
case 'd':
|
||||
return 'RetentionPolicy_TTL_DMs';
|
||||
case 'p':
|
||||
return 'RetentionPolicy_TTL_Groups';
|
||||
case 'c':
|
||||
default:
|
||||
return 'RetentionPolicy_TTL_Channels';
|
||||
}
|
||||
};
|
||||
|
||||
const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => {
|
||||
const query = useQueryClient();
|
||||
const t = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const isFederated = isRoomFederated(room);
|
||||
const isAbacManaged = useIsABACManagedRoom(room);
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const roomType = 'prid' in room ? 'discussion' : room.teamMain ? 'team' : 'channel';
|
||||
|
||||
const retentionPolicy = useRetentionPolicy(room);
|
||||
const retentionMaxAgeDefault = msToTimeUnit(TIMEUNIT.days, useSetting(getRetentionSetting(room.t), 2592000000)) ?? 30;
|
||||
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<EditRoomInfoFormData>({ 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,
|
||||
retentionOverrideGlobal,
|
||||
roomType: roomTypeP,
|
||||
reactWhenReadOnly,
|
||||
} = watch();
|
||||
|
||||
const {
|
||||
canChangeType,
|
||||
canSetReadOnly,
|
||||
canSetReactWhenReadOnly,
|
||||
canEditRoomRetentionPolicy,
|
||||
canArchiveOrUnarchive,
|
||||
canViewName,
|
||||
canViewTopic,
|
||||
canViewAnnouncement,
|
||||
canViewArchived,
|
||||
canViewDescription,
|
||||
canViewType,
|
||||
canViewReadOnly,
|
||||
canViewHideSysMes,
|
||||
canViewJoinCode,
|
||||
} = useEditRoomPermissions(room);
|
||||
|
||||
const changeArchiving = archived !== !!room.archived;
|
||||
|
||||
const saveAction = useEndpoint('POST', '/v1/rooms.saveRoomSettings');
|
||||
|
||||
const handleArchive = useArchiveRoom(room);
|
||||
|
||||
// TODO: add payload validation
|
||||
const handleUpdateRoomData = useStableCallback(
|
||||
async ({
|
||||
hideSysMes,
|
||||
joinCodeRequired,
|
||||
retentionEnabled,
|
||||
retentionOverrideGlobal,
|
||||
retentionMaxAge,
|
||||
retentionExcludePinned,
|
||||
retentionFilesOnly,
|
||||
retentionIgnoreThreads,
|
||||
...formData
|
||||
}: EditRoomInfoFormData) => {
|
||||
const data = getDirtyFields<Partial<typeof defaultValues>>(formData, dirtyFields);
|
||||
delete data.archived;
|
||||
|
||||
try {
|
||||
await saveAction({
|
||||
rid: room._id,
|
||||
...data,
|
||||
...((data.joinCode || 'joinCodeRequired' in data) && { joinCode: joinCodeRequired ? data.joinCode : '' }),
|
||||
...((dirtyFields.hideSysMes || dirtyFields.systemMessages) && {
|
||||
systemMessages: hideSysMes ? (data.systemMessages ?? defaultValues.systemMessages) : [],
|
||||
}),
|
||||
retentionEnabled,
|
||||
retentionOverrideGlobal,
|
||||
...(retentionEnabled &&
|
||||
retentionOverrideGlobal && {
|
||||
retentionMaxAge,
|
||||
retentionExcludePinned,
|
||||
retentionFilesOnly,
|
||||
retentionIgnoreThreads,
|
||||
}),
|
||||
});
|
||||
|
||||
await query.invalidateQueries({ queryKey: roomsQueryKeys.info(room._id) });
|
||||
dispatchToastMessage({ type: 'success', message: t('Room_updated_successfully') });
|
||||
onClickClose();
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handleSave = useStableCallback((data: EditRoomInfoFormData) =>
|
||||
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 = useId();
|
||||
const roomNameField = useId();
|
||||
const roomDescriptionField = useId();
|
||||
const roomAnnouncementField = useId();
|
||||
const roomTopicField = useId();
|
||||
const roomTypeField = useId();
|
||||
const readOnlyField = useId();
|
||||
const reactWhenReadOnlyField = useId();
|
||||
const archivedField = useId();
|
||||
const joinCodeRequiredField = useId();
|
||||
const hideSysMesField = useId();
|
||||
const retentionEnabledField = useId();
|
||||
const retentionOverrideGlobalField = useId();
|
||||
const retentionMaxAgeField = useId();
|
||||
const retentionExcludePinnedField = useId();
|
||||
const retentionFilesOnlyField = useId();
|
||||
const retentionIgnoreThreads = useId();
|
||||
|
||||
const showAdvancedSettings = canViewReadOnly || readOnly || canViewArchived || canViewJoinCode || canViewHideSysMes;
|
||||
const showRetentionPolicy = canEditRoomRetentionPolicy && retentionPolicy?.enabled;
|
||||
|
||||
const showAccordion = showAdvancedSettings || showRetentionPolicy;
|
||||
|
||||
return (
|
||||
<ContextualbarDialog>
|
||||
<ContextualbarHeader>
|
||||
{onClickBack && <ContextualbarBack onClick={onClickBack} />}
|
||||
<ContextualbarTitle>{t(`${title[roomType]}`)}</ContextualbarTitle>
|
||||
{onClickClose && <ContextualbarClose onClick={onClickClose} />}
|
||||
</ContextualbarHeader>
|
||||
<ContextualbarScrollableContent padding={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('Required_field', { 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} disabled={isAbacManaged} />
|
||||
)}
|
||||
/>
|
||||
</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 || isAbacManaged}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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 || isAbacManaged} 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>
|
||||
)}
|
||||
</FieldGroup>
|
||||
{showAccordion && (
|
||||
<Accordion>
|
||||
{showAdvancedSettings && (
|
||||
<AccordionItem title={t('Advanced_settings')}>
|
||||
<FieldGroup>
|
||||
<Box is='h5' fontScale='h5' color='titles-labels'>
|
||||
{t('Security_and_permissions')}
|
||||
</Box>
|
||||
{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}
|
||||
aria-describedby={`${hideSysMesField}-hint`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<Controller
|
||||
control={control}
|
||||
name='systemMessages'
|
||||
render={({ field }) => (
|
||||
<MultiSelect
|
||||
{...field}
|
||||
options={sysMesOptions}
|
||||
disabled={!hideSysMes || isFederated}
|
||||
placeholder={t('Select_messages_to_hide')}
|
||||
aria-label={t('Select_messages_to_hide')}
|
||||
aria-describedby={`${hideSysMesField}-hint`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<FieldHint id={`${hideSysMesField}-hint`}>{t('Hide_System_Messages_Hint')}</FieldHint>
|
||||
</FieldRow>
|
||||
</Field>
|
||||
)}
|
||||
</FieldGroup>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{showRetentionPolicy && (
|
||||
<AccordionItem 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} checked={value} />
|
||||
)}
|
||||
/>
|
||||
</FieldRow>
|
||||
</Field>
|
||||
{retentionOverrideGlobal && (
|
||||
<>
|
||||
<Callout type='danger'>
|
||||
<RawText>{t('RetentionPolicyRoom_ReadTheDocs', { retentionPolicyUrl: links.retentionPolicy })}</RawText>
|
||||
</Callout>
|
||||
<Field>
|
||||
<FieldLabel htmlFor={retentionMaxAgeField}>
|
||||
{t('RetentionPolicyRoom_MaxAge', { max: retentionMaxAgeDefault })}
|
||||
</FieldLabel>
|
||||
<FieldRow>
|
||||
<Controller
|
||||
control={control}
|
||||
name='retentionMaxAge'
|
||||
render={({ field: { onChange, ...field } }) => (
|
||||
<NumberInput
|
||||
id={retentionMaxAgeField}
|
||||
{...field}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(Number(e.currentTarget.value))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
<Field>
|
||||
<FieldRow>
|
||||
<FieldLabel htmlFor={retentionIgnoreThreads}>{t('RetentionPolicy_DoNotPruneThreads')}</FieldLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='retentionIgnoreThreads'
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<ToggleSwitch id={retentionIgnoreThreads} {...field} checked={value} />
|
||||
)}
|
||||
/>
|
||||
</FieldRow>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</FieldGroup>
|
||||
</AccordionItem>
|
||||
)}
|
||||
</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>
|
||||
</ContextualbarDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditRoomInfo;
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings';
|
||||
import { useRoomToolbox } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import EditRoomInfo from './EditRoomInfo';
|
||||
import { useRoom } from '../../../contexts/RoomContext';
|
||||
|
||||
const EditRoomInfoWithData = ({ onClickBack }: { onClickBack: () => void }) => {
|
||||
const room = useRoom() as IRoomWithRetentionPolicy;
|
||||
const { closeTab } = useRoomToolbox();
|
||||
|
||||
return <EditRoomInfo onClickClose={closeTab} onClickBack={onClickBack} room={room} />;
|
||||
};
|
||||
|
||||
export default EditRoomInfoWithData;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './EditRoomInfoWithData';
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { IRoomWithRetentionPolicy, RoomType, MessageTypesValues } from '@rocket.chat/core-typings';
|
||||
import { usePermission } from '@rocket.chat/ui-contexts';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { msToTimeUnit, TIMEUNIT } from '../../../../../lib/convertTimeUnit';
|
||||
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
|
||||
import { useRetentionPolicy } from '../../../hooks/useRetentionPolicy';
|
||||
|
||||
export type EditRoomInfoFormData = {
|
||||
roomName: string;
|
||||
roomTopic: string;
|
||||
roomAnnouncement: string;
|
||||
roomDescription: string;
|
||||
roomType: RoomType;
|
||||
roomAvatar?: string;
|
||||
readOnly: boolean;
|
||||
reactWhenReadOnly: boolean;
|
||||
archived: boolean;
|
||||
joinCodeRequired: boolean;
|
||||
hideSysMes: boolean;
|
||||
encrypted: boolean;
|
||||
retentionEnabled: boolean;
|
||||
retentionOverrideGlobal: boolean;
|
||||
retentionMaxAge: number;
|
||||
retentionExcludePinned: boolean;
|
||||
retentionFilesOnly: boolean;
|
||||
retentionIgnoreThreads: boolean;
|
||||
showChannels: boolean;
|
||||
showDiscussions: boolean;
|
||||
joinCode: string;
|
||||
systemMessages: MessageTypesValues[];
|
||||
};
|
||||
|
||||
export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy): Partial<EditRoomInfoFormData> => {
|
||||
const retentionPolicy = useRetentionPolicy(room);
|
||||
const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id);
|
||||
|
||||
const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room;
|
||||
|
||||
return useMemo(
|
||||
(): Partial<EditRoomInfoFormData> => ({
|
||||
roomName: t === 'd' && room.usernames ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, room),
|
||||
roomType: t,
|
||||
readOnly: !!ro,
|
||||
reactWhenReadOnly,
|
||||
archived: !!archived,
|
||||
roomTopic: topic ?? '',
|
||||
roomDescription: description ?? '',
|
||||
roomAnnouncement: announcement ?? '',
|
||||
roomAvatar: undefined,
|
||||
joinCode: '',
|
||||
joinCodeRequired: !!joinCodeRequired,
|
||||
systemMessages: Array.isArray(sysMes) ? sysMes : [],
|
||||
hideSysMes: Array.isArray(sysMes) ? !!sysMes?.length : !!sysMes,
|
||||
encrypted,
|
||||
...(canEditRoomRetentionPolicy &&
|
||||
retentionPolicy?.enabled && {
|
||||
retentionEnabled: retention?.enabled ?? retentionPolicy.isActive,
|
||||
retentionOverrideGlobal: !!retention?.overrideGlobal,
|
||||
retentionMaxAge: retention?.maxAge ?? msToTimeUnit(TIMEUNIT.days, retentionPolicy.maxAge),
|
||||
retentionExcludePinned: retention?.excludePinned ?? retentionPolicy.excludePinned,
|
||||
retentionFilesOnly: retention?.filesOnly ?? retentionPolicy.filesOnly,
|
||||
retentionIgnoreThreads: retention?.ignoreThreads ?? retentionPolicy.ignoreThreads,
|
||||
}),
|
||||
}),
|
||||
[
|
||||
announcement,
|
||||
archived,
|
||||
description,
|
||||
joinCodeRequired,
|
||||
retention,
|
||||
retentionPolicy,
|
||||
ro,
|
||||
room,
|
||||
sysMes,
|
||||
t,
|
||||
topic,
|
||||
encrypted,
|
||||
reactWhenReadOnly,
|
||||
canEditRoomRetentionPolicy,
|
||||
],
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useEditRoomPermissions } from './useEditRoomPermissions';
|
||||
import { createFakeRoom } from '../../../../../../tests/mocks/data';
|
||||
import { useTeamInfoQuery } from '../../../../../hooks/useTeamInfoQuery';
|
||||
|
||||
jest.mock('../../../../../lib/rooms/roomCoordinator', () => ({
|
||||
roomCoordinator: {
|
||||
getRoomDirectives: () => ({
|
||||
allowRoomSettingChange: () => true,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../hooks/useTeamInfoQuery');
|
||||
|
||||
const mockUseTeamInfoQuery = useTeamInfoQuery as jest.MockedFunction<typeof useTeamInfoQuery>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseTeamInfoQuery.mockReturnValue({ data: undefined } as ReturnType<typeof useTeamInfoQuery>);
|
||||
});
|
||||
|
||||
describe('useEditRoomPermissions', () => {
|
||||
describe('canChangeType - non-team room', () => {
|
||||
it('should allow changing type from private to channel when user has create-c permission', () => {
|
||||
const room = createFakeRoom({ t: 'p', default: false });
|
||||
const wrapper = mockAppRoot().withPermission('create-c').build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow changing type from channel to private when user has create-p permission', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false });
|
||||
const wrapper = mockAppRoot().withPermission('create-p').build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow changing type when user lacks the required permission', () => {
|
||||
const room = createFakeRoom({ t: 'p', default: false });
|
||||
const wrapper = mockAppRoot().build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canChangeType - team main room', () => {
|
||||
const teamRoomId = 'team-room-id';
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseTeamInfoQuery.mockReturnValue({ data: { roomId: teamRoomId } } as ReturnType<typeof useTeamInfoQuery>);
|
||||
});
|
||||
|
||||
it('should allow changing type from private to channel when user has create-c', () => {
|
||||
const room = createFakeRoom({ t: 'p', default: false, teamId: 'team1', teamMain: true });
|
||||
const wrapper = mockAppRoot().withPermission('create-c').build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow changing type from private to channel when user lacks create-c', () => {
|
||||
const room = createFakeRoom({ t: 'p', default: false, teamId: 'team1', teamMain: true });
|
||||
const wrapper = mockAppRoot().build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canChangeType - team channel room', () => {
|
||||
const teamRoomId = 'team-room-id';
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseTeamInfoQuery.mockReturnValue({ data: { roomId: teamRoomId } } as ReturnType<typeof useTeamInfoQuery>);
|
||||
});
|
||||
|
||||
it('should allow changing type from private to channel when user has create-team-channel', () => {
|
||||
const room = createFakeRoom({ t: 'p', default: false, teamId: 'team1' });
|
||||
const wrapper = mockAppRoot().withPermission('create-team-channel').build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow changing type from private to channel when user lacks create-team-channel', () => {
|
||||
const room = createFakeRoom({ t: 'p', default: false, teamId: 'team1' });
|
||||
const wrapper = mockAppRoot().build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow changing type from channel to private when user has create-team-group', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false, teamId: 'team1' });
|
||||
const wrapper = mockAppRoot().withPermission('create-team-group').build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow changing type from channel to private when user lacks create-team-group', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false, teamId: 'team1' });
|
||||
const wrapper = mockAppRoot().build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canChangeType).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('other permissions', () => {
|
||||
it('should reflect canSetReadOnly based on set-readonly permission', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false });
|
||||
const wrapperWith = mockAppRoot().withPermission('set-readonly').build();
|
||||
const wrapperWithout = mockAppRoot().build();
|
||||
|
||||
const { result: withPerm } = renderHook(() => useEditRoomPermissions(room), { wrapper: wrapperWith });
|
||||
const { result: withoutPerm } = renderHook(() => useEditRoomPermissions(room), { wrapper: wrapperWithout });
|
||||
|
||||
expect(withPerm.current.canSetReadOnly).toBe(true);
|
||||
expect(withoutPerm.current.canSetReadOnly).toBe(false);
|
||||
});
|
||||
|
||||
it('should reflect canSetReactWhenReadOnly based on set-react-when-readonly permission', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false });
|
||||
const wrapperWith = mockAppRoot().withPermission('set-react-when-readonly').build();
|
||||
const wrapperWithout = mockAppRoot().build();
|
||||
|
||||
const { result: withPerm } = renderHook(() => useEditRoomPermissions(room), { wrapper: wrapperWith });
|
||||
const { result: withoutPerm } = renderHook(() => useEditRoomPermissions(room), { wrapper: wrapperWithout });
|
||||
|
||||
expect(withPerm.current.canSetReactWhenReadOnly).toBe(true);
|
||||
expect(withoutPerm.current.canSetReactWhenReadOnly).toBe(false);
|
||||
});
|
||||
|
||||
it('should reflect canEditRoomRetentionPolicy based on edit-room-retention-policy permission', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false });
|
||||
const wrapperWith = mockAppRoot().withPermission('edit-room-retention-policy').build();
|
||||
const wrapperWithout = mockAppRoot().build();
|
||||
|
||||
const { result: withPerm } = renderHook(() => useEditRoomPermissions(room), { wrapper: wrapperWith });
|
||||
const { result: withoutPerm } = renderHook(() => useEditRoomPermissions(room), { wrapper: wrapperWithout });
|
||||
|
||||
expect(withPerm.current.canEditRoomRetentionPolicy).toBe(true);
|
||||
expect(withoutPerm.current.canEditRoomRetentionPolicy).toBe(false);
|
||||
});
|
||||
|
||||
it('should reflect canArchiveOrUnarchive when user has archive-room permission', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false });
|
||||
const wrapper = mockAppRoot().withPermission('archive-room').build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canArchiveOrUnarchive).toBe(true);
|
||||
});
|
||||
|
||||
it('should reflect canArchiveOrUnarchive when user has unarchive-room permission', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false });
|
||||
const wrapper = mockAppRoot().withPermission('unarchive-room').build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canArchiveOrUnarchive).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for canArchiveOrUnarchive when user has neither permission', () => {
|
||||
const room = createFakeRoom({ t: 'c', default: false });
|
||||
const wrapper = mockAppRoot().build();
|
||||
|
||||
const { result } = renderHook(() => useEditRoomPermissions(room), { wrapper });
|
||||
|
||||
expect(result.current.canArchiveOrUnarchive).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings';
|
||||
import { usePermission, useAtLeastOnePermission, useRole } from '@rocket.chat/ui-contexts';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig';
|
||||
import { useTeamInfoQuery } from '../../../../../hooks/useTeamInfoQuery';
|
||||
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
|
||||
|
||||
const getCanChangeType = (room: IRoom | IRoomWithRetentionPolicy, canCreateChannel: boolean, canCreateGroup: boolean, isAdmin: boolean) =>
|
||||
(!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup));
|
||||
|
||||
export const useEditRoomPermissions = (room: IRoom | IRoomWithRetentionPolicy) => {
|
||||
const isAdmin = useRole('admin');
|
||||
const canCreateChannel = usePermission('create-c');
|
||||
const canCreateGroup = usePermission('create-p');
|
||||
|
||||
const teamId = room.teamId || '';
|
||||
const teamMain = room.teamMain || '';
|
||||
const { data: teamInfo } = useTeamInfoQuery(teamId);
|
||||
|
||||
const canCreateTeamChannel = usePermission('create-team-channel', teamInfo?.roomId);
|
||||
const canCreateTeamGroup = usePermission('create-team-group', teamInfo?.roomId);
|
||||
|
||||
const canChangeType = getCanChangeType(
|
||||
room,
|
||||
teamId && !teamMain ? canCreateTeamChannel : canCreateChannel,
|
||||
teamId && !teamMain ? canCreateTeamGroup : canCreateGroup,
|
||||
isAdmin,
|
||||
);
|
||||
const canSetReadOnly = usePermission('set-readonly', room._id);
|
||||
const canSetReactWhenReadOnly = usePermission('set-react-when-readonly', room._id);
|
||||
const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id);
|
||||
const canArchiveOrUnarchive = useAtLeastOnePermission(
|
||||
useMemo(() => ['archive-room', 'unarchive-room'], []),
|
||||
room._id,
|
||||
);
|
||||
|
||||
const [
|
||||
canViewName,
|
||||
canViewTopic,
|
||||
canViewAnnouncement,
|
||||
canViewArchived,
|
||||
canViewDescription,
|
||||
canViewType,
|
||||
canViewReadOnly,
|
||||
canViewHideSysMes,
|
||||
canViewJoinCode,
|
||||
canViewReactWhenReadOnly,
|
||||
] = useMemo(() => {
|
||||
const isAllowed =
|
||||
roomCoordinator.getRoomDirectives(room.t)?.allowRoomSettingChange ||
|
||||
(() => {
|
||||
undefined;
|
||||
});
|
||||
return [
|
||||
isAllowed(room, RoomSettingsEnum.NAME),
|
||||
isAllowed(room, RoomSettingsEnum.TOPIC),
|
||||
isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT),
|
||||
isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE),
|
||||
isAllowed(room, RoomSettingsEnum.DESCRIPTION),
|
||||
isAllowed(room, RoomSettingsEnum.TYPE),
|
||||
isAllowed(room, RoomSettingsEnum.READ_ONLY),
|
||||
isAllowed(room, RoomSettingsEnum.SYSTEM_MESSAGES),
|
||||
isAllowed(room, RoomSettingsEnum.JOIN_CODE),
|
||||
isAllowed(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY),
|
||||
];
|
||||
}, [room]);
|
||||
|
||||
return {
|
||||
canChangeType,
|
||||
canSetReadOnly,
|
||||
canSetReactWhenReadOnly,
|
||||
canEditRoomRetentionPolicy,
|
||||
canArchiveOrUnarchive,
|
||||
canViewName,
|
||||
canViewTopic,
|
||||
canViewAnnouncement,
|
||||
canViewArchived,
|
||||
canViewDescription,
|
||||
canViewType,
|
||||
canViewReadOnly,
|
||||
canViewHideSysMes,
|
||||
canViewJoinCode,
|
||||
canViewReactWhenReadOnly,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user