From 6357447c35a896f735c9a1a67070b2524db43712 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 6 Jul 2021 18:14:28 -0300 Subject: [PATCH] [NEW] Convert Team to Channel (#22476) * wip * Add extra param to listRoomsOfUser to filter rooms user can delete * update the way permission was being checked * new: convert team to channel * fix: lastOwner warning * fix: review * improve: convert ChannelDesertationTable to ts * Remove unused prop Co-authored-by: Douglas Fabris Co-authored-by: Kevin Aleman Co-authored-by: Tasso Evangelista --- app/api/server/v1/teams.ts | 4 +- client/contexts/ServerContext/endpoints.ts | 2 + .../endpoints/v1/teams/listRoomsOfUser.ts | 17 ++++ .../contextualBar/Info/ConvertToTeamModal.js | 23 ------ .../Info/RoomInfo/RoomInfoWithData.js | 14 +++- ...tionTable.js => ChannelDesertionTable.tsx} | 31 ++++++-- .../BaseConvertToChannelModal.tsx | 79 +++++++++++++++++++ .../ConvertToChannelModal.tsx | 57 +++++++++++++ .../ModalSteps/FirstStep.tsx | 65 +++++++++++++++ .../ModalSteps/SecondStep.tsx | 45 +++++++++++ .../teams/ConvertToChannelModal/index.ts | 1 + .../views/teams/contextualBar/ChannelRow.js | 10 ++- .../teams/contextualBar/info/Leave/StepOne.js | 2 +- .../teams/contextualBar/info/TeamsInfo.js | 10 ++- ...sInfoWithLogic.js => TeamsInfoWithData.js} | 35 +++++++- .../views/teams/contextualBar/info/index.js | 4 +- .../RemoveUsersModal/RemoveUsersFirstStep.js | 2 +- .../RemoveUsersModal/RemoveUsersModal.js | 1 - packages/rocketchat-i18n/i18n/en.i18n.json | 6 ++ server/sdk/types/ITeamService.ts | 2 +- server/services/team/service.ts | 17 +++- 21 files changed, 378 insertions(+), 49 deletions(-) create mode 100644 client/contexts/ServerContext/endpoints/v1/teams/listRoomsOfUser.ts delete mode 100644 client/views/room/contextualBar/Info/ConvertToTeamModal.js rename client/views/teams/{contextualBar/ChannelDesertionTable.js => ChannelDesertionTable.tsx} (59%) create mode 100644 client/views/teams/ConvertToChannelModal/BaseConvertToChannelModal.tsx create mode 100644 client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx create mode 100644 client/views/teams/ConvertToChannelModal/ModalSteps/FirstStep.tsx create mode 100644 client/views/teams/ConvertToChannelModal/ModalSteps/SecondStep.tsx create mode 100644 client/views/teams/ConvertToChannelModal/index.ts rename client/views/teams/contextualBar/info/{TeamsInfoWithLogic.js => TeamsInfoWithData.js} (85%) diff --git a/app/api/server/v1/teams.ts b/app/api/server/v1/teams.ts index 4f6758398af0..80242c6590a3 100644 --- a/app/api/server/v1/teams.ts +++ b/app/api/server/v1/teams.ts @@ -206,7 +206,7 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { get() { const { offset, count } = this.getPaginationItems(); - const { teamId, teamName, userId } = this.queryParams; + const { teamId, teamName, userId, canUserDelete = false } = this.queryParams; const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); if (!team) { @@ -219,7 +219,7 @@ API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { return API.v1.unauthorized(); } - const { records, total } = Promise.await(Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, { offset, count })); + const { records, total } = Promise.await(Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, canUserDelete, { offset, count })); return API.v1.success({ rooms: records, diff --git a/client/contexts/ServerContext/endpoints.ts b/client/contexts/ServerContext/endpoints.ts index c92257fc3d6a..cbf4ff9a2275 100644 --- a/client/contexts/ServerContext/endpoints.ts +++ b/client/contexts/ServerContext/endpoints.ts @@ -28,6 +28,7 @@ import { AutocompleteChannelAndPrivateEndpoint as RoomsAutocompleteEndpoint } fr import { RoomsInfo as RoomsInfoEndpoint } from './endpoints/v1/rooms/roomsInfo'; import { AddRoomsEndpoint as TeamsAddRoomsEndpoint } from './endpoints/v1/teams/addRooms'; import { ListRoomsEndpoint } from './endpoints/v1/teams/listRooms'; +import { ListRoomsOfUserEndpoint } from './endpoints/v1/teams/listRoomsOfUser'; import { AutocompleteEndpoint as UsersAutocompleteEndpoint } from './endpoints/v1/users/autocomplete'; import { SendEmailCodeEndpoint } from './endpoints/v1/users/twoFactorAuth/sendEmailCode'; @@ -52,6 +53,7 @@ export type ServerEndpoints = { 'rooms.autocomplete.channelAndPrivate': RoomsAutocompleteEndpoint; 'rooms.autocomplete.availableForTeams': RoomsAutocompleteTeamsEndpoint; 'teams.listRooms': ListRoomsEndpoint; + 'teams.listRoomsOfUser': ListRoomsOfUserEndpoint; 'teams.addRooms': TeamsAddRoomsEndpoint; 'livechat/visitors.info': LivechatVisitorInfoEndpoint; 'livechat/room.onHold': LivechatRoomOnHoldEndpoint; diff --git a/client/contexts/ServerContext/endpoints/v1/teams/listRoomsOfUser.ts b/client/contexts/ServerContext/endpoints/v1/teams/listRoomsOfUser.ts new file mode 100644 index 000000000000..32e113b57ab9 --- /dev/null +++ b/client/contexts/ServerContext/endpoints/v1/teams/listRoomsOfUser.ts @@ -0,0 +1,17 @@ +import { IRoom } from '../../../../../../definition/IRoom'; +import { IRecordsWithTotal } from '../../../../../../definition/ITeam'; + +export type ListRoomsOfUserEndpoint = { + GET: (params: { + teamId: string; + teamName?: string; + userId?: string; + canUserDelete?: boolean; + offset?: number; + count?: number; + }) => Omit, 'records'> & { + count: number; + offset: number; + rooms: IRecordsWithTotal['records']; + }; +}; diff --git a/client/views/room/contextualBar/Info/ConvertToTeamModal.js b/client/views/room/contextualBar/Info/ConvertToTeamModal.js deleted file mode 100644 index 5bb5d298abc4..000000000000 --- a/client/views/room/contextualBar/Info/ConvertToTeamModal.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import GenericModal from '../../../../components/GenericModal'; -import { useTranslation } from '../../../../contexts/TranslationContext'; - -const ChannelToTeamModal = ({ onClose, onConfirm }) => { - const t = useTranslation(); - - return ( - - {` ${t('Converting_channel_to_a_team')}`} - - ); -}; - -export default ChannelToTeamModal; diff --git a/client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js b/client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js index e48f125fa489..eae8779cb032 100644 --- a/client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js +++ b/client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js @@ -17,7 +17,6 @@ import { useEndpointActionExperimental } from '../../../../../hooks/useEndpointA import WarningModal from '../../../../admin/apps/WarningModal'; import { useTabBarClose } from '../../../providers/ToolboxProvider'; import ChannelToTeamModal from '../ChannelToTeamModal/ChannelToTeamModal'; -import ConvertToTeamModal from '../ConvertToTeamModal'; import RoomInfo from './RoomInfo'; const retentionPolicyMaxAge = { @@ -180,7 +179,18 @@ const RoomInfoWithData = ({ rid, openEditing, onClickBack, onEnterRoom, resetSta } }; - setModal(); + setModal( + + {t('Converting_channel_to_a_team')} + , + ); }); const onClickEnterRoom = useMutableCallback(() => onEnterRoom(room)); diff --git a/client/views/teams/contextualBar/ChannelDesertionTable.js b/client/views/teams/ChannelDesertionTable.tsx similarity index 59% rename from client/views/teams/contextualBar/ChannelDesertionTable.js rename to client/views/teams/ChannelDesertionTable.tsx index 5e1f276e9a58..dfa1401abe04 100644 --- a/client/views/teams/contextualBar/ChannelDesertionTable.js +++ b/client/views/teams/ChannelDesertionTable.tsx @@ -1,12 +1,24 @@ import { Box, CheckBox } from '@rocket.chat/fuselage'; -import React from 'react'; +import React, { FC, ReactElement } from 'react'; -import GenericTable from '../../../components/GenericTable'; -import { useTranslation } from '../../../contexts/TranslationContext'; -import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; -import ChannelRow from './ChannelRow'; +import { IRoom } from '../../../definition/IRoom'; +import GenericTable from '../../components/GenericTable'; +import { useTranslation } from '../../contexts/TranslationContext'; +import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; +import ChannelRow from './contextualBar/ChannelRow'; -const ChannelDesertionTable = ({ +type ChannelDesertionTableProps = { + lastOwnerWarning: boolean | undefined; + rooms: Array | undefined; + eligibleRoomsLength: number | undefined; + params?: {}; + onChangeParams?: () => void; + onChangeRoomSelection: (room: IRoom) => void; + selectedRooms: { [key: string]: IRoom }; + onToggleAllRooms: () => void; +}; + +const ChannelDesertionTable: FC = ({ rooms, eligibleRoomsLength, params, @@ -21,7 +33,10 @@ const ChannelDesertionTable = ({ const selectedRoomsLength = Object.values(selectedRooms).filter(Boolean).length; const checked = eligibleRoomsLength === selectedRoomsLength; - const indeterminate = eligibleRoomsLength > selectedRoomsLength && selectedRoomsLength > 0; + const indeterminate = + eligibleRoomsLength && eligibleRoomsLength > selectedRoomsLength + ? selectedRoomsLength > 0 + : false; const formatDate = useFormatDateAndTime(); @@ -51,7 +66,7 @@ const ChannelDesertionTable = ({ fixed={false} pagination={false} > - {(room, key) => ( + {(room: IRoom, key: string): ReactElement => ( void; + onCancel: () => void; + onConfirm: () => Array; + currentStep?: string; + rooms?: Array; +}; + +const BaseConvertToChannelModal: FC = ({ + onClose, + onCancel, + onConfirm, + rooms, + currentStep = rooms?.length === 0 ? STEPS.CONFIRM_CONVERT : STEPS.LIST_ROOMS, +}) => { + const [step, setStep] = useState(currentStep); + const [selectedRooms, setSelectedRooms] = useState<{ [key: string]: IRoom }>({}); + + const onContinue = useMutableCallback(() => setStep(STEPS.CONFIRM_CONVERT)); + const onReturn = useMutableCallback(() => setStep(STEPS.LIST_ROOMS)); + + const eligibleRooms = rooms; + + const onChangeRoomSelection = useCallback((room) => { + setSelectedRooms((selectedRooms) => { + if (selectedRooms[room._id]) { + delete selectedRooms[room._id]; + return { ...selectedRooms }; + } + return { ...selectedRooms, [room._id]: room }; + }); + }, []); + + const onToggleAllRooms = useMutableCallback(() => { + if (Object.values(selectedRooms).filter(Boolean).length === 0 && eligibleRooms) { + return setSelectedRooms(Object.fromEntries(eligibleRooms.map((room) => [room._id, room]))); + } + setSelectedRooms({}); + }); + + if (step === STEPS.CONFIRM_CONVERT) { + return ( + 0 ? onReturn : onCancel} + deletedRooms={selectedRooms} + rooms={rooms} + /> + ); + } + + return ( + + ); +}; + +export default BaseConvertToChannelModal; diff --git a/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx b/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx new file mode 100644 index 000000000000..faddf8c2931b --- /dev/null +++ b/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx @@ -0,0 +1,57 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import React, { FC, useMemo } from 'react'; + +import { IRoom } from '../../../../definition/IRoom'; +import GenericModal from '../../../components/GenericModal'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { useEndpointData } from '../../../hooks/useEndpointData'; +import { AsyncStatePhase } from '../../../lib/asyncState'; +import BaseConvertToChannelModal from './BaseConvertToChannelModal'; + +type ConvertToChannelModalProps = { + onClose: () => void; + onCancel: () => void; + onConfirm: () => Array; + teamId: string; + userId: string; +}; + +const ConvertToChannelModal: FC = ({ + onClose, + onCancel, + onConfirm, + teamId, + userId, +}) => { + const t = useTranslation(); + + const { value, phase } = useEndpointData( + 'teams.listRoomsOfUser', + useMemo(() => ({ teamId, userId, canUserDelete: true }), [teamId, userId]), + ); + + if (phase === AsyncStatePhase.LOADING) { + return ( + } + confirmText={t('Cancel')} + onConfirm={onClose} + > + + + ); + } + + return ( + + ); +}; + +export default ConvertToChannelModal; diff --git a/client/views/teams/ConvertToChannelModal/ModalSteps/FirstStep.tsx b/client/views/teams/ConvertToChannelModal/ModalSteps/FirstStep.tsx new file mode 100644 index 000000000000..4f033cbd7844 --- /dev/null +++ b/client/views/teams/ConvertToChannelModal/ModalSteps/FirstStep.tsx @@ -0,0 +1,65 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +import { IRoom } from '../../../../../definition/IRoom'; +import GenericModal from '../../../../components/GenericModal'; +import { useTranslation } from '../../../../contexts/TranslationContext'; +import ChannelDesertionTable from '../../ChannelDesertionTable'; + +type FirstStepProps = { + onClose: () => void; + onCancel: () => void; + onConfirm: () => void; + onToggleAllRooms: () => void; + onChangeRoomSelection: (room: IRoom) => void; + rooms: Array | undefined; + eligibleRoomsLength: number | undefined; + selectedRooms: { [key: string]: IRoom }; +}; + +const FirstStep: FC = ({ + onClose, + onCancel, + onConfirm, + rooms, + onToggleAllRooms, + onChangeRoomSelection, + selectedRooms, + eligibleRoomsLength, + ...props +}) => { + const t = useTranslation(); + + return ( + + + {t('Select_the_teams_channels_you_would_like_to_delete')} + + + + {t('Notice_that_public_channels_will_be_public_and_visible_to_everyone')} + + + + + ); +}; + +export default FirstStep; diff --git a/client/views/teams/ConvertToChannelModal/ModalSteps/SecondStep.tsx b/client/views/teams/ConvertToChannelModal/ModalSteps/SecondStep.tsx new file mode 100644 index 000000000000..1324c1bbfd1d --- /dev/null +++ b/client/views/teams/ConvertToChannelModal/ModalSteps/SecondStep.tsx @@ -0,0 +1,45 @@ +import { Icon } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +import { IRoom } from '../../../../../definition/IRoom'; +import GenericModal from '../../../../components/GenericModal'; +import { useTranslation } from '../../../../contexts/TranslationContext'; + +type SecondStepsProps = { + onClose: () => void; + onCancel: () => void; + onConfirm: (deletedRooms: { [key: string]: IRoom }) => void; + deletedRooms: { + [key: string]: IRoom; + }; + rooms: Array | undefined; +}; + +const SecondStep: FC = ({ + onClose, + onCancel, + onConfirm, + deletedRooms = {}, + rooms = [], + ...props +}) => { + const t = useTranslation(); + + return ( + } + cancelText={rooms?.length > 0 ? t('Back') : t('Cancel')} + confirmText={t('Convert')} + title={t('Confirmation')} + onClose={onClose} + onCancel={onCancel} + onConfirm={(): void => onConfirm(deletedRooms)} + > + {t('You_are_converting_team_to_channel')} + + ); +}; + +export default SecondStep; diff --git a/client/views/teams/ConvertToChannelModal/index.ts b/client/views/teams/ConvertToChannelModal/index.ts new file mode 100644 index 000000000000..186e9f906ffb --- /dev/null +++ b/client/views/teams/ConvertToChannelModal/index.ts @@ -0,0 +1 @@ +export { default } from './ConvertToChannelModal'; diff --git a/client/views/teams/contextualBar/ChannelRow.js b/client/views/teams/contextualBar/ChannelRow.js index 03f503dd0dfe..822a2339aac4 100644 --- a/client/views/teams/contextualBar/ChannelRow.js +++ b/client/views/teams/contextualBar/ChannelRow.js @@ -4,7 +4,7 @@ import React from 'react'; import { useRoomIcon } from '../../../hooks/useRoomIcon'; -const ChannelRow = ({ onChange, selected, room, lastOwnerWarning = '', formatDate }) => { +const ChannelRow = ({ onChange, selected, room, lastOwnerWarning, formatDate }) => { const { name, fname, ts, isLastOwner } = room; const handleChange = useMutableCallback(() => onChange(room)); @@ -12,11 +12,15 @@ const ChannelRow = ({ onChange, selected, room, lastOwnerWarning = '', formatDat return ( - + {fname ?? name} - {isLastOwner && ( + {lastOwnerWarning && isLastOwner && ( )} diff --git a/client/views/teams/contextualBar/info/Leave/StepOne.js b/client/views/teams/contextualBar/info/Leave/StepOne.js index 6849ba074466..c8242f18c24f 100644 --- a/client/views/teams/contextualBar/info/Leave/StepOne.js +++ b/client/views/teams/contextualBar/info/Leave/StepOne.js @@ -2,7 +2,7 @@ import React from 'react'; import GenericModal from '../../../../../components/GenericModal'; import { useTranslation } from '../../../../../contexts/TranslationContext'; -import ChannelDesertionTable from '../../ChannelDesertionTable'; +import ChannelDesertionTable from '../../../ChannelDesertionTable'; export const StepOne = ({ rooms, diff --git a/client/views/teams/contextualBar/info/TeamsInfo.js b/client/views/teams/contextualBar/info/TeamsInfo.js index bd6c9827288f..8dd8d900bf7d 100644 --- a/client/views/teams/contextualBar/info/TeamsInfo.js +++ b/client/views/teams/contextualBar/info/TeamsInfo.js @@ -26,6 +26,7 @@ const TeamsInfo = ({ onClickEdit, onClickDelete, onClickViewChannels, + onClickConvertToChannel, }) => { const t = useTranslation(); @@ -62,8 +63,15 @@ const TeamsInfo = ({ icon: 'sign-out', }, }), + ...(onClickConvertToChannel && { + convertToChannel: { + label: t('Convert_to_channel'), + action: onClickConvertToChannel, + icon: 'hash', + }, + }), }), - [t, onClickHide, onClickLeave, onClickEdit, onClickDelete], + [t, onClickHide, onClickLeave, onClickEdit, onClickDelete, onClickConvertToChannel], ); const { actions: actionsDefinition, menu: menuOptions } = useActionSpread(memoizedActions); diff --git a/client/views/teams/contextualBar/info/TeamsInfoWithLogic.js b/client/views/teams/contextualBar/info/TeamsInfoWithData.js similarity index 85% rename from client/views/teams/contextualBar/info/TeamsInfoWithLogic.js rename to client/views/teams/contextualBar/info/TeamsInfoWithData.js index 312c680193de..3bec8ea3def1 100644 --- a/client/views/teams/contextualBar/info/TeamsInfoWithLogic.js +++ b/client/views/teams/contextualBar/info/TeamsInfoWithData.js @@ -11,9 +11,11 @@ import { useMethod } from '../../../../contexts/ServerContext'; import { useSetting } from '../../../../contexts/SettingsContext'; import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext'; import { useTranslation } from '../../../../contexts/TranslationContext'; +import { useUserId } from '../../../../contexts/UserContext'; import { useDontAskAgain } from '../../../../hooks/useDontAskAgain'; import { useEndpointActionExperimental } from '../../../../hooks/useEndpointAction'; import { useTabBarClose, useTabBarOpen } from '../../../room/providers/ToolboxProvider'; +import ConvertToChannelModal from '../../ConvertToChannelModal'; import DeleteTeamModal from './Delete'; import LeaveTeamModal from './Leave'; import TeamsInfo from './TeamsInfo'; @@ -34,6 +36,7 @@ function TeamsInfoWithLogic({ room, openEditing }) { const onClickClose = useTabBarClose(); const openTabbar = useTabBarOpen(); const t = useTranslation(); + const userId = useUserId(); room.type = room.t; room.rid = room._id; @@ -56,6 +59,8 @@ function TeamsInfoWithLogic({ room, openEditing }) { const deleteTeam = useEndpointActionExperimental('POST', 'teams.delete'); const leaveTeam = useEndpointActionExperimental('POST', 'teams.leave'); + const convertTeamToChannel = useEndpointActionExperimental('POST', 'teams.convertToChannel'); + const hideTeam = useMethod('hideRoom'); const router = useRoute('home'); @@ -135,8 +140,36 @@ function TeamsInfoWithLogic({ room, openEditing }) { const onClickViewChannels = useCallback(() => openTabbar('team-channels'), [openTabbar]); + const onClickConvertToChannel = useMutableCallback(() => { + const onConfirm = async (roomsToRemove) => { + try { + await convertTeamToChannel({ + teamId: room.teamId, + roomsToRemove: Object.keys(roomsToRemove), + }); + + dispatchToastMessage({ type: 'success', message: t('Success') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + closeModal(); + } + }; + + setModal( + , + ); + }); + return ( } diff --git a/client/views/teams/contextualBar/info/index.js b/client/views/teams/contextualBar/info/index.js index 781b7fd66ccd..acd71c705d03 100644 --- a/client/views/teams/contextualBar/info/index.js +++ b/client/views/teams/contextualBar/info/index.js @@ -7,7 +7,7 @@ import { useTranslation } from '../../../../contexts/TranslationContext'; import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; import { useEndpointData } from '../../../../hooks/useEndpointData'; import EditChannelWithData from '../../../room/contextualBar/Info/EditRoomInfo'; -import TeamsInfoWithLogic from './TeamsInfoWithLogic'; +import TeamsInfoWithData from './TeamsInfoWithData'; export default function TeamsInfoWithRooms({ rid }) { const [editing, setEditing] = useState(false); @@ -39,6 +39,6 @@ export default function TeamsInfoWithRooms({ rid }) { return editing ? ( ) : ( - + ); } diff --git a/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersFirstStep.js b/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersFirstStep.js index 5dbce1fa1121..f32aa67f00fb 100644 --- a/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersFirstStep.js +++ b/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersFirstStep.js @@ -3,7 +3,7 @@ import React from 'react'; import GenericModal from '../../../../../components/GenericModal'; import { useTranslation } from '../../../../../contexts/TranslationContext'; -import ChannelDesertionTable from '../../ChannelDesertionTable'; +import ChannelDesertionTable from '../../../ChannelDesertionTable'; const RemoveUsersFirstStep = ({ onClose, diff --git a/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js b/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js index d2853ac591c2..06fb36a103f7 100644 --- a/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js +++ b/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js @@ -30,7 +30,6 @@ const RemoveUsersModal = ({ teamId, userId, onClose, onCancel, onConfirm }) => { variant='warning' onClose={onClose} title={} - confirmText={} confirmText={t('Cancel')} onConfirm={onClose} > diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 54c870b51efc..239c62d9fff1 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -921,6 +921,8 @@ "Compact": "Compact", "Condensed": "Condensed", "Condition": "Condition", + "Convert_to_channel": "Convert to Channel", + "Converting_team_to_channel": "Converting Team to Channel", "Commit_details": "Commit Details", "Completed": "Completed", "Computer": "Computer", @@ -3006,6 +3008,7 @@ "Not_verified": "Not verified", "Nothing": "Nothing", "Nothing_found": "Nothing found", + "Notice_that_public_channels_will_be_public_and_visible_to_everyone": "Notice that public Channels will be public and visible to everyone.", "Notification_Desktop_Default_For": "Show Desktop Notifications For", "Notification_Desktop_Audio_Default_For": "Play Desktop Notifications Audio For", "Notification_Mobile_Default_For": "Push Mobile Notifications For", @@ -3616,6 +3619,7 @@ "Select_service_to_login": "Select a service to login to load your picture or upload one directly from your computer", "Select_tag": "Select a tag", "Select_the_channels_you_want_the_user_to_be_removed_from": "Select the channels you want the user to be removed from", + "Select_the_teams_channels_you_would_like_to_delete": "Select the Team’s Channels you would like to delete, the ones you do not select will be moved to the Workspace.", "Select_user": "Select user", "Select_users": "Select users", "Selected_agents": "Selected agents", @@ -3894,6 +3898,7 @@ "Teams": "Teams", "Teams_about_the_channels": "And about the Channels?", "Teams_channels_didnt_leave": "You did not select the following Channels so you are not leaving them:", + "Teams_channels_last_owner_delete_channel_warning": "You are the last owner of this Channel. Once you convert the Team into a channel, the Channel will be moved to the Workspace.", "Teams_channels_last_owner_leave_channel_warning": "You are the last owner of this Channel. Once you leave the Team, the Channel will be kept inside the Team but you will managing it from outside.", "Teams_leaving_team": "You are leaving this Team.", "Teams_channels": "Team's Channels", @@ -4514,6 +4519,7 @@ "yesterday": "yesterday", "Yesterday": "Yesterday", "You": "You", + "You_are_converting_team_to_channel": "You are converting this Team to a Channel.", "you_are_in_preview_mode_of": "You are in preview mode of channel #__room_name__", "you_are_in_preview_mode_of_incoming_livechat": "You are in preview mode of this chat", "You_are_logged_in_as": "You are logged in as", diff --git a/server/sdk/types/ITeamService.ts b/server/sdk/types/ITeamService.ts index 2918e1abfaea..a5ec4fa22ec2 100644 --- a/server/sdk/types/ITeamService.ts +++ b/server/sdk/types/ITeamService.ts @@ -60,7 +60,7 @@ export interface ITeamService { addRooms(uid: string, rooms: Array, teamId: string): Promise>; removeRoom(uid: string, rid: string, teamId: string, canRemoveAnyRoom: boolean): Promise; listRooms(uid: string, teamId: string, filter: IListRoomsFilter, pagination: IPaginationOptions): Promise>; - listRoomsOfUser(uid: string, teamId: string, userId: string, allowPrivateTeam: boolean, pagination: IPaginationOptions): Promise>; + listRoomsOfUser(uid: string, teamId: string, userId: string, allowPrivateTeam: boolean, showCanDeleteOnly: boolean, pagination: IPaginationOptions): Promise>; updateRoom(uid: string, rid: string, isDefault: boolean, canUpdateAnyRoom: boolean): Promise; list(uid: string, paginationOptions?: IPaginationOptions, queryOptions?: IQueryOptions): Promise>; listAll(options?: IPaginationOptions): Promise>; diff --git a/server/services/team/service.ts b/server/services/team/service.ts index ef6d5543c106..0bc5106439a7 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -23,7 +23,7 @@ import { TEAM_TYPE, } from '../../../definition/ITeam'; import { IUser } from '../../../definition/IUser'; -import { Room } from '../../sdk'; +import { Room, Authorization } from '../../sdk'; import { IListRoomsFilter, ITeamAutocompleteResult, @@ -484,7 +484,7 @@ export class TeamService extends ServiceClass implements ITeamService { }; } - async listRoomsOfUser(uid: string, teamId: string, userId: string, allowPrivateTeam: boolean, { offset: skip, count: limit }: IPaginationOptions = { offset: 0, count: 50 }): Promise> { + async listRoomsOfUser(uid: string, teamId: string, userId: string, allowPrivateTeam: boolean, showCanDeleteOnly: boolean, { offset: skip, count: limit }: IPaginationOptions = { offset: 0, count: 50 }): Promise> { if (!teamId) { throw new Error('missing-teamId'); } @@ -498,7 +498,18 @@ export class TeamService extends ServiceClass implements ITeamService { } const teamRooms = await this.RoomsModel.findByTeamId(teamId, { projection: { _id: 1, t: 1 } }).toArray(); - const teamRoomIds = teamRooms.filter((room) => room.t === 'p' || room.t === 'c').map((room) => room._id); + let teamRoomIds: any[]; + if (showCanDeleteOnly) { + for await (const room of teamRooms) { + const roomType = room.t; + const canDeleteRoom = await Authorization.hasPermission(userId, roomType === 'c' ? 'delete-c' : 'delete-p', room._id); + room.userCanDelete = canDeleteRoom; + } + + teamRoomIds = teamRooms.filter((room) => (room.t === 'c' || room.t === 'p') && room.userCanDelete).map((room) => room._id); + } else { + teamRoomIds = teamRooms.filter((room) => room.t === 'p' || room.t === 'c').map((room) => room._id); + } const subscriptionsCursor = this.SubscriptionsModel.findByUserIdAndRoomIds(userId, teamRoomIds); const subscriptionRoomIds = (await subscriptionsCursor.toArray()).map((subscription) => subscription.rid);