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,88 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { Box, CheckBox } from '@rocket.chat/fuselage';
|
||||
import { GenericTable, GenericTableHeaderCell, GenericTableHeader, GenericTableBody, useSort } from '@rocket.chat/ui-client';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ChannelDesertionTableRow from './ChannelDesertionTableRow';
|
||||
|
||||
export type ChannelDesertionTableProps = {
|
||||
lastOwnerWarning?: string;
|
||||
rooms?: (Serialized<IRoom> & { isLastOwner?: boolean })[];
|
||||
eligibleRoomsLength: number | undefined;
|
||||
onChangeRoomSelection: (room: Serialized<IRoom>) => void;
|
||||
selectedRooms: { [key: string]: Serialized<IRoom> };
|
||||
onToggleAllRooms: () => void;
|
||||
};
|
||||
|
||||
const ChannelDesertionTable = ({
|
||||
rooms,
|
||||
eligibleRoomsLength,
|
||||
onChangeRoomSelection,
|
||||
selectedRooms,
|
||||
onToggleAllRooms,
|
||||
lastOwnerWarning,
|
||||
}: ChannelDesertionTableProps) => {
|
||||
const { sortBy, sortDirection, setSort } = useSort<'name' | 'ts'>('name');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const selectedRoomsLength = Object.values(selectedRooms).filter(Boolean).length;
|
||||
|
||||
const checked = eligibleRoomsLength === selectedRoomsLength;
|
||||
const indeterminate = eligibleRoomsLength && eligibleRoomsLength > selectedRoomsLength ? selectedRoomsLength > 0 : false;
|
||||
|
||||
const results = useMemo(() => {
|
||||
if (!rooms) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const direction = sortDirection === 'asc' ? 1 : -1;
|
||||
|
||||
return rooms.toSorted((a, b) => {
|
||||
const aValue = a[sortBy] ?? '';
|
||||
const bValue = b[sortBy] ?? '';
|
||||
if (!aValue && !bValue) {
|
||||
return 0;
|
||||
}
|
||||
if (!aValue) {
|
||||
return 1;
|
||||
}
|
||||
if (!bValue) {
|
||||
return -1;
|
||||
}
|
||||
return aValue.localeCompare(bValue) * direction;
|
||||
});
|
||||
}, [rooms, sortBy, sortDirection]);
|
||||
|
||||
return (
|
||||
<Box display='flex' flexDirection='column' height='x200' marginBlockStart={24}>
|
||||
<GenericTable fixed={false}>
|
||||
<GenericTableHeader>
|
||||
<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='ts' sort='ts' onClick={setSort} direction={sortDirection} active={sortBy === 'ts'}>
|
||||
<Box width='100%' textAlign='end'>
|
||||
{t('Joined_at')}
|
||||
</Box>
|
||||
</GenericTableHeaderCell>
|
||||
</GenericTableHeader>
|
||||
<GenericTableBody>
|
||||
{results?.map((room, key) => (
|
||||
<ChannelDesertionTableRow
|
||||
key={key}
|
||||
room={room}
|
||||
onChange={onChangeRoomSelection}
|
||||
selected={'_id' in room && room._id ? !!selectedRooms[room._id] : false}
|
||||
lastOwnerWarning={lastOwnerWarning}
|
||||
/>
|
||||
))}
|
||||
</GenericTableBody>
|
||||
</GenericTable>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelDesertionTable;
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { Box, CheckBox, Icon, Margins } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { GenericTableRow, GenericTableCell } from '@rocket.chat/ui-client';
|
||||
import { useId } from 'react';
|
||||
|
||||
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
|
||||
|
||||
export type ChannelDesertionTableRowProps = {
|
||||
onChange: (room: Serialized<IRoom> & { isLastOwner?: boolean }) => void;
|
||||
selected: boolean;
|
||||
room: Serialized<IRoom> & { isLastOwner?: boolean };
|
||||
lastOwnerWarning?: string;
|
||||
};
|
||||
|
||||
const ChannelDesertionTableRow = ({ room, onChange, selected, lastOwnerWarning }: ChannelDesertionTableRowProps) => {
|
||||
const { name, fname, ts, isLastOwner } = room;
|
||||
const formatDate = useFormatDateAndTime();
|
||||
const checkboxId = useId();
|
||||
const handleChange = useStableCallback(() => onChange(room));
|
||||
|
||||
return (
|
||||
<GenericTableRow action>
|
||||
<GenericTableCell maxWidth='x300' withTruncatedText>
|
||||
<CheckBox id={checkboxId} checked={selected} onChange={handleChange} disabled={isLastOwner} />
|
||||
<Box is='label' htmlFor={checkboxId} style={{ cursor: isLastOwner ? undefined : 'pointer' }}>
|
||||
<Margins inline={8}>
|
||||
<Icon name={room.t === 'p' ? 'hashtag-lock' : 'hashtag'} />
|
||||
{fname ?? name}
|
||||
{isLastOwner && <Icon size='x16' name='info-circled' color='status-font-on-danger' title={lastOwnerWarning} />}
|
||||
</Margins>
|
||||
</Box>
|
||||
</GenericTableCell>
|
||||
<GenericTableCell align='end' withTruncatedText>
|
||||
{formatDate(ts)}
|
||||
</GenericTableCell>
|
||||
</GenericTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelDesertionTableRow;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ChannelDesertionTable';
|
||||
@@ -0,0 +1,50 @@
|
||||
import { AutoComplete, Option, Box } from '@rocket.chat/fuselage';
|
||||
import { RoomAvatar } from '@rocket.chat/ui-avatar';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
|
||||
export type TeamAutocompleteProps = Omit<ComponentProps<typeof AutoComplete>, 'filter'>;
|
||||
|
||||
const TeamAutocomplete = ({ value, onChange, ...props }: TeamAutocompleteProps) => {
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const teamsAutoCompleteEndpoint = useEndpoint('GET', '/v1/teams.autocomplete');
|
||||
const { data, isSuccess } = useQuery({
|
||||
queryKey: ['teamsAutoComplete', filter],
|
||||
queryFn: async () => teamsAutoCompleteEndpoint({ name: filter }),
|
||||
});
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
isSuccess
|
||||
? data?.teams.map(({ name, teamId, _id, avatarETag, t }) => ({
|
||||
value: teamId as string,
|
||||
label: { name, avatarETag, type: t, _id },
|
||||
}))
|
||||
: [],
|
||||
[data, isSuccess],
|
||||
);
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
renderSelected={({ selected: { value, label: room } }) => (
|
||||
<Box value={value}>
|
||||
<RoomAvatar size='x20' room={room} /> {room.name}
|
||||
</Box>
|
||||
)}
|
||||
renderItem={({ value, label: room, ...props }) => (
|
||||
<Option key={value} {...props} label={room.name} avatar={<RoomAvatar size='x20' room={room} />} />
|
||||
)}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(TeamAutocomplete);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './TeamAutocomplete';
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Field,
|
||||
FieldLabel,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalFooterControllers,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RoomsAvailableForTeamsAutoComplete from './RoomsAvailableForTeamsAutoComplete';
|
||||
|
||||
type AddExistingModalFormData = {
|
||||
rooms: IRoom['_id'][];
|
||||
};
|
||||
|
||||
export type AddExistingModalProps = {
|
||||
teamId: string;
|
||||
onClose: () => void;
|
||||
reload?: () => void;
|
||||
};
|
||||
|
||||
// TODO: Use GenericModal instead of Modal
|
||||
const AddExistingModal = ({ teamId, onClose, reload }: AddExistingModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
|
||||
const addRoomEndpoint = useEndpoint('POST', '/v1/teams.addRooms');
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isDirty },
|
||||
handleSubmit,
|
||||
} = useForm<AddExistingModalFormData>({ defaultValues: { rooms: [] } });
|
||||
|
||||
const handleAddChannels = useCallback(
|
||||
async ({ rooms }: AddExistingModalFormData) => {
|
||||
try {
|
||||
await addRoomEndpoint({
|
||||
rooms,
|
||||
teamId,
|
||||
});
|
||||
|
||||
dispatchToastMessage({ type: 'success', message: t('Channels_added') });
|
||||
reload?.();
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[addRoomEndpoint, teamId, onClose, dispatchToastMessage, reload, t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-label={t('Team_Add_existing_channels')}
|
||||
wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit(handleAddChannels)} {...props} />}
|
||||
>
|
||||
<ModalHeader>
|
||||
<ModalTitle>{t('Team_Add_existing_channels')}</ModalTitle>
|
||||
<ModalClose onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Field marginBlockEnd={24}>
|
||||
<FieldLabel>{t('Channels')}</FieldLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name='rooms'
|
||||
render={({ field: { value, onChange } }) => <RoomsAvailableForTeamsAutoComplete value={value} onChange={onChange} />}
|
||||
/>
|
||||
</Field>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<ModalFooterControllers>
|
||||
<Button onClick={onClose}>{t('Cancel')}</Button>
|
||||
<Button disabled={!isDirty} type='submit' primary>
|
||||
{t('Add')}
|
||||
</Button>
|
||||
</ModalFooterControllers>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AddExistingModal);
|
||||
@@ -0,0 +1,69 @@
|
||||
import { AutoComplete, Box, Option, Chip } from '@rocket.chat/fuselage';
|
||||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
|
||||
import { RoomAvatar } from '@rocket.chat/ui-avatar';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
|
||||
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
|
||||
|
||||
export type RoomsAvailableForTeamsAutoCompleteProps = Omit<ComponentProps<typeof AutoComplete>, 'filter'>;
|
||||
|
||||
const RoomsAvailableForTeamsAutoComplete = ({ value, onChange, ...props }: RoomsAvailableForTeamsAutoCompleteProps) => {
|
||||
const [filter, setFilter] = useState('');
|
||||
const debouncedFilter = useDebouncedValue(filter, 1000);
|
||||
|
||||
const roomsAvailableForTeamsAutoCompleteEndpoint = useEndpoint('GET', '/v1/rooms.autocomplete.availableForTeams');
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['roomsAvailableForTeamsAutoComplete', debouncedFilter],
|
||||
queryFn: async () => roomsAvailableForTeamsAutoCompleteEndpoint({ name: debouncedFilter }),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.items.map((room) => ({
|
||||
value: room._id,
|
||||
label: {
|
||||
name: roomCoordinator.getRoomName(room.t, { _id: room._id, name: room.name, fname: room.fname, prid: room.prid }),
|
||||
type: room.t,
|
||||
avatarETag: room.avatarETag,
|
||||
},
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
{...props}
|
||||
multiple
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
renderSelected={({ selected: { value, label }, onRemove }) => (
|
||||
<Chip key={value} height='x20' value={value} onClick={onRemove} marginBlock={2} marginInlineEnd={4}>
|
||||
<RoomAvatar size='x20' room={{ ...label, type: label?.type || 'c', _id: value }} />
|
||||
<Box is='span' margin='none' marginInlineStart={4}>
|
||||
{label.name}
|
||||
</Box>
|
||||
</Chip>
|
||||
)}
|
||||
renderItem={({ value, label, ...props }) => (
|
||||
<Option
|
||||
key={value}
|
||||
{...props}
|
||||
label={label.name}
|
||||
avatar={<RoomAvatar room={{ _id: value, type: label.type, avatarETag: label.avatarETag }} size='x20' />}
|
||||
/>
|
||||
)}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(RoomsAvailableForTeamsAutoComplete);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './AddExistingModal';
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
IconButton,
|
||||
Option,
|
||||
OptionAvatar,
|
||||
OptionColumn,
|
||||
OptionContent,
|
||||
OptionMenu,
|
||||
OptionSkeleton,
|
||||
Tag,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks';
|
||||
import { RoomAvatar } from '@rocket.chat/ui-avatar';
|
||||
import { usePermission } from '@rocket.chat/ui-contexts';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import TeamsChannelItemMenu from './TeamsChannelItemMenu';
|
||||
import { usePreventPropagation } from '../../../../hooks/usePreventPropagation';
|
||||
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
|
||||
|
||||
export type TeamsChannelItemProps = {
|
||||
room: IRoom;
|
||||
mainRoom: IRoom;
|
||||
onClickView: (room: IRoom) => void;
|
||||
reload: () => void;
|
||||
};
|
||||
|
||||
const TeamsChannelItem = ({ room, mainRoom, onClickView, reload }: TeamsChannelItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
const rid = room._id;
|
||||
const type = room.t;
|
||||
|
||||
const [showButton, setShowButton] = useState();
|
||||
|
||||
const canRemoveTeamChannel = usePermission('remove-team-channel', mainRoom._id);
|
||||
const canEditTeamChannel = usePermission('edit-team-channel', mainRoom._id);
|
||||
const canDeleteChannel = usePermission(`delete-${type}`, rid);
|
||||
const canDeleteTeamChannel = usePermission(`delete-team-${type === 'c' ? 'channel' : 'group'}`, mainRoom._id);
|
||||
const canDelete = canDeleteChannel && canDeleteTeamChannel;
|
||||
|
||||
const isReduceMotionEnabled = usePrefersReducedMotion();
|
||||
const handleMenuEvent = {
|
||||
[isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setShowButton,
|
||||
};
|
||||
|
||||
const onClick = usePreventPropagation();
|
||||
|
||||
if (!room) {
|
||||
return <OptionSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Option id={room._id} data-rid={room._id} {...handleMenuEvent} onClick={() => onClickView(room)}>
|
||||
<OptionAvatar>
|
||||
<RoomAvatar room={room} size='x28' />
|
||||
</OptionAvatar>
|
||||
<OptionColumn>{room.t === 'c' ? <Icon name='hash' size='x15' /> : <Icon name='hashtag-lock' size='x15' />}</OptionColumn>
|
||||
<OptionContent>
|
||||
<Box display='inline-flex' alignItems='center'>
|
||||
{roomCoordinator.getRoomName(room.t, room)}{' '}
|
||||
{room.teamDefault ? (
|
||||
<Box marginInline={4}>
|
||||
<Tag>{t('Team_Auto-join')}</Tag>
|
||||
</Box>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Box>
|
||||
</OptionContent>
|
||||
{(canRemoveTeamChannel || canEditTeamChannel || canDelete) && (
|
||||
<OptionMenu onClick={onClick}>
|
||||
{showButton ? <TeamsChannelItemMenu room={room} mainRoom={mainRoom} reload={reload} /> : <IconButton tiny icon='kebab' />}
|
||||
</OptionMenu>
|
||||
)}
|
||||
</Option>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsChannelItem;
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { CheckBox } from '@rocket.chat/fuselage';
|
||||
import { GenericMenu } from '@rocket.chat/ui-client';
|
||||
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useRemoveRoomFromTeam } from './hooks/useRemoveRoomFromTeam';
|
||||
import { useToggleAutoJoin } from './hooks/useToggleAutoJoin';
|
||||
import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom';
|
||||
|
||||
const TeamsChannelItemMenu = ({ room, mainRoom, reload }: { room: IRoom; mainRoom: IRoom; reload?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { handleRemoveRoom, canRemoveTeamChannel } = useRemoveRoomFromTeam(room, { reload });
|
||||
const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload });
|
||||
const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload, mainRoom });
|
||||
|
||||
const toggleAutoJoin = {
|
||||
id: 'toggleAutoJoin',
|
||||
icon: room.t === 'c' ? 'hash' : 'hashtag-lock',
|
||||
content: t('Team_Auto-join'),
|
||||
onClick: handleToggleAutoJoin,
|
||||
addon: <CheckBox checked={room.teamDefault} />,
|
||||
};
|
||||
|
||||
const removeRoom = {
|
||||
id: 'removeRoom',
|
||||
icon: 'cross',
|
||||
content: t('Team_Remove_from_team'),
|
||||
onClick: handleRemoveRoom,
|
||||
variant: 'danger',
|
||||
};
|
||||
|
||||
const deleteRoom = {
|
||||
id: 'deleteRoom',
|
||||
icon: 'trash',
|
||||
content: t('Delete'),
|
||||
onClick: handleDelete,
|
||||
variant: 'danger',
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericMenu
|
||||
title={t('More')}
|
||||
placement='bottom-end'
|
||||
sections={[
|
||||
{
|
||||
title: '',
|
||||
items: [canEditTeamChannel && toggleAutoJoin, canRemoveTeamChannel && removeRoom, canDeleteRoom && deleteRoom].filter(
|
||||
Boolean,
|
||||
) as GenericMenuItemProps[],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsChannelItemMenu;
|
||||
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable testing-library/no-container */
|
||||
/* eslint-disable testing-library/no-node-access */
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
|
||||
import TeamsChannels from './TeamsChannels';
|
||||
import { createFakeRoom } from '../../../../../tests/mocks/data';
|
||||
|
||||
jest.mock('../../../../lib/rooms/roomCoordinator', () => ({
|
||||
roomCoordinator: {
|
||||
getRouteLink: () => undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
const mainRoom = createFakeRoom({ name: 'Main Room' });
|
||||
const fakeRooms = Array.from({ length: 10 }, (_, index) => createFakeRoom({ t: 'c', name: `Fake Room ${index}` }));
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, 'getComputedStyle', {
|
||||
value: () => {
|
||||
return {
|
||||
getPropertyPriority: () => undefined,
|
||||
getPropertyValue: () => undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Replace this with the storybook & snapshot
|
||||
it('should render scrollbars', async () => {
|
||||
const { container } = render(
|
||||
<TeamsChannels
|
||||
text=''
|
||||
type='all'
|
||||
reload={() => undefined}
|
||||
loadMoreItems={() => undefined}
|
||||
setText={() => undefined}
|
||||
setType={() => undefined}
|
||||
onClickClose={() => undefined}
|
||||
onClickAddExisting={() => undefined}
|
||||
onClickView={() => undefined}
|
||||
onClickCreateNew={() => undefined}
|
||||
total={fakeRooms.length}
|
||||
loading={false}
|
||||
mainRoom={mainRoom}
|
||||
channels={fakeRooms}
|
||||
/>,
|
||||
{
|
||||
wrapper: mockAppRoot().build(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('[data-overlayscrollbars]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-overlayscrollbars]')).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import type { SelectOption } from '@rocket.chat/fuselage';
|
||||
import { Box, Icon, TextInput, Select, Throbber, ButtonGroup, Button } from '@rocket.chat/fuselage';
|
||||
import { useStableCallback, useAutoFocus, useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import {
|
||||
VirtualizedScrollbars,
|
||||
ContextualbarHeader,
|
||||
ContextualbarIcon,
|
||||
ContextualbarTitle,
|
||||
ContextualbarClose,
|
||||
ContextualbarContent,
|
||||
ContextualbarFooter,
|
||||
ContextualbarEmptyContent,
|
||||
ContextualbarSection,
|
||||
ContextualbarDialog,
|
||||
} from '@rocket.chat/ui-client';
|
||||
import type { ChangeEvent, Dispatch, SetStateAction, SyntheticEvent } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import TeamsChannelItem from './TeamsChannelItem';
|
||||
import InfiniteListAnchor from '../../../../components/InfiniteListAnchor';
|
||||
|
||||
export type TeamsChannelsProps = {
|
||||
loading: boolean;
|
||||
channels: IRoom[];
|
||||
mainRoom: IRoom;
|
||||
text: string;
|
||||
type: 'all' | 'autoJoin';
|
||||
setText: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
setType: Dispatch<SetStateAction<'all' | 'autoJoin'>>;
|
||||
onClickClose: () => void;
|
||||
onClickAddExisting: false | ((e: SyntheticEvent) => void);
|
||||
onClickCreateNew: false | ((e: SyntheticEvent) => void);
|
||||
total: number;
|
||||
loadMoreItems: () => void;
|
||||
onClickView: (room: IRoom) => void;
|
||||
reload: () => void;
|
||||
};
|
||||
|
||||
const TeamsChannels = ({
|
||||
loading,
|
||||
channels = [],
|
||||
mainRoom,
|
||||
text,
|
||||
type,
|
||||
setText,
|
||||
setType,
|
||||
onClickClose,
|
||||
onClickAddExisting,
|
||||
onClickCreateNew,
|
||||
total,
|
||||
loadMoreItems,
|
||||
onClickView,
|
||||
reload,
|
||||
}: TeamsChannelsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useAutoFocus<HTMLInputElement>(true);
|
||||
|
||||
const options: SelectOption[] = useMemo(
|
||||
() => [
|
||||
['all', t('All')],
|
||||
['autoJoin', t('Team_Auto-join')],
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const lm = useStableCallback(() => !loading && loadMoreItems());
|
||||
|
||||
const loadMoreChannels = useDebouncedCallback(
|
||||
() => {
|
||||
if (channels.length >= total) {
|
||||
return;
|
||||
}
|
||||
|
||||
lm();
|
||||
},
|
||||
300,
|
||||
[lm, channels],
|
||||
);
|
||||
|
||||
return (
|
||||
<ContextualbarDialog>
|
||||
<ContextualbarHeader>
|
||||
<ContextualbarIcon name='hash' />
|
||||
<ContextualbarTitle>{t('Team_Channels')}</ContextualbarTitle>
|
||||
{onClickClose && <ContextualbarClose onClick={onClickClose} />}
|
||||
</ContextualbarHeader>
|
||||
<ContextualbarSection>
|
||||
<TextInput
|
||||
placeholder={t('Search')}
|
||||
value={text}
|
||||
ref={inputRef}
|
||||
onChange={setText}
|
||||
endAddon={<Icon name='magnifier' size='x20' />}
|
||||
/>
|
||||
<Box width='x144' marginInlineStart={8}>
|
||||
<Select onChange={(val) => setType(val as 'all' | 'autoJoin')} value={type} options={options} />
|
||||
</Box>
|
||||
</ContextualbarSection>
|
||||
<ContextualbarContent padding={12}>
|
||||
{loading && (
|
||||
<Box paddingInline={24} paddingBlock={12}>
|
||||
<Throbber size='x12' />
|
||||
</Box>
|
||||
)}
|
||||
{!loading && channels.length === 0 && <ContextualbarEmptyContent title={t('No_channels_in_team')} />}
|
||||
{!loading && channels.length > 0 && (
|
||||
<>
|
||||
<Box paddingInline={18} paddingBlock={12}>
|
||||
<Box is='span' color='hint' fontScale='p2'>
|
||||
{t('Showing')}: {channels.length}
|
||||
</Box>
|
||||
|
||||
<Box is='span' color='hint' fontScale='p2' marginInlineStart={8}>
|
||||
{t('Total')}: {total}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box width='full' height='full' role='list' overflow='hidden' flexShrink={1}>
|
||||
<VirtualizedScrollbars>
|
||||
<Virtuoso
|
||||
totalCount={total}
|
||||
data={channels}
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
components={{ Footer: () => <InfiniteListAnchor loadMore={loadMoreChannels} /> }}
|
||||
itemContent={(index, data) => (
|
||||
<TeamsChannelItem onClickView={onClickView} room={data} mainRoom={mainRoom} reload={reload} key={index} />
|
||||
)}
|
||||
/>
|
||||
</VirtualizedScrollbars>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</ContextualbarContent>
|
||||
{(onClickAddExisting || onClickCreateNew) && (
|
||||
<ContextualbarFooter>
|
||||
<ButtonGroup stretch>
|
||||
{onClickAddExisting && (
|
||||
<Button onClick={onClickAddExisting} width='50%'>
|
||||
{t('Team_Add_existing')}
|
||||
</Button>
|
||||
)}
|
||||
{onClickCreateNew && (
|
||||
<Button onClick={onClickCreateNew} width='50%'>
|
||||
{t('Create_new')}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</ContextualbarFooter>
|
||||
)}
|
||||
</ContextualbarDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsChannels;
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { useLocalStorage, useDebouncedValue, useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { useSetModal, usePermission, useAtLeastOnePermission, useRoomToolbox } from '@rocket.chat/ui-contexts';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import AddExistingModal from './AddExistingModal';
|
||||
import TeamsChannels from './TeamsChannels';
|
||||
import { useTeamsChannelList } from './hooks/useTeamsChannelList';
|
||||
import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator';
|
||||
import CreateChannelModal from '../../../../navbar/NavBarPagesGroup/actions/CreateChannelModal';
|
||||
import { useRoom } from '../../../room/contexts/RoomContext';
|
||||
|
||||
const TeamsChannelsWithData = () => {
|
||||
const room = useRoom();
|
||||
const setModal = useSetModal();
|
||||
const { closeTab } = useRoomToolbox();
|
||||
const canAddExistingRoomToTeam = usePermission('move-room-to-team', room._id);
|
||||
const canCreateRoomInTeam = useAtLeastOnePermission(['create-team-channel', 'create-team-group'], room._id);
|
||||
|
||||
const { teamId } = room;
|
||||
|
||||
if (!teamId) {
|
||||
throw new Error('Invalid teamId');
|
||||
}
|
||||
|
||||
const [type, setType] = useLocalStorage<'all' | 'autoJoin'>('channels-list-type', 'all');
|
||||
const [text, setText] = useState('');
|
||||
const debouncedText = useDebouncedValue(text, 800);
|
||||
|
||||
const { isPending, data, fetchNextPage, refetch } = useTeamsChannelList({ teamId, text: debouncedText, type });
|
||||
|
||||
const handleTextChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setText(event.currentTarget.value);
|
||||
}, []);
|
||||
|
||||
const handleAddExisting = useStableCallback(() => {
|
||||
setModal(<AddExistingModal teamId={teamId} onClose={() => setModal(null)} reload={refetch} />);
|
||||
});
|
||||
|
||||
const handleCreateNew = useStableCallback(() => {
|
||||
setModal(<CreateChannelModal teamId={teamId} mainRoom={room} onClose={() => setModal(null)} reload={refetch} />);
|
||||
});
|
||||
|
||||
const goToRoom = useStableCallback((room: IRoom) => {
|
||||
roomCoordinator.openRouteLink(room.t, room);
|
||||
});
|
||||
|
||||
return (
|
||||
<TeamsChannels
|
||||
loading={isPending}
|
||||
mainRoom={room}
|
||||
type={type}
|
||||
text={text}
|
||||
setType={setType}
|
||||
setText={handleTextChange}
|
||||
channels={data?.channels ?? []}
|
||||
total={data?.total ?? 0}
|
||||
onClickClose={closeTab}
|
||||
onClickAddExisting={canAddExistingRoomToTeam && handleAddExisting}
|
||||
onClickCreateNew={canCreateRoomInTeam && handleCreateNew}
|
||||
onClickView={goToRoom}
|
||||
loadMoreItems={fetchNextPage}
|
||||
reload={refetch}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamsChannelsWithData;
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useEndpoint, usePermission, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
|
||||
|
||||
export const useRemoveRoomFromTeam = (room: IRoom, { reload }: { reload?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const setModal = useSetModal();
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const canRemoveTeamChannel = usePermission('remove-team-channel', room._id);
|
||||
|
||||
const removeRoomEndpoint = useEndpoint('POST', '/v1/teams.removeRoom');
|
||||
|
||||
const handleRemoveRoom = () => {
|
||||
const onConfirmAction = async () => {
|
||||
if (!room.teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeRoomEndpoint({ teamId: room.teamId, roomId: room._id });
|
||||
dispatchToastMessage({ type: 'error', message: t('Room_has_been_removed') });
|
||||
reload?.();
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
} finally {
|
||||
setModal(null);
|
||||
}
|
||||
};
|
||||
|
||||
return setModal(
|
||||
<GenericModal variant='danger' onCancel={() => setModal(null)} onConfirm={onConfirmAction} confirmText={t('Remove')}>
|
||||
{t('Team_Remove_from_team_modal_content', {
|
||||
teamName: roomCoordinator.getRoomName(room.t, room),
|
||||
})}
|
||||
</GenericModal>,
|
||||
);
|
||||
};
|
||||
|
||||
return { handleRemoveRoom, canRemoveTeamChannel };
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
import { teamsQueryKeys } from '../../../../../lib/queryKeys';
|
||||
import { getConfig } from '../../../../../lib/utils/getConfig';
|
||||
import { mapMessageFromApi } from '../../../../../lib/utils/mapMessageFromApi';
|
||||
|
||||
type TeamsChannelListOptions = {
|
||||
teamId: string;
|
||||
type: 'all' | 'autoJoin';
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const useTeamsChannelList = ({ teamId, type, text }: TeamsChannelListOptions) => {
|
||||
const listTeamRooms = useEndpoint('GET', '/v1/teams.listRooms');
|
||||
|
||||
const count = parseInt(`${getConfig('teamsChannelListSize', 10)}`, 10);
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: teamsQueryKeys.listChannels(teamId, { type, text }),
|
||||
queryFn: async ({ pageParam: offset }) => {
|
||||
const { rooms, total } = await listTeamRooms({
|
||||
teamId,
|
||||
offset,
|
||||
count,
|
||||
filter: text,
|
||||
type,
|
||||
});
|
||||
|
||||
return {
|
||||
items: rooms.map(({ _updatedAt, lastMessage, lm, ts, webRtcCallStartTime, usersWaitingForE2EKeys, ...room }): IRoom => ({
|
||||
...(lm && { lm: new Date(lm) }),
|
||||
...(ts && { ts: new Date(ts) }),
|
||||
_updatedAt: new Date(_updatedAt),
|
||||
...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }),
|
||||
...(webRtcCallStartTime && { webRtcCallStartTime: new Date(webRtcCallStartTime) }),
|
||||
...(usersWaitingForE2EKeys && {
|
||||
usersWaitingForE2EKeys: usersWaitingForE2EKeys?.map(({ userId, ts }) => ({ userId, ts: new Date(ts) })),
|
||||
}),
|
||||
...room,
|
||||
})),
|
||||
itemCount: total,
|
||||
};
|
||||
},
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage, _, lastOffset) => {
|
||||
const nextOffset = lastOffset + count;
|
||||
if (nextOffset >= lastPage.itemCount) return undefined;
|
||||
return nextOffset;
|
||||
},
|
||||
select: ({ pages }) => ({
|
||||
channels: pages.flatMap((page) => page.items),
|
||||
total: pages.at(-1)?.itemCount,
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { useEndpoint, usePermission, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
|
||||
|
||||
export const useToggleAutoJoin = (room: IRoom, { reload, mainRoom }: { reload?: () => void; mainRoom: IRoom }) => {
|
||||
const dispatchToastMessage = useToastMessageDispatch();
|
||||
const updateRoomEndpoint = useEndpoint('POST', '/v1/teams.updateRoom');
|
||||
const canEditTeamChannel = usePermission('edit-team-channel', room._id);
|
||||
const maxNumberOfAutoJoinMembers = useSetting('API_User_Limit', 500);
|
||||
|
||||
const handleToggleAutoJoin = async () => {
|
||||
// Sanity check, the setting has a default value, therefore it should always be defined
|
||||
if (!maxNumberOfAutoJoinMembers) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { room: updatedRoom } = await updateRoomEndpoint({
|
||||
roomId: room._id,
|
||||
isDefault: !room.teamDefault,
|
||||
});
|
||||
if (updatedRoom.teamDefault) {
|
||||
// If the number of members in the mainRoom (the team) is greater than the limit, show an info message
|
||||
// informing that not all members will be auto-joined to the channel
|
||||
const messageType = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'info' : 'success';
|
||||
const message = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'Team_Auto-join_exceeded_user_limit' : 'Team_Auto-join_updated';
|
||||
|
||||
dispatchToastMessage({
|
||||
type: messageType,
|
||||
message: t(message, {
|
||||
channelName: roomCoordinator.getRoomName(room.t, room),
|
||||
numberOfMembers: updatedRoom.usersCount,
|
||||
limit: maxNumberOfAutoJoinMembers,
|
||||
}),
|
||||
});
|
||||
}
|
||||
reload?.();
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
};
|
||||
|
||||
return { handleToggleAutoJoin, canEditTeamChannel };
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './TeamsChannelsWithData';
|
||||
@@ -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],
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { usePermission } from '@rocket.chat/ui-contexts';
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
import RemoveUsersFirstStep from './RemoveUsersFirstStep';
|
||||
import RemoveUsersSecondStep from './RemoveUsersSecondStep';
|
||||
|
||||
const STEPS = {
|
||||
LIST_ROOMS: 'LIST_ROOMS',
|
||||
CONFIRM_DELETE: 'CONFIRM_DELETE',
|
||||
};
|
||||
|
||||
export type BaseRemoveUsersModalProps = {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: (deletedRooms: { [key: string]: Serialized<IRoom> }) => void;
|
||||
currentStep?: string;
|
||||
rooms?: (Serialized<IRoom> & { isLastOwner?: boolean })[];
|
||||
username?: string;
|
||||
};
|
||||
|
||||
const BaseRemoveUsersModal = ({
|
||||
onClose,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
rooms,
|
||||
currentStep = rooms?.length === 0 ? STEPS.CONFIRM_DELETE : STEPS.LIST_ROOMS,
|
||||
username,
|
||||
}: BaseRemoveUsersModalProps) => {
|
||||
const [step, setStep] = useState(currentStep);
|
||||
|
||||
const [selectedRooms, setSelectedRooms] = useState<Record<string, Serialized<IRoom> & { isLastOwner?: boolean }>>({});
|
||||
|
||||
const onContinue = useStableCallback(() => setStep(STEPS.CONFIRM_DELETE));
|
||||
const onReturn = useStableCallback(() => setStep(STEPS.LIST_ROOMS));
|
||||
|
||||
const canViewUserRooms = usePermission('view-all-team-channels');
|
||||
|
||||
const eligibleRooms = rooms?.filter(({ isLastOwner }) => !isLastOwner);
|
||||
|
||||
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) {
|
||||
return setSelectedRooms(Object.fromEntries(eligibleRooms?.map((room) => [room._id, room]) ?? []));
|
||||
}
|
||||
setSelectedRooms({});
|
||||
});
|
||||
|
||||
if (step === STEPS.CONFIRM_DELETE || !canViewUserRooms) {
|
||||
return (
|
||||
<RemoveUsersSecondStep
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
onCancel={(rooms?.length ?? 0) > 0 ? onReturn : onCancel}
|
||||
deletedRooms={selectedRooms}
|
||||
rooms={rooms}
|
||||
username={username}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RemoveUsersFirstStep
|
||||
onConfirm={onContinue}
|
||||
onClose={onClose}
|
||||
onCancel={onCancel}
|
||||
rooms={rooms}
|
||||
selectedRooms={selectedRooms}
|
||||
onToggleAllRooms={onToggleAllRooms}
|
||||
onChangeRoomSelection={onChangeRoomSelection}
|
||||
eligibleRoomsLength={eligibleRooms?.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseRemoveUsersModal;
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { Serialized, IRoom } 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 RemoveUsersFirstStepProps = {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
rooms?: (Serialized<IRoom> & { isLastOwner?: boolean })[];
|
||||
onToggleAllRooms: () => void;
|
||||
onChangeRoomSelection: (room: Serialized<IRoom> & { isLastOwner?: boolean }) => void;
|
||||
selectedRooms: { [key: string]: Serialized<IRoom> };
|
||||
eligibleRoomsLength: number | undefined;
|
||||
};
|
||||
|
||||
const RemoveUsersFirstStep = ({
|
||||
onClose,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
rooms,
|
||||
onToggleAllRooms,
|
||||
onChangeRoomSelection,
|
||||
selectedRooms,
|
||||
eligibleRoomsLength,
|
||||
...props
|
||||
}: RemoveUsersFirstStepProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
variant='warning'
|
||||
icon='warning'
|
||||
title={t('Teams_removing_member')}
|
||||
cancelText={t('Cancel')}
|
||||
confirmText={t('Continue')}
|
||||
onClose={onClose}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
{...props}
|
||||
>
|
||||
<Box marginBlockEnd={24} fontScale='p2'>
|
||||
{t('Select_the_channels_you_want_the_user_to_be_removed_from')}
|
||||
</Box>
|
||||
<ChannelDesertionTable
|
||||
lastOwnerWarning={t('Teams_channels_last_owner_leave_channel_warning')}
|
||||
onToggleAllRooms={onToggleAllRooms}
|
||||
rooms={rooms}
|
||||
onChangeRoomSelection={onChangeRoomSelection}
|
||||
selectedRooms={selectedRooms}
|
||||
eligibleRoomsLength={eligibleRoomsLength}
|
||||
/>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveUsersFirstStep;
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Serialized, 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 BaseRemoveUsersModal from './BaseRemoveUsersModal';
|
||||
import { teamsQueryKeys, usersQueryKeys } from '../../../../../lib/queryKeys';
|
||||
|
||||
export type RemoveUsersModalProps = {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: (deletedRooms: { [key: string]: Serialized<IRoom> }) => void;
|
||||
teamId: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const RemoveUsersModal = ({ teamId, userId, onClose, onCancel, onConfirm }: RemoveUsersModalProps) => {
|
||||
const listRoomsOfUser = useEndpoint('GET', '/v1/teams.listRoomsOfUser');
|
||||
const { isPending, data } = useQuery({
|
||||
queryKey: teamsQueryKeys.roomsOfUser(teamId, userId),
|
||||
queryFn: () => listRoomsOfUser({ teamId, userId }),
|
||||
});
|
||||
|
||||
const getUserInfo = useEndpoint('GET', '/v1/users.info');
|
||||
const { data: userData } = useQuery({
|
||||
queryKey: usersQueryKeys.userInfo({ uid: userId }),
|
||||
queryFn: () => getUserInfo({ userId }),
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return <GenericModalSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseRemoveUsersModal
|
||||
onClose={onClose}
|
||||
username={userData?.user.username ?? ''}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
rooms={data?.rooms ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveUsersModal;
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Serialized, IRoom } from '@rocket.chat/core-typings';
|
||||
import { Icon } from '@rocket.chat/fuselage';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type RemoveUsersSecondStepProps = {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
onConfirm: (deletedRooms: { [key: string]: Serialized<IRoom> }) => void;
|
||||
deletedRooms: {
|
||||
[key: string]: Serialized<IRoom>;
|
||||
};
|
||||
rooms?: { _id: string; t: string; name?: string; fname?: string; isLastOwner?: boolean }[];
|
||||
username?: string;
|
||||
};
|
||||
|
||||
const RemoveUsersSecondStep = ({
|
||||
onClose,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
deletedRooms = {},
|
||||
username,
|
||||
rooms = [],
|
||||
...props
|
||||
}: RemoveUsersSecondStepProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
variant='danger'
|
||||
icon={<Icon name='modal-warning' size='x24' color='status-font-on-warning' />}
|
||||
cancelText={rooms?.length > 0 ? t('Back') : t('Cancel')}
|
||||
confirmText={t('Remove')}
|
||||
title={t('Confirmation')}
|
||||
onClose={onClose}
|
||||
onCancel={onCancel}
|
||||
onConfirm={() => onConfirm(deletedRooms)}
|
||||
{...props}
|
||||
>
|
||||
{t('Teams_removing__username__from_team', { username })}
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveUsersSecondStep;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './RemoveUsersModal';
|
||||
Reference in New Issue
Block a user