diff --git a/.changeset/cold-beds-hope.md b/.changeset/cold-beds-hope.md new file mode 100644 index 00000000000..33fc910e424 --- /dev/null +++ b/.changeset/cold-beds-hope.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue not allowing override retention policy in channels diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts index 4f6c0678094..e17faebea38 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.ts @@ -117,14 +117,10 @@ const validators: RoomSettingsValidators = { } }, async retentionEnabled({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.enabled) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.enabled) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -132,14 +128,10 @@ const validators: RoomSettingsValidators = { } }, async retentionMaxAge({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.maxAge) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.maxAge) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -147,14 +139,10 @@ const validators: RoomSettingsValidators = { } }, async retentionExcludePinned({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.excludePinned) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.excludePinned) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -162,14 +150,10 @@ const validators: RoomSettingsValidators = { } }, async retentionFilesOnly({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.filesOnly) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.filesOnly) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -177,14 +161,10 @@ const validators: RoomSettingsValidators = { } }, async retentionIgnoreThreads({ userId, value, room, rid }) { - if (!hasRetentionPolicy(room)) { - throw new Meteor.Error('error-action-not-allowed', 'Room does not have retention policy', { - method: 'saveRoomSettings', - action: 'Editing_room', - }); - } - - if (!(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && value !== room.retention.ignoreThreads) { + if ( + !(await hasPermissionAsync(userId, 'edit-room-retention-policy', rid)) && + (!hasRetentionPolicy(room) || value !== room.retention.ignoreThreads) + ) { throw new Meteor.Error('error-action-not-allowed', 'Editing room retention policy is not allowed', { method: 'saveRoomSettings', action: 'Editing_room', @@ -469,7 +449,7 @@ export async function saveRoomSettings( rid, }); - if (setting === 'retentionOverrideGlobal') { + if (setting === 'retentionOverrideGlobal' && settings.retentionOverrideGlobal === false) { delete settings.retentionMaxAge; delete settings.retentionExcludePinned; delete settings.retentionFilesOnly; diff --git a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx index 4e8e44b1f93..39242161ed4 100644 --- a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx +++ b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx @@ -52,7 +52,7 @@ export const Default: ComponentStory = () => ( - + ); diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx index 27202afa496..06f6ed133dc 100644 --- a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx @@ -4,23 +4,26 @@ import type { FC } from 'react'; import React from 'react'; import { useFormattedRelativeTime } from '../../hooks/useFormattedRelativeTime'; +import { getMaxAgeInMS } from '../../views/room/hooks/useRetentionPolicy'; type RetentionPolicyCalloutProps = { - filesOnlyDefault: boolean; - excludePinnedDefault: boolean; - maxAgeDefault: number; + filesOnly: boolean; + excludePinned: boolean; + maxAge: number; }; -const RetentionPolicyCallout: FC = ({ filesOnlyDefault, excludePinnedDefault, maxAgeDefault }) => { +const RetentionPolicyCallout: FC = ({ filesOnly, excludePinned, maxAge }) => { const t = useTranslation(); - const time = useFormattedRelativeTime(maxAgeDefault); + const time = useFormattedRelativeTime(getMaxAgeInMS(maxAge)); return ( - - {filesOnlyDefault && excludePinnedDefault &&

{t('RetentionPolicy_RoomWarning_FilesOnly', { time })}

} - {filesOnlyDefault && !excludePinnedDefault &&

{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time })}

} - {!filesOnlyDefault && excludePinnedDefault &&

{t('RetentionPolicy_RoomWarning', { time })}

} - {!filesOnlyDefault && !excludePinnedDefault &&

{t('RetentionPolicy_RoomWarning_Unpinned', { time })}

} + +
+ {filesOnly && excludePinned &&

{t('RetentionPolicy_RoomWarning_FilesOnly', { time })}

} + {filesOnly && !excludePinned &&

{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time })}

} + {!filesOnly && excludePinned &&

{t('RetentionPolicy_RoomWarning', { time })}

} + {!filesOnly && !excludePinned &&

{t('RetentionPolicy_RoomWarning_Unpinned', { time })}

} +
); }; diff --git a/apps/meteor/client/hooks/useFormatRelativeTime.ts b/apps/meteor/client/hooks/useFormatRelativeTime.ts deleted file mode 100644 index 5f0a0675cf6..00000000000 --- a/apps/meteor/client/hooks/useFormatRelativeTime.ts +++ /dev/null @@ -1,14 +0,0 @@ -import moment from 'moment'; -import { useCallback } from 'react'; - -export const useFormatRelativeTime = (): ((timeMs: number) => string) => - useCallback((timeMs: number) => { - moment.relativeTimeThreshold('s', 60); - moment.relativeTimeThreshold('ss', 0); - moment.relativeTimeThreshold('m', 60); - moment.relativeTimeThreshold('h', 24); - moment.relativeTimeThreshold('d', 31); - moment.relativeTimeThreshold('M', 12); - - return moment.duration(timeMs).humanize(); - }, []); diff --git a/apps/meteor/client/views/room/body/RetentionPolicyWarning.tsx b/apps/meteor/client/views/room/body/RetentionPolicyWarning.tsx index 6455d391425..87a061c682e 100644 --- a/apps/meteor/client/views/room/body/RetentionPolicyWarning.tsx +++ b/apps/meteor/client/views/room/body/RetentionPolicyWarning.tsx @@ -1,9 +1,10 @@ import { Icon } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useMemo } from 'react'; +import React from 'react'; -import { useFormatRelativeTime } from '../../../hooks/useFormatRelativeTime'; +import { useFormattedRelativeTime } from '../../../hooks/useFormattedRelativeTime'; +import { getMaxAgeInMS } from '../hooks/useRetentionPolicy'; type RetentionPolicyWarningProps = { filesOnly: boolean; @@ -13,13 +14,16 @@ type RetentionPolicyWarningProps = { const RetentionPolicyWarning = ({ filesOnly, excludePinned, maxAge }: RetentionPolicyWarningProps): ReactElement => { const t = useTranslation(); - - const formatRelativeTime = useFormatRelativeTime(); - const time = useMemo(() => formatRelativeTime(maxAge), [formatRelativeTime, maxAge]); + const time = useFormattedRelativeTime(getMaxAgeInMS(maxAge)); if (filesOnly) { return ( -
+
{' '} {excludePinned ? t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time }) @@ -29,7 +33,12 @@ const RetentionPolicyWarning = ({ filesOnly, excludePinned, maxAge }: RetentionP } return ( -
+
{' '} {excludePinned ? t('RetentionPolicy_RoomWarning_Unpinned', { time }) : t('RetentionPolicy_RoomWarning', { time })}
diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 5f63f9f571c..0535b65c6cf 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -22,6 +22,7 @@ import { useRoomToolbox } from '../contexts/RoomToolboxContext'; import { useUserCard } from '../contexts/UserCardContext'; import { useDateScroll } from '../hooks/useDateScroll'; import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; +import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessageButton from './JumpToRecentMessageButton'; import LeaderBar from './LeaderBar'; @@ -39,7 +40,6 @@ import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; -import { useRetentionPolicy } from './hooks/useRetentionPolicy'; import { useHandleUnread } from './hooks/useUnreadMessages'; const RoomBody = (): ReactElement => { @@ -291,7 +291,7 @@ const RoomBody = (): ReactElement => {
  • {isLoadingMoreMessages ? : null}
  • ) : (
  • - {retentionPolicy ? : null} + {retentionPolicy?.isActive ? : null}
  • )} diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx index bc7714da06a..6b8d0777910 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -23,6 +23,7 @@ import { 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 type { ChangeEvent } from 'react'; import React, { useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; @@ -39,6 +40,7 @@ import RawText from '../../../../../components/RawText'; import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; import { getDirtyFields } from '../../../../../lib/getDirtyFields'; import { useArchiveRoom } from '../../../../hooks/roomActions/useArchiveRoom'; +import { useRetentionPolicy } from '../../../hooks/useRetentionPolicy'; import { useEditRoomInitialValues } from './useEditRoomInitialValues'; import { useEditRoomPermissions } from './useEditRoomPermissions'; @@ -54,6 +56,18 @@ const title = { discussion: 'Edit_discussion' as TranslationKey, }; +const getRetentionSetting = (roomType: IRoomWithRetentionPolicy['t']): string => { + switch (roomType) { + case 'd': + return 'RetentionPolicy_MaxAge_DMs'; + case 'p': + return 'RetentionPolicy_MaxAge_Groups'; + case 'c': + default: + return 'RetentionPolicy_MaxAge_Channels'; + } +}; + const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -61,7 +75,8 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => // eslint-disable-next-line no-nested-ternary const roomType = 'prid' in room ? 'discussion' : room.teamId ? 'team' : 'channel'; - const retentionPolicy = useSetting('RetentionPolicy_Enabled'); + const retentionPolicy = useRetentionPolicy(room); + const retentionMaxAgeDefault = useSetting(getRetentionSetting(room.t)) ?? 30; const defaultValues = useEditRoomInitialValues(room); const namesValidation = useSetting('UTF8_Channel_Names_Validation'); const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); @@ -97,7 +112,6 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => joinCodeRequired, hideSysMes, retentionEnabled, - retentionMaxAge, retentionOverrideGlobal, roomType: roomTypeP, reactWhenReadOnly, @@ -128,26 +142,46 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => const handleArchive = useArchiveRoom(room); - const handleUpdateRoomData = useEffectEvent(async ({ hideSysMes, joinCodeRequired, ...formData }) => { - const data = getDirtyFields(formData, dirtyFields); - delete data.archived; + // TODO: add payload validation + const handleUpdateRoomData = useEffectEvent( + async ({ + hideSysMes, + joinCodeRequired, + retentionEnabled, + retentionOverrideGlobal, + retentionMaxAge, + retentionExcludePinned, + retentionFilesOnly, + ...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, - }), - }); + try { + await saveAction({ + rid: room._id, + ...data, + ...((data.joinCode || 'joinCodeRequired' in data) && { joinCode: joinCodeRequired ? data.joinCode : '' }), + ...((data.systemMessages || !hideSysMes) && { + systemMessages: hideSysMes && data.systemMessages, + }), + retentionEnabled, + retentionOverrideGlobal, + ...(retentionEnabled && + retentionOverrideGlobal && { + retentionMaxAge, + retentionExcludePinned, + retentionFilesOnly, + }), + }); - dispatchToastMessage({ type: 'success', message: t('Room_updated_successfully') }); - onClickClose(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); + 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)), @@ -431,7 +465,7 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => )} - {retentionPolicy && ( + {retentionPolicy?.enabled && ( @@ -468,7 +502,9 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => {t('RetentionPolicyRoom_ReadTheDocs')} - {t('RetentionPolicyRoom_MaxAge', { max: retentionMaxAge })} + + {t('RetentionPolicyRoom_MaxAge', { max: retentionMaxAgeDefault })} + onChange(Math.max(1, Number(currentValue)))} + onChange={(e: ChangeEvent) => onChange(Number(e.currentTarget.value))} /> )} /> diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts index f36802bb9f5..128f6c3c66f 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -1,29 +1,13 @@ import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; -import { useSetting } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; - -const getPolicyRoomType = (roomType: IRoomWithRetentionPolicy['t']) => { - switch (roomType) { - case 'c': - return 'Channels'; - case 'p': - return 'Groups'; - case 'd': - return 'DMs'; - } -}; +import { useRetentionPolicy } from '../../../hooks/useRetentionPolicy'; export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { + const retentionPolicy = useRetentionPolicy(room); const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room; - const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled'); - const maxAgeDefault = useSetting(`RetentionPolicy_MaxAge_${getPolicyRoomType(room.t)}`) || 30; - const retentionEnabledDefault = useSetting(`RetentionPolicy_AppliesTo${getPolicyRoomType(room.t)}`); - const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned'); - const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly'); - return useMemo( () => ({ roomName: t === 'd' && room.usernames ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, room), @@ -40,25 +24,21 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { systemMessages: Array.isArray(sysMes) ? sysMes : [], hideSysMes: Array.isArray(sysMes) ? !!sysMes?.length : !!sysMes, encrypted, - ...(retentionPolicyEnabled && { - retentionEnabled: retention?.enabled ?? retentionEnabledDefault, + ...(retentionPolicy?.enabled && { + retentionEnabled: retention?.enabled ?? retentionPolicy.isActive, retentionOverrideGlobal: !!retention?.overrideGlobal, - retentionMaxAge: Math.min(retention?.maxAge, maxAgeDefault) || maxAgeDefault, - retentionExcludePinned: retention?.excludePinned ?? excludePinnedDefault, - retentionFilesOnly: retention?.filesOnly ?? filesOnlyDefault, + retentionMaxAge: retention?.maxAge ?? retentionPolicy.maxAge, + retentionExcludePinned: retention?.excludePinned ?? retentionPolicy.excludePinned, + retentionFilesOnly: retention?.filesOnly ?? retentionPolicy.filesOnly, }), }), [ announcement, archived, description, - excludePinnedDefault, - filesOnlyDefault, joinCodeRequired, - maxAgeDefault, retention, - retentionEnabledDefault, - retentionPolicyEnabled, + retentionPolicy, ro, room, sysMes, diff --git a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx index 80abe104c88..5c45e8d095f 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx @@ -17,7 +17,7 @@ import RetentionPolicyCallout from '../../../../../components/InfoPanel/Retentio import MarkdownText from '../../../../../components/MarkdownText'; import type { Action } from '../../../../hooks/useActionSpread'; import { useActionSpread } from '../../../../hooks/useActionSpread'; -import { useRetentionPolicy } from '../../../body/hooks/useRetentionPolicy'; +import { useRetentionPolicy } from '../../../hooks/useRetentionPolicy'; import { useRoomActions } from '../hooks/useRoomActions'; type RoomInfoProps = { @@ -135,11 +135,11 @@ const RoomInfo = ({ room, icon, onClickBack, onClickClose, onClickEnterRoom, onC )} - {retentionPolicy && ( + {retentionPolicy?.isActive && ( )} diff --git a/apps/meteor/client/views/room/body/hooks/useRetentionPolicy.ts b/apps/meteor/client/views/room/hooks/useRetentionPolicy.ts similarity index 92% rename from apps/meteor/client/views/room/body/hooks/useRetentionPolicy.ts rename to apps/meteor/client/views/room/hooks/useRetentionPolicy.ts index f77bc5bf994..ce16df8bc32 100644 --- a/apps/meteor/client/views/room/body/hooks/useRetentionPolicy.ts +++ b/apps/meteor/client/views/room/hooks/useRetentionPolicy.ts @@ -16,6 +16,8 @@ type RetentionPolicySettings = { maxAgeDMs: number; }; +export const getMaxAgeInMS = (maxAge: number) => maxAge * 24 * 60 * 60 * 1000; + const isActive = (room: IRoom, { enabled, appliesToChannels, appliesToGroups, appliesToDMs }: RetentionPolicySettings): boolean => { if (!enabled) { return false; @@ -75,6 +77,8 @@ export const useRetentionPolicy = ( room: IRoom | undefined, ): | { + enabled: boolean; + isActive: boolean; filesOnly: boolean; excludePinned: boolean; maxAge: number; @@ -92,13 +96,15 @@ export const useRetentionPolicy = ( maxAgeDMs: useSetting('RetentionPolicy_MaxAge_DMs') as number, } as const; - if (!room || !isActive(room, settings)) { + if (!room) { return undefined; } return { + enabled: settings.enabled, + isActive: isActive(room, settings), filesOnly: extractFilesOnly(room, settings), excludePinned: extractExcludePinned(room, settings), - maxAge: getMaxAge(room, settings) * 24 * 60 * 60 * 1000, + maxAge: getMaxAge(room, settings), }; }; diff --git a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx index 809e20100c9..12e092b4876 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx @@ -200,11 +200,7 @@ const TeamsInfo = ({ )} {retentionPolicyEnabled && ( - + )} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 01237e68cfb..4f55c4f088e 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -13,6 +13,10 @@ export class HomeContent { return this.page.locator('main header'); } + get channelRetentionPolicyWarning(): Locator { + return this.page.locator('main').getByRole('alert', { name: 'Retention policy warning banner' }); + } + get inputMessage(): Locator { return this.page.locator('[name="msg"]'); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-room.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-room.ts index ece3d27fd25..fac98e630ca 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-room.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-room.ts @@ -38,4 +38,28 @@ export class HomeFlextabRoom { get btnSave(): Locator { return this.page.locator('role=button[name="Save"]'); } + + get calloutRetentionPolicy(): Locator { + return this.page.getByRole('dialog').getByRole('alert', { name: 'Retention policy warning callout' }); + } + + get pruneAccordion(): Locator { + return this.page.getByRole('dialog').getByRole('button', { name: 'Prune' }); + } + + getMaxAgeLabel(maxAge = '30') { + return this.page.getByRole('dialog').getByText(`Maximum message age in days (default: ${maxAge})`) + } + + get inputRetentionMaxAge(): Locator { + return this.page.getByRole('dialog').locator('input[name="retentionMaxAge"]'); + } + + get checkboxPruneMessages(): Locator { + return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Automatically prune old messages' }) }); + } + + get checkboxOverrideGlobalRetention(): Locator { + return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Override global retention policy' }) }); + } } diff --git a/apps/meteor/tests/e2e/retention-policy.spec.ts b/apps/meteor/tests/e2e/retention-policy.spec.ts new file mode 100644 index 00000000000..94acdc6d65f --- /dev/null +++ b/apps/meteor/tests/e2e/retention-policy.spec.ts @@ -0,0 +1,138 @@ +import { Users } from './fixtures/userStates'; +import { HomeChannel } from './page-objects'; +import { createTargetChannel, createTargetPrivateChannel, setSettingValueById } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe.serial('retention-policy', () => { + let poHomeChannel: HomeChannel; + let targetChannel: string; + let targetGroup: string; + + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); + targetGroup = await createTargetPrivateChannel(api); + }) + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + + await page.goto('/home'); + }); + + test.describe('retention policy disabled', () => { + test('should not show prune banner in channel', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + + await expect(poHomeChannel.content.channelRetentionPolicyWarning).not.toBeVisible(); + }); + + test('should not show prune section on edit channel', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + + await expect(poHomeChannel.tabs.room.pruneAccordion).not.toBeVisible(); + }); + }); + + test.describe('retention policy enabled', () => { + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'RetentionPolicy_Enabled', true); + }) + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'RetentionPolicy_Enabled', false); + await setSettingValueById(api, 'RetentionPolicy_AppliesToChannels', false); + await setSettingValueById(api, 'RetentionPolicy_AppliesToGroups', false); + await setSettingValueById(api, 'RetentionPolicy_AppliesToDMs', false); + await setSettingValueById(api, 'RetentionPolicy_MaxAge_Channels', 30); + }); + + test('should not show prune banner even with retention policy setting enabled in any type of room', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await expect(poHomeChannel.content.channelRetentionPolicyWarning).not.toBeVisible(); + + await poHomeChannel.sidenav.openChat(targetGroup); + await expect(poHomeChannel.content.channelRetentionPolicyWarning).not.toBeVisible(); + + await poHomeChannel.sidenav.openChat('user1'); + await expect(poHomeChannel.content.channelRetentionPolicyWarning).not.toBeVisible(); + }); + + test('should show prune section in edit channel', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + + await expect(poHomeChannel.tabs.room.pruneAccordion).toBeVisible(); + }); + + test.describe('retention policy applies enabled by default', () => { + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'RetentionPolicy_AppliesToChannels', true); + await setSettingValueById(api, 'RetentionPolicy_AppliesToGroups', true); + await setSettingValueById(api, 'RetentionPolicy_AppliesToDMs', true); + }); + + test('should prune old messages checkbox enabled by default in channel and show retention policy banner', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await expect(poHomeChannel.content.channelRetentionPolicyWarning).toBeVisible(); + + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + await poHomeChannel.tabs.room.pruneAccordion.click(); + await expect(poHomeChannel.tabs.room.checkboxPruneMessages).toBeChecked(); + }); + + test('should prune old messages checkbox enabled by default in group and show retention policy banner', async () => { + await poHomeChannel.sidenav.openChat(targetGroup); + await expect(poHomeChannel.content.channelRetentionPolicyWarning).toBeVisible(); + + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + await poHomeChannel.tabs.room.pruneAccordion.click(); + await expect(poHomeChannel.tabs.room.checkboxPruneMessages).toBeChecked(); + }); + + test('should show retention policy banner in DMs', async () => { + await poHomeChannel.sidenav.openChat('user1'); + await expect(poHomeChannel.content.channelRetentionPolicyWarning).toBeVisible(); + }); + }); + + test.describe('retention policy override', () => { + test.beforeAll(async ({ api }) => { + expect((await setSettingValueById(api, 'RetentionPolicy_MaxAge_Channels', 15)).status()).toBe(200); + }); + + test('should display the default max age in edit channel', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + await poHomeChannel.tabs.room.pruneAccordion.click(); + await poHomeChannel.tabs.room.checkboxOverrideGlobalRetention.click(); + + await expect(poHomeChannel.tabs.room.getMaxAgeLabel('15')).toBeVisible(); + }); + + test('should display overridden retention max age value', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + await poHomeChannel.tabs.room.pruneAccordion.click(); + await poHomeChannel.tabs.room.checkboxOverrideGlobalRetention.click(); + await poHomeChannel.tabs.room.inputRetentionMaxAge.fill('365'); + await poHomeChannel.tabs.room.btnSave.click(); + await poHomeChannel.dismissToast(); + + await poHomeChannel.tabs.btnRoomInfo.click(); + await poHomeChannel.tabs.room.btnEdit.click(); + await poHomeChannel.tabs.room.pruneAccordion.click(); + + await expect(poHomeChannel.tabs.room.getMaxAgeLabel('15')).toBeVisible(); + await expect(poHomeChannel.tabs.room.inputRetentionMaxAge).toHaveValue('365'); + }); + }); + }); +}); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 1dc586fb0c4..c59f3887106 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -4500,6 +4500,8 @@ "RetentionPolicyRoom_MaxAge": "Maximum message age in days (default: {{max}})", "RetentionPolicyRoom_OverrideGlobal": "Override global retention policy", "RetentionPolicyRoom_ReadTheDocs": "Watch out! Tweaking these settings without utmost care can destroy all message history. Please read the documentation before turning the feature on here.", + "Retention_policy_warning_banner": "Retention policy warning banner", + "Retention_policy_warning_callout": "Retention policy warning callout", "Retry": "Retry", "Return_to_home": "Return to home", "Return_to_previous_page": "Return to previous page",