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,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';
|
||||
Reference in New Issue
Block a user