chore: Remove old deprecated `GenericTable` in favor of v2 (#29594)
parent
0c94802afd
commit
c197624093
@ -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> |
||||
); |
||||
@ -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'; |
||||
|
||||
@ -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,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; |
||||
|
||||
@ -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,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,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…
Reference in new issue