Skip to content

Commit

Permalink
[NEW] Convert Team to Channel (#22476)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Kevin Aleman <[email protected]>
Co-authored-by: Tasso Evangelista <[email protected]>
  • Loading branch information
4 people authored Jul 6, 2021
1 parent f4e3d00 commit 6357447
Show file tree
Hide file tree
Showing 21 changed files with 378 additions and 49 deletions.
4 changes: 2 additions & 2 deletions app/api/server/v1/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions client/contexts/ServerContext/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IRecordsWithTotal<IRoom>, 'records'> & {
count: number;
offset: number;
rooms: IRecordsWithTotal<IRoom>['records'];
};
};
23 changes: 0 additions & 23 deletions client/views/room/contextualBar/Info/ConvertToTeamModal.js

This file was deleted.

14 changes: 12 additions & 2 deletions client/views/room/contextualBar/Info/RoomInfo/RoomInfoWithData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -180,7 +179,18 @@ const RoomInfoWithData = ({ rid, openEditing, onClickBack, onEnterRoom, resetSta
}
};

setModal(<ConvertToTeamModal onClose={closeModal} onConfirm={onConfirm} />);
setModal(
<GenericModal
title={t('Confirmation')}
variant='warning'
onClose={closeModal}
onCancel={closeModal}
onConfirm={onConfirm}
confirmText={t('Convert')}
>
{t('Converting_channel_to_a_team')}
</GenericModal>,
);
});

const onClickEnterRoom = useMutableCallback(() => onEnterRoom(room));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IRoom & { isLastOwner?: string }> | undefined;
eligibleRoomsLength: number | undefined;
params?: {};
onChangeParams?: () => void;
onChangeRoomSelection: (room: IRoom) => void;
selectedRooms: { [key: string]: IRoom };
onToggleAllRooms: () => void;
};

const ChannelDesertionTable: FC<ChannelDesertionTableProps> = ({
rooms,
eligibleRoomsLength,
params,
Expand All @@ -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();

Expand Down Expand Up @@ -51,7 +66,7 @@ const ChannelDesertionTable = ({
fixed={false}
pagination={false}
>
{(room, key) => (
{(room: IRoom, key: string): ReactElement => (
<ChannelRow
formatDate={formatDate}
room={room}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { FC, useState, useCallback } from 'react';

import { IRoom } from '../../../../definition/IRoom';
import FirstStep from './ModalSteps/FirstStep';
import SecondStep from './ModalSteps/SecondStep';

const STEPS = {
LIST_ROOMS: 'LIST_ROOMS',
CONFIRM_CONVERT: 'CONFIRM_CONVERT',
};

type BaseConvertToChannelModalProps = {
onClose: () => void;
onCancel: () => void;
onConfirm: () => Array<IRoom>;
currentStep?: string;
rooms?: Array<IRoom & { isLastOwner?: string }>;
};

const BaseConvertToChannelModal: FC<BaseConvertToChannelModalProps> = ({
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 (
<SecondStep
onConfirm={onConfirm}
onClose={onClose}
onCancel={rooms && rooms.length > 0 ? onReturn : onCancel}
deletedRooms={selectedRooms}
rooms={rooms}
/>
);
}

return (
<FirstStep
onConfirm={onContinue}
onClose={onClose}
onCancel={onCancel}
rooms={rooms}
selectedRooms={selectedRooms}
onToggleAllRooms={onToggleAllRooms}
onChangeRoomSelection={onChangeRoomSelection}
eligibleRoomsLength={eligibleRooms?.length}
/>
);
};

export default BaseConvertToChannelModal;
57 changes: 57 additions & 0 deletions client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx
Original file line number Diff line number Diff line change
@@ -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<IRoom>;
teamId: string;
userId: string;
};

const ConvertToChannelModal: FC<ConvertToChannelModalProps> = ({
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 (
<GenericModal
variant='warning'
onClose={onClose}
title={<Skeleton width='50%' />}
confirmText={t('Cancel')}
onConfirm={onClose}
>
<Skeleton width='full' />
</GenericModal>
);
}

return (
<BaseConvertToChannelModal
onClose={onClose}
onCancel={onCancel}
onConfirm={onConfirm}
rooms={value?.rooms}
/>
);
};

export default ConvertToChannelModal;
65 changes: 65 additions & 0 deletions client/views/teams/ConvertToChannelModal/ModalSteps/FirstStep.tsx
Original file line number Diff line number Diff line change
@@ -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<IRoom & { isLastOwner?: string }> | undefined;
eligibleRoomsLength: number | undefined;
selectedRooms: { [key: string]: IRoom };
};

const FirstStep: FC<FirstStepProps> = ({
onClose,
onCancel,
onConfirm,
rooms,
onToggleAllRooms,
onChangeRoomSelection,
selectedRooms,
eligibleRoomsLength,
...props
}) => {
const t = useTranslation();

return (
<GenericModal
variant='warning'
icon='warning'
title={t('Converting_team_to_channel')}
cancelText={t('Cancel')}
confirmText={t('Continue')}
onClose={onClose}
onCancel={onCancel}
onConfirm={onConfirm}
{...props}
>
<Box mbe='x24' fontScale='p1'>
{t('Select_the_teams_channels_you_would_like_to_delete')}
</Box>

<Box mbe='x24' fontScale='p1'>
{t('Notice_that_public_channels_will_be_public_and_visible_to_everyone')}
</Box>

<ChannelDesertionTable
lastOwnerWarning={undefined}
onToggleAllRooms={onToggleAllRooms}
rooms={rooms}
onChangeRoomSelection={onChangeRoomSelection}
selectedRooms={selectedRooms}
eligibleRoomsLength={eligibleRoomsLength}
/>
</GenericModal>
);
};

export default FirstStep;
Loading

0 comments on commit 6357447

Please sign in to comment.