chore: Remove old deprecated `GenericTable` in favor of v2 (#29594)

pull/29556/head^2
Douglas Fabris 3 years ago committed by GitHub
parent 0c94802afd
commit c197624093
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 87
      apps/meteor/client/components/GenericTable/GenericTable.stories.tsx
  2. 132
      apps/meteor/client/components/GenericTable/GenericTable.tsx
  3. 5
      apps/meteor/client/components/GenericTable/GenericTableBody.tsx
  4. 5
      apps/meteor/client/components/GenericTable/GenericTableCell.tsx
  5. 11
      apps/meteor/client/components/GenericTable/GenericTableHeader.tsx
  6. 8
      apps/meteor/client/components/GenericTable/GenericTableHeaderCell.tsx
  7. 20
      apps/meteor/client/components/GenericTable/GenericTableLoadingRow.tsx
  8. 0
      apps/meteor/client/components/GenericTable/GenericTableLoadingTable.tsx
  9. 5
      apps/meteor/client/components/GenericTable/GenericTableRow.tsx
  10. 27
      apps/meteor/client/components/GenericTable/HeaderCell.tsx
  11. 23
      apps/meteor/client/components/GenericTable/V2/GenericTable.tsx
  12. 5
      apps/meteor/client/components/GenericTable/V2/GenericTableBody.tsx
  13. 5
      apps/meteor/client/components/GenericTable/V2/GenericTableCell.tsx
  14. 11
      apps/meteor/client/components/GenericTable/V2/GenericTableHeader.tsx
  15. 5
      apps/meteor/client/components/GenericTable/V2/GenericTableRow.tsx
  16. 23
      apps/meteor/client/components/GenericTable/index.ts
  17. 3
      apps/meteor/client/views/admin/customSounds/CustomSoundsTable/CustomSoundRow.tsx
  18. 6
      apps/meteor/client/views/admin/permissions/PermissionsTable/RoleHeader.tsx
  19. 187
      apps/meteor/client/views/admin/settings/groups/voip/VoipExtensionsPage.tsx
  20. 5
      apps/meteor/client/views/omnichannel/businessHours/BusinessHoursPage.js
  21. 2
      apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx
  22. 219
      apps/meteor/client/views/omnichannel/directory/calls/CallTable.tsx
  23. 4
      apps/meteor/client/views/omnichannel/directory/calls/CallTableRow.tsx
  24. 241
      apps/meteor/client/views/omnichannel/directory/chats/ChatTable.tsx
  25. 6
      apps/meteor/client/views/omnichannel/directory/contacts/ContactTab.tsx
  26. 158
      apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx
  27. 55
      apps/meteor/client/views/omnichannel/queueList/QueueListPage.tsx
  28. 145
      apps/meteor/client/views/omnichannel/queueList/QueueListTable.tsx
  29. 1
      apps/meteor/client/views/omnichannel/queueList/index.ts
  30. 114
      apps/meteor/client/views/omnichannel/queueList/index.tsx
  31. 81
      apps/meteor/client/views/teams/contextualBar/info/Delete/ChannelDeletionTable.js
  32. 78
      apps/meteor/client/views/teams/contextualBar/info/Delete/ChannelDeletionTable.tsx
  33. 35
      apps/meteor/client/views/teams/contextualBar/info/Delete/ChannelDeletionTableRow.tsx
  34. 29
      apps/meteor/client/views/teams/contextualBar/info/Delete/ChannelRow.js
  35. 18
      apps/meteor/ee/client/omnichannel/BusinessHoursRow.js
  36. 103
      apps/meteor/ee/client/omnichannel/BusinessHoursTable.js
  37. 2
      apps/meteor/ee/client/omnichannel/BusinessHoursTable.stories.tsx
  38. 43
      apps/meteor/ee/client/omnichannel/BusinessHoursTableContainer.js
  39. 75
      apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesPage.tsx
  40. 219
      apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesRoute.tsx
  41. 203
      apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesTable.tsx
  42. 32
      apps/meteor/ee/client/omnichannel/cannedResponses/RemoveCannedResponseButton.tsx
  43. 4
      apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx
  44. 72
      apps/meteor/ee/client/omnichannel/priorities/PrioritiesTable.tsx

@ -1,8 +1,17 @@
import { TextInput, Box, Icon, Table } from '@rocket.chat/fuselage';
import { TextInput, Box, Icon } from '@rocket.chat/fuselage';
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';
import GenericTable from '.';
import {
GenericTable,
GenericTableHeaderCell,
GenericTableCell,
GenericTableRow,
GenericTableHeader,
GenericTableBody,
GenericTableLoadingTable,
} from '.';
import GenericNoResults from '../GenericNoResults/GenericNoResults';
export default {
title: 'Components/GenericTable',
@ -16,33 +25,59 @@ export default {
],
} as ComponentMeta<typeof GenericTable>;
const header = [
<GenericTable.HeaderCell key='name'>Name</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key='email'>Email</GenericTable.HeaderCell>,
];
const headers = (
<>
<GenericTableHeaderCell key='name'>Name</GenericTableHeaderCell>
<GenericTableHeaderCell key='email'>Email</GenericTableHeaderCell>
</>
);
const results = Array.from({ length: 10 }, (_, i) => ({
_id: i,
name: `John Doe #${i}`,
email: `john.doe.n${i}@example.com`,
}));
const filter = (
<>
<Box mb='x16' is='form' display='flex' flexDirection='column'>
<TextInput flexShrink={0} placeholder='Search...' addon={<Icon name='magnifier' size='x20' />} />
</Box>
</>
);
const renderFilter = () => (
<Box mb='x16' is='form' display='flex' flexDirection='column'>
<TextInput flexShrink={0} placeholder='Search...' addon={<Icon name='magnifier' size='x20' />} />
</Box>
export const Default: ComponentStory<typeof GenericTable> = () => (
<>
{filter}
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{results?.map(({ _id, name, email }: any) => (
<GenericTableRow key={_id}>
<GenericTableCell>{name}</GenericTableCell>
<GenericTableCell>{email}</GenericTableCell>
</GenericTableRow>
))}
</GenericTableBody>
</GenericTable>
</>
);
const renderRow = ({ _id, name, email }: any) => (
<Table.Row key={_id}>
<Table.Cell>{name}</Table.Cell>
<Table.Cell>{email}</Table.Cell>
</Table.Row>
export const Loading: ComponentStory<typeof GenericTable> = () => (
<>
{filter}
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingTable headerCells={2} />
</GenericTableBody>
</GenericTable>
</>
);
export const Default: ComponentStory<typeof GenericTable> = (args) => (
<GenericTable {...args} header={header} renderFilter={renderFilter} renderRow={renderRow} />
export const NoResults: ComponentStory<typeof GenericTable> = () => (
<>
{filter}
<GenericNoResults />
</>
);
Default.storyName = 'GenericTable';
Default.args = {
results: Array.from({ length: 10 }, (_, i) => ({
_id: i,
name: `John Doe #${i}`,
email: `john.doe.n${i}@example.com`,
})),
total: 1,
};

@ -1,121 +1,23 @@
import { Pagination, Tile } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactNode, ReactElement, Key, Ref, RefAttributes } from 'react';
import React, { useState, useEffect, forwardRef, useMemo } from 'react';
import flattenChildren from 'react-keyed-flatten-children';
import { Box, Table } from '@rocket.chat/fuselage';
import type { ReactNode, TableHTMLAttributes } from 'react';
import React, { forwardRef } from 'react';
import { GenericTable as GenericTableV2 } from './V2/GenericTable';
import { GenericTableBody } from './V2/GenericTableBody';
import { GenericTableHeader } from './V2/GenericTableHeader';
import { GenericTableLoadingTable } from './V2/GenericTableLoadingTable';
import { usePagination } from './hooks/usePagination';
import ScrollableContentWrapper from '../ScrollableContentWrapper';
const defaultParamsValue = { text: '', current: 0, itemsPerPage: 25 } as const;
const defaultSetParamsValue = (): void => undefined;
export type GenericTableParams = {
text?: string;
current: number;
itemsPerPage: 25 | 50 | 100;
};
type GenericTableProps<FilterProps extends { onChange?: (params: GenericTableParams) => void }, ResultProps> = {
type GenericTableProps = {
fixed?: boolean;
header?: ReactNode;
params?: GenericTableParams;
setParams?: React.Dispatch<React.SetStateAction<GenericTableParams>>;
children?: (props: ResultProps, key: number) => ReactElement;
renderFilter?: (props: FilterProps) => ReactElement;
renderRow?: (props: ResultProps) => ReactElement;
results?: ResultProps[];
total?: number;
pagination?: boolean;
} & FilterProps;
const GenericTable = forwardRef(function GenericTable<
FilterProps extends { onChange?: (params: GenericTableParams) => void },
ResultProps extends { _id?: Key } | object,
>(
{
children,
fixed = true,
header,
params: paramsDefault = defaultParamsValue,
setParams = defaultSetParamsValue,
renderFilter,
renderRow: RenderRowComponent,
results,
total,
pagination = true,
...props
}: GenericTableProps<FilterProps, ResultProps>,
ref: Ref<HTMLElement>,
) {
const t = useTranslation();
const [filter, setFilter] = useState(paramsDefault);
const { itemsPerPage, setItemsPerPage, current, setCurrent, itemsPerPageLabel, showingResultsLabel } = usePagination();
const params = useDebouncedValue(filter, 500);
useEffect(() => {
setParams((prevParams) => {
setCurrent(prevParams.text === params.text ? current : 0);
return {
...params,
text: params.text || '',
current: prevParams.text === params.text ? current : 0,
itemsPerPage,
};
});
}, [params, current, itemsPerPage, setParams, setCurrent, setItemsPerPage]);
const headerCells = useMemo(() => flattenChildren(header).length, [header]);
const isLoading = !results;
children: ReactNode;
} & TableHTMLAttributes<HTMLTableElement>;
export const GenericTable = forwardRef<HTMLElement, GenericTableProps>(function GenericTable({ fixed = true, children, ...props }, ref) {
return (
<>
{typeof renderFilter === 'function'
? renderFilter({ ...props, onChange: setFilter } as any) // TODO: ugh
: null}
{results && !results.length ? (
<Tile fontScale='p2' elevation='0' color='hint' textAlign='center'>
{t('No_data_found')}
</Tile>
) : (
<>
<GenericTableV2 fixed={fixed} ref={ref}>
{header && <GenericTableHeader>{header}</GenericTableHeader>}
<GenericTableBody>
{isLoading && <GenericTableLoadingTable headerCells={headerCells} />}
{!isLoading &&
((RenderRowComponent &&
results?.map((props, index) => <RenderRowComponent key={'_id' in props ? props._id : index} {...props} />)) ||
(children && results?.map(children)))}
</GenericTableBody>
</GenericTableV2>
{pagination && (
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
itemsPerPageLabel={itemsPerPageLabel}
showingResultsLabel={showingResultsLabel}
count={total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/>
)}
</>
)}
</>
<Box mi='neg-x24' pi='x24' flexShrink={1} flexGrow={1} ref={ref} overflow='hidden'>
<ScrollableContentWrapper overflowX>
{/* TODO: Fix fuselage */}
<Table fixed={fixed} sticky {...(props as any)}>
{children}
</Table>
</ScrollableContentWrapper>
</Box>
);
}) as <TFilterProps extends { onChange?: (params: GenericTableParams) => void }, TResultProps extends { _id?: Key } | object>(
props: GenericTableProps<TFilterProps, TResultProps> & RefAttributes<HTMLElement>,
) => ReactElement | null;
export default GenericTable;
});

@ -0,0 +1,5 @@
import { TableBody } from '@rocket.chat/fuselage';
import type { FC, ComponentProps } from 'react';
import React from 'react';
export const GenericTableBody: FC<ComponentProps<typeof TableBody>> = (props) => <TableBody {...props} />;

@ -0,0 +1,5 @@
import { TableCell } from '@rocket.chat/fuselage';
import type { ComponentProps, FC } from 'react';
import React from 'react';
export const GenericTableCell: FC<ComponentProps<typeof TableCell>> = (props) => <TableCell {...props} />;

@ -0,0 +1,11 @@
import { TableHead } from '@rocket.chat/fuselage';
import type { FC, ComponentProps } from 'react';
import React from 'react';
import { GenericTableRow } from './GenericTableRow';
export const GenericTableHeader: FC<ComponentProps<typeof TableHead>> = ({ children, ...props }) => (
<TableHead {...props}>
<GenericTableRow>{children}</GenericTableRow>
</TableHead>
);

@ -1,8 +1,8 @@
import { Box, Table } from '@rocket.chat/fuselage';
import { Box, TableCell } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactElement } from 'react';
import React, { useCallback } from 'react';
import SortIcon from '../SortIcon';
import SortIcon from './SortIcon';
type GenericTableHeaderCellProps<T extends string> = Omit<ComponentProps<typeof Box>, 'onClick'> & {
active?: boolean;
@ -21,11 +21,11 @@ export const GenericTableHeaderCell = <T extends string = string>({
}: GenericTableHeaderCellProps<T>): ReactElement => {
const fn = useCallback(() => onClick && sort && onClick(sort), [sort, onClick]);
return (
<Table.Cell clickable={!!sort} onClick={fn} {...props}>
<TableCell clickable={!!sort} onClick={fn} {...props}>
<Box display='flex' alignItems='center' wrap='no-wrap'>
{children}
{sort && <SortIcon direction={active ? direction : undefined} />}
</Box>
</Table.Cell>
</TableCell>
);
};

@ -1,14 +1,10 @@
import { Box, Skeleton, Table } from '@rocket.chat/fuselage';
import { Box, Skeleton, TableRow, TableCell } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import React from 'react';
type GenericTableLoadingRowRowProps = {
cols: number;
};
export const GenericTableLoadingRow = ({ cols }: GenericTableLoadingRowRowProps): ReactElement => (
<Table.Row>
<Table.Cell>
export const GenericTableLoadingRow = ({ cols }: { cols: number }): ReactElement => (
<TableRow>
<TableCell>
<Box display='flex'>
<Skeleton variant='rect' height={40} width={40} />
<Box mi='x8' flexGrow={1}>
@ -16,11 +12,11 @@ export const GenericTableLoadingRow = ({ cols }: GenericTableLoadingRowRowProps)
<Skeleton width='100%' />
</Box>
</Box>
</Table.Cell>
</TableCell>
{Array.from({ length: cols - 1 }, (_, i) => (
<Table.Cell key={i}>
<TableCell key={i}>
<Skeleton width='100%' />
</Table.Cell>
</TableCell>
))}
</Table.Row>
</TableRow>
);

@ -0,0 +1,5 @@
import { TableRow } from '@rocket.chat/fuselage';
import type { ComponentProps, FC } from 'react';
import React from 'react';
export const GenericTableRow: FC<ComponentProps<typeof TableRow>> = (props) => <TableRow {...props} />;

@ -1,27 +0,0 @@
import { Box, Table } from '@rocket.chat/fuselage';
import type { ComponentProps, FC } from 'react';
import React, { useCallback } from 'react';
import SortIcon from './SortIcon';
type HeaderCellProps = {
active?: boolean;
direction?: 'asc' | 'desc';
sort?: string;
clickable?: boolean;
onClick?: (sort: string) => void;
} & Omit<ComponentProps<typeof Box>, 'onClick'>;
const HeaderCell: FC<HeaderCellProps> = ({ children, active, direction, sort, onClick, ...props }) => {
const fn = useCallback(() => onClick && sort && onClick(sort), [sort, onClick]);
return (
<Table.Cell clickable={!!sort} onClick={fn} {...props}>
<Box display='flex' alignItems='center' wrap='no-wrap'>
{children}
{sort && <SortIcon direction={active ? direction : undefined} />}
</Box>
</Table.Cell>
);
};
export default HeaderCell;

@ -1,23 +0,0 @@
import { Box, Table } from '@rocket.chat/fuselage';
import type { ReactNode, TableHTMLAttributes } from 'react';
import React, { forwardRef } from 'react';
import ScrollableContentWrapper from '../../ScrollableContentWrapper';
type GenericTableProps = {
fixed?: boolean;
children: ReactNode;
} & TableHTMLAttributes<HTMLTableElement>;
export const GenericTable = forwardRef<HTMLElement, GenericTableProps>(function GenericTable({ fixed = true, children, ...props }, ref) {
return (
<Box mi='neg-x24' pi='x24' flexShrink={1} flexGrow={1} ref={ref} overflow='hidden'>
<ScrollableContentWrapper overflowX>
{/* TODO: Fix fuselage */}
<Table fixed={fixed} sticky {...(props as any)}>
{children}
</Table>
</ScrollableContentWrapper>
</Box>
);
});

@ -1,5 +0,0 @@
import { Table } from '@rocket.chat/fuselage';
import type { FC } from 'react';
import React from 'react';
export const GenericTableBody: FC = (props) => <Table.Body {...props} />;

@ -1,5 +0,0 @@
import { Table } from '@rocket.chat/fuselage';
import type { ComponentProps, FC } from 'react';
import React from 'react';
export const GenericTableCell: FC<ComponentProps<typeof Table.Cell>> = (props) => <Table.Cell {...props} />;

@ -1,11 +0,0 @@
import { Table } from '@rocket.chat/fuselage';
import type { FC } from 'react';
import React from 'react';
import { GenericTableRow } from './GenericTableRow';
export const GenericTableHeader: FC = ({ children, ...props }) => (
<Table.Head {...props}>
<GenericTableRow>{children}</GenericTableRow>
</Table.Head>
);

@ -1,5 +0,0 @@
import { Table } from '@rocket.chat/fuselage';
import type { ComponentProps, FC } from 'react';
import React from 'react';
export const GenericTableRow: FC<ComponentProps<typeof Table.Row>> = (props) => <Table.Row {...props} />;

@ -1,15 +1,8 @@
import GenericTable from './GenericTable';
import HeaderCell from './HeaderCell';
export default Object.assign(GenericTable, {
HeaderCell,
});
export * from './V2/GenericTable';
export * from './V2/GenericTableBody';
export * from './V2/GenericTableCell';
export * from './V2/GenericTableHeader';
export * from './V2/GenericTableHeaderCell';
export * from './V2/GenericTableLoadingRow';
export * from './V2/GenericTableLoadingTable';
export * from './V2/GenericTableRow';
export * from './GenericTable';
export * from './GenericTableBody';
export * from './GenericTableCell';
export * from './GenericTableHeader';
export * from './GenericTableHeaderCell';
export * from './GenericTableLoadingRow';
export * from './GenericTableLoadingTable';
export * from './GenericTableRow';

@ -3,8 +3,7 @@ import { useCustomSound, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useCallback, useState } from 'react';
import { GenericTableCell } from '../../../../components/GenericTable/V2/GenericTableCell';
import { GenericTableRow } from '../../../../components/GenericTable/V2/GenericTableRow';
import { GenericTableCell, GenericTableRow } from '../../../../components/GenericTable';
type CustomSoundRowProps = {
onClick: (soundId: string) => () => void;

@ -5,7 +5,7 @@ import { useRoute } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { memo } from 'react';
import GenericTable from '../../../../components/GenericTable';
import { GenericTableHeaderCell } from '../../../../components/GenericTable';
type RoleHeaderProps = {
_id: IRole['_id'];
@ -24,14 +24,14 @@ const RoleHeader = ({ _id, name, description }: RoleHeaderProps): ReactElement =
});
return (
<GenericTable.HeaderCell clickable pi='x4' p='x8'>
<GenericTableHeaderCell pi='x4' p='x8'>
<Button secondary onClick={handleEditRole}>
<Margins inline='x2'>
<span>{description || name}</span>
<Icon name='edit' size='x16' />
</Margins>
</Button>
</GenericTable.HeaderCell>
</GenericTableHeaderCell>
);
};

@ -1,27 +1,31 @@
import { Box, Chip, Table, Button } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts';
import type { FC } from 'react';
import React, { useMemo, useCallback, useState } from 'react';
import { Box, Chip, Button, Pagination } from '@rocket.chat/fuselage';
import { useSetModal, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React, { useMemo } from 'react';
import GenericTable from '../../../../../components/GenericTable';
import GenericNoResults from '../../../../../components/GenericNoResults';
import {
GenericTable,
GenericTableHeaderCell,
GenericTableRow,
GenericTableCell,
GenericTableHeader,
GenericTableBody,
GenericTableLoadingRow,
} from '../../../../../components/GenericTable';
import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination';
import Page from '../../../../../components/Page';
import UserAvatar from '../../../../../components/avatar/UserAvatar';
import { useEndpointData } from '../../../../../hooks/useEndpointData';
import AssignAgentButton from './AssignAgentButton';
import AssignAgentModal from './AssignAgentModal';
import RemoveAgentButton from './RemoveAgentButton';
const VoipExtensionsPage: FC = () => {
const VoipExtensionsPage = () => {
const t = useTranslation();
const setModal = useSetModal();
const [params, setParams] = useState<{ current: number; itemsPerPage: 25 | 50 | 100 }>({
current: 0,
itemsPerPage: 25,
});
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const { itemsPerPage, current } = useDebouncedValue(params, 500);
const query = useMemo(
() => ({
...(itemsPerPage && { count: itemsPerPage }),
@ -30,66 +34,27 @@ const VoipExtensionsPage: FC = () => {
[itemsPerPage, current],
);
const { value: data, reload } = useEndpointData('/v1/omnichannel/extensions', { params: query });
const header = useMemo(
() =>
[
<GenericTable.HeaderCell key='extension-number' w='x156'>
{t('Extension_Number')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key='agent-name' w='x160'>
{t('Agent_Name')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key='extension-status' w='x120'>
{t('Extension_Status')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key='queues' w='x120'>
{t('Queues')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key='remove-add' w='x60' />,
].filter(Boolean),
[t],
);
const getExtensions = useEndpoint('GET', '/v1/omnichannel/extensions');
const { data, isSuccess, isLoading, refetch } = useQuery(['omnichannel-extensions', query], async () => getExtensions(query), {
refetchOnWindowFocus: false,
});
const renderRow = useCallback(
({ _id, extension, username, name, state, queues }) => (
<Table.Row key={_id} tabIndex={0}>
<Table.Cell withTruncatedText>{extension}</Table.Cell>
<Table.Cell withTruncatedText>
{username ? (
<Box display='flex' alignItems='center'>
<UserAvatar size={'x28'} username={username} />
<Box display='flex' mi='x8'>
<Box display='flex' flexDirection='column' alignSelf='center'>
<Box fontScale='p2m' color='default'>
{name || username}
</Box>
</Box>
</Box>
</Box>
) : (
t('Free')
)}
</Table.Cell>
<Table.Cell withTruncatedText>{state}</Table.Cell>
<Table.Cell withTruncatedText maxHeight='x36'>
<Box display='flex' flexDirection='row' alignItems='center' title={queues?.join(', ')}>
{queues?.map(
(queue: string, index: number) =>
index <= 1 && (
<Chip mie='x4' key={queue} value={queue}>
{queue}
</Chip>
),
)}
{queues?.length > 2 && `+${(queues.length - 2).toString()}`}
</Box>
</Table.Cell>
{username ? <RemoveAgentButton username={username} reload={reload} /> : <AssignAgentButton extension={extension} reload={reload} />}
</Table.Row>
),
[reload, t],
const headers = (
<>
<GenericTableHeaderCell key='extension-number' w='x156'>
{t('Extension_Number')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='agent-name' w='x160'>
{t('Agent_Name')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='extension-status' w='x120'>
{t('Extension_Status')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='queues' w='x120'>
{t('Queues')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='remove-add' w='x60' />
</>
);
return (
@ -99,20 +64,78 @@ const VoipExtensionsPage: FC = () => {
{data?.total} {t('Extensions')}
</Box>
<Box>
<Button primary onClick={(): void => setModal(<AssignAgentModal closeModal={(): void => setModal()} reload={reload} />)}>
<Button primary onClick={(): void => setModal(<AssignAgentModal closeModal={(): void => setModal()} reload={refetch} />)}>
{t('Associate_Agent')}
</Button>
</Box>
</Box>
<GenericTable
header={header}
renderRow={renderRow}
results={data?.extensions.map((extension) => ({ _id: extension.extension, ...extension }))}
total={data?.total}
params={params}
setParams={setParams}
// renderFilter={({ onChange, ...props }) => <FilterByText onChange={onChange} {...props} />}
/>
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingRow cols={4} />
</GenericTableBody>
</GenericTable>
)}
{isSuccess && data?.extensions.length === 0 && <GenericNoResults />}
{isSuccess && data?.extensions.length > 0 && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data?.extensions.map(({ extension, username, name, state, queues }) => (
<GenericTableRow key={extension} tabIndex={0}>
<GenericTableCell withTruncatedText>{extension}</GenericTableCell>
<GenericTableCell withTruncatedText>
{username ? (
<Box display='flex' alignItems='center'>
<UserAvatar size={'x28'} username={username} />
<Box display='flex' mi='x8'>
<Box display='flex' flexDirection='column' alignSelf='center'>
<Box fontScale='p2m' color='default'>
{name || username}
</Box>
</Box>
</Box>
</Box>
) : (
t('Free')
)}
</GenericTableCell>
<GenericTableCell withTruncatedText>{state}</GenericTableCell>
<GenericTableCell withTruncatedText maxHeight='x36'>
<Box display='flex' flexDirection='row' alignItems='center' title={queues?.join(', ')}>
{queues?.map(
(queue: string, index: number) =>
index <= 1 && (
<Chip mie='x4' key={queue} value={queue}>
{queue}
</Chip>
),
)}
{queues && queues?.length > 2 && `+${(queues.length - 2).toString()}`}
</Box>
</GenericTableCell>
{username ? (
<RemoveAgentButton username={username} reload={refetch} />
) : (
<AssignAgentButton extension={extension} reload={refetch} />
)}
</GenericTableRow>
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
)}
</Page.Content>
);
};

@ -7,10 +7,9 @@ import Page from '../../../components/Page';
const BusinessHoursPage = () => {
const t = useTranslation();
const router = useRoute('omnichannel-businessHours');
const Table = useMemo(() => lazy(() => import('../../../../ee/client/omnichannel/BusinessHoursTableContainer')), []);
const BusinessHoursTable = useMemo(() => lazy(() => import('../../../../ee/client/omnichannel/BusinessHoursTable')), []);
const handleNew = useMutableCallback(() => {
router.push({
@ -28,7 +27,7 @@ const BusinessHoursPage = () => {
</ButtonGroup>
</Page.Header>
<Page.Content>
<Table />
<BusinessHoursTable />
</Page.Content>
</Page>
);

@ -10,6 +10,7 @@ import { useOmnichannelPriorities } from '../../../../ee/client/omnichannel/hook
import { PriorityIcon } from '../../../../ee/client/omnichannel/priorities/PriorityIcon';
import GenericNoResults from '../../../components/GenericNoResults';
import {
GenericTable,
GenericTableBody,
GenericTableCell,
GenericTableHeader,
@ -17,7 +18,6 @@ import {
GenericTableLoadingTable,
GenericTableRow,
} from '../../../components/GenericTable';
import { GenericTable } from '../../../components/GenericTable/V2/GenericTable';
import { usePagination } from '../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../components/GenericTable/hooks/useSort';
import Page from '../../../components/Page';

@ -1,69 +1,43 @@
import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import type { FC } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { Pagination } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useTranslation, useEndpoint, useUserId } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React, { useState, useMemo } from 'react';
import GenericTable from '../../../../components/GenericTable';
import { useEndpointData } from '../../../../hooks/useEndpointData';
import FilterByText from '../../../../components/FilterByText';
import GenericNoResults from '../../../../components/GenericNoResults/GenericNoResults';
import {
GenericTable,
GenericTableBody,
GenericTableHeader,
GenericTableHeaderCell,
GenericTableLoadingRow,
} from '../../../../components/GenericTable';
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../../components/GenericTable/hooks/useSort';
import { CallTableRow } from './CallTableRow';
const useQuery = (
{
text,
itemsPerPage,
current,
}: {
text?: string;
itemsPerPage: 25 | 50 | 100;
current: number;
},
[column, direction]: string[],
userIdLoggedIn: string | null,
): {
sort: string;
open: 'false';
roomName: string;
agents: string[];
count?: number;
current?: number;
} =>
useMemo(
const CallTable = () => {
const t = useTranslation();
const userIdLoggedIn = useUserId();
const [text, setText] = useState('');
const directoryRoute = useRoute('omnichannel-directory');
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const { sortBy, sortDirection, setSort } = useSort<'fname' | 'phone' | 'queue' | 'ts' | 'callDuration' | 'direction'>('fname');
const query = useMemo(
() => ({
sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }),
open: 'false',
sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`,
open: 'false' as const,
roomName: text || '',
agents: userIdLoggedIn ? [userIdLoggedIn] : [],
...(itemsPerPage && { count: itemsPerPage }),
...(current && { offset: current }),
}),
[column, current, direction, itemsPerPage, userIdLoggedIn, text],
[sortBy, current, sortDirection, itemsPerPage, userIdLoggedIn, text],
);
const CallTable: FC = () => {
const [params, setParams] = useState<{ text?: string; current: number; itemsPerPage: 25 | 50 | 100 }>({
text: '',
current: 0,
itemsPerPage: 25,
});
const [sort, setSort] = useState<[string, 'asc' | 'desc']>(['closedAt', 'desc']);
const t = useTranslation();
const debouncedParams = useDebouncedValue(params, 500);
const debouncedSort = useDebouncedValue(sort, 500);
const userIdLoggedIn = Meteor.userId();
const query = useQuery(debouncedParams, debouncedSort, userIdLoggedIn);
const directoryRoute = useRoute('omnichannel-directory');
const onHeaderClick = useMutableCallback((id) => {
const [sortBy, sortDirection] = sort;
if (sortBy === id) {
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
return;
}
setSort([id, 'asc']);
});
const onRowClick = useMutableCallback((id, token) => {
directoryRoute.push(
{
@ -75,74 +49,81 @@ const CallTable: FC = () => {
);
});
const { value: data } = useEndpointData('/v1/voip/rooms', { params: query });
const getVoipRooms = useEndpoint('GET', '/v1/voip/rooms');
const { data, isSuccess, isLoading } = useQuery(['voip-rooms', query], async () => getVoipRooms(query));
const header = useMemo(
() =>
[
<GenericTable.HeaderCell
key={'fname'}
direction={sort[1]}
active={sort[0] === 'fname'}
onClick={onHeaderClick}
sort='fname'
w='x400'
>
{t('Contact_Name')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key={'phone'}
direction={sort[1]}
active={sort[0] === 'phone'}
onClick={onHeaderClick}
sort='phone'
w='x200'
>
{t('Phone')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'queue'} direction={sort[1]} active={sort[0] === 'queue'} onClick={onHeaderClick} sort='ts' w='x100'>
{t('Queue')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'ts'} direction={sort[1]} active={sort[0] === 'ts'} onClick={onHeaderClick} sort='ts' w='x200'>
{t('Started_At')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key={'callDuration'}
direction={sort[1]}
active={sort[0] === 'callDuration'}
onClick={onHeaderClick}
sort='callDuration'
w='x120'
>
{t('Talk_Time')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key='direction'
direction={sort[1]}
active={sort[0] === 'direction'}
onClick={onHeaderClick}
sort='direction'
w='x200'
>
{t('Direction')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key='call' width={44} />,
].filter(Boolean),
[sort, onHeaderClick, t],
const headers = (
<>
<GenericTableHeaderCell key='fname' direction={sortDirection} active={sortBy === 'fname'} onClick={setSort} sort='fname' w='x400'>
{t('Contact_Name')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='phone' direction={sortDirection} active={sortBy === 'phone'} onClick={setSort} sort='phone' w='x200'>
{t('Phone')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key={'queue'} direction={sortDirection} active={sortBy === 'queue'} onClick={setSort} sort='ts' w='x100'>
{t('Queue')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key={'ts'} direction={sortDirection} active={sortBy === 'ts'} onClick={setSort} sort='ts' w='x200'>
{t('Started_At')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key={'callDuration'}
direction={sortDirection}
active={sortBy === 'callDuration'}
onClick={setSort}
sort='callDuration'
w='x120'
>
{t('Talk_Time')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key='direction'
direction={sortDirection}
active={sortBy === 'direction'}
onClick={setSort}
sort='direction'
w='x200'
>
{t('Direction')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='call' width={44} />
</>
);
const renderRow = useCallback((room) => <CallTableRow room={room} onRowClick={onRowClick} />, [onRowClick]);
return (
<GenericTable
header={header}
renderRow={renderRow}
results={data?.rooms}
total={data?.total}
setParams={setParams}
params={params}
// renderFilter={({ onChange, ...props }: any): ReactElement => <FilterByText onChange={onChange} {...props} />}
/>
<>
<FilterByText onChange={({ text }) => setText(text)} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingRow cols={7} />
</GenericTableBody>
</GenericTable>
)}
{isSuccess && data?.rooms.length === 0 && <GenericNoResults />}
{isSuccess && data?.rooms.length > 0 && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data?.rooms.map((room) => (
<CallTableRow key={room._id} room={room} onRowClick={onRowClick} />
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
)}
</>
);
};

@ -1,4 +1,4 @@
import type { IVoipRoom } from '@rocket.chat/core-typings';
import type { IVoipRoom, Serialized } from '@rocket.chat/core-typings';
import { Table } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import moment from 'moment';
@ -10,7 +10,7 @@ import { useIsCallReady } from '../../../../contexts/CallContext';
import { CallDialpadButton } from '../components/CallDialpadButton';
type CallTableRowProps = {
room: IVoipRoom;
room: Serialized<IVoipRoom>;
onRowClick(_id: string, token?: string): void;
};

@ -1,71 +1,45 @@
import { Table, Tag, Box } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import { Tag, Box, Pagination, States, StatesIcon, StatesTitle, StatesActions, StatesAction } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useTranslation, useUserId } from '@rocket.chat/ui-contexts';
import moment from 'moment';
import type { FC, ReactElement } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import FilterByText from '../../../../components/FilterByText';
import GenericTable, { GenericTableLoadingTable } from '../../../../components/GenericTable';
import GenericNoResults from '../../../../components/GenericNoResults/GenericNoResults';
import {
GenericTable,
GenericTableBody,
GenericTableCell,
GenericTableHeader,
GenericTableHeaderCell,
GenericTableLoadingTable,
GenericTableRow,
} from '../../../../components/GenericTable';
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../../components/GenericTable/hooks/useSort';
import { useCurrentChats } from '../../currentChats/hooks/useCurrentChats';
const useQuery = (
{
text,
itemsPerPage,
current,
}: {
text?: string;
itemsPerPage: 25 | 50 | 100;
current: number;
},
[column, direction]: string[],
userIdLoggedIn: string | null,
): {
sort: string;
open: boolean;
roomName: string;
agents: string[];
count?: number;
current?: number;
} =>
useMemo(
const ChatTable = () => {
const t = useTranslation();
const [text, setText] = useState('');
const userIdLoggedIn = useUserId();
const directoryRoute = useRoute('omnichannel-directory');
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const { sortBy, sortDirection, setSort } = useSort<'fname' | 'department' | 'ts' | 'chatDuration' | 'closedAt'>('fname');
const query = useMemo(
() => ({
sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }),
sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`,
open: false,
roomName: text || '',
agents: userIdLoggedIn ? [userIdLoggedIn] : [],
...(itemsPerPage && { count: itemsPerPage }),
...(current && { offset: current }),
}),
[column, current, direction, itemsPerPage, userIdLoggedIn, text],
[sortBy, current, sortDirection, itemsPerPage, userIdLoggedIn, text],
);
const ChatTable: FC = () => {
const [params, setParams] = useState<{ text?: string; current: number; itemsPerPage: 25 | 50 | 100 }>({
text: '',
current: 0,
itemsPerPage: 25,
});
const [sort, setSort] = useState<[string, 'asc' | 'desc']>(['closedAt', 'desc']);
const t = useTranslation();
const debouncedParams = useDebouncedValue(params, 500);
const debouncedSort = useDebouncedValue(sort, 500);
const userIdLoggedIn = Meteor.userId();
const query = useQuery(debouncedParams, debouncedSort, userIdLoggedIn);
const directoryRoute = useRoute('omnichannel-directory');
const onHeaderClick = useMutableCallback((id) => {
const [sortBy, sortDirection] = sort;
if (sortBy === id) {
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
return;
}
setSort([id, 'asc']);
});
const onRowClick = useMutableCallback((id) =>
directoryRoute.push({
page: 'chats',
@ -74,60 +48,53 @@ const ChatTable: FC = () => {
}),
);
const header = useMemo(
() =>
[
<GenericTable.HeaderCell
key={'fname'}
direction={sort[1]}
active={sort[0] === 'fname'}
onClick={onHeaderClick}
sort='fname'
w='x400'
>
{t('Contact_Name')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key={'department'}
direction={sort[1]}
active={sort[0] === 'department'}
onClick={onHeaderClick}
sort='department'
w='x200'
>
{t('Department')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'ts'} direction={sort[1]} active={sort[0] === 'ts'} onClick={onHeaderClick} sort='ts' w='x200'>
{t('Started_At')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key={'chatDuration'}
direction={sort[1]}
active={sort[0] === 'chatDuration'}
onClick={onHeaderClick}
sort='chatDuration'
w='x120'
>
{t('Chat_Duration')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key={'closedAt'}
direction={sort[1]}
active={sort[0] === 'closedAt'}
onClick={onHeaderClick}
sort='closedAt'
w='x200'
>
{t('Closed_At')}
</GenericTable.HeaderCell>,
].filter(Boolean),
[sort, onHeaderClick, t],
const headers = (
<>
<GenericTableHeaderCell key='fname' direction={sortDirection} active={sortBy === 'fname'} onClick={setSort} sort='fname' w='x400'>
{t('Contact_Name')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key='department'
direction={sortDirection}
active={sortBy === 'department'}
onClick={setSort}
sort='department'
w='x200'
>
{t('Department')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='ts' direction={sortDirection} active={sortBy === 'ts'} onClick={setSort} sort='ts' w='x200'>
{t('Started_At')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key='chatDuration'
direction={sortDirection}
active={sortBy === 'chatDuration'}
onClick={setSort}
sort='chatDuration'
w='x120'
>
{t('Chat_Duration')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key='closedAt'
direction={sortDirection}
active={sortBy === 'closedAt'}
onClick={setSort}
sort='closedAt'
w='x200'
>
{t('Closed_At')}
</GenericTableHeaderCell>
</>
);
const { data, isLoading, isSuccess, isError, refetch } = useCurrentChats(query);
const renderRow = useCallback(
({ _id, fname, ts, closedAt, department, tags }) => (
<Table.Row key={_id} tabIndex={0} role='link' onClick={(): void => onRowClick(_id)} action qa-user-id={_id}>
<Table.Cell withTruncatedText>
<GenericTableRow key={_id} tabIndex={0} role='link' onClick={(): void => onRowClick(_id)} action qa-user-id={_id}>
<GenericTableCell withTruncatedText>
<Box display='flex' flexDirection='column'>
<Box withTruncatedText>{fname}</Box>
{tags && (
@ -151,35 +118,55 @@ const ChatTable: FC = () => {
</Box>
)}
</Box>
</Table.Cell>
<Table.Cell withTruncatedText>{department ? department.name : ''}</Table.Cell>
<Table.Cell withTruncatedText>{moment(ts).format('L LTS')}</Table.Cell>
<Table.Cell withTruncatedText>{moment(closedAt).from(moment(ts), true)}</Table.Cell>
<Table.Cell withTruncatedText>{moment(closedAt).format('L LTS')}</Table.Cell>
</Table.Row>
</GenericTableCell>
<GenericTableCell withTruncatedText>{department ? department.name : ''}</GenericTableCell>
<GenericTableCell withTruncatedText>{moment(ts).format('L LTS')}</GenericTableCell>
<GenericTableCell withTruncatedText>{moment(closedAt).from(moment(ts), true)}</GenericTableCell>
<GenericTableCell withTruncatedText>{moment(closedAt).format('L LTS')}</GenericTableCell>
</GenericTableRow>
),
[onRowClick],
);
const result = useCurrentChats(query);
if (result.isLoading) {
return <GenericTableLoadingTable headerCells={6} />;
}
if (result.error) {
return <Box mbs='x16'>{t('Something_went_wrong')}</Box>;
}
return (
<GenericTable
header={header}
renderRow={renderRow}
results={result.data?.rooms}
total={result.data?.total}
setParams={setParams}
params={params}
renderFilter={({ onChange, ...props }: any): ReactElement => <FilterByText onChange={onChange} {...props} />}
/>
<>
<FilterByText onChange={({ text }) => setText(text)} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingTable headerCells={5} />
</GenericTableBody>
</GenericTable>
)}
{isSuccess && data?.rooms.length === 0 && <GenericNoResults />}
{isSuccess && data?.rooms.length > 0 && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>{data?.rooms.map((room) => renderRow(room))}</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
)}
{isError && (
<States>
<StatesIcon name='warning' variation='danger' />
<StatesTitle>{t('Something_went_wrong')}</StatesTitle>
<StatesActions>
<StatesAction onClick={() => refetch()}>{t('Reload_page')}</StatesAction>
</StatesActions>
</States>
)}
</>
);
};

@ -4,14 +4,14 @@ import React from 'react';
import NotAuthorizedPage from '../../../notAuthorized/NotAuthorizedPage';
import ContactTable from './ContactTable';
function ContactTab(props) {
const ContactTab = () => {
const hasAccess = usePermission('view-l-room');
if (hasAccess) {
return <ContactTable {...props} />;
return <ContactTable />;
}
return <NotAuthorizedPage />;
}
};
export default ContactTab;

@ -13,7 +13,7 @@ import {
GenericTableBody,
GenericTableRow,
GenericTableHeaderCell,
GenericTableLoadingTable,
GenericTableLoadingRow,
} from '../../../../components/GenericTable';
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../../components/GenericTable/hooks/useSort';
@ -63,7 +63,35 @@ function ContactTable(): ReactElement {
}),
);
const { data: contactResult, isLoading, isError, isSuccess, refetch } = useCurrentContacts(query);
const { data, isLoading, isError, isSuccess, refetch } = useCurrentContacts(query);
const headers = (
<>
<GenericTableHeaderCell key='username' direction={sortDirection} active={sortBy === 'username'} onClick={setSort} sort='username'>
{t('Username')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
{t('Name')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='phone' direction={sortDirection} active={sortBy === 'phone'} onClick={setSort} sort='phone'>
{t('Phone')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key='email'
direction={sortDirection}
active={sortBy === 'visitorEmails.address'}
onClick={setSort}
sort='visitorEmails.address'
>
{t('Email')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='lastchat' direction={sortDirection} active={sortBy === 'lastchat'} onClick={setSort} sort='lastchat'>
{t('Last_Chat')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='call' width={44} />
</>
);
return (
<>
<FilterByText
@ -72,75 +100,55 @@ function ContactTable(): ReactElement {
onButtonClick={onButtonNewClick}
onChange={({ text }): void => setTerm(text)}
/>
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingRow cols={6} />
</GenericTableBody>
</GenericTable>
)}
{isSuccess && data?.visitors.length > 0 && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data?.visitors.map(({ _id, username, fname, name, visitorEmails, phone, lastChat }) => {
const phoneNumber = (phone?.length && phone[0].phoneNumber) || '';
const visitorEmail = visitorEmails?.length && visitorEmails[0].address;
<GenericTable>
<GenericTableHeader>
<GenericTableHeaderCell
key={'username'}
direction={sortDirection}
active={sortBy === 'username'}
onClick={setSort}
sort='username'
>
{t('Username')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key={'name'} direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
{t('Name')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key={'phone'} direction={sortDirection} active={sortBy === 'phone'} onClick={setSort} sort='phone'>
{t('Phone')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key={'email'}
direction={sortDirection}
active={sortBy === 'visitorEmails.address'}
onClick={setSort}
sort='visitorEmails.address'
>
{t('Email')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key={'lastchat'}
direction={sortDirection}
active={sortBy === 'lastchat'}
onClick={setSort}
sort='lastchat'
>
{t('Last_Chat')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='call' width={44} />
</GenericTableHeader>
<GenericTableBody>
{contactResult &&
contactResult.visitors.map(({ _id, username, fname, name, visitorEmails, phone, lastChat }) => {
const phoneNumber = (phone?.length && phone[0].phoneNumber) || '';
const visitorEmail = visitorEmails?.length && visitorEmails[0].address;
return (
<GenericTableRow
action
key={_id}
tabIndex={0}
role='link'
height='40px'
qa-user-id={_id}
rcx-show-call-button-on-hover
onClick={onRowClick(_id)}
>
<GenericTableCell withTruncatedText>{username}</GenericTableCell>
<GenericTableCell withTruncatedText>{parseOutboundPhoneNumber(fname || name)}</GenericTableCell>
<GenericTableCell withTruncatedText>{parseOutboundPhoneNumber(phoneNumber)}</GenericTableCell>
<GenericTableCell withTruncatedText>{visitorEmail}</GenericTableCell>
<GenericTableCell withTruncatedText>{lastChat && formatDate(lastChat.ts)}</GenericTableCell>
<GenericTableCell>{isCallReady && <CallDialpadButton phoneNumber={phoneNumber} />}</GenericTableCell>
</GenericTableRow>
);
})}
{isLoading && <GenericTableLoadingTable headerCells={6} />}
{isError && <Box mbs='x16'>{t('Something_went_wrong')}</Box>}
</GenericTableBody>
</GenericTable>
return (
<GenericTableRow
action
key={_id}
tabIndex={0}
role='link'
height='40px'
qa-user-id={_id}
rcx-show-call-button-on-hover
onClick={onRowClick(_id)}
>
<GenericTableCell withTruncatedText>{username}</GenericTableCell>
<GenericTableCell withTruncatedText>{parseOutboundPhoneNumber(fname || name)}</GenericTableCell>
<GenericTableCell withTruncatedText>{parseOutboundPhoneNumber(phoneNumber)}</GenericTableCell>
<GenericTableCell withTruncatedText>{visitorEmail}</GenericTableCell>
<GenericTableCell withTruncatedText>{lastChat && formatDate(lastChat.ts)}</GenericTableCell>
<GenericTableCell>{isCallReady && <CallDialpadButton phoneNumber={phoneNumber} />}</GenericTableCell>
</GenericTableRow>
);
})}
</GenericTableBody>
</GenericTable>
<Pagination
current={current}
itemsPerPage={itemsPerPage}
count={data?.total}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
{...paginationProps}
/>
</>
)}
{isError && (
<Box mbs='x20'>
<States>
@ -155,16 +163,6 @@ function ContactTable(): ReactElement {
</States>
</Box>
)}
{isSuccess && (
<Pagination
current={current}
itemsPerPage={itemsPerPage}
count={contactResult.total}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
{...paginationProps}
/>
)}
</>
);
}

@ -1,49 +1,20 @@
import type { Dispatch, Key, ReactElement, ReactNode, SetStateAction } from 'react';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import GenericTable from '../../../components/GenericTable';
import Page from '../../../components/Page';
import { QueueListFilter } from './QueueListFilter';
import QueueListTable from './QueueListTable';
type QueueListPagePropsParamsType = {
servedBy: string;
status: string;
departmentId: string;
itemsPerPage: 25 | 50 | 100;
current: number;
};
const QueueListPage = () => {
const t = useTranslation();
type QueueListPagePropsType = {
title: string;
header: ReactNode;
data?: {
queue: {
chats: number;
department: { _id: string; name: string };
user: { _id: string; username: string; status: string };
}[];
count: number;
offset: number;
total: number;
};
params: QueueListPagePropsParamsType;
setParams: Dispatch<SetStateAction<QueueListPagePropsParamsType>>;
renderRow: (props: { _id?: Key }) => ReactElement;
return (
<Page>
<Page.Header title={t('Livechat_Queue')} />
<Page.Content>
<QueueListTable />
</Page.Content>
</Page>
);
};
export const QueueListPage = ({ title, header, data, renderRow, params, setParams }: QueueListPagePropsType): ReactElement => (
<Page>
<Page.Header title={title} />
<Page.Content>
<GenericTable
header={header}
renderFilter={({ onChange, ...props }: any): ReactElement => <QueueListFilter setFilter={onChange} {...props} />}
renderRow={renderRow}
results={data?.queue}
total={data?.total}
params={params}
setParams={setParams as (params: Pick<QueueListPagePropsParamsType, 'itemsPerPage' | 'current'>) => void}
/>
</Page.Content>
</Page>
);
export default QueueListPage;

@ -0,0 +1,145 @@
import { Box, Pagination } from '@rocket.chat/fuselage';
import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import React, { useMemo, useState } from 'react';
import GenericNoResults from '../../../components/GenericNoResults';
import {
GenericTable,
GenericTableHeader,
GenericTableHeaderCell,
GenericTableBody,
GenericTableRow,
GenericTableCell,
GenericTableLoadingRow,
} from '../../../components/GenericTable';
import { usePagination } from '../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../components/GenericTable/hooks/useSort';
import UserAvatar from '../../../components/avatar/UserAvatar';
import { QueueListFilter } from './QueueListFilter';
const QueueListTable = (): ReactElement => {
const t = useTranslation();
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const { sortBy, sortDirection, setSort } = useSort<'servedBy' | 'department' | 'total' | 'status'>('servedBy');
const [filters, setFilters] = useState<{
servedBy: string;
status: string;
departmentId: string;
}>({
servedBy: '',
status: '',
departmentId: '',
});
const mediaQuery = useMediaQuery('(min-width: 1024px)');
const headers = (
<>
{mediaQuery && (
<GenericTableHeaderCell key='servedBy' direction={sortDirection} active={sortBy === 'servedBy'} onClick={setSort} sort='servedBy'>
{t('Served_By')}
</GenericTableHeaderCell>
)}
<GenericTableHeaderCell
key='department'
direction={sortDirection}
active={sortBy === 'department'}
onClick={setSort}
sort='department'
>
{t('Department')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='total' direction={sortDirection} active={sortBy === 'total'} onClick={setSort} sort='total'>
{t('Total')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='status' direction={sortDirection} active={sortBy === 'status'} onClick={setSort} sort='status'>
{t('Status')}
</GenericTableHeaderCell>
</>
);
const query = useMemo(() => {
const query: {
agentId?: string;
includeOfflineAgents?: 'true' | 'false';
departmentId?: string;
sort: string;
count: number;
} = {
sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`,
...(itemsPerPage && { count: itemsPerPage }),
...(current && { offset: current }),
};
if (filters.status !== 'online') {
query.includeOfflineAgents = 'true';
}
if (filters.servedBy) {
query.agentId = filters.servedBy;
}
if (filters.departmentId) {
query.departmentId = filters.departmentId;
}
return query;
}, [sortBy, sortDirection, itemsPerPage, current, filters.status, filters.departmentId, filters.servedBy]);
const getLivechatQueue = useEndpoint('GET', '/v1/livechat/queue');
const { data, isSuccess, isLoading } = useQuery(['livechat-queue', query], async () => getLivechatQueue(query), {
refetchOnWindowFocus: false,
});
return (
<>
<QueueListFilter setFilter={setFilters} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingRow cols={4} />
</GenericTableBody>
</GenericTable>
)}
{isSuccess && data?.queue.length === 0 && <GenericNoResults />}
{isSuccess && data?.queue.length > 0 && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data?.queue.map(({ user, department, chats }) => (
<GenericTableRow key={user._id} tabIndex={0}>
<GenericTableCell withTruncatedText>
<Box display='flex' alignItems='center' mb='5px'>
<UserAvatar size={mediaQuery ? 'x28' : 'x40'} username={user.username} />
<Box display='flex' mi='x8'>
{user.username}
</Box>
</Box>
</GenericTableCell>
<GenericTableCell withTruncatedText>{department ? department.name : ''}</GenericTableCell>
<GenericTableCell withTruncatedText>{chats}</GenericTableCell>
<GenericTableCell withTruncatedText>{user.status === 'online' ? t('Online') : t('Offline')}</GenericTableCell>
</GenericTableRow>
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
)}
</>
);
};
export default QueueListTable;

@ -0,0 +1 @@
export { default } from './QueueListPage';

@ -1,114 +0,0 @@
import { Box, Table } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import GenericTable from '../../../components/GenericTable';
import UserAvatar from '../../../components/avatar/UserAvatar';
import { useEndpointData } from '../../../hooks/useEndpointData';
import { QueueListPage } from './QueueListPage';
import { useQuery } from './hooks/useQuery';
const QueueList = (): ReactElement => {
const t = useTranslation();
const [sort, setSort] = useState<[string, 'asc' | 'desc']>(['servedBy', 'desc']);
const onHeaderClick = useMutableCallback((id) => {
const [sortBy, sortDirection] = sort;
if (sortBy === id) {
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
return;
}
setSort([id, 'asc']);
});
const mediaQuery = useMediaQuery('(min-width: 1024px)');
const header = useMemo(
() =>
[
mediaQuery && (
<GenericTable.HeaderCell
key={'servedBy'}
direction={sort[1]}
active={sort[0] === 'servedBy'}
onClick={onHeaderClick}
sort='servedBy'
>
{t('Served_By')}
</GenericTable.HeaderCell>
),
<GenericTable.HeaderCell
key={'department'}
direction={sort[1]}
active={sort[0] === 'departmend'}
onClick={onHeaderClick}
sort='department'
>
{t('Department')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'total'} direction={sort[1]} active={sort[0] === 'total'} onClick={onHeaderClick} sort='total'>
{t('Total')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'status'} direction={sort[1]} active={sort[0] === 'status'} onClick={onHeaderClick} sort='status'>
{t('Status')}
</GenericTable.HeaderCell>,
].filter(Boolean),
[mediaQuery, sort, onHeaderClick, t],
);
const renderRow = useCallback(
({ user, department, chats }) => {
const getStatusText = (): string => {
if (user.status === 'online') {
return t('Online');
}
return t('Offline');
};
return (
<Table.Row key={user._id} tabIndex={0}>
<Table.Cell withTruncatedText>
<Box display='flex' alignItems='center' mb='5px'>
<UserAvatar size={mediaQuery ? 'x28' : 'x40'} username={user.username} />
<Box display='flex' mi='x8'>
{user.username}
</Box>
</Box>
</Table.Cell>
<Table.Cell withTruncatedText>{department ? department.name : ''}</Table.Cell>
<Table.Cell withTruncatedText>{chats}</Table.Cell>
<Table.Cell withTruncatedText>{getStatusText()}</Table.Cell>
</Table.Row>
);
},
[mediaQuery, t],
);
const [params, setParams] = useState<{
servedBy: string;
status: string;
departmentId: string;
itemsPerPage: 25 | 50 | 100;
current: number;
}>({
servedBy: '',
status: '',
departmentId: '',
itemsPerPage: 25,
current: 0,
});
const debouncedParams = useDebouncedValue(params, 500);
const debouncedSort = useDebouncedValue(sort, 500);
const query = useQuery(debouncedParams, debouncedSort);
const { value: data } = useEndpointData('/v1/livechat/queue', { params: query });
return (
<QueueListPage title={t('Livechat_Queue')} header={header} data={data} renderRow={renderRow} params={params} setParams={setParams} />
);
};
export default QueueList;

@ -1,81 +0,0 @@
import { Box, CheckBox } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { useState, useCallback } from 'react';
import GenericTable from '../../../../../components/GenericTable';
import ChannelRow from './ChannelRow';
const ChannelDeletionTable = ({ rooms, params, onChangeParams, onChangeRoomSelection, selectedRooms, onToggleAllRooms }) => {
const [sort, setSort] = useState(['name', 'asc']);
const t = useTranslation();
const selectedRoomsLength = Object.values(selectedRooms).filter(Boolean).length;
const onHeaderClick = useCallback(
(id) => {
const [sortBy, sortDirection] = sort;
if (sortBy === id) {
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
return;
}
setSort([id, 'asc']);
},
[sort],
);
const getSortedChannels = () => {
if (rooms) {
const sortedRooms = [...rooms];
const [sortBy, sortOrder] = sort;
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 (sortOrder === 'desc') {
return sortedRooms?.reverse();
}
return sortedRooms;
}
};
const checked = rooms.length === selectedRoomsLength;
const indeterminate = rooms.length > selectedRoomsLength && selectedRoomsLength > 0;
return (
<Box display='flex' flexDirection='column' height='x200' mbs='x24'>
<GenericTable
header={
<>
<GenericTable.HeaderCell key='name' sort='name' onClick={onHeaderClick} direction={sort[1]} active={sort[0] === 'name'}>
<CheckBox indeterminate={indeterminate} checked={checked} onChange={onToggleAllRooms} />
<Box mi='x8'>{t('Channel_name')}</Box>
</GenericTable.HeaderCell>
<GenericTable.HeaderCell
key='usersCount'
sort='usersCount'
onClick={onHeaderClick}
direction={sort[1]}
active={sort[0] === 'usersCount'}
>
<Box width='100%' textAlign='end'>
{t('Members')}
</Box>
</GenericTable.HeaderCell>
</>
}
results={getSortedChannels()}
params={params}
setParams={onChangeParams}
fixed={false}
pagination={false}
>
{({ key, ...room }) => <ChannelRow room={room} key={key} onChange={onChangeRoomSelection} selected={!!selectedRooms[room._id]} />}
</GenericTable>
</Box>
);
};
export default ChannelDeletionTable;

@ -0,0 +1,78 @@
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
import { Box, CheckBox } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import { GenericTable, GenericTableHeaderCell, GenericTableBody, GenericTableHeader } from '../../../../../components/GenericTable';
import { useSort } from '../../../../../components/GenericTable/hooks/useSort';
import ChannelDeletionTableRow from './ChannelDeletionTableRow';
type ChannelDeletationTable = {
rooms: Serialized<IRoom>[];
onToggleAllRooms: () => void;
onChangeRoomSelection: (room: Serialized<IRoom>) => void;
selectedRooms: { [key: string]: Serialized<IRoom> };
};
const ChannelDeletionTable = ({ rooms, onChangeRoomSelection, selectedRooms, onToggleAllRooms }: ChannelDeletationTable) => {
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 mi='x8'>{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' mbs='x24'>
<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,35 @@
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
import { CheckBox, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React from 'react';
import { GenericTableRow, GenericTableCell } from '../../../../../components/GenericTable';
import { RoomIcon } from '../../../../../components/RoomIcon';
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 = useMutableCallback(() => 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;

@ -1,29 +0,0 @@
import { CheckBox, Table, Icon, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React from 'react';
import { useRoomIcon } from '../../../../../hooks/useRoomIcon';
const ChannelRow = ({ onChange, selected, room }) => {
const { name, fname, usersCount } = room;
const handleChange = useMutableCallback(() => onChange(room));
return (
<Table.Row action>
<Table.Cell maxWidth='x300' withTruncatedText>
<CheckBox checked={selected} onChange={handleChange} />
<Margins inline='x8'>
<Icon size='x16' {...useRoomIcon(room)} />
{fname ?? name}
</Margins>
</Table.Cell>
<Table.Cell align='end' withTruncatedText>
{usersCount}
</Table.Cell>
</Table.Row>
);
};
export default ChannelRow;

@ -1,7 +1,7 @@
import { Table } from '@rocket.chat/fuselage';
import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
import React, { memo, useMemo } from 'react';
import { GenericTableRow, GenericTableCell } from '../../../client/components/GenericTable';
import RemoveBusinessHourButton from './RemoveBusinessHourButton';
function BusinessHoursRow(props) {
@ -43,17 +43,17 @@ function BusinessHoursRow(props) {
};
return (
<Table.Row key={_id} role='link' action tabIndex={0} onClick={handleClick} onKeyDown={handleKeyDown}>
<Table.Cell withTruncatedText>{name || t('Default')}</Table.Cell>
<Table.Cell withTruncatedText>{t(timezone.name)}</Table.Cell>
<Table.Cell withTruncatedText>{openDays.join(', ')}</Table.Cell>
<Table.Cell withTruncatedText>{active ? t('Yes') : t('No')}</Table.Cell>
<GenericTableRow key={_id} role='link' action tabIndex={0} onClick={handleClick} onKeyDown={handleKeyDown}>
<GenericTableCell withTruncatedText>{name || t('Default')}</GenericTableCell>
<GenericTableCell withTruncatedText>{t(timezone.name)}</GenericTableCell>
<GenericTableCell withTruncatedText>{openDays.join(', ')}</GenericTableCell>
<GenericTableCell withTruncatedText>{active ? t('Yes') : t('No')}</GenericTableCell>
{name && (
<Table.Cell withTruncatedText onClick={preventClickPropagation}>
<GenericTableCell withTruncatedText onClick={preventClickPropagation}>
<RemoveBusinessHourButton _id={_id} reload={reload} type={type} />
</Table.Cell>
</GenericTableCell>
)}
</Table.Row>
</GenericTableRow>
);
}

@ -1,37 +1,92 @@
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import { Pagination, States, StatesIcon, StatesActions, StatesAction, StatesTitle } from '@rocket.chat/fuselage';
import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React, { useMemo, useState } from 'react';
import FilterByText from '../../../client/components/FilterByText';
import GenericTable from '../../../client/components/GenericTable';
import { useResizeInlineBreakpoint } from '../../../client/hooks/useResizeInlineBreakpoint';
import GenericNoResults from '../../../client/components/GenericNoResults';
import {
GenericTable,
GenericTableBody,
GenericTableHeaderCell,
GenericTableHeader,
GenericTableLoadingRow,
} from '../../../client/components/GenericTable';
import { usePagination } from '../../../client/components/GenericTable/hooks/usePagination';
import BusinessHoursRow from './BusinessHoursRow';
function BusinessHoursTable({ businessHours, totalbusinessHours, params, onChangeParams, reload }) {
const BusinessHoursTable = () => {
const t = useTranslation();
const [text, setText] = useState('');
const [ref, onMediumBreakpoint] = useResizeInlineBreakpoint([600], 200);
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const query = useMemo(
() => ({
count: itemsPerPage,
offset: current,
name: text,
}),
[itemsPerPage, current, text],
);
const getBusinessHours = useEndpoint('GET', '/v1/livechat/business-hours');
const { data, isLoading, isSuccess, isError, refetch } = useQuery(['livechat-buiness-hours', query], async () => getBusinessHours(query));
const headers = (
<>
<GenericTableHeaderCell>{t('Name')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Timezone')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Open_Days')}</GenericTableHeaderCell>
<GenericTableHeaderCell width='x100'>{t('Enabled')}</GenericTableHeaderCell>
<GenericTableHeaderCell width='x100'>{t('Remove')}</GenericTableHeaderCell>
</>
);
return (
<GenericTable
ref={ref}
header={
<>
<FilterByText onChange={({ text }) => setText(text)} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingRow cols={5} />
</GenericTableBody>
</GenericTable>
)}
{isSuccess && data?.businessHours.length === 0 && <GenericNoResults />}
{isSuccess && data?.businessHours.length > 0 && (
<>
<GenericTable.HeaderCell>{t('Name')}</GenericTable.HeaderCell>
<GenericTable.HeaderCell>{t('Timezone')}</GenericTable.HeaderCell>
<GenericTable.HeaderCell>{t('Open_Days')}</GenericTable.HeaderCell>
<GenericTable.HeaderCell width='x100'>{t('Enabled')}</GenericTable.HeaderCell>
<GenericTable.HeaderCell width='x100'>{t('Remove')}</GenericTable.HeaderCell>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data?.businessHours.map((businessHour) => (
<BusinessHoursRow key={businessHour._id} reload={refetch} {...businessHour} />
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data.total || 0}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
}
results={businessHours}
total={totalbusinessHours}
params={params}
setParams={onChangeParams}
renderFilter={({ onChange, ...props }) => <FilterByText onChange={onChange} {...props} />}
>
{(props) => <BusinessHoursRow key={props._id} medium={onMediumBreakpoint} reload={reload} {...props} />}
</GenericTable>
)}
{isError && (
<States>
<StatesIcon name='warning' variation='danger' />
<StatesTitle>{t('Something_went_wrong')}</StatesTitle>
<StatesActions>
<StatesAction onClick={() => refetch()}>{t('Reload_page')}</StatesAction>
</StatesActions>
</States>
)}
</>
);
}
};
export default BusinessHoursTable;

@ -14,7 +14,7 @@ export default {
},
} as ComponentMeta<typeof BusinessHoursTable>;
export const Default: ComponentStory<typeof BusinessHoursTable> = (args) => <BusinessHoursTable {...args} />;
export const Default: ComponentStory<typeof BusinessHoursTable> = (_args) => <BusinessHoursTable />;
Default.storyName = 'BusinessHoursTable';
Default.args = {
businessHours: [

@ -1,43 +0,0 @@
import { Callout } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { useMemo, useState } from 'react';
import { AsyncStatePhase } from '../../../client/hooks/useAsyncState';
import { useEndpointData } from '../../../client/hooks/useEndpointData';
import BusinessHoursTable from './BusinessHoursTable';
const BusinessHoursTableContainer = () => {
const t = useTranslation();
const [params, setParams] = useState(() => ({ current: 0, itemsPerPage: 25, text: '' }));
const {
value: data,
phase: state,
reload,
} = useEndpointData('/v1/livechat/business-hours', {
params: useMemo(
() => ({
count: params.itemsPerPage,
offset: params.current,
name: params.text,
}),
[params],
),
});
if (state === AsyncStatePhase.REJECTED) {
return <Callout>{t('Error')}: error</Callout>;
}
return (
<BusinessHoursTable
businessHours={data?.businessHours}
totalbusinessHours={data?.total}
params={params}
onChangeParams={setParams}
reload={reload}
/>
);
};
export default BusinessHoursTableContainer;

@ -1,49 +1,41 @@
import { Button, ButtonGroup } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
import type { FC, ReactElement, Dispatch, SetStateAction } from 'react';
import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import GenericNoResults from '../../../../client/components/GenericNoResults';
import GenericTable from '../../../../client/components/GenericTable';
import Page from '../../../../client/components/Page';
import CannedResponseEditWithData from './CannedResponseEditWithData';
import CannedResponseNew from './CannedResponseNew';
import CannedResponsesTable from './CannedResponsesTable';
type CannedResponsesPageProps = {
data: any;
header: ReactElement[];
setParams: Dispatch<SetStateAction<{ current: number; itemsPerPage: 25 | 50 | 100 }>>;
params: { current: number; itemsPerPage: 25 | 50 | 100 };
title: string;
renderFilter?: (props: any) => ReactElement;
renderRow?: (props: any) => ReactElement;
totalCannedResponses: number;
busy?: boolean;
};
const CannedResponsesPage: FC<CannedResponsesPageProps> = ({
data,
header,
setParams,
params,
title,
renderRow,
renderFilter,
totalCannedResponses,
busy,
}) => {
const CannedResponsesPage = () => {
const t = useTranslation();
const Route = useRoute('omnichannel-canned-responses');
const cannedResponseRoute = useRoute('omnichannel-canned-responses');
const queryClient = useQueryClient();
const handleClick = useMutableCallback(() =>
Route.push({
cannedResponseRoute.push({
context: 'new',
}),
);
const context = useRouteParameter('context');
const id = useRouteParameter('id');
const reload = useMutableCallback(() => queryClient.invalidateQueries(['canned-responses']));
if (context === 'edit' && id) {
return <CannedResponseEditWithData reload={reload} totalDataReload={reload} cannedResponseId={id} />;
}
if (context === 'new') {
return <CannedResponseNew reload={reload} totalDataReload={reload} />;
}
return (
<Page>
<Page.Header title={title}>
<Page.Header title={t('Canned_Responses')}>
<ButtonGroup>
<Button onClick={handleClick} title={t('New_Canned_Response')}>
{t('New')}
@ -51,26 +43,7 @@ const CannedResponsesPage: FC<CannedResponsesPageProps> = ({
</ButtonGroup>
</Page.Header>
<Page.Content>
{totalCannedResponses < 1 ? (
<GenericNoResults
icon='baloon-exclamation'
title={t('No_Canned_Responses_Yet')}
description={t('No_Canned_Responses_Yet-description')}
buttonTitle={t('Create_your_First_Canned_Response')}
buttonAction={handleClick}
></GenericNoResults>
) : (
<GenericTable
renderFilter={renderFilter}
header={header}
renderRow={renderRow}
results={data?.cannedResponses}
total={data?.total}
setParams={setParams}
params={params}
aria-busy={busy}
/>
)}
<CannedResponsesTable />
</Page.Content>
</Page>
);

@ -1,229 +1,18 @@
import { Table, Box } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useRouteParameter, useRoute, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { FC, ReactElement } from 'react';
import React, { useMemo, useCallback, useState } from 'react';
import { usePermission } from '@rocket.chat/ui-contexts';
import type { FC } from 'react';
import React from 'react';
import GenericTable from '../../../../client/components/GenericTable';
import PageSkeleton from '../../../../client/components/PageSkeleton';
import UserAvatar from '../../../../client/components/avatar/UserAvatar';
import { useForm } from '../../../../client/hooks/useForm';
import { useFormatDateAndTime } from '../../../../client/hooks/useFormatDateAndTime';
import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage';
import CannedResponseEditWithData from './CannedResponseEditWithData';
import CannedResponseFilter from './CannedResponseFilter';
import CannedResponseNew from './CannedResponseNew';
import CannedResponsesPage from './CannedResponsesPage';
import RemoveCannedResponseButton from './RemoveCannedResponseButton';
const CannedResponsesRoute: FC = () => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const canViewCannedResponses = usePermission('manage-livechat-canned-responses');
const isMonitor = usePermission('save-department-canned-responses');
const isManager = usePermission('save-all-canned-responses');
type CannedResponseFilterValues = {
sharing: string;
createdBy: string;
tags: Array<{ value: string; label: string }>;
text: string;
firstMessage: string;
};
type Scope = 'global' | 'department' | 'user';
const { values, handlers } = useForm({
sharing: '',
createdBy: '',
tags: [],
text: '',
});
const { sharing, createdBy, text } = values as CannedResponseFilterValues;
const { handleSharing, handleCreatedBy, handleText } = handlers;
const [params, setParams] = useState<{ current: number; itemsPerPage: 25 | 50 | 100 }>({
current: 0,
itemsPerPage: 25,
});
const [sort, setSort] = useState<[string, 'asc' | 'desc' | undefined]>(['shortcut', 'asc']);
const debouncedParams = useDebouncedValue(params, 500);
const debouncedSort = useDebouncedValue(sort, 500);
const debouncedText = useDebouncedValue(text, 500);
const queryClient = useQueryClient();
const getCannedResponses = useEndpoint('GET', '/v1/canned-responses');
const { data } = useQuery(
['canned-responses', '/v1/canned-responses', { debouncedText, debouncedSort, debouncedParams, sharing, createdBy }],
() =>
getCannedResponses({
text: debouncedText,
sort: JSON.stringify({ [debouncedSort[0]]: debouncedSort[1] === 'asc' ? 1 : -1 }),
...(sharing && { scope: sharing }),
...(createdBy && createdBy !== 'all' && { createdBy }),
...(debouncedParams.itemsPerPage && { count: debouncedParams.itemsPerPage }),
...(debouncedParams.current && { offset: debouncedParams.current }),
}),
);
const { data: totalData, isInitialLoading: totalDataLoading } = useQuery(['canned-responses', '/v1/canned-responses'], () =>
getCannedResponses({}),
);
const reload = useMutableCallback(() => queryClient.invalidateQueries(['canned-responses', '/v1/canned-responses']));
const cannedResponsesRoute = useRoute('omnichannel-canned-responses');
const context = useRouteParameter('context');
const id = useRouteParameter('id');
const onHeaderClick = useMutableCallback((id) => {
const [sortBy, sortDirection] = sort;
if (sortBy === id) {
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
return;
}
setSort([id, 'asc']);
});
const onRowClick = useMutableCallback((id, scope) => (): void => {
if (scope === 'global' && isMonitor && !isManager) {
dispatchToastMessage({
type: 'error',
message: t('Not_authorized'),
});
return;
}
cannedResponsesRoute.push({
context: 'edit',
id,
});
});
const defaultOptions = useMemo(
() => ({
global: t('Public'),
department: t('Department'),
user: t('Private'),
}),
[t],
);
const getTime = useFormatDateAndTime();
const header = useMemo(
() =>
[
<GenericTable.HeaderCell
key={'shortcut'}
direction={sort[1]}
active={sort[0] === 'shortcut'}
onClick={onHeaderClick}
sort='shortcut'
>
{t('Shortcut')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'sharing'} direction={sort[1]} active={sort[0] === 'scope'} onClick={onHeaderClick} sort='scope'>
{t('Sharing')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key={'createdBy'}
direction={sort[1]}
active={sort[0] === 'createdBy'}
onClick={onHeaderClick}
sort='createdBy'
>
{t('Created_by')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell
key={'createdAt'}
direction={sort[1]}
active={sort[0] === '_createdAt'}
onClick={onHeaderClick}
sort='_createdAt'
>
{t('Created_at')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'tags'} direction={sort[1]} active={sort[0] === 'tags'} onClick={onHeaderClick} sort='tags'>
{t('Tags')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key={'remove'} w='x60'>
{t('Remove')}
</GenericTable.HeaderCell>,
].filter(Boolean),
[sort, onHeaderClick, t],
);
const renderRow = useCallback(
({ _id, shortcut, scope, createdBy, _createdAt, tags = [] }): ReactElement => (
<Table.Row key={_id} tabIndex={0} role='link' onClick={onRowClick(_id, scope)} action qa-user-id={_id}>
<Table.Cell withTruncatedText>{shortcut}</Table.Cell>
<Table.Cell withTruncatedText>{defaultOptions[scope as Scope]}</Table.Cell>
<Table.Cell withTruncatedText>
<Box display='flex' alignItems='center'>
<UserAvatar size='x24' username={createdBy.username} />
<Box display='flex' withTruncatedText mi='x8'>
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText>
<Box fontScale='p2m' withTruncatedText color='default'>
{createdBy.username}
</Box>
</Box>
</Box>
</Box>
</Table.Cell>
<Table.Cell withTruncatedText>{getTime(_createdAt)}</Table.Cell>
<Table.Cell withTruncatedText>{tags.join(', ')}</Table.Cell>
{!(scope === 'global' && isMonitor && !isManager) && (
<RemoveCannedResponseButton _id={_id} reload={reload} totalDataReload={reload} />
)}
</Table.Row>
),
[getTime, onRowClick, reload, defaultOptions, isMonitor, isManager],
);
if (context === 'edit' && id) {
return <CannedResponseEditWithData reload={reload} totalDataReload={reload} cannedResponseId={id} />;
}
if (context === 'new') {
return <CannedResponseNew reload={reload} totalDataReload={reload} />;
}
if (!canViewCannedResponses) {
return <NotAuthorizedPage />;
}
if (totalDataLoading) {
return <PageSkeleton></PageSkeleton>;
}
return (
<CannedResponsesPage
setParams={setParams}
renderFilter={(): ReactElement => (
<CannedResponseFilter
sharingValue={sharing}
createdByValue={createdBy}
shortcutValue={text}
setSharing={handleSharing}
setCreatedBy={handleCreatedBy}
setShortcut={handleText}
/>
)}
params={params}
data={data}
header={header}
renderRow={renderRow}
title={t('Canned_Responses')}
totalCannedResponses={totalData?.total || 0}
busy={text !== debouncedText}
></CannedResponsesPage>
);
return <CannedResponsesPage />;
};
export default CannedResponsesRoute;

@ -0,0 +1,203 @@
import { Box, Pagination } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation, usePermission, useToastMessageDispatch, useRoute, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React, { useMemo } from 'react';
import GenericNoResults from '../../../../client/components/GenericNoResults';
import {
GenericTable,
GenericTableBody,
GenericTableHeader,
GenericTableHeaderCell,
GenericTableLoadingRow,
GenericTableRow,
GenericTableCell,
} from '../../../../client/components/GenericTable';
import { usePagination } from '../../../../client/components/GenericTable/hooks/usePagination';
import { useSort } from '../../../../client/components/GenericTable/hooks/useSort';
import UserAvatar from '../../../../client/components/avatar/UserAvatar';
import { useForm } from '../../../../client/hooks/useForm';
import { useFormatDateAndTime } from '../../../../client/hooks/useFormatDateAndTime';
import CannedResponseFilter from './CannedResponseFilter';
import RemoveCannedResponseButton from './RemoveCannedResponseButton';
type CannedResponseFilterValues = {
sharing: string;
createdBy: string;
tags: Array<{ value: string; label: string }>;
text: string;
firstMessage: string;
};
type Scope = 'global' | 'department' | 'user';
const CannedResponsesTable = () => {
const t = useTranslation();
const cannedResponseRoute = useRoute('omnichannel-canned-responses');
const dispatchToastMessage = useToastMessageDispatch();
const isMonitor = usePermission('save-department-canned-responses');
const isManager = usePermission('save-all-canned-responses');
const { values, handlers } = useForm({
sharing: '',
createdBy: '',
tags: [],
text: '',
});
const { sharing, createdBy, text } = values as CannedResponseFilterValues;
const { handleSharing, handleCreatedBy, handleText } = handlers;
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const { sortBy, setSort, sortDirection } = useSort<'shortcut' | 'scope' | 'tags' | '_createdAt' | 'createdBy'>('shortcut');
const debouncedText = useDebouncedValue(text, 500);
const query = useMemo(
() => ({
text: debouncedText,
sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }),
...(sharing && { scope: sharing }),
...(createdBy && createdBy !== 'all' && { createdBy }),
...(itemsPerPage && { count: itemsPerPage }),
...(current && { offset: current }),
}),
[createdBy, current, debouncedText, itemsPerPage, sharing, sortBy, sortDirection],
);
const getCannedResponses = useEndpoint('GET', '/v1/canned-responses');
const { data, isLoading, isSuccess, refetch } = useQuery(['canned-responses', debouncedText], () => getCannedResponses(query));
const getTime = useFormatDateAndTime();
const handleClick = useMutableCallback(() =>
cannedResponseRoute.push({
context: 'new',
}),
);
const onRowClick = useMutableCallback((id, scope) => (): void => {
if (scope === 'global' && isMonitor && !isManager) {
dispatchToastMessage({
type: 'error',
message: t('Not_authorized'),
});
return;
}
cannedResponseRoute.push({
context: 'edit',
id,
});
});
const defaultOptions = useMemo(
() => ({
global: t('Public'),
department: t('Department'),
user: t('Private'),
}),
[t],
);
const headers = (
<>
<GenericTableHeaderCell key='shortcut' direction={sortDirection} active={sortBy === 'shortcut'} onClick={setSort} sort='shortcut'>
{t('Shortcut')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='sharing' direction={sortDirection} active={sortBy === 'scope'} onClick={setSort} sort='scope'>
{t('Sharing')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='createdBy' direction={sortDirection} active={sortBy === 'createdBy'} onClick={setSort} sort='createdBy'>
{t('Created_by')}
</GenericTableHeaderCell>
<GenericTableHeaderCell
key={'createdAt'}
direction={sortDirection}
active={sortBy === '_createdAt'}
onClick={setSort}
sort='_createdAt'
>
{t('Created_at')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='tags' direction={sortDirection} active={sortBy === 'tags'} onClick={setSort} sort='tags'>
{t('Tags')}
</GenericTableHeaderCell>
<GenericTableHeaderCell key='remove' w='x60'>
{t('Remove')}
</GenericTableHeaderCell>
</>
);
return (
<>
<CannedResponseFilter
sharingValue={sharing}
createdByValue={createdBy}
shortcutValue={text}
setSharing={handleSharing}
setCreatedBy={handleCreatedBy}
setShortcut={handleText}
/>
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingRow cols={6} />
</GenericTableBody>
</GenericTable>
)}
{isSuccess && data?.cannedResponses.length === 0 && (
<GenericNoResults
icon='baloon-exclamation'
title={t('No_Canned_Responses_Yet')}
description={t('No_Canned_Responses_Yet-description')}
buttonTitle={t('Create_your_First_Canned_Response')}
buttonAction={handleClick}
/>
)}
{isSuccess && data?.cannedResponses.length > 0 && (
<>
<GenericTable aria-busy={text !== debouncedText}>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data?.cannedResponses.map(({ _id, shortcut, scope, createdBy, _createdAt, tags = [] }) => (
<GenericTableRow key={_id} tabIndex={0} role='link' onClick={onRowClick(_id, scope)} action qa-user-id={_id}>
<GenericTableCell withTruncatedText>{shortcut}</GenericTableCell>
<GenericTableCell withTruncatedText>{defaultOptions[scope as Scope]}</GenericTableCell>
<GenericTableCell withTruncatedText>
<Box display='flex' alignItems='center'>
<UserAvatar size='x24' username={createdBy.username} />
<Box display='flex' withTruncatedText mi='x8'>
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText>
<Box fontScale='p2m' withTruncatedText color='default'>
{createdBy.username}
</Box>
</Box>
</Box>
</Box>
</GenericTableCell>
<GenericTableCell withTruncatedText>{getTime(_createdAt)}</GenericTableCell>
<GenericTableCell withTruncatedText>{tags.join(', ')}</GenericTableCell>
{!(scope === 'global' && isMonitor && !isManager) && <RemoveCannedResponseButton _id={_id} reload={refetch} />}
</GenericTableRow>
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
)}
</>
);
};
export default CannedResponsesTable;

@ -1,46 +1,40 @@
import { Table, IconButton } from '@rocket.chat/fuselage';
import { IconButton } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useSetModal, useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import type { FC } from 'react';
import React from 'react';
import GenericModal from '../../../../client/components/GenericModal';
import { GenericTableCell } from '../../../../client/components/GenericTable';
type RemoveCannedResponseButtonProps = {
_id: string;
reload: () => void;
totalDataReload: () => void;
};
const RemoveCannedResponseButton: FC<RemoveCannedResponseButtonProps> = ({ _id, reload, totalDataReload }) => {
const cannedResponsesRoute = useRoute('omnichannel-canned-responses');
const removeCannedResponse = useMethod('removeCannedResponse');
const RemoveCannedResponseButton: FC<RemoveCannedResponseButtonProps> = ({ _id, reload }) => {
const t = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const t = useTranslation();
const cannedResponsesRoute = useRoute('omnichannel-canned-responses');
const handleRemoveClick = useMutableCallback(async () => {
try {
await removeCannedResponse(_id);
} catch (error) {
console.log(error);
}
cannedResponsesRoute.push({});
});
const removeCannedResponse = useMethod('removeCannedResponse');
const handleDelete = useMutableCallback((e) => {
e.stopPropagation();
const onDeleteCannedResponse: () => Promise<void> = async () => {
try {
await handleRemoveClick();
await removeCannedResponse(_id);
reload();
totalDataReload();
cannedResponsesRoute.push({});
dispatchToastMessage({ type: 'success', message: t('Canned_Response_Removed') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setModal(null);
}
setModal(null);
};
setModal(
<GenericModal
variant='danger'
@ -53,9 +47,9 @@ const RemoveCannedResponseButton: FC<RemoveCannedResponseButtonProps> = ({ _id,
});
return (
<Table.Cell fontScale='p2' color='hint' withTruncatedText>
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
<IconButton icon='trash' small title={t('Remove')} onClick={handleDelete} />
</Table.Cell>
</GenericTableCell>
);
};

@ -30,7 +30,7 @@ export const PrioritiesPage = ({ priorityId, context }: PrioritiesPageProps): Re
const savePriority = useEndpoint('PUT', `/v1/livechat/priorities/:priorityId`, { priorityId });
const resetPriorities = useEndpoint('POST', '/v1/livechat/priorities.reset');
const { data: priorities } = useOmnichannelPriorities();
const { data: priorities, isLoading } = useOmnichannelPriorities();
const isPrioritiesDirty = useMemo(() => !!priorities.length && priorities.some((p) => p.dirty), [priorities]);
@ -83,7 +83,7 @@ export const PrioritiesPage = ({ priorityId, context }: PrioritiesPageProps): Re
</ButtonGroup>
</Page.Header>
<Page.Content>
<PrioritiesTable data={priorities} onRowClick={onRowClick} />
<PrioritiesTable priorities={priorities} isLoading={isLoading} onRowClick={onRowClick} />
</Page.Content>
</Page>

@ -1,40 +1,64 @@
import type { ILivechatPriority, Serialized } from '@rocket.chat/core-typings';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useCallback, useMemo } from 'react';
import React from 'react';
import GenericTable, { GenericTableCell, GenericTableRow } from '../../../../client/components/GenericTable';
import GenericNoResults from '../../../../client/components/GenericNoResults';
import {
GenericTable,
GenericTableHeaderCell,
GenericTableCell,
GenericTableRow,
GenericTableHeader,
GenericTableBody,
GenericTableLoadingTable,
} from '../../../../client/components/GenericTable';
import { PriorityIcon } from './PriorityIcon';
type PrioritiesTableProps = {
data?: Serialized<ILivechatPriority>[];
priorities?: Serialized<ILivechatPriority>[];
onRowClick: (id: string) => void;
isLoading: boolean;
};
export const PrioritiesTable = ({ data, onRowClick }: PrioritiesTableProps): ReactElement => {
export const PrioritiesTable = ({ priorities, onRowClick, isLoading }: PrioritiesTableProps): ReactElement => {
const t = useTranslation();
const renderRow = useCallback(
({ _id, name, i18n, sortItem, dirty }) => (
<GenericTableRow key={_id} tabIndex={0} role='link' onClick={(): void => onRowClick(_id)} action qa-row-id={_id}>
<GenericTableCell withTruncatedText>
<PriorityIcon level={sortItem} />
</GenericTableCell>
<GenericTableCell withTruncatedText>{dirty ? name : t(i18n)}</GenericTableCell>
</GenericTableRow>
),
[onRowClick, t],
);
const header = useMemo(
() => [
<GenericTable.HeaderCell key='icon' w='100px'>
const headers = (
<>
<GenericTableHeaderCell key='icon' w='100px'>
{t('Icon')}
</GenericTable.HeaderCell>,
<GenericTable.HeaderCell key='name'>{t('Name')}</GenericTable.HeaderCell>,
],
[t],
</GenericTableHeaderCell>
<GenericTableHeaderCell key='name'>{t('Name')}</GenericTableHeaderCell>
</>
);
return <GenericTable results={data} header={header} renderRow={renderRow} pagination={false} />;
return (
<>
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingTable headerCells={2} />
</GenericTableBody>
</GenericTable>
)}
{priorities?.length === 0 && <GenericNoResults />}
{priorities && priorities?.length > 0 && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{priorities?.map(({ _id, name, i18n, sortItem, dirty }) => (
<GenericTableRow key={_id} tabIndex={0} role='link' onClick={(): void => onRowClick(_id)} action qa-row-id={_id}>
<GenericTableCell withTruncatedText>
<PriorityIcon level={sortItem} />
</GenericTableCell>
<GenericTableCell withTruncatedText>{dirty ? name : i18n}</GenericTableCell>
</GenericTableRow>
))}
</GenericTableBody>
</GenericTable>
)}
</>
);
};

Loading…
Cancel
Save