From b5d32be229aa1795afd937ed9ca7e445301fb34a Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Fri, 26 Sep 2025 22:04:25 +0200 Subject: [PATCH] refactor: improve padding in dialog content and enhance row selection functionality in ArchivedChats and DataTable components --- .../Nav/SettingsTabs/Data/SharedLinks.tsx | 2 +- .../SettingsTabs/General/ArchivedChats.tsx | 6 +- .../src/components/DataTable/DataTable.tsx | 196 ++++++++++-------- 3 files changed, 117 insertions(+), 87 deletions(-) diff --git a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx index 0ed78fb30b..154dd3954d 100644 --- a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx @@ -323,7 +323,7 @@ export default function SharedLinks() { {localize('com_ui_manage')} - + {localize('com_nav_shared_links')} diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx index 9e1a0c368f..db0ac2ae7e 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx @@ -364,7 +364,7 @@ export default function ArchivedChatsTable() { {localize('com_ui_manage')} - + {localize('com_nav_archived_chats')} @@ -381,8 +381,8 @@ export default function ArchivedChatsTable() { debounce: 300, }, selection: { - enableRowSelection: false, - showCheckboxes: false, + enableRowSelection: true, + showCheckboxes: true, }, }} filterValue={searchValue} diff --git a/packages/client/src/components/DataTable/DataTable.tsx b/packages/client/src/components/DataTable/DataTable.tsx index 8f021c2adf..951e21128a 100644 --- a/packages/client/src/components/DataTable/DataTable.tsx +++ b/packages/client/src/components/DataTable/DataTable.tsx @@ -9,6 +9,7 @@ import { type VisibilityState, type ColumnDef, type Row, + type Table as TTable, } from '@tanstack/react-table'; import type { DataTableProps, ProcessedDataRow } from './DataTable.types'; import { SelectionCheckbox, MemoizedTableRow, SkeletonRows } from './DataTableComponents'; @@ -51,7 +52,7 @@ function DataTable, TValue>({ selection: { enableRowSelection = true, showCheckboxes = true } = {}, search: { enableSearch = true, debounce: debounceDelay = 300 } = {}, skeleton: { count: skeletonCount = 10 } = {}, - virtualization: { overscan = 5 } = {}, + virtualization: { overscan = 10 } = {}, } = config || {}; const [columnVisibility, setColumnVisibility] = useState({}); @@ -59,7 +60,27 @@ function DataTable, TValue>({ const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(filterValue); const [internalSorting, setInternalSorting] = useState(defaultSort); - const [isScrollingFetching, setIsScrollingFetching] = useState(false); + + const selectedCount = Object.keys(optimizedRowSelection).length; + const isAllSelected = useMemo( + () => data.length > 0 && selectedCount === data.length, + [data.length, selectedCount], + ); + const isIndeterminate = selectedCount > 0 && !isAllSelected; + + const getRowId = useCallback( + (row: TData, index?: number) => String(row.id ?? `row-${index ?? 0}`), + [], + ); + + const selectedRows = useMemo(() => { + if (Object.keys(optimizedRowSelection).length === 0) return []; + + const dataMap = new Map(data.map((item, index) => [getRowId(item, index), item])); + return Object.keys(optimizedRowSelection) + .map((id) => dataMap.get(id)) + .filter(Boolean) as TData[]; + }, [optimizedRowSelection, data, getRowId]); const cleanupTimers = useCallback(() => { if (scrollRAFRef.current) { @@ -75,17 +96,6 @@ function DataTable, TValue>({ const debouncedTerm = useDebounced(searchTerm, debounceDelay); const finalSorting = sorting ?? internalSorting; - const processedData = useMemo[]>(() => { - return data.map((item, index) => { - const id = item.id; - return { - ...item, - _id: String(id ?? `row-${index}`), - _index: index, - }; - }); - }, [data]); - const calculatedVisibility = useMemo(() => { const newVisibility: VisibilityState = {}; columns.forEach((col) => { @@ -127,27 +137,44 @@ function DataTable, TValue>({ } }, [data]); - const tableColumns = useMemo((): ColumnDef, TValue>[] => { + const tableColumns = useMemo((): ColumnDef[] => { if (!enableRowSelection || !showCheckboxes) { - return columns.map((col) => col as unknown as ColumnDef, TValue>); + return columns.map((col) => col as unknown as ColumnDef); } - const selectColumn: ColumnDef, TValue> = { + const selectColumn: ColumnDef = { id: 'select', - header: ({ table }) => ( -
- table.toggleAllRowsSelected(value)} - ariaLabel={localize('com_ui_select_all')} - /> -
- ), + header: () => { + const extraCheckboxProps = (isIndeterminate ? { indeterminate: true } : {}) as Record< + string, + unknown + >; + return ( +
+ { + if (isAllSelected || !value) { + setOptimizedRowSelection({}); + } else { + const allSelection = data.reduce>((acc, item, index) => { + acc[getRowId(item, index)] = true; + return acc; + }, {}); + setOptimizedRowSelection(allSelection); + } + }} + ariaLabel={localize('com_ui_select_all')} + {...extraCheckboxProps} + /> +
+ ); + }, cell: ({ row }) => { const rowDescription = row.original.name ? `named ${row.original.name}` @@ -172,16 +199,13 @@ function DataTable, TValue>({ }, }; - return [ - selectColumn, - ...columns.map((col) => col as unknown as ColumnDef, TValue>), - ]; + return [selectColumn, ...columns.map((col) => col as unknown as ColumnDef)]; }, [columns, enableRowSelection, showCheckboxes, localize]); - const table = useReactTable>({ - data: processedData, + const table = useReactTable({ + data, columns: tableColumns, - getRowId: (row) => row._id, + getRowId: getRowId, getCoreRowModel: getCoreRowModel(), enableRowSelection, enableMultiRowSelection: true, @@ -198,13 +222,13 @@ function DataTable, TValue>({ }); const rowVirtualizer = useVirtualizer({ - count: processedData.length, + count: data.length, getScrollElement: () => tableContainerRef.current, - estimateSize: useCallback(() => 50, []), + estimateSize: useCallback(() => 60, []), overscan, measureElement: - typeof window !== 'undefined' && navigator.userAgent.indexOf('Firefox') === -1 - ? (element) => element?.getBoundingClientRect().height ?? 50 + typeof window !== 'undefined' + ? (element) => element?.getBoundingClientRect().height ?? 60 : undefined, }); @@ -216,11 +240,20 @@ function DataTable, TValue>({ const { rows } = table.getRowModel(); const headerGroups = table.getHeaderGroups(); - const selectedCount = Object.keys(optimizedRowSelection).length; const showSkeletons = isLoading || (isFetching && !isFetchingNextPage); const shouldShowSearch = enableSearch && onFilterChange; + // useEffect(() => { + // if (data.length > 1000) { + // const cleanup = setTimeout(() => { + // rowVirtualizer.scrollToIndex(0, { align: 'start' }); + // rowVirtualizer.measure(); + // }, 1000); + // return () => clearTimeout(cleanup); + // } + // }, [data.length, rowVirtualizer]); + useEffect(() => { setSearchTerm(filterValue); }, [filterValue]); @@ -232,44 +265,28 @@ function DataTable, TValue>({ } }, [debouncedTerm, filterValue, onFilterChange, setOptimizedRowSelection]); - const handleScroll = useCallback(() => { - if (scrollRAFRef.current !== null) { - cancelAnimationFrame(scrollRAFRef.current); - } + const handleScroll = useMemo(() => { + let rafId: number | null = null; + let timeoutId: number | null = null; - scrollRAFRef.current = requestAnimationFrame(() => { - if (scrollTimeoutRef.current !== null) { - clearTimeout(scrollTimeoutRef.current); - } + return () => { + if (rafId) cancelAnimationFrame(rafId); - scrollTimeoutRef.current = window.setTimeout(() => { - if ( - !fetchNextPage || - !hasNextPage || - isFetchingNextPage || - isScrollingFetching || - !tableContainerRef.current - ) { - return; - } + rafId = requestAnimationFrame(() => { + if (timeoutId) clearTimeout(timeoutId); - const { scrollTop, scrollHeight, clientHeight } = tableContainerRef.current; - const scrollBottom = scrollTop + clientHeight; - const threshold = scrollHeight - 200; + timeoutId = window.setTimeout(() => { + const container = tableContainerRef.current; + if (!container || !fetchNextPage || !hasNextPage || isFetchingNextPage) return; - if (scrollBottom >= threshold) { - setIsScrollingFetching(true); - fetchNextPage().finally(() => { - setIsScrollingFetching(false); - }); - } - - scrollTimeoutRef.current = null; - }, 50); - - scrollRAFRef.current = null; - }); - }, [fetchNextPage, hasNextPage, isFetchingNextPage, isScrollingFetching]); + const { scrollTop, scrollHeight, clientHeight } = container; + if (scrollTop + clientHeight >= scrollHeight - 200) { + fetchNextPage().finally(); + } + }, 100); + }); + }; + }, [fetchNextPage, hasNextPage, isFetchingNextPage]); useEffect(() => { const scrollElement = tableContainerRef.current; @@ -315,11 +332,10 @@ function DataTable, TValue>({ {customActionsRenderer && customActionsRenderer({ selectedCount, - selectedRows: table.getSelectedRowModel().rows.map((r) => r.original as TData), - table, + selectedRows, + table: table as unknown as TTable>, })} -
, TValue>({ const isSelectHeader = header.id === 'select'; const meta = header.column.columnDef.meta as { className?: string } | undefined; const canSort = header.column.getCanSort(); - const sortAriaLabel = canSort - ? `${header.column.columnDef.header} column, ${header.column.getIsSorted() === 'asc' ? 'ascending' : header.column.getIsSorted() === 'desc' ? 'descending' : 'sortable'}` - : undefined; + let sortAriaLabel: string | undefined; + if (canSort) { + const sortState = header.column.getIsSorted(); + let sortStateLabel = 'sortable'; + if (sortState === 'asc') { + sortStateLabel = 'ascending'; + } else if (sortState === 'desc') { + sortStateLabel = 'descending'; + } + + const headerLabel = + typeof header.column.columnDef.header === 'string' + ? header.column.columnDef.header + : header.column.id; + + sortAriaLabel = `${headerLabel ?? ''} column, ${sortStateLabel}`; + } const handleSortingKeyDown = (e: React.KeyboardEvent) => { if (canSort && (e.key === 'Enter' || e.key === ' ')) { @@ -427,7 +457,7 @@ function DataTable, TValue>({ if (!row) return null; return ( } virtualIndex={virtualRow.index} />