snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
import FirstStep from './ModalSteps/FirstStep';
|
||||
import SecondStep from './ModalSteps/SecondStep';
|
||||
|
||||
const STEPS = {
|
||||
LIST_ROOMS: 'LIST_ROOMS',
|
||||
CONFIRM_CONVERT: 'CONFIRM_CONVERT',
|
||||
};
|
||||
|
||||
export type BaseConvertToChannelModalProps = {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: (deletedRooms: { [key: string]: Serialized<IRoom> }) => void;
|
||||
currentStep?: string;
|
||||
rooms?: (Serialized<IRoom> & { isLastOwner?: boolean })[];
|
||||
};
|
||||
|
||||
const BaseConvertToChannelModal = ({
|
||||
onClose,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
rooms,
|
||||
currentStep = rooms?.length === 0 ? STEPS.CONFIRM_CONVERT : STEPS.LIST_ROOMS,
|
||||
}: BaseConvertToChannelModalProps) => {
|
||||
const [step, setStep] = useState(currentStep);
|
||||
const [selectedRooms, setSelectedRooms] = useState<{ [key: string]: Serialized<IRoom> }>({});
|
||||
|
||||
const onContinue = useStableCallback(() => setStep(STEPS.CONFIRM_CONVERT));
|
||||
const onReturn = useStableCallback(() => setStep(STEPS.LIST_ROOMS));
|
||||
|
||||
const eligibleRooms = rooms;
|
||||
|
||||
const onChangeRoomSelection = useCallback((room: Serialized<IRoom>) => {
|
||||
setSelectedRooms((selectedRooms) => {
|
||||
if (selectedRooms[room._id]) {
|
||||
delete selectedRooms[room._id];
|
||||
return { ...selectedRooms };
|
||||
}
|
||||
return { ...selectedRooms, [room._id]: room };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onToggleAllRooms = useStableCallback(() => {
|
||||
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;
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { GenericModalSkeleton } from '@rocket.chat/ui-client';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import BaseConvertToChannelModal from './BaseConvertToChannelModal';
|
||||
import { teamsQueryKeys } from '../../../../../lib/queryKeys';
|
||||
|
||||
export type ConvertToChannelModalProps = {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: (deletedRooms: { [key: string]: Serialized<IRoom> }) => void;
|
||||
teamId: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const ConvertToChannelModal = ({ onClose, onCancel, onConfirm, teamId, userId }: ConvertToChannelModalProps) => {
|
||||
const listRoomsOfUser = useEndpoint('GET', '/v1/teams.listRoomsOfUser');
|
||||
const { isPending, data } = useQuery({
|
||||
queryKey: teamsQueryKeys.roomsOfUser(teamId, userId, { canUserDelete: true }),
|
||||
queryFn: () => listRoomsOfUser({ teamId, userId, canUserDelete: 'true' }),
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return <GenericModalSkeleton />;
|
||||
}
|
||||
|
||||
return <BaseConvertToChannelModal onClose={onClose} onCancel={onCancel} onConfirm={onConfirm} rooms={data?.rooms} />;
|
||||
};
|
||||
|
||||
export default ConvertToChannelModal;
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ChannelDesertionTable from '../../../../ChannelDesertionTable';
|
||||
|
||||
export type FirstStepProps = {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
onToggleAllRooms: () => void;
|
||||
onChangeRoomSelection: (room: Serialized<IRoom>) => void;
|
||||
rooms?: (Serialized<IRoom> & { isLastOwner?: boolean })[];
|
||||
eligibleRoomsLength: number | undefined;
|
||||
selectedRooms: { [key: string]: Serialized<IRoom> };
|
||||
};
|
||||
|
||||
const FirstStep = ({
|
||||
onClose,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
rooms,
|
||||
onToggleAllRooms,
|
||||
onChangeRoomSelection,
|
||||
selectedRooms,
|
||||
eligibleRoomsLength,
|
||||
...props
|
||||
}: FirstStepProps) => {
|
||||
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 marginBlockEnd={24} fontScale='p2'>
|
||||
{t('Select_the_teams_channels_you_would_like_to_delete')}
|
||||
</Box>
|
||||
|
||||
<Box marginBlockEnd={24} fontScale='p2'>
|
||||
{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;
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { Icon } from '@rocket.chat/fuselage';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type SecondStepsProps = {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: (deletedRooms: { [key: string]: Serialized<IRoom> }) => void;
|
||||
deletedRooms: {
|
||||
[key: string]: Serialized<IRoom>;
|
||||
};
|
||||
rooms?: (Serialized<IRoom> & { isLastOwner?: boolean })[];
|
||||
};
|
||||
|
||||
const SecondStep = ({ onClose, onCancel, onConfirm, deletedRooms = {}, rooms = [], ...props }: SecondStepsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
{...props}
|
||||
variant='warning'
|
||||
icon={<Icon name='modal-warning' size='x24' color='status-font-on-warning' />}
|
||||
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')}
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondStep;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ConvertToChannelModal';
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { Box, CheckBox } from '@rocket.chat/fuselage';
|
||||
import { GenericTable, GenericTableHeaderCell, GenericTableBody, GenericTableHeader, useSort } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ChannelDeletionTableRow from './ChannelDeletionTableRow';
|
||||
|
||||
export type ChannelDeletionTableProps = {
|
||||
rooms: Serialized<IRoom>[];
|
||||
onToggleAllRooms: () => void;
|
||||
onChangeRoomSelection: (room: Serialized<IRoom>) => void;
|
||||
selectedRooms: { [key: string]: Serialized<IRoom> };
|
||||
};
|
||||
|
||||
const ChannelDeletionTable = ({ rooms, onChangeRoomSelection, selectedRooms, onToggleAllRooms }: ChannelDeletionTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { sortBy, sortDirection, setSort } = useSort<'name' | 'usersCount'>('name');
|
||||
|
||||
const selectedRoomsLength = Object.values(selectedRooms).filter(Boolean).length;
|
||||
|
||||
const getSortedChannels = () => {
|
||||
if (rooms) {
|
||||
const sortedRooms = [...rooms];
|
||||
if (sortBy === 'name') {
|
||||
sortedRooms.sort((a, b) => (a.name && b.name ? a.name.localeCompare(b.name) : 0));
|
||||
}
|
||||
if (sortBy === 'usersCount') {
|
||||
sortedRooms.sort((a, b) => a.usersCount - b.usersCount);
|
||||
}
|
||||
if (sortDirection === 'desc') {
|
||||
return sortedRooms?.reverse();
|
||||
}
|
||||
return sortedRooms;
|
||||
}
|
||||
};
|
||||
|
||||
const sortedRooms = getSortedChannels();
|
||||
|
||||
const checked = rooms.length === selectedRoomsLength;
|
||||
const indeterminate = rooms.length > selectedRoomsLength && selectedRoomsLength > 0;
|
||||
|
||||
const headers = (
|
||||
<>
|
||||
<GenericTableHeaderCell key='name' sort='name' onClick={setSort} direction={sortDirection} active={sortBy === 'name'}>
|
||||
<CheckBox indeterminate={indeterminate} checked={checked} onChange={onToggleAllRooms} />
|
||||
<Box marginInline={8}>{t('Channel_name')}</Box>
|
||||
</GenericTableHeaderCell>
|
||||
<GenericTableHeaderCell
|
||||
key='usersCount'
|
||||
sort='usersCount'
|
||||
onClick={setSort}
|
||||
direction={sortDirection}
|
||||
active={sortBy === 'usersCount'}
|
||||
>
|
||||
<Box width='100%' textAlign='end'>
|
||||
{t('Members')}
|
||||
</Box>
|
||||
</GenericTableHeaderCell>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box display='flex' flexDirection='column' height='x200' marginBlockStart={24}>
|
||||
<GenericTable>
|
||||
<GenericTableHeader>{headers}</GenericTableHeader>
|
||||
<GenericTableBody>
|
||||
{sortedRooms?.map((room) => (
|
||||
<ChannelDeletionTableRow room={room} key={room._id} onChange={onChangeRoomSelection} selected={!!selectedRooms[room._id]} />
|
||||
))}
|
||||
</GenericTableBody>
|
||||
</GenericTable>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelDeletionTable;
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { CheckBox, Margins } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { GenericTableRow, GenericTableCell } from '@rocket.chat/ui-client';
|
||||
|
||||
import { RoomIcon } from '../../../../../../components/RoomIcon';
|
||||
|
||||
export type ChannelDeletionTableRowProps = {
|
||||
room: Serialized<IRoom>;
|
||||
onChange: (room: Serialized<IRoom>) => void;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
const ChannelDeletionTableRow = ({ room, onChange, selected }: ChannelDeletionTableRowProps) => {
|
||||
const { name, fname, usersCount } = room;
|
||||
const handleChange = useStableCallback(() => onChange(room));
|
||||
|
||||
return (
|
||||
<GenericTableRow action>
|
||||
<GenericTableCell maxWidth='x300' withTruncatedText>
|
||||
<CheckBox checked={selected} onChange={handleChange} />
|
||||
<Margins inline='x8'>
|
||||
<RoomIcon room={room} />
|
||||
{fname ?? name}
|
||||
</Margins>
|
||||
</GenericTableCell>
|
||||
<GenericTableCell align='end' withTruncatedText>
|
||||
{usersCount}
|
||||
</GenericTableCell>
|
||||
</GenericTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelDeletionTableRow;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ChannelDeletionTable';
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ChannelDeletionTable from './ChannelDeletionTable';
|
||||
|
||||
export type DeleteTeamChannelsProps = {
|
||||
rooms: Serialized<IRoom>[];
|
||||
onCancel: () => void;
|
||||
selectedRooms: { [key: string]: Serialized<IRoom> };
|
||||
onToggleAllRooms: () => void;
|
||||
onConfirm: () => void;
|
||||
onChangeRoomSelection: (room: Serialized<IRoom>) => void;
|
||||
};
|
||||
|
||||
const DeleteTeamChannels = ({
|
||||
rooms,
|
||||
onCancel,
|
||||
selectedRooms,
|
||||
onToggleAllRooms,
|
||||
onConfirm,
|
||||
onChangeRoomSelection,
|
||||
}: DeleteTeamChannelsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
variant='warning'
|
||||
title={t('Teams_about_the_channels')}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
onClose={onCancel}
|
||||
confirmText={t('Continue')}
|
||||
>
|
||||
<Box withRichContent marginBlockEnd={16}>
|
||||
<Box is='span' color='status-font-on-danger' fontWeight='bold'>
|
||||
{t('Team_Delete_Channel_modal_content_danger')}{' '}
|
||||
</Box>
|
||||
{t('Teams_delete_team_Warning')}
|
||||
</Box>
|
||||
<Box>
|
||||
{t('Teams_delete_team_choose_channels')} {t('Teams_delete_team_public_notice')}
|
||||
</Box>
|
||||
<ChannelDeletionTable
|
||||
rooms={rooms}
|
||||
onToggleAllRooms={onToggleAllRooms}
|
||||
onChangeRoomSelection={onChangeRoomSelection}
|
||||
selectedRooms={selectedRooms}
|
||||
/>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteTeamChannels;
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RoomLinkList from './RoomLinkList';
|
||||
|
||||
export type DeleteTeamConfirmationProps = {
|
||||
deletedRooms: { [key: string]: Serialized<IRoom> };
|
||||
keptRooms: { [key: string]: Serialized<IRoom> };
|
||||
onConfirm: (roomsToDelete: IRoom['_id'][]) => void;
|
||||
onReturn?: () => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const DeleteTeamConfirmation = ({ deletedRooms, keptRooms, onConfirm, onReturn, onCancel }: DeleteTeamConfirmationProps) => {
|
||||
const { t } = useTranslation();
|
||||
const roomIds = Object.values(deletedRooms).map(({ _id }) => _id);
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
variant='danger'
|
||||
title={t('Delete_roomType', { roomType: 'team' })}
|
||||
onConfirm={() => onConfirm(roomIds)}
|
||||
onCancel={onReturn}
|
||||
confirmText={t('Yes_delete_it')}
|
||||
cancelText={t('Back')}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<p>{t('Delete_roomType_description', { roomType: 'team' })}</p>
|
||||
{!!Object.values(deletedRooms).length && (
|
||||
<>
|
||||
<br />
|
||||
<p>
|
||||
{t('Teams_deleted_channels')} <RoomLinkList rooms={deletedRooms} />
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{!!Object.values(keptRooms).length && (
|
||||
<>
|
||||
<br />
|
||||
<p>
|
||||
{t('Teams_kept_channels')} <RoomLinkList rooms={keptRooms} />
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteTeamConfirmation;
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { StoryObj, Meta } from '@storybook/react';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import DeleteTeamModal from '.';
|
||||
import DeleteTeamChannels from './DeleteTeamChannels';
|
||||
import DeleteTeamConfirmation from './DeleteTeamConfirmation';
|
||||
|
||||
export default {
|
||||
component: DeleteTeamModal,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} satisfies Meta<typeof DeleteTeamModal>;
|
||||
|
||||
export const Default: StoryObj<typeof DeleteTeamModal> = {
|
||||
name: 'DeleteTeamModal',
|
||||
|
||||
args: {
|
||||
teamId: '123',
|
||||
onConfirm: action('onConfirm'),
|
||||
onCancel: action('onCancel'),
|
||||
},
|
||||
};
|
||||
|
||||
export const ChannelsStep: StoryObj<typeof DeleteTeamChannels> = {
|
||||
render: (args) => <DeleteTeamChannels {...args} />,
|
||||
|
||||
args: {
|
||||
rooms: Array.from({ length: 15 }).map((_, i) => ({
|
||||
_id: `${i}`,
|
||||
fname: `Room #${i}`,
|
||||
name: `room-${i}`,
|
||||
usersCount: 10 * i,
|
||||
type: 'p',
|
||||
t: 'p',
|
||||
msgs: 10,
|
||||
u: {
|
||||
_id: 'user',
|
||||
},
|
||||
autoTranslateLanguage: 'english',
|
||||
_updatedAt: '2022-02-02 09:00',
|
||||
})),
|
||||
selectedRooms: {},
|
||||
onConfirm: action('onConfirm'),
|
||||
onCancel: action('onCancel'),
|
||||
onChangeRoomSelection: action('onChangeRoomSelection'),
|
||||
onToggleAllRooms: action('onToggleAllRooms'),
|
||||
},
|
||||
};
|
||||
|
||||
export const ConfirmationStep: StoryObj<typeof DeleteTeamConfirmation> = {
|
||||
render: (args) => <DeleteTeamConfirmation {...args} />,
|
||||
|
||||
args: {
|
||||
deletedRooms: {
|
||||
test123: {
|
||||
_id: `123`,
|
||||
fname: `Room 123`,
|
||||
name: `room-123`,
|
||||
usersCount: 10,
|
||||
t: 'p',
|
||||
msgs: 10,
|
||||
u: {
|
||||
_id: 'user',
|
||||
},
|
||||
autoTranslateLanguage: 'english',
|
||||
_updatedAt: '2022-02-02 09:00',
|
||||
},
|
||||
},
|
||||
keptRooms: {
|
||||
test123: {
|
||||
_id: `123`,
|
||||
fname: `Room 123`,
|
||||
name: `room-123`,
|
||||
usersCount: 10,
|
||||
t: 'p',
|
||||
msgs: 10,
|
||||
u: {
|
||||
_id: 'user',
|
||||
},
|
||||
autoTranslateLanguage: 'english',
|
||||
_updatedAt: '2022-02-02 09:00',
|
||||
},
|
||||
},
|
||||
onConfirm: async (_roomsToDelete) => action('onConfirm'),
|
||||
onCancel: action('onCancel'),
|
||||
onReturn: action('onReturn'),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useState } from 'react';
|
||||
|
||||
import DeleteTeamChannels from './DeleteTeamChannels';
|
||||
import DeleteTeamConfirmation from './DeleteTeamConfirmation';
|
||||
|
||||
const STEPS = { LIST_ROOMS: 'LIST_ROOMS', CONFIRM_DELETE: 'CONFIRM_DELETE' };
|
||||
|
||||
export type DeleteTeamModalProps = {
|
||||
onCancel: () => void;
|
||||
onConfirm: (roomsToDelete: IRoom['_id'][]) => void;
|
||||
rooms: Serialized<IRoom>[];
|
||||
};
|
||||
|
||||
const DeleteTeamModal = ({ onCancel, onConfirm, rooms }: DeleteTeamModalProps) => {
|
||||
const hasRooms = rooms && rooms.length > 0;
|
||||
|
||||
const [step, setStep] = useState(hasRooms ? STEPS.LIST_ROOMS : STEPS.CONFIRM_DELETE);
|
||||
const [deletedRooms, setDeletedRooms] = useState<{ [key: string]: Serialized<IRoom> }>({});
|
||||
const [keptRooms, setKeptRooms] = useState<{ [key: string]: Serialized<IRoom> }>({});
|
||||
|
||||
const onChangeRoomSelection = useStableCallback((room: Serialized<IRoom>) => {
|
||||
if (deletedRooms[room._id]) {
|
||||
setDeletedRooms((deletedRooms) => {
|
||||
delete deletedRooms[room._id];
|
||||
return { ...deletedRooms };
|
||||
});
|
||||
return;
|
||||
}
|
||||
setDeletedRooms((deletedRooms) => ({ ...deletedRooms, [room._id]: room }));
|
||||
});
|
||||
|
||||
const onToggleAllRooms = useStableCallback(() => {
|
||||
if (Object.values(deletedRooms).filter(Boolean).length === 0) {
|
||||
return setDeletedRooms(Object.fromEntries(rooms.map((room) => [room._id, room])));
|
||||
}
|
||||
setDeletedRooms({});
|
||||
});
|
||||
|
||||
const onSelectRooms = useStableCallback(() => {
|
||||
const keptRooms = Object.fromEntries(rooms.filter((room) => !deletedRooms[room._id]).map((room) => [room._id, room]));
|
||||
setKeptRooms(keptRooms);
|
||||
setStep(STEPS.CONFIRM_DELETE);
|
||||
});
|
||||
|
||||
if (step === STEPS.CONFIRM_DELETE) {
|
||||
return (
|
||||
<DeleteTeamConfirmation
|
||||
onConfirm={onConfirm}
|
||||
onReturn={hasRooms ? () => setStep(STEPS.LIST_ROOMS) : undefined}
|
||||
onCancel={onCancel}
|
||||
deletedRooms={deletedRooms}
|
||||
keptRooms={keptRooms}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DeleteTeamChannels
|
||||
rooms={rooms}
|
||||
onCancel={onCancel}
|
||||
selectedRooms={deletedRooms}
|
||||
onToggleAllRooms={onToggleAllRooms}
|
||||
onConfirm={onSelectRooms}
|
||||
onChangeRoomSelection={onChangeRoomSelection}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteTeamModal;
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { GenericModalSkeleton } from '@rocket.chat/ui-client';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import DeleteTeamModal from './DeleteTeamModal';
|
||||
|
||||
export type DeleteTeamModalWithRoomsProps = {
|
||||
teamId: string;
|
||||
onConfirm: (roomsToDelete: IRoom['_id'][]) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const DeleteTeamModalWithRooms = ({ teamId, onConfirm, onCancel }: DeleteTeamModalWithRoomsProps) => {
|
||||
const query = useMemo(() => ({ teamId }), [teamId]);
|
||||
const getTeamsListRooms = useEndpoint('GET', '/v1/teams.listRooms');
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['getTeamsListRooms', query],
|
||||
queryFn: async () => getTeamsListRooms(query),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <GenericModalSkeleton />;
|
||||
}
|
||||
return <DeleteTeamModal onCancel={onCancel} onConfirm={onConfirm} rooms={data?.rooms || []} />;
|
||||
};
|
||||
|
||||
export default DeleteTeamModalWithRooms;
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Serialized, IRoom } from '@rocket.chat/core-typings';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
|
||||
|
||||
export type RoomLinkListProps = { rooms: { [key: string]: Serialized<IRoom> } };
|
||||
|
||||
const RoomLinkList = ({ rooms }: RoomLinkListProps) => {
|
||||
const roomsArray = Object.values(rooms);
|
||||
|
||||
return (
|
||||
<>
|
||||
{roomsArray.map((room, i) => {
|
||||
const roomLink = roomCoordinator.getRouteLink(room.t, room);
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<a href={roomLink || undefined}>#{room.fname ?? room.name}</a>
|
||||
{i === roomsArray.length - 1 ? '.' : ', '}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomLinkList;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './DeleteTeamModalWithRooms';
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { StoryObj, Meta } from '@storybook/react';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import LeaveTeamModal from './LeaveTeamModal';
|
||||
import LeaveTeamModalChannels from './LeaveTeamModalChannels';
|
||||
import LeaveTeamModalConfirmation from './LeaveTeamModalConfirmation';
|
||||
|
||||
export default {
|
||||
component: LeaveTeamModal,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} satisfies Meta<typeof LeaveTeamModal>;
|
||||
|
||||
export const ChannelsStep: StoryObj<typeof LeaveTeamModalChannels> = {
|
||||
render: (args) => <LeaveTeamModalChannels {...args} />,
|
||||
name: 'LeaveTeamModalChannels',
|
||||
|
||||
args: {
|
||||
rooms: Array.from({ length: 15 }).map((_, i) => ({
|
||||
_id: `${i}`,
|
||||
fname: `Room #${i}`,
|
||||
name: `room-${i}`,
|
||||
usersCount: 10 * i,
|
||||
type: 'p',
|
||||
t: 'p',
|
||||
isLastOwner: false,
|
||||
msgs: 10,
|
||||
u: {
|
||||
_id: 'user',
|
||||
},
|
||||
autoTranslateLanguage: 'english',
|
||||
_updatedAt: '2022-02-02 09:00',
|
||||
})),
|
||||
selectedRooms: {},
|
||||
onConfirm: action('onConfirm'),
|
||||
onCancel: action('onCancel'),
|
||||
onChangeRoomSelection: action('onChangeRoomSelection'),
|
||||
onToggleAllRooms: action('onToggleAllRooms'),
|
||||
},
|
||||
};
|
||||
|
||||
export const ConfirmationStep: StoryObj<typeof LeaveTeamModalConfirmation> = {
|
||||
render: (args) => <LeaveTeamModalConfirmation {...args} />,
|
||||
name: 'StepTwo',
|
||||
|
||||
args: {
|
||||
onConfirm: action('onConfirm'),
|
||||
onCancel: action('onCancel'),
|
||||
onClose: action('onClose'),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import LeaveTeamModalChannels from './LeaveTeamModalChannels';
|
||||
import LeaveTeamModalConfirmation from './LeaveTeamModalConfirmation';
|
||||
|
||||
const LEAVE_TEAM_STEPS = {
|
||||
LIST_ROOMS: 'LIST_ROOMS',
|
||||
CONFIRM_LEAVE: 'CONFIRM_LEAVE',
|
||||
} as const;
|
||||
|
||||
export type LeaveTeamModalProps = {
|
||||
rooms: (Serialized<IRoom> & { isLastOwner?: boolean })[];
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
const LeaveTeamModal = ({ rooms, onCancel, onConfirm }: LeaveTeamModalProps) => {
|
||||
const memoizedRooms = useMemo(() => rooms, [rooms]);
|
||||
const [step, setStep] = useState(memoizedRooms.length === 0 ? LEAVE_TEAM_STEPS.CONFIRM_LEAVE : LEAVE_TEAM_STEPS.LIST_ROOMS);
|
||||
|
||||
const [selectedRooms, setSelectedRooms] = useState<{ [key: string]: Serialized<IRoom> & { isLastOwner?: boolean } }>({});
|
||||
const lastOwnerRooms = rooms.filter(({ isLastOwner }) => isLastOwner);
|
||||
|
||||
const handleContinue = useCallback(() => setStep(LEAVE_TEAM_STEPS.CONFIRM_LEAVE), []);
|
||||
const handleReturn = useCallback(() => setStep(LEAVE_TEAM_STEPS.LIST_ROOMS), []);
|
||||
|
||||
const onChangeRoomSelection = useCallback((room: Serialized<IRoom>) => {
|
||||
setSelectedRooms((selectedRooms) => {
|
||||
if (selectedRooms[room._id]) {
|
||||
delete selectedRooms[room._id];
|
||||
return { ...selectedRooms };
|
||||
}
|
||||
return { ...selectedRooms, [room._id]: room };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onToggleAllRooms = useStableCallback(() => {
|
||||
setSelectedRooms((selectedRooms) => {
|
||||
if (Object.values(selectedRooms).filter(Boolean).length === 0) {
|
||||
return Object.fromEntries(rooms.filter(({ isLastOwner }) => !isLastOwner).map((room) => [room._id, room]));
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
if (step === LEAVE_TEAM_STEPS.CONFIRM_LEAVE) {
|
||||
return (
|
||||
<LeaveTeamModalConfirmation
|
||||
selectedRooms={selectedRooms}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onCancel}
|
||||
onCancel={rooms.length > 0 ? handleReturn : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LeaveTeamModalChannels
|
||||
rooms={rooms}
|
||||
onCancel={onCancel}
|
||||
eligibleRoomsLength={rooms.length - lastOwnerRooms.length}
|
||||
selectedRooms={selectedRooms}
|
||||
onToggleAllRooms={onToggleAllRooms}
|
||||
onConfirm={handleContinue}
|
||||
onChangeRoomSelection={onChangeRoomSelection}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveTeamModal;
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ChannelDesertionTable from '../../../../ChannelDesertionTable';
|
||||
|
||||
export type LeaveTeamModalChannelsProps = {
|
||||
rooms?: (Serialized<IRoom> & { isLastOwner?: boolean })[];
|
||||
onToggleAllRooms: () => void;
|
||||
onChangeRoomSelection: (room: Serialized<IRoom> & { isLastOwner?: boolean }) => void;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
eligibleRoomsLength: number;
|
||||
selectedRooms: {
|
||||
[key: string]: Serialized<IRoom> & { isLastOwner?: boolean };
|
||||
};
|
||||
};
|
||||
|
||||
const LeaveTeamModalChannels = ({
|
||||
rooms,
|
||||
onToggleAllRooms,
|
||||
onChangeRoomSelection,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
eligibleRoomsLength,
|
||||
selectedRooms,
|
||||
}: LeaveTeamModalChannelsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
variant='warning'
|
||||
title={t('Teams_leave')}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
onClose={onCancel}
|
||||
confirmText={t('Continue')}
|
||||
>
|
||||
{t('Teams_leave_channels')}
|
||||
<ChannelDesertionTable
|
||||
rooms={rooms}
|
||||
lastOwnerWarning={t('Teams_channels_last_owner_leave_channel_warning')}
|
||||
onToggleAllRooms={onToggleAllRooms}
|
||||
eligibleRoomsLength={eligibleRoomsLength}
|
||||
onChangeRoomSelection={onChangeRoomSelection}
|
||||
selectedRooms={selectedRooms}
|
||||
/>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveTeamModalChannels;
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type LeaveTeamModalConfirmationProps = {
|
||||
onConfirm: (selectedRooms?: { [key: string]: Serialized<IRoom> & { isLastOwner?: boolean } }) => void;
|
||||
onClose: () => void;
|
||||
onCancel?: () => void;
|
||||
selectedRooms: {
|
||||
[key: string]: Serialized<IRoom> & { isLastOwner?: boolean };
|
||||
};
|
||||
};
|
||||
|
||||
const LeaveTeamModalConfirmation = ({ selectedRooms, onConfirm, onCancel, onClose }: LeaveTeamModalConfirmationProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
variant='danger'
|
||||
title={t('Confirmation')}
|
||||
onConfirm={(): void => onConfirm(selectedRooms)}
|
||||
onCancel={onCancel || onClose}
|
||||
onClose={onClose}
|
||||
confirmText={t('Leave')}
|
||||
cancelText={onCancel ? t('Back') : t('Cancel')}
|
||||
>
|
||||
{t('Teams_leaving_team')}
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveTeamModalConfirmation;
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ITeam } from '@rocket.chat/core-typings';
|
||||
import { GenericModalSkeleton } from '@rocket.chat/ui-client';
|
||||
import { useUserId, useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import LeaveTeamModal from './LeaveTeamModal/LeaveTeamModal';
|
||||
|
||||
export type LeaveTeamWithDataProps = {
|
||||
teamId: ITeam['_id'];
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
const LeaveTeamWithData = ({ teamId, onCancel, onConfirm }: LeaveTeamWithDataProps) => {
|
||||
const userId = useUserId();
|
||||
|
||||
if (!userId) {
|
||||
throw Error('No user found');
|
||||
}
|
||||
|
||||
const getRoomsOfUser = useEndpoint('GET', '/v1/teams.listRoomsOfUser');
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['teams.listRoomsOfUser'],
|
||||
queryFn: () => getRoomsOfUser({ teamId, userId }),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <GenericModalSkeleton />;
|
||||
}
|
||||
|
||||
return <LeaveTeamModal onCancel={onCancel} onConfirm={onConfirm} rooms={data?.rooms || []} />;
|
||||
};
|
||||
|
||||
export default LeaveTeamWithData;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LeaveTeamWithData';
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { Contextualbar } from '@rocket.chat/ui-client';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import TeamsInfo from './TeamsInfo';
|
||||
|
||||
const room = {
|
||||
_id: 'awdawd',
|
||||
fname: 'rocketchat-frontend-team',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero',
|
||||
announcement:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero',
|
||||
topic:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam mollis nisi vel arcu bibendum vehicula. Integer vitae suscipit libero',
|
||||
} as IRoom;
|
||||
|
||||
export default {
|
||||
component: TeamsInfo,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
},
|
||||
decorators: [(fn) => <Contextualbar height='100vh'>{fn()}</Contextualbar>],
|
||||
args: {
|
||||
room,
|
||||
},
|
||||
} satisfies Meta<typeof TeamsInfo>;
|
||||
|
||||
export const Default = {};
|
||||
|
||||
export const Archived = {
|
||||
args: {
|
||||
room: { ...room, archived: true },
|
||||
},
|
||||
};
|
||||
|
||||
export const Broadcast = {
|
||||
args: {
|
||||
room: { ...room, broadcast: true },
|
||||
},
|
||||
};
|
||||
144
apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx
Normal file
144
apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { Box, Button, Callout, IconButton } from '@rocket.chat/fuselage';
|
||||
import { RoomAvatar } from '@rocket.chat/ui-avatar';
|
||||
import {
|
||||
GenericMenu,
|
||||
ContextualbarHeader,
|
||||
ContextualbarIcon,
|
||||
ContextualbarTitle,
|
||||
ContextualbarClose,
|
||||
ContextualbarScrollableContent,
|
||||
ContextualbarDialog,
|
||||
InfoPanel,
|
||||
InfoPanelAction,
|
||||
InfoPanelActionGroup,
|
||||
InfoPanelAvatar,
|
||||
InfoPanelField,
|
||||
InfoPanelLabel,
|
||||
InfoPanelSection,
|
||||
InfoPanelText,
|
||||
InfoPanelTitle,
|
||||
} from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useTeamActions } from './useTeamActions';
|
||||
import RetentionPolicyCallout from '../../../../components/InfoPanel/RetentionPolicyCallout';
|
||||
import MarkdownText from '../../../../components/MarkdownText';
|
||||
import RoomInfoABACSection from '../../../room/contextualBar/Info/RoomInfo/ABAC/RoomInfoABACSection';
|
||||
import { useSplitRoomActions } from '../../../room/contextualBar/Info/hooks/useSplitRoomActions';
|
||||
import { useRetentionPolicy } from '../../../room/hooks/useRetentionPolicy';
|
||||
|
||||
export type TeamsInfoProps = {
|
||||
room: IRoom;
|
||||
onClickEdit?: () => void;
|
||||
onClickClose?: () => void;
|
||||
onClickViewChannels: () => void;
|
||||
};
|
||||
|
||||
const TeamsInfo = ({ room, onClickClose, onClickEdit, onClickViewChannels }: TeamsInfoProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const retentionPolicy = useRetentionPolicy(room);
|
||||
const memoizedActions = useTeamActions(room, { onClickEdit });
|
||||
|
||||
const { buttons: actions, menu } = useSplitRoomActions(memoizedActions);
|
||||
|
||||
return (
|
||||
<ContextualbarDialog>
|
||||
<ContextualbarHeader>
|
||||
<ContextualbarIcon name='info-circled' />
|
||||
<ContextualbarTitle>{t('Teams_Info')}</ContextualbarTitle>
|
||||
{onClickClose && <ContextualbarClose onClick={onClickClose} />}
|
||||
</ContextualbarHeader>
|
||||
<ContextualbarScrollableContent padding={24}>
|
||||
<InfoPanel>
|
||||
<InfoPanelSection maxWidth='x332' marginInline='auto'>
|
||||
<InfoPanelAvatar>
|
||||
<RoomAvatar size='x332' room={room} />
|
||||
</InfoPanelAvatar>
|
||||
|
||||
<InfoPanelActionGroup>
|
||||
{actions.items.map(({ id, content, icon, onClick }) => (
|
||||
<InfoPanelAction key={id} label={content} onClick={onClick} icon={icon} />
|
||||
))}
|
||||
{menu && (
|
||||
<GenericMenu
|
||||
title={t('More')}
|
||||
placement='bottom-end'
|
||||
detached
|
||||
button={<IconButton icon='kebab' secondary flexShrink={0} flexGrow={0} maxHeight='initial' />}
|
||||
sections={menu}
|
||||
/>
|
||||
)}
|
||||
</InfoPanelActionGroup>
|
||||
</InfoPanelSection>
|
||||
|
||||
<InfoPanelSection>
|
||||
{room.archived && (
|
||||
<Box marginBlock={16}>
|
||||
<Callout type='warning'>{t('Room_archived')}</Callout>
|
||||
</Box>
|
||||
)}
|
||||
</InfoPanelSection>
|
||||
|
||||
<InfoPanelSection>
|
||||
<InfoPanelTitle title={room.fname || room.name || ''} icon='team' />
|
||||
</InfoPanelSection>
|
||||
|
||||
<InfoPanelSection>
|
||||
{room.broadcast && (
|
||||
<InfoPanelField>
|
||||
<InfoPanelLabel>
|
||||
<b>{t('Broadcast_channel')}</b> {t('Broadcast_channel_Description')}
|
||||
</InfoPanelLabel>
|
||||
</InfoPanelField>
|
||||
)}
|
||||
|
||||
{room.description && (
|
||||
<InfoPanelField>
|
||||
<InfoPanelLabel>{t('Description')}</InfoPanelLabel>
|
||||
<InfoPanelText withTruncatedText={false}>
|
||||
<MarkdownText variant='inline' content={room.description} />
|
||||
</InfoPanelText>
|
||||
</InfoPanelField>
|
||||
)}
|
||||
|
||||
{room.announcement && (
|
||||
<InfoPanelField>
|
||||
<InfoPanelLabel>{t('Announcement')}</InfoPanelLabel>
|
||||
<InfoPanelText withTruncatedText={false}>
|
||||
<MarkdownText variant='inline' content={room.announcement} />
|
||||
</InfoPanelText>
|
||||
</InfoPanelField>
|
||||
)}
|
||||
|
||||
{room.topic && (
|
||||
<InfoPanelField>
|
||||
<InfoPanelLabel>{t('Topic')}</InfoPanelLabel>
|
||||
<InfoPanelText withTruncatedText={false}>
|
||||
<MarkdownText variant='inline' content={room.topic} />
|
||||
</InfoPanelText>
|
||||
</InfoPanelField>
|
||||
)}
|
||||
|
||||
{onClickViewChannels && (
|
||||
<InfoPanelField>
|
||||
<InfoPanelLabel>{t('Teams_channels')}</InfoPanelLabel>
|
||||
<InfoPanelText>
|
||||
<Button onClick={onClickViewChannels} small>
|
||||
{t('View_channels')}
|
||||
</Button>
|
||||
</InfoPanelText>
|
||||
</InfoPanelField>
|
||||
)}
|
||||
|
||||
{retentionPolicy?.isActive && <RetentionPolicyCallout room={room} />}
|
||||
<RoomInfoABACSection room={room} />
|
||||
</InfoPanelSection>
|
||||
</InfoPanel>
|
||||
</ContextualbarScrollableContent>
|
||||
</ContextualbarDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsInfo;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { usePermission, useRoomToolbox } from '@rocket.chat/ui-contexts';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import TeamsInfo from './TeamsInfo';
|
||||
import { useRoom } from '../../../room/contexts/RoomContext';
|
||||
import EditChannelWithData from '../../../room/contextualBar/Info/EditRoomInfo';
|
||||
|
||||
const TeamsInfoWithData = () => {
|
||||
const room = useRoom();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const { openTab, closeTab } = useRoomToolbox();
|
||||
|
||||
const canEdit = usePermission('edit-team-channel', room._id);
|
||||
const onClickBack = useStableCallback(() => setEditing(false));
|
||||
const onClickViewChannels = useCallback(() => openTab('team-channels'), [openTab]);
|
||||
|
||||
if (editing) {
|
||||
return <EditChannelWithData onClickBack={onClickBack} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TeamsInfo
|
||||
room={room}
|
||||
onClickEdit={canEdit ? () => setEditing(true) : undefined}
|
||||
onClickClose={closeTab}
|
||||
onClickViewChannels={onClickViewChannels}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsInfoWithData;
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { usePermission, useSetModal, useToastMessageDispatch, useUserId } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ConvertToChannelModal from './ConvertToChannelModal';
|
||||
import { useEndpointMutation } from '../../../../hooks/useEndpointMutation';
|
||||
|
||||
export const useConvertToChannel = ({ _id, teamId }: IRoom) => {
|
||||
const { t } = useTranslation();
|
||||
const setModal = useSetModal();
|
||||
const userId = useUserId();
|
||||
const canEdit = usePermission('edit-team-channel', _id);
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const { mutateAsync: convertTeamToChannel } = useEndpointMutation('POST', '/v1/teams.convertToChannel', {
|
||||
onSuccess: () => {
|
||||
dispatchToastMessage({ type: 'success', message: t('Team_converted_to_channel') });
|
||||
},
|
||||
onSettled: () => {
|
||||
setModal(null);
|
||||
},
|
||||
});
|
||||
|
||||
const onClickConvertToChannel = useStableCallback(() => {
|
||||
if (!userId || !teamId) {
|
||||
throw new Error('Invalid teamId or userId');
|
||||
}
|
||||
|
||||
const onConfirm = async (roomsToRemove: { [key: string]: Serialized<IRoom> }) => {
|
||||
await convertTeamToChannel({
|
||||
teamId,
|
||||
roomsToRemove: Object.keys(roomsToRemove),
|
||||
});
|
||||
};
|
||||
|
||||
setModal(
|
||||
<ConvertToChannelModal
|
||||
onClose={() => setModal(null)}
|
||||
onCancel={() => setModal(null)}
|
||||
onConfirm={onConfirm}
|
||||
teamId={teamId}
|
||||
userId={userId}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
return canEdit ? onClickConvertToChannel : undefined;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useRouter, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import LeaveTeam from './LeaveTeam';
|
||||
import { useEndpointMutation } from '../../../../hooks/useEndpointMutation';
|
||||
|
||||
export const useLeaveTeam = ({ teamId }: IRoom) => {
|
||||
const { t } = useTranslation();
|
||||
const setModal = useSetModal();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync: leaveTeam } = useEndpointMutation('POST', '/v1/teams.leave', {
|
||||
onSuccess: () => {
|
||||
dispatchToastMessage({ type: 'success', message: t('Teams_left_team_successfully') });
|
||||
router.navigate('/home');
|
||||
},
|
||||
onSettled: () => {
|
||||
setModal(null);
|
||||
},
|
||||
});
|
||||
|
||||
// const canLeave = usePermission('leave-team'); /* && room.cl !== false && joined */
|
||||
const handleLeaveTeam = useStableCallback(() => {
|
||||
if (!teamId) {
|
||||
throw new Error('Invalid teamId');
|
||||
}
|
||||
|
||||
const onConfirm = async (selectedRooms: { [key: string]: Serialized<IRoom> & { isLastOwner?: boolean } } = {}) => {
|
||||
const roomsLeft = Object.keys(selectedRooms);
|
||||
const roomsToLeave = Array.isArray(roomsLeft) && roomsLeft.length > 0 ? roomsLeft : [];
|
||||
|
||||
await leaveTeam({
|
||||
teamId,
|
||||
...(roomsToLeave.length && { rooms: roomsToLeave }),
|
||||
});
|
||||
};
|
||||
|
||||
setModal(<LeaveTeam onConfirm={onConfirm} onCancel={() => setModal(null)} teamId={teamId} />);
|
||||
});
|
||||
|
||||
return handleLeaveTeam;
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useConvertToChannel } from './useConvertToChannel';
|
||||
import { useLeaveTeam } from './useLeaveTeam';
|
||||
import { useHideRoomAction } from '../../../../hooks/useHideRoomAction';
|
||||
import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom';
|
||||
|
||||
type GenProps = {
|
||||
onClickEdit?: () => void;
|
||||
};
|
||||
|
||||
export const useTeamActions = (room: IRoom, { onClickEdit }: GenProps) => {
|
||||
const { t } = useTranslation();
|
||||
const hideTeam = useHideRoomAction({ rid: room._id, type: room.t, name: room.name ?? '' });
|
||||
const convertToChannel = useConvertToChannel(room);
|
||||
const { handleDelete, canDeleteRoom } = useDeleteRoom(room);
|
||||
const leaveTeam = useLeaveTeam(room);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
items: [
|
||||
{
|
||||
id: 'hide',
|
||||
content: t('Hide'),
|
||||
icon: 'eye-off' as const,
|
||||
onClick: hideTeam,
|
||||
},
|
||||
...(onClickEdit
|
||||
? [
|
||||
{
|
||||
id: 'edit',
|
||||
content: t('Edit'),
|
||||
icon: 'edit' as const,
|
||||
onClick: onClickEdit,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(leaveTeam
|
||||
? [
|
||||
{
|
||||
id: 'leave',
|
||||
content: t('Leave'),
|
||||
icon: 'sign-out' as const,
|
||||
onClick: leaveTeam,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(convertToChannel
|
||||
? [
|
||||
{
|
||||
id: 'convert_team_to_channel',
|
||||
content: t('Convert_to_channel'),
|
||||
icon: 'hash' as const,
|
||||
onClick: convertToChannel,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(canDeleteRoom
|
||||
? [
|
||||
{
|
||||
id: 'delete',
|
||||
content: t('Delete'),
|
||||
icon: 'trash' as const,
|
||||
onClick: handleDelete,
|
||||
variant: 'danger',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
[t, hideTeam, leaveTeam, onClickEdit, handleDelete, canDeleteRoom, convertToChannel],
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user