diff --git a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx index 32948eb42243..04b07e9cd627 100644 --- a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx @@ -71,7 +71,7 @@ const RoomAvatarEditor = ({ disabled = false, room, roomAvatar, onChangeAvatar } danger icon='trash' title={t('Accounts_SetDefaultAvatar')} - disabled={roomAvatar === null || isRoomFederated(room) || disabled} + disabled={!roomAvatar || isRoomFederated(room) || disabled} onClick={clickReset} /> diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index b12b5e3e1ab9..8130f2e9ec8b 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -13,18 +13,17 @@ import { TextAreaInput, } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; -import GenericModal from '../../../components/GenericModal'; import RoomAvatarEditor from '../../../components/avatar/RoomAvatarEditor'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; import { useForm } from '../../../hooks/useForm'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; -import DeleteTeamModalWithRooms from '../../teams/contextualBar/info/DeleteTeam'; +import { useDeleteRoom } from '../../hooks/roomActions/useDeleteRoom'; type EditRoomProps = { room: Pick; @@ -65,11 +64,6 @@ const getInitialValues = (room: Pick): EditRoomFormV const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => { const t = useTranslation(); - const [deleting, setDeleting] = useState(false); - - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const { values, handlers, hasUnsavedChanges, reset } = useForm(getInitialValues(room)); const [canViewName, canViewTopic, canViewAnnouncement, canViewArchived, canViewDescription, canViewType, canViewReadOnly] = @@ -119,9 +113,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => const changeArchivation = archived !== !!room.archived; - const roomsRoute = useRoute('admin-rooms'); - - const canDelete = usePermission(`delete-${room.t}`); + const { handleDelete, canDeleteRoom, isDeleting } = useDeleteRoom(room, { reload: onDelete }); const archiveSelector = room.archived ? 'unarchive' : 'archive'; const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; @@ -161,61 +153,6 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => handleRoomType(roomType === 'p' ? 'c' : 'p'); }); - const deleteRoom = useEndpoint('POST', '/v1/rooms.delete'); - const deleteTeam = useEndpoint('POST', '/v1/teams.delete'); - - const handleDelete = useMutableCallback(() => { - const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { - try { - setDeleting(true); - setModal(null); - await deleteTeam({ teamId: room.teamId as string, ...(roomsToRemove.length && { roomsToRemove }) }); - dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - if (room.teamMain) { - setModal( - setModal(null)} teamId={room.teamId as string} />, - ); - - return; - } - - const handleDeleteRoom = async (): Promise => { - try { - setDeleting(true); - setModal(null); - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - setModal( - setModal(null)} - onCancel={(): void => setModal(null)} - confirmText={t('Yes_delete_it')} - > - {t('Delete_Room_Warning')} - , - ); - }); - return ( <> e.preventDefault())}> @@ -227,7 +164,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Name')} - + {room.t !== 'd' && ( @@ -246,7 +183,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Topic')} - + )} @@ -281,7 +218,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Private')} - + {t('Just_invited_people_can_access_this_channel')} @@ -292,7 +229,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Read_only')} - + {t('Only_authorized_users_can_write_new_messages')} @@ -314,7 +251,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Room_archivation_state_true')} - + @@ -325,7 +262,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Default')} - + @@ -333,7 +270,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Favorite')} - + @@ -341,22 +278,22 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Featured')} - + - - - diff --git a/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx new file mode 100644 index 000000000000..be4728732284 --- /dev/null +++ b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx @@ -0,0 +1,89 @@ +import type { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetModal, useToastMessageDispatch, useRouter, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React from 'react'; + +import GenericModal from '../../../components/GenericModal'; +import DeleteTeamModal from '../../teams/contextualBar/info/DeleteTeam'; + +export const useDeleteRoom = (room: IRoom | Pick, { reload }: { reload?: () => void } = {}) => { + const t = useTranslation(); + const router = useRouter(); + const setModal = useSetModal(); + const dispatchToastMessage = useToastMessageDispatch(); + const hasPermissionToDelete = usePermission(`delete-${room.t}`, room._id); + const canDeleteRoom = isRoomFederated(room) ? false : hasPermissionToDelete; + + const isAdminRoute = router.getRouteName() === 'admin-rooms'; + + const deleteRoomEndpoint = useEndpoint('POST', '/v1/rooms.delete'); + const deleteTeamEndpoint = useEndpoint('POST', '/v1/teams.delete'); + + const deleteRoomMutation = useMutation({ + mutationFn: deleteRoomEndpoint, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); + if (isAdminRoute) { + return router.navigate('/admin/rooms'); + } + + return router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + setModal(null); + reload?.(); + }, + }); + + const deleteTeamMutation = useMutation({ + mutationFn: deleteTeamEndpoint, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); + if (isAdminRoute) { + return router.navigate('/admin/rooms'); + } + + return router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + setModal(null); + reload?.(); + }, + }); + + const isDeleting = deleteTeamMutation.isLoading || deleteRoomMutation.isLoading; + + const handleDelete = useMutableCallback(() => { + const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { + if (!room.teamId) { + return; + } + + deleteTeamMutation.mutateAsync({ teamId: room.teamId, ...(roomsToRemove.length && { roomsToRemove }) }); + }; + + if (room.teamMain && room.teamId) { + return setModal( setModal(null)} teamId={room.teamId} />); + } + + const handleDeleteRoom = async () => { + deleteRoomMutation.mutateAsync({ roomId: room._id }); + }; + + setModal( + setModal(null)} confirmText={t('Yes_delete_it')}> + {t('Delete_Room_Warning')} + , + ); + }); + + return { handleDelete, canDeleteRoom, isDeleting }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js deleted file mode 100644 index 22c84fbdebf1..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js +++ /dev/null @@ -1,514 +0,0 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { - Field, - TextInput, - PasswordInput, - ToggleSwitch, - MultiSelect, - Accordion, - Callout, - NumberInput, - FieldGroup, - FieldLabel, - FieldRow, - FieldHint, - Button, - ButtonGroup, - Box, - TextAreaInput, -} from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { - useSetModal, - useSetting, - usePermission, - useAtLeastOnePermission, - useRole, - useMethod, - useTranslation, - useRouter, -} from '@rocket.chat/ui-contexts'; -import React, { useCallback, useMemo, useRef } from 'react'; - -import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; -import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes'; -import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; -import { - ContextualbarHeader, - ContextualbarBack, - ContextualbarTitle, - ContextualbarClose, - ContextualbarScrollableContent, - ContextualbarFooter, -} from '../../../../../components/Contextualbar'; -import GenericModal from '../../../../../components/GenericModal'; -import RawText from '../../../../../components/RawText'; -import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; -import { useEndpointAction } from '../../../../../hooks/useEndpointAction'; -import { useForm } from '../../../../../hooks/useForm'; -import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; - -const typeMap = { - c: 'Channels', - p: 'Groups', - d: 'DMs', -}; - -const useInitialValues = (room, settings) => { - const { - t, - ro, - archived, - topic, - description, - announcement, - joinCodeRequired, - sysMes, - encrypted, - retention = {}, - reactWhenReadOnly, - } = room; - - const { retentionPolicyEnabled, maxAgeDefault } = settings; - - const retentionEnabledDefault = useSetting(`RetentionPolicy_AppliesTo${typeMap[room.t]}`); - const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned'); - const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly'); - - return useMemo( - () => ({ - roomName: t === 'd' ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, { type: 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: !!sysMes?.length, - encrypted, - ...(retentionPolicyEnabled && { - retentionEnabled: retention.enabled ?? retentionEnabledDefault, - retentionOverrideGlobal: !!retention.overrideGlobal, - retentionMaxAge: Math.min(retention.maxAge, maxAgeDefault) || maxAgeDefault, - retentionExcludePinned: retention.excludePinned ?? excludePinnedDefault, - retentionFilesOnly: retention.filesOnly ?? filesOnlyDefault, - }), - }), - [ - announcement, - archived, - description, - excludePinnedDefault, - filesOnlyDefault, - joinCodeRequired, - maxAgeDefault, - retention.enabled, - retention.excludePinned, - retention.filesOnly, - retention.maxAge, - retention.overrideGlobal, - retentionEnabledDefault, - retentionPolicyEnabled, - ro, - room, - sysMes, - t, - topic, - encrypted, - reactWhenReadOnly, - ], - ); -}; - -const getCanChangeType = (room, canCreateChannel, canCreateGroup, isAdmin) => - (!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup)); - -function EditChannel({ room, onClickClose, onClickBack }) { - const t = useTranslation(); - - const setModal = useSetModal(); - - const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled'); - const maxAgeDefault = useSetting(`RetentionPolicy_MaxAge_${typeMap[room.t]}`) || 30; - - const saveData = useRef({}); - const router = useRouter(); - - const onChange = useCallback(({ initialValue, value, key }) => { - const { current } = saveData; - if (JSON.stringify(initialValue) !== JSON.stringify(value)) { - if (key === 'systemMessages' && value?.length > 0) { - current.hideSysMes = true; - } - current[key] = value; - } else { - delete current[key]; - } - }, []); - - const { values, handlers, hasUnsavedChanges, reset, commit } = useForm( - useInitialValues(room, { retentionPolicyEnabled, maxAgeDefault }), - onChange, - ); - - const sysMesOptions = useMemo(() => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel)]), [t]); - - const { - roomName, - roomType, - readOnly, - encrypted, - roomAvatar, - archived, - roomTopic, - roomDescription, - roomAnnouncement, - reactWhenReadOnly, - joinCode, - joinCodeRequired, - systemMessages, - hideSysMes, - retentionEnabled, - retentionOverrideGlobal, - retentionMaxAge, - retentionExcludePinned, - retentionFilesOnly, - } = values; - - const { - handleJoinCode, - handleJoinCodeRequired, - handleSystemMessages, - handleEncrypted, - handleHideSysMes, - handleRoomName, - handleReadOnly, - handleArchived, - handleRoomAvatar, - handleReactWhenReadOnly, - handleRoomType, - handleRoomTopic, - handleRoomDescription, - handleRoomAnnouncement, - handleRetentionEnabled, - handleRetentionOverrideGlobal, - handleRetentionMaxAge, - handleRetentionExcludePinned, - handleRetentionFilesOnly, - } = handlers; - - const [ - canViewName, - canViewTopic, - canViewAnnouncement, - canViewArchived, - canViewDescription, - canViewType, - canViewReadOnly, - canViewHideSysMes, - canViewJoinCode, - canViewEncrypted, - ] = useMemo(() => { - const isAllowed = roomCoordinator.getRoomDirectives(room.t).allowRoomSettingChange || (() => {}); - 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), - isAllowed(room, RoomSettingsEnum.E2E), - ]; - }, [room]); - - const isAdmin = useRole('admin'); - - const canCreateChannel = usePermission('create-c'); - const canCreateGroup = usePermission('create-p'); - const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); - const canSetRo = usePermission('set-readonly', room._id); - const canSetReactWhenRo = 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 canDelete = usePermission(`delete-${room.t}`); - const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); - - const changeArchivation = archived !== !!room.archived; - const archiveSelector = room.archived ? 'unarchive' : 'archive'; - const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; - const saveAction = useEndpointAction('POST', '/v1/rooms.saveRoomSettings', { - successMessage: t('Room_updated_successfully'), - }); - const archiveAction = useEndpointAction('POST', '/v1/rooms.changeArchivationState', { successMessage: t(archiveMessage) }); - - const handleSave = useMutableCallback(async () => { - const { joinCodeRequired, hideSysMes, ...data } = saveData.current; - delete data.archived; - const save = () => - saveAction({ - rid: room._id, - ...data, - ...(joinCode && { joinCode: joinCodeRequired ? joinCode : '' }), - ...((data.systemMessages || !hideSysMes) && { - systemMessages: hideSysMes && systemMessages, - }), - }); - - const archive = () => archiveAction({ rid: room._id, action: archiveSelector }); - - await Promise.all([hasUnsavedChanges && save(), changeArchivation && archive()].filter(Boolean)); - saveData.current = {}; - commit(); - }); - - const deleteRoom = useMethod('eraseRoom'); - - const handleDelete = useMutableCallback(() => { - const onCancel = () => setModal(undefined); - const onConfirm = async () => { - await deleteRoom(room._id); - onCancel(); - router.navigate('/home'); - }; - - setModal( - - {t('Delete_Room_Warning')} - , - ); - }); - - const changeRoomType = useMutableCallback(() => { - handleRoomType(roomType === 'p' ? 'c' : 'p'); - }); - - const onChangeMaxAge = useMutableCallback((e) => { - handleRetentionMaxAge(Math.max(1, Number(e.currentTarget.value))); - }); - - const isFederated = useMemo(() => isRoomFederated(room), [room]); - - return ( - <> - - {onClickBack && } - {room.teamId ? t('edit-team') : t('edit-room')} - {onClickClose && } - - e.preventDefault())}> - - - - - {t('Name')} - - - - - {canViewDescription && ( - - {t('Description')} - - - - - )} - {canViewAnnouncement && ( - - {t('Announcement')} - - - - - )} - {canViewTopic && ( - - {t('Topic')} - - - - - )} - {canViewType && ( - - - {t('Private')} - - - - - {t('Teams_New_Private_Description_Enabled')} - - )} - {canViewReadOnly && ( - - - {t('Read_only')} - - - - - {t('Only_authorized_users_can_write_new_messages')} - - )} - {readOnly && ( - - - {t('React_when_read_only')} - - - - - {t('Only_authorized_users_can_react_to_messages')} - - )} - {canViewArchived && ( - - - {t('Room_archivation_state_true')} - - - - - - )} - {canViewJoinCode && ( - - - {t('Password_to_access')} - - - - - - - - - )} - {canViewHideSysMes && ( - - - {t('Hide_System_Messages')} - - - - - - - - - )} - {canViewEncrypted && ( - - - {t('Encrypted')} - - - - - - )} - {retentionPolicyEnabled && ( - - - - - - {t('RetentionPolicyRoom_Enabled')} - - - - - - - - {t('RetentionPolicyRoom_OverrideGlobal')} - - - - - - {retentionOverrideGlobal && ( - <> - - {t('RetentionPolicyRoom_ReadTheDocs')} - - - {t('RetentionPolicyRoom_MaxAge', { max: maxAgeDefault })} - - - - - - - {t('RetentionPolicyRoom_ExcludePinned')} - - - - - - - - {t('RetentionPolicyRoom_FilesOnly')} - - - - - - - )} - - - - )} - - - - - - - - - - - - ); -} - -export default EditChannel; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js deleted file mode 100644 index e64dcc17562a..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useRoom } from '../../../contexts/RoomContext'; -import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; -import EditChannel from './EditChannel'; - -function EditChannelWithData({ onClickBack }) { - const room = useRoom(); - const { closeTab } = useRoomToolbox(); - - return ; -} - -export default EditChannelWithData; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx new file mode 100644 index 000000000000..b2c552fd3d87 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -0,0 +1,487 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { + Field, + FieldRow, + FieldLabel, + FieldHint, + TextInput, + PasswordInput, + ToggleSwitch, + MultiSelect, + Accordion, + Callout, + NumberInput, + FieldGroup, + Button, + ButtonGroup, + Box, + TextAreaInput, +} from '@rocket.chat/fuselage'; +import { useMutableCallback, 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 { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom'; +import { useEditRoomInitialValues } from './useEditRoomInitialValues'; +import { useEditRoomPermissions } from './useEditRoomPermissions'; + +type EditRoomInfoProps = { + room: IRoomWithRetentionPolicy; + onClickClose: () => void; + onClickBack: () => void; +}; + +const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const isFederated = useMemo(() => isRoomFederated(room), [room]); + + const retentionPolicy = useSetting('RetentionPolicy_Enabled'); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room); + const defaultValues = useEditRoomInitialValues(room); + + const { + watch, + reset, + control, + handleSubmit, + formState: { isDirty, dirtyFields, errors }, + } = useForm({ mode: 'onBlur', defaultValues }); + + const sysMesOptions: SelectOption[] = useMemo( + () => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel as TranslationKey)]), + [t], + ); + + const { readOnly, archived, joinCodeRequired, hideSysMes, retentionEnabled, retentionMaxAge, retentionOverrideGlobal } = 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 archiveAction = useEndpoint('POST', '/v1/rooms.changeArchivationState'); + + const handleUpdateRoomData = useMutableCallback(async ({ hideSysMes, ...formData }) => { + const data = getDirtyFields(formData, dirtyFields); + + try { + await saveAction({ + rid: room._id, + ...data, + ...(data.joinCode && { 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 handleArchive = useMutableCallback(async () => { + try { + await archiveAction({ rid: room._id, action: room.archived ? 'unarchive' : 'archive' }); + dispatchToastMessage({ type: 'success', message: room.archived ? t('Room_has_been_unarchived') : t('Room_has_been_archived') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const handleSave = useMutableCallback(async (data) => { + await Promise.all([isDirty && handleUpdateRoomData(data), changeArchiving && handleArchive()].filter(Boolean)); + }); + + 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 ( + <> + + {onClickBack && } + {room.teamId ? t('edit-team') : t('edit-room')} + {onClickClose && } + + +
+ + } + /> + + + + + {t('Name')} + + + } + /> + + {errors.roomName && {errors.roomName.message}} + + {canViewDescription && ( + + {t('Description')} + + } + /> + + + )} + {canViewAnnouncement && ( + + {t('Announcement')} + + } + /> + + + )} + {canViewTopic && ( + + {t('Topic')} + + } + /> + + + )} + {canViewType && ( + + + {t('Private')} + + ( + onChange(value === 'p' ? 'c' : 'p')} + aria-describedby={`${roomTypeField}-hint`} + /> + )} + /> + + + {t('Teams_New_Private_Description_Enabled')} + + )} + {canViewReadOnly && ( + + + {t('Read_only')} + + ( + + )} + /> + + + {t('Only_authorized_users_can_write_new_messages')} + + )} + {readOnly && ( + + + {t('React_when_read_only')} + + ( + + )} + /> + + + {t('Only_authorized_users_can_react_to_messages')} + + )} + {canViewArchived && ( + + + {t('Room_archivation_state_true')} + + ( + + )} + /> + + + + )} + {canViewJoinCode && ( + + + {t('Password_to_access')} + + ( + + )} + /> + + + + } + /> + + + )} + {canViewHideSysMes && ( + + + {t('Hide_System_Messages')} + + ( + + )} + /> + + + + ( + + )} + /> + + + )} + {canViewEncrypted && ( + + + {t('Encrypted')} + + ( + + )} + /> + + + + )} + + {retentionPolicy && ( + + + + + + {t('RetentionPolicyRoom_Enabled')} + + ( + + )} + /> + + + + + + {t('RetentionPolicyRoom_OverrideGlobal')} + + ( + + )} + /> + + + + {retentionOverrideGlobal && ( + <> + + {t('RetentionPolicyRoom_ReadTheDocs')} + + + {t('RetentionPolicyRoom_MaxAge', { max: retentionMaxAge })} + + ( + onChange(Math.max(1, Number(currentValue)))} + /> + )} + /> + + + + + {t('RetentionPolicyRoom_ExcludePinned')} + + ( + + )} + /> + + + + + + {t('RetentionPolicyRoom_FilesOnly')} + + ( + + )} + /> + + + + + )} + + + + )} +
+
+ + + + + + + + + + + ); +}; + +export default EditRoomInfo; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx new file mode 100644 index 000000000000..ad758c1bc8a6 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx @@ -0,0 +1,15 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import React from 'react'; + +import { useRoom } from '../../../contexts/RoomContext'; +import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; +import EditRoomInfo from './EditRoomInfo'; + +const EditRoomInfoWithData = ({ onClickBack }: { onClickBack: () => void }) => { + const room = useRoom() as IRoomWithRetentionPolicy; + const { closeTab } = useRoomToolbox(); + + return ; +}; + +export default EditRoomInfoWithData; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts index 4083ad9a958f..d8b31e17800c 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts @@ -1 +1 @@ -export { default } from './EditChannelWithData'; +export { default } from './EditRoomInfoWithData'; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts new file mode 100644 index 000000000000..f36802bb9f56 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -0,0 +1,71 @@ +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'; + } +}; + +export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { + 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), + 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, + ...(retentionPolicyEnabled && { + retentionEnabled: retention?.enabled ?? retentionEnabledDefault, + retentionOverrideGlobal: !!retention?.overrideGlobal, + retentionMaxAge: Math.min(retention?.maxAge, maxAgeDefault) || maxAgeDefault, + retentionExcludePinned: retention?.excludePinned ?? excludePinnedDefault, + retentionFilesOnly: retention?.filesOnly ?? filesOnlyDefault, + }), + }), + [ + announcement, + archived, + description, + excludePinnedDefault, + filesOnlyDefault, + joinCodeRequired, + maxAgeDefault, + retention, + retentionEnabledDefault, + retentionPolicyEnabled, + ro, + room, + sysMes, + t, + topic, + encrypted, + reactWhenReadOnly, + ], + ); +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts new file mode 100644 index 000000000000..7b9e8c353941 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts @@ -0,0 +1,77 @@ +import type { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { usePermission, useAtLeastOnePermission, useRole } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; +import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; +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 canChangeType = getCanChangeType(room, canCreateChannel, 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 canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); + + const [ + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + ] = 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), + isAllowed(room, RoomSettingsEnum.E2E), + ]; + }, [room]); + + return { + canChangeType, + canSetReadOnly, + canSetReactWhenReadOnly, + canEditRoomRetentionPolicy, + canArchiveOrUnarchive, + canToggleEncryption, + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx b/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx deleted file mode 100644 index b812e896bab9..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useTranslation, useEndpoint, usePermission, useRouter } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import GenericModal from '../../../../../../components/GenericModal'; - -// TODO: resetState for TeamsChannels -export const useRoomDelete = (room: IRoom, resetState?: () => void) => { - const t = useTranslation(); - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const router = useRouter(); - - const hasPermissionToDelete = usePermission(room.t === 'c' ? 'delete-c' : 'delete-p', room._id); - const canDelete = isRoomFederated(room) ? false : hasPermissionToDelete; - - const deleteRoom = useEndpoint('POST', room.t === 'c' ? '/v1/channels.delete' : '/v1/groups.delete'); - - const handleDelete = useMutableCallback(() => { - const onConfirm = async () => { - try { - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - if (resetState) { - return resetState(); - } - - router.navigate('/home'); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - setModal(null); - }; - - setModal( - setModal(null)} confirmText={t('Yes_delete_it')}> - {t('Delete_Room_Warning')} - , - ); - }); - - return canDelete ? handleDelete : null; -}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts b/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts index d0ab03bd9ee7..638cd23b66ed 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts @@ -1,10 +1,9 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; import type { IRoom } from '@rocket.chat/core-typings'; import { useTranslation } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; +import { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom'; import { useRoomConvertToTeam } from './actions/useRoomConvertToTeam'; -import { useRoomDelete } from './actions/useRoomDelete'; import { useRoomHide } from './actions/useRoomHide'; import { useRoomLeave } from './actions/useRoomLeave'; import { useRoomMoveToTeam } from './actions/useRoomMoveToTeam'; @@ -16,11 +15,10 @@ type RoomActions = { export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: RoomActions, resetState?: () => void) => { const t = useTranslation(); - const isFederated = isRoomFederated(room); const handleHide = useRoomHide(room); const handleLeave = useRoomLeave(room); - const handleDelete = useRoomDelete(room, resetState); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload: resetState }); const handleMoveToTeam = useRoomMoveToTeam(room); const handleConvertToTeam = useRoomConvertToTeam(room); @@ -40,7 +38,7 @@ export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: R action: onClickEdit, }, }), - ...(!isFederated && + ...(canDeleteRoom && handleDelete && { delete: { label: t('Delete'), @@ -77,7 +75,7 @@ export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: R }, }), }), - [onClickEdit, t, handleDelete, handleMoveToTeam, handleConvertToTeam, handleHide, handleLeave, onClickEnterRoom, isFederated], + [onClickEdit, t, handleDelete, handleMoveToTeam, handleConvertToTeam, handleHide, handleLeave, onClickEnterRoom, canDeleteRoom], ); return memoizedActions; diff --git a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js index 0b72b3f164a6..f5cb4a44c5d2 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js +++ b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js @@ -16,10 +16,10 @@ import { GenericModalDoNotAskAgain } from '../../../../components/GenericModal'; import { useDontAskAgain } from '../../../../hooks/useDontAskAgain'; import { useEndpointAction } from '../../../../hooks/useEndpointAction'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom'; import { useRoom } from '../../../room/contexts/RoomContext'; import { useRoomToolbox } from '../../../room/contexts/RoomToolboxContext'; import ConvertToChannelModal from '../../ConvertToChannelModal'; -import DeleteTeamModal from './DeleteTeam'; import LeaveTeam from './LeaveTeam'; import TeamsInfo from './TeamsInfo'; @@ -56,7 +56,6 @@ const TeamsInfoWithLogic = ({ openEditing }) => { const setModal = useSetModal(); const closeModal = useMutableCallback(() => setModal()); - const deleteTeam = useEndpointAction('POST', '/v1/teams.delete'); const leaveTeam = useEndpointAction('POST', '/v1/teams.leave'); const convertTeamToChannel = useEndpointAction('POST', '/v1/teams.convertToChannel'); @@ -64,28 +63,11 @@ const TeamsInfoWithLogic = ({ openEditing }) => { const router = useRouter(); - const canDelete = usePermission('delete-team', room._id); const canEdit = usePermission('edit-team-channel', room._id); // const canLeave = usePermission('leave-team'); /* && room.cl !== false && joined */ - const onClickDelete = useMutableCallback(() => { - const onConfirm = async (deletedRooms) => { - const roomsToRemove = Array.isArray(deletedRooms) && deletedRooms.length > 0 ? deletedRooms : []; - - try { - await deleteTeam({ teamId: room.teamId, ...(roomsToRemove.length && { roomsToRemove }) }); - dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); - router.navigate('/home'); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - closeModal(); - } - }; - - setModal(); - }); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room); const onClickLeave = useMutableCallback(() => { const onConfirm = async (roomsLeft) => { @@ -174,7 +156,7 @@ const TeamsInfoWithLogic = ({ openEditing }) => { retentionPolicy={retentionPolicyEnabled && retentionPolicy} onClickEdit={canEdit && openEditing} onClickClose={closeTab} - onClickDelete={canDelete && onClickDelete} + onClickDelete={canDeleteRoom && handleDelete} onClickLeave={/* canLeave && */ onClickLeave} onClickHide={/* joined && */ handleHide} onClickViewChannels={onClickViewChannels}