diff --git a/client/components/FilterByText.tsx b/client/components/FilterByText.tsx index 0c06774c6e6dc..170c0c1944adb 100644 --- a/client/components/FilterByText.tsx +++ b/client/components/FilterByText.tsx @@ -6,19 +6,22 @@ import { useTranslation } from '../contexts/TranslationContext'; type FilterByTextProps = { placeholder?: string; onChange: (filter: { text: string }) => void; - displayButton: boolean; + inputRef?: () => void; +}; + +type FilterByTextPropsWithButton = FilterByTextProps & { + displayButton: true; textButton: string; onButtonClick: () => void; - inputRef: () => void; }; +const isFilterByTextPropsWithButton = (props: any): props is FilterByTextPropsWithButton => + 'displayButton' in props && props.displayButton === true; const FilterByText: FC = ({ placeholder, onChange: setFilter, - displayButton: display = false, - textButton = '', - onButtonClick, inputRef, + children: _, ...props }) => { const t = useTranslation(); @@ -53,9 +56,11 @@ const FilterByText: FC = ({ onChange={handleInputChange} value={text} /> - + {isFilterByTextPropsWithButton(props) && ( + + )} ); }; diff --git a/client/components/GenericTable/GenericTable.tsx b/client/components/GenericTable/GenericTable.tsx index 99124d8186fd5..c6e5317d24ed4 100644 --- a/client/components/GenericTable/GenericTable.tsx +++ b/client/components/GenericTable/GenericTable.tsx @@ -1,20 +1,23 @@ -import { Box, Pagination, Table, Tile } from '@rocket.chat/fuselage'; +import { Pagination, Tile } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import React, { useState, useEffect, - useCallback, forwardRef, ReactNode, ReactElement, Key, - RefAttributes, + useMemo, + Ref, } from 'react'; import flattenChildren from 'react-keyed-flatten-children'; import { useTranslation } from '../../contexts/TranslationContext'; -import ScrollableContentWrapper from '../ScrollableContentWrapper'; -import LoadingRow from './LoadingRow'; +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'; const defaultParamsValue = { text: '', current: 0, itemsPerPage: 25 } as const; const defaultSetParamsValue = (): void => undefined; @@ -37,14 +40,10 @@ type GenericTableProps< pagination?: boolean; } & FilterProps; -const GenericTable: { - < - FilterProps extends { onChange?: (params: GenericTableParams) => void }, - ResultProps extends { _id?: Key }, - >( - props: GenericTableProps & RefAttributes, - ): ReactElement | null; -} = forwardRef(function GenericTable( +const GenericTable = forwardRef(function GenericTable< + FilterProps extends { onChange?: (params: GenericTableParams) => void }, + ResultProps extends { _id?: Key }, +>( { children, fixed = true, @@ -57,41 +56,31 @@ const GenericTable: { total, pagination = true, ...props - }, - ref, + }: GenericTableProps, + ref: Ref, ) { const t = useTranslation(); const [filter, setFilter] = useState(paramsDefault); - const [itemsPerPage, setItemsPerPage] = useState<25 | 50 | 100>(25); - - const [current, setCurrent] = useState(0); + const { + itemsPerPage, + setItemsPerPage, + current, + setCurrent, + itemsPerPageLabel, + showingResultsLabel, + } = usePagination(); const params = useDebouncedValue(filter, 500); useEffect(() => { - setParams({ ...params, current, itemsPerPage }); + setParams({ text: params.text || '', current, itemsPerPage }); }, [params, current, itemsPerPage, setParams]); - const Loading = useCallback(() => { - const headerCells = flattenChildren(header); - return ( - <> - {Array.from({ length: 10 }, (_, i) => ( - - ))} - - ); - }, [header]); - - const showingResultsLabel = useCallback( - ({ count, current, itemsPerPage }) => - t('Showing_results_of', current + 1, Math.min(current + itemsPerPage, count), count), - [t], - ); + const headerCells = useMemo(() => flattenChildren(header).length, [header]); - const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]); + const isLoading = !results; return ( <> @@ -104,28 +93,18 @@ const GenericTable: { ) : ( <> - - - - {header && ( - - {header} - - )} - - {RenderRow && - (results ? ( - results.map((props, index) => ( - - )) - ) : ( - - ))} - {children && (results ? results.map(children) : )} - -
-
-
+ + {header && {header}} + + {isLoading && } + {!isLoading && + ((RenderRow && + results?.map((props, index: number) => ( + + ))) || + (children && results?.map(children)))} + + {pagination && ( (function GenericTable( + { fixed = true, children }, + ref, +) { + return ( + + + + {children} +
+
+
+ ); +}); diff --git a/client/components/GenericTable/V2/GenericTableBody.tsx b/client/components/GenericTable/V2/GenericTableBody.tsx new file mode 100644 index 0000000000000..945688f9efafe --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableBody.tsx @@ -0,0 +1,4 @@ +import { Table } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +export const GenericTableBody: FC = (props) => ; diff --git a/client/components/GenericTable/V2/GenericTableCell.tsx b/client/components/GenericTable/V2/GenericTableCell.tsx new file mode 100644 index 0000000000000..883de10b3f3ad --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableCell.tsx @@ -0,0 +1,6 @@ +import { Table } from '@rocket.chat/fuselage'; +import React, { ComponentProps, FC } from 'react'; + +export const GenericTableCell: FC> = (props) => ( + +); diff --git a/client/components/GenericTable/V2/GenericTableHeader.tsx b/client/components/GenericTable/V2/GenericTableHeader.tsx new file mode 100644 index 0000000000000..90f3a340f5a1e --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableHeader.tsx @@ -0,0 +1,10 @@ +import { Table } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +import { GenericTableRow } from './GenericTableRow'; + +export const GenericTableHeader: FC = ({ children, ...props }) => ( + + {children} + +); diff --git a/client/components/GenericTable/V2/GenericTableHeaderCell.tsx b/client/components/GenericTable/V2/GenericTableHeaderCell.tsx new file mode 100644 index 0000000000000..a4db4fbb31600 --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableHeaderCell.tsx @@ -0,0 +1,30 @@ +import { Box, Table } from '@rocket.chat/fuselage'; +import React, { ComponentProps, ReactElement, useCallback } from 'react'; + +import SortIcon from '../SortIcon'; + +type GenericTableHeaderCellProps = Omit, 'onClick'> & { + active?: boolean; + direction?: 'asc' | 'desc'; + sort?: T; + onClick?: (sort: T) => void; +}; + +export const GenericTableHeaderCell = ({ + children, + active, + direction, + sort, + onClick, + ...props +}: GenericTableHeaderCellProps): ReactElement => { + const fn = useCallback(() => onClick && sort && onClick(sort), [sort, onClick]); + return ( + + + {children} + {sort && } + + + ); +}; diff --git a/client/components/GenericTable/V2/GenericTableLoadingRow.tsx b/client/components/GenericTable/V2/GenericTableLoadingRow.tsx new file mode 100644 index 0000000000000..ecc34e2e5e7f3 --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableLoadingRow.tsx @@ -0,0 +1,25 @@ +import { Box, Skeleton, Table } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +type GenericTableLoadingRowRowProps = { + cols: number; +}; + +export const GenericTableLoadingRow = ({ cols }: GenericTableLoadingRowRowProps): ReactElement => ( + + + + + + + + + + + {Array.from({ length: cols - 1 }, (_, i) => ( + + + + ))} + +); diff --git a/client/components/GenericTable/V2/GenericTableLoadingTable.tsx b/client/components/GenericTable/V2/GenericTableLoadingTable.tsx new file mode 100644 index 0000000000000..e7f2501bc7f4d --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableLoadingTable.tsx @@ -0,0 +1,15 @@ +import React, { ReactElement } from 'react'; + +import { GenericTableLoadingRow } from './GenericTableLoadingRow'; + +export const GenericTableLoadingTable = ({ + headerCells, +}: { + headerCells: number; +}): ReactElement => ( + <> + {Array.from({ length: 10 }, (_, i) => ( + + ))} + +); diff --git a/client/components/GenericTable/V2/GenericTableRow.tsx b/client/components/GenericTable/V2/GenericTableRow.tsx new file mode 100644 index 0000000000000..cd31eb47d65c9 --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableRow.tsx @@ -0,0 +1,6 @@ +import { Table } from '@rocket.chat/fuselage'; +import React, { ComponentProps, FC } from 'react'; + +export const GenericTableRow: FC> = (props) => ( + +); diff --git a/client/components/GenericTable/hooks/useCurrent.ts b/client/components/GenericTable/hooks/useCurrent.ts new file mode 100644 index 0000000000000..fc70a9f252d20 --- /dev/null +++ b/client/components/GenericTable/hooks/useCurrent.ts @@ -0,0 +1,9 @@ +import { useState } from 'react'; + +export const useCurrent = ( + currentInitialValue = 0, +): [number, React.Dispatch>] => { + const [current, setCurrent] = useState(currentInitialValue); + + return [current, setCurrent]; +}; diff --git a/client/components/GenericTable/hooks/useItemsPerPage.ts b/client/components/GenericTable/hooks/useItemsPerPage.ts new file mode 100644 index 0000000000000..3d9c36807a61e --- /dev/null +++ b/client/components/GenericTable/hooks/useItemsPerPage.ts @@ -0,0 +1,11 @@ +import { useState } from 'react'; + +type UseItemsPerPageValue = 25 | 50 | 100; + +export const useItemsPerPage = ( + itemsPerPageInitialValue: UseItemsPerPageValue = 25, +): [UseItemsPerPageValue, React.Dispatch>] => { + const [itemsPerPage, setItemsPerPage] = useState(itemsPerPageInitialValue); + + return [itemsPerPage, setItemsPerPage]; +}; diff --git a/client/components/GenericTable/hooks/useItemsPerPageLabel.ts b/client/components/GenericTable/hooks/useItemsPerPageLabel.ts new file mode 100644 index 0000000000000..79e65d88f3702 --- /dev/null +++ b/client/components/GenericTable/hooks/useItemsPerPageLabel.ts @@ -0,0 +1,8 @@ +import { useCallback } from 'react'; + +import { useTranslation } from '../../../contexts/TranslationContext'; + +export const useItemsPerPageLabel = (): (() => string) => { + const t = useTranslation(); + return useCallback(() => t('Items_per_page:'), [t]); +}; diff --git a/client/components/GenericTable/hooks/usePagination.ts b/client/components/GenericTable/hooks/usePagination.ts new file mode 100644 index 0000000000000..3f0558f4ac995 --- /dev/null +++ b/client/components/GenericTable/hooks/usePagination.ts @@ -0,0 +1,30 @@ +import { useCurrent } from './useCurrent'; +import { useItemsPerPage } from './useItemsPerPage'; +import { useItemsPerPageLabel } from './useItemsPerPageLabel'; +import { useShowingResultsLabel } from './useShowingResultsLabel'; + +export const usePagination = (): { + current: ReturnType[0]; + setCurrent: ReturnType[1]; + itemsPerPage: ReturnType[0]; + setItemsPerPage: ReturnType[1]; + itemsPerPageLabel: ReturnType; + showingResultsLabel: ReturnType; +} => { + const [itemsPerPage, setItemsPerPage] = useItemsPerPage(); + + const [current, setCurrent] = useCurrent(); + + const itemsPerPageLabel = useItemsPerPageLabel(); + + const showingResultsLabel = useShowingResultsLabel(); + + return { + itemsPerPage, + setItemsPerPage, + current, + setCurrent, + itemsPerPageLabel, + showingResultsLabel, + }; +}; diff --git a/client/components/GenericTable/hooks/useShowingResultsLabel.ts b/client/components/GenericTable/hooks/useShowingResultsLabel.ts new file mode 100644 index 0000000000000..b7a54ac084f90 --- /dev/null +++ b/client/components/GenericTable/hooks/useShowingResultsLabel.ts @@ -0,0 +1,19 @@ +import { Pagination } from '@rocket.chat/fuselage'; +import { ComponentProps, useCallback } from 'react'; + +import { useTranslation } from '../../../contexts/TranslationContext'; + +type Props< + T extends ComponentProps['showingResultsLabel'] = ComponentProps< + typeof Pagination + >['showingResultsLabel'], +> = T extends (...args: any[]) => any ? Parameters : never; + +export const useShowingResultsLabel = (): ((...params: Props) => string) => { + const t = useTranslation(); + return useCallback( + ({ count, current, itemsPerPage }) => + t('Showing_results_of', current + 1, Math.min(current + itemsPerPage, count), count), + [t], + ); +}; diff --git a/client/components/GenericTable/hooks/useSort.ts b/client/components/GenericTable/hooks/useSort.ts new file mode 100644 index 0000000000000..6858404208fcd --- /dev/null +++ b/client/components/GenericTable/hooks/useSort.ts @@ -0,0 +1,34 @@ +import { useCallback, useState } from 'react'; + +type Direction = 'asc' | 'desc'; + +export const useSort = ( + by: T, + initialDirection: Direction = 'asc', +): { + sortBy: T; + sortDirection: Direction; + setSort: (sortBy: T, direction?: Direction | undefined) => void; +} => { + const [sort, _setSort] = useState<[T, Direction]>(() => [by, initialDirection]); + + const setSort = useCallback((id: T, direction?: Direction | undefined) => { + _setSort(([sortBy, sortDirection]) => { + if (direction) { + return [id, direction]; + } + + if (sortBy === id) { + return [id, sortDirection === 'asc' ? 'desc' : 'asc']; + } + + return [id, 'asc']; + }); + }, []); + + return { + sortBy: sort[0], + sortDirection: sort[1], + setSort, + }; +}; diff --git a/client/components/GenericTable/index.ts b/client/components/GenericTable/index.ts index 8da6df3fc14e1..3a51504430245 100644 --- a/client/components/GenericTable/index.ts +++ b/client/components/GenericTable/index.ts @@ -4,3 +4,12 @@ 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'; diff --git a/client/views/admin/customEmoji/CustomEmoji.js b/client/views/admin/customEmoji/CustomEmoji.js deleted file mode 100644 index b5474ab6d3ef1..0000000000000 --- a/client/views/admin/customEmoji/CustomEmoji.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Box, Table } from '@rocket.chat/fuselage'; -import React, { useMemo } from 'react'; - -import FilterByText from '../../../components/FilterByText'; -import GenericTable from '../../../components/GenericTable'; -import { useTranslation } from '../../../contexts/TranslationContext'; - -function CustomEmoji({ data, sort, onClick, onHeaderClick, setParams, params }) { - const t = useTranslation(); - - const header = useMemo( - () => [ - - {t('Name')} - , - - {t('Aliases')} - , - ], - [onHeaderClick, sort, t], - ); - - const renderRow = (emojis) => { - const { _id, name, aliases } = emojis; - return ( - - - {name} - - - {aliases} - - - ); - }; - - return ( - } - /> - ); -} - -export default CustomEmoji; diff --git a/client/views/admin/customEmoji/CustomEmoji.tsx b/client/views/admin/customEmoji/CustomEmoji.tsx new file mode 100644 index 0000000000000..28db8f4b4cec9 --- /dev/null +++ b/client/views/admin/customEmoji/CustomEmoji.tsx @@ -0,0 +1,121 @@ +import { Box, Pagination } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import React, { FC, MutableRefObject, useEffect, useMemo, useState } from 'react'; + +import FilterByText from '../../../components/FilterByText'; +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 { useTranslation } from '../../../contexts/TranslationContext'; +import { useEndpointData } from '../../../hooks/useEndpointData'; +import { AsyncStatePhase } from '../../../lib/asyncState'; + +type CustomEmojiProps = { + reload: MutableRefObject<() => void>; + onClick: (emoji: string) => () => void; +}; + +const CustomEmoji: FC = function CustomEmoji({ onClick, reload }) { + const t = useTranslation(); + + const { + current, + itemsPerPage, + setItemsPerPage: onSetItemsPerPage, + setCurrent: onSetCurrent, + ...paginationProps + } = usePagination(); + + const [text, setText] = useState(''); + + const { sortBy, sortDirection, setSort } = useSort<'name'>('name'); + + const query = useDebouncedValue( + useMemo( + () => ({ + query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), + sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }), + count: itemsPerPage, + offset: current, + }), + [text, itemsPerPage, current, sortBy, sortDirection], + ), + 500, + ); + + const { value: data, phase, reload: reloadEndPoint } = useEndpointData('emoji-custom.all', query); + + useEffect(() => { + reload.current = reloadEndPoint; + }, [reload, reloadEndPoint]); + return ( + <> + setText(text)} /> + + + + {t('Name')} + + + {t('Aliases')} + + + + {phase === AsyncStatePhase.LOADING && } + {phase === AsyncStatePhase.RESOLVED && + data && + data.emojis && + data.emojis.length > 0 && + data?.emojis.map((emojis) => ( + + + {emojis.name} + + + {emojis.aliases} + + + ))} + {/* {phase === AsyncStatePhase.RESOLVED && + !data.emojis.length + ))} */} + + + {phase === AsyncStatePhase.RESOLVED && ( + + )} + + ); +}; + +export default CustomEmoji; diff --git a/client/views/admin/customEmoji/CustomEmojiRoute.js b/client/views/admin/customEmoji/CustomEmojiRoute.js index d1430d21062a5..36cb32d84a62e 100644 --- a/client/views/admin/customEmoji/CustomEmojiRoute.js +++ b/client/views/admin/customEmoji/CustomEmojiRoute.js @@ -1,6 +1,5 @@ import { Button, Icon } from '@rocket.chat/fuselage'; -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useCallback, useRef } from 'react'; import NotAuthorizedPage from '../../../components/NotAuthorizedPage'; import Page from '../../../components/Page'; @@ -8,7 +7,6 @@ import VerticalBar from '../../../components/VerticalBar'; import { usePermission } from '../../../contexts/AuthorizationContext'; import { useRoute, useRouteParameter } from '../../../contexts/RouterContext'; import { useTranslation } from '../../../contexts/TranslationContext'; -import { useEndpointData } from '../../../hooks/useEndpointData'; import AddCustomEmoji from './AddCustomEmoji'; import CustomEmoji from './CustomEmoji'; import EditCustomEmojiWithData from './EditCustomEmojiWithData'; @@ -20,24 +18,6 @@ function CustomEmojiRoute() { const canManageEmoji = usePermission('manage-emoji'); const t = useTranslation(); - - const [params, setParams] = useState(() => ({ text: '', current: 0, itemsPerPage: 25 })); - const [sort, setSort] = useState(() => ['name', 'asc']); - - const { text, itemsPerPage, current } = useDebouncedValue(params, 500); - const [column, direction] = useDebouncedValue(sort, 500); - const query = useMemo( - () => ({ - query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), - sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }), - [text, itemsPerPage, current, column, direction], - ); - - const { value: data, reload } = useEndpointData('emoji-custom.all', query); - const handleItemClick = (_id) => () => { route.push({ context: 'edit', @@ -45,16 +25,6 @@ function CustomEmojiRoute() { }); }; - const handleHeaderClick = (id) => { - setSort(([sortBy, sortDirection]) => { - if (sortBy === id) { - return [id, sortDirection === 'asc' ? 'desc' : 'asc']; - } - - return [id, 'asc']; - }); - }; - const handleNewButtonClick = useCallback(() => { route.push({ context: 'new' }); }, [route]); @@ -63,8 +33,10 @@ function CustomEmojiRoute() { route.push({}); }; + const reload = useRef(() => null); + const handleChange = useCallback(() => { - reload(); + reload.current(); }, [reload]); if (!canManageEmoji) { @@ -80,14 +52,7 @@ function CustomEmojiRoute() { - + {context && ( diff --git a/definition/rest/v1/emojiCustom.ts b/definition/rest/v1/emojiCustom.ts index 0a49286ecfd53..c97caa027463e 100644 --- a/definition/rest/v1/emojiCustom.ts +++ b/definition/rest/v1/emojiCustom.ts @@ -1,6 +1,17 @@ import type { ICustomEmojiDescriptor } from '../../ICustomEmojiDescriptor'; +import { PaginatedRequest } from '../helpers/PaginatedRequest'; +import { PaginatedResult } from '../helpers/PaginatedResult'; export type EmojiCustomEndpoints = { + 'emoji-custom.all': { + GET: ( + params: { query: string } & PaginatedRequest & { + sort: string; // {name: 'asc' | 'desc';}>; + } + ) => { + emojis: ICustomEmojiDescriptor[]; + } & PaginatedResult; + }; 'emoji-custom.list': { GET: (params: { query: string }) => { emojis?: {