diff --git a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx index 1e7f5803e..ef5511ac4 100644 --- a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx +++ b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx @@ -4,7 +4,7 @@ import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; import { AnimatePresence } from 'motion/react'; import { useOverlayScrollbars } from 'overlayscrollbars-react'; -import { +import React, { CSSProperties, Ref, UIEvent, @@ -31,6 +31,143 @@ import { import { ItemListHandle } from '/@/renderer/components/item-list/types'; import { LibraryItem } from '/@/shared/types/domain-types'; +interface VirtualizedGridListProps { + data: unknown[]; + enableExpansion: boolean; + enableSelection: boolean; + gap: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; + internalState: ItemListStateActions; + itemGridRef: React.RefObject; + itemType: LibraryItem; + onRowsRendered: (visibleRows: { startIndex: number; stopIndex: number }) => void; + onScroll: (e: UIEvent) => void; + tableMeta: null | { + columnCount: number; + itemHeight: number; + rowCount: number; + }; +} + +const VirtualizedGridList = React.memo( + ({ + data, + enableExpansion, + enableSelection, + gap, + internalState, + itemGridRef, + itemType, + onRowsRendered, + onScroll, + tableMeta, + }: VirtualizedGridListProps) => { + const elements = useMemo(() => { + if (!tableMeta) { + return []; + } + + return data + .map((d, i) => { + return { + data: d, + index: i, + }; + }) + .reduce( + (acc, d) => { + if (d.index % (tableMeta?.columnCount || 0) === 0) { + acc.push([]); + } + const prev = acc[acc.length - 1]; + prev.push(d); + return acc; + }, + [] as { data: any; index: number }[][], + ); + }, [tableMeta, data]); + + const itemProps: GridItemProps = { + columns: tableMeta?.columnCount || 0, + data: elements, + enableExpansion, + enableSelection, + gap, + internalState, + itemType, + }; + + return ( + + ); + }, +); + +VirtualizedGridList.displayName = 'VirtualizedGridList'; + +// Throttled function moved outside component for better performance +const createThrottledSetTableMeta = (itemsPerRow?: number) => { + return throttle( + ( + width: number, + dataLength: number, + type: LibraryItem, + setTableMeta: (meta: any) => void, + ) => { + const isSm = width >= 600; + const isMd = width >= 768; + const isLg = width >= 960; + const isXl = width >= 1200; + const is2xl = width >= 1440; + const is3xl = width >= 1920; + const is4xl = width >= 2560; + + let dynamicItemsPerRow = 2; + + if (is4xl) { + dynamicItemsPerRow = 12; + } else if (is3xl) { + dynamicItemsPerRow = 10; + } else if (is2xl) { + dynamicItemsPerRow = 8; + } else if (isXl) { + dynamicItemsPerRow = 6; + } else if (isLg) { + dynamicItemsPerRow = 5; + } else if (isMd) { + dynamicItemsPerRow = 4; + } else if (isSm) { + dynamicItemsPerRow = 3; + } else { + dynamicItemsPerRow = 2; + } + + const setItemsPerRow = itemsPerRow || dynamicItemsPerRow; + + const widthPerItem = Number(width) / setItemsPerRow; + const itemHeight = widthPerItem + getDataRowsCount(type) * 26; + + if (widthPerItem === 0) { + return; + } + + setTableMeta({ + columnCount: setItemsPerRow, + itemHeight, + rowCount: Math.ceil(dataLength / setItemsPerRow), + }); + }, + 200, + ); +}; + export interface GridItemProps { columns: number; data: any[]; @@ -155,56 +292,13 @@ export const ItemGridList = ({ rowCount: number; }>(null); - // Throttled function to update table meta + // Use throttled function created outside component for better performance const throttledSetTableMeta = useMemo(() => { - return throttle((width: number, dataLength: number, type: LibraryItem) => { - const isSm = width >= 600; - const isMd = width >= 768; - const isLg = width >= 960; - const isXl = width >= 1200; - const is2xl = width >= 1440; - const is3xl = width >= 1920; - const is4xl = width >= 2560; - - let dynamicItemsPerRow = 2; - - if (is4xl) { - dynamicItemsPerRow = 12; - } else if (is3xl) { - dynamicItemsPerRow = 10; - } else if (is2xl) { - dynamicItemsPerRow = 8; - } else if (isXl) { - dynamicItemsPerRow = 6; - } else if (isLg) { - dynamicItemsPerRow = 5; - } else if (isMd) { - dynamicItemsPerRow = 4; - } else if (isSm) { - dynamicItemsPerRow = 3; - } else { - dynamicItemsPerRow = 2; - } - - const setItemsPerRow = itemsPerRow || dynamicItemsPerRow; - - const widthPerItem = Number(width) / setItemsPerRow; - const itemHeight = widthPerItem + getDataRowsCount(type) * 26; - - if (widthPerItem === 0) { - return; - } - - setTableMeta({ - columnCount: setItemsPerRow, - itemHeight, - rowCount: Math.ceil(dataLength / setItemsPerRow), - }); - }, 200); + return createThrottledSetTableMeta(itemsPerRow); }, [itemsPerRow]); useLayoutEffect(() => { - throttledSetTableMeta(containerWidth, data.length, itemType); + throttledSetTableMeta(containerWidth, data.length, itemType, setTableMeta); }, [containerWidth, data.length, itemType, throttledSetTableMeta]); const handleOnRowsRendered = useCallback( @@ -237,41 +331,6 @@ export const ItemGridList = ({ ], ); - const elements = useMemo(() => { - if (!tableMeta) { - return []; - } - - return data - .map((d, i) => { - return { - data: d, - index: i, - }; - }) - .reduce( - (acc, d) => { - if (d.index % (tableMeta?.columnCount || 0) === 0) { - acc.push([]); - } - const prev = acc[acc.length - 1]; - prev.push(d); - return acc; - }, - [] as { data: any; index: number }[][], - ); - }, [tableMeta, data]); - - const itemProps: GridItemProps = { - columns: tableMeta?.columnCount || 0, - data: elements, - enableExpansion, - enableSelection, - gap, - internalState, - itemType, - }; - useEffect(() => { if (!initialTop || isInitialScrollPositionSet.current || !tableMeta?.itemHeight) return; isInitialScrollPositionSet.current = true; @@ -319,14 +378,17 @@ export const ItemGridList = ({ data-overlayscrollbars-initialize="" ref={mergedContainerRef} > - {hasExpanded && ( diff --git a/src/renderer/components/item-list/item-table-list/item-table-list.tsx b/src/renderer/components/item-list/item-table-list/item-table-list.tsx index 434fc1c91..102c0d1c9 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list.tsx @@ -4,7 +4,7 @@ import { useMergedRef } from '@mantine/hooks'; import clsx from 'clsx'; import { AnimatePresence } from 'motion/react'; import { useOverlayScrollbars } from 'overlayscrollbars-react'; -import { +import React, { type JSXElementConstructor, Ref, useCallback, @@ -28,6 +28,354 @@ import { parseTableColumns } from '/@/renderer/components/item-list/helpers/pars import { ItemListHandle, ItemTableListColumnConfig } from '/@/renderer/components/item-list/types'; import { LibraryItem } from '/@/shared/types/domain-types'; +interface VirtualizedTableGridProps { + calculatedColumnWidths: number[]; + CellComponent: JSXElementConstructor>; + cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; + data: unknown[]; + enableAlternateRowColors: boolean; + enableExpansion: boolean; + enableHeader: boolean; + enableHorizontalBorders: boolean; + enableRowHoverHighlight: boolean; + enableSelection: boolean; + enableVerticalBorders: boolean; + getRowHeight: (index: number, cellProps: TableItemProps) => number; + headerHeight: number; + internalState: ItemListStateActions; + itemType: LibraryItem; + mergedRowRef: React.Ref; + onCellsRendered?: GridProps['onCellsRendered']; + parsedColumns: ReturnType; + pinnedLeftColumnCount: number; + pinnedLeftColumnRef: React.RefObject; + pinnedRightColumnCount: number; + pinnedRightColumnRef: React.RefObject; + pinnedRowCount: number; + pinnedRowRef: React.RefObject; + showLeftShadow: boolean; + showRightShadow: boolean; + size: 'compact' | 'default' | 'large'; + totalColumnCount: number; + totalRowCount: number; +} + +const VirtualizedTableGrid = React.memo( + ({ + calculatedColumnWidths, + CellComponent, + cellPadding, + data, + enableAlternateRowColors, + enableExpansion, + enableHeader, + enableHorizontalBorders, + enableRowHoverHighlight, + enableSelection, + enableVerticalBorders, + getRowHeight, + headerHeight, + internalState, + itemType, + mergedRowRef, + onCellsRendered, + parsedColumns, + pinnedLeftColumnCount, + pinnedLeftColumnRef, + pinnedRightColumnCount, + pinnedRightColumnRef, + pinnedRowCount, + pinnedRowRef, + showLeftShadow, + showRightShadow, + size, + totalColumnCount, + totalRowCount, + }: VirtualizedTableGridProps) => { + const columnWidth = useCallback( + (index: number) => calculatedColumnWidths[index], + [calculatedColumnWidths], + ); + + const itemProps: TableItemProps = useMemo( + () => ({ + cellPadding, + columns: parsedColumns, + data: enableHeader ? [null, ...data] : data, + enableAlternateRowColors, + enableExpansion, + enableHeader, + enableHorizontalBorders, + enableRowHoverHighlight, + enableSelection, + enableVerticalBorders, + getRowHeight, + internalState, + itemType, + size, + }), + [ + cellPadding, + parsedColumns, + enableHeader, + data, + enableAlternateRowColors, + enableExpansion, + enableHorizontalBorders, + enableRowHoverHighlight, + enableSelection, + enableVerticalBorders, + getRowHeight, + internalState, + itemType, + size, + ], + ); + + const PinnedRowCell = useCallback( + (cellProps: CellComponentProps & TableItemProps) => { + return ( + + ); + }, + [pinnedLeftColumnCount, CellComponent], + ); + + const PinnedColumnCell = useCallback( + (cellProps: CellComponentProps & TableItemProps) => { + return ( + + ); + }, + [pinnedRowCount, CellComponent], + ); + + const PinnedRightColumnCell = useCallback( + (cellProps: CellComponentProps & TableItemProps) => { + return ( + + ); + }, + [pinnedLeftColumnCount, pinnedRowCount, totalColumnCount, CellComponent], + ); + + const PinnedRightIntersectionCell = useCallback( + (cellProps: CellComponentProps & TableItemProps) => { + return ( + + ); + }, + [pinnedLeftColumnCount, totalColumnCount, CellComponent], + ); + + const RowCell = useCallback( + (cellProps: CellComponentProps) => { + return ( + + ); + }, + [pinnedLeftColumnCount, pinnedRowCount, CellComponent], + ); + + return ( +
+
0).reduce( + (a, _, i) => a + columnWidth(i), + 0, + )}px`, + }} + > + {!!(pinnedLeftColumnCount || pinnedRowCount) && ( +
0, + ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`, + }} + > + + {enableHeader &&
} +
+ )} + {!!pinnedLeftColumnCount && ( +
+ { + return getRowHeight(index + pinnedRowCount, cellProps); + }} + /> +
+ )} +
+
+ {!!pinnedRowCount && ( +
0, + ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`, + } as React.CSSProperties + } + > + { + return columnWidth(index + pinnedLeftColumnCount); + }} + rowCount={Array.from({ length: pinnedRowCount }, () => 0).length} + rowHeight={getRowHeight} + /> + {enableHeader &&
} +
+ )} +
+ { + return columnWidth(index + pinnedLeftColumnCount); + }} + onCellsRendered={onCellsRendered} + rowCount={totalRowCount} + rowHeight={(index, cellProps) => { + return getRowHeight(index + pinnedRowCount, cellProps); + }} + /> + {pinnedLeftColumnCount > 0 && showLeftShadow && ( +
+ )} + {pinnedRightColumnCount > 0 && showRightShadow && ( +
+ )} +
+
+ {!!pinnedRightColumnCount && ( +
0, + ).reduce( + (a, _, i) => + a + columnWidth(i + pinnedLeftColumnCount + totalColumnCount), + 0, + )}px`, + }} + > + {!!(pinnedRightColumnCount || pinnedRowCount) && ( +
0, + ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`, + }} + > + { + return columnWidth( + index + pinnedLeftColumnCount + totalColumnCount, + ); + }} + rowCount={pinnedRowCount} + rowHeight={getRowHeight} + /> + {enableHeader && ( +
+ )} +
+ )} +
+ { + return columnWidth( + index + pinnedLeftColumnCount + totalColumnCount, + ); + }} + rowCount={totalRowCount} + rowHeight={(index, cellProps) => { + return getRowHeight(index + pinnedRowCount, cellProps); + }} + /> +
+
+ )} +
+ ); + }, +); + +VirtualizedTableGrid.displayName = 'VirtualizedTableGrid'; + export interface TableItemProps { cellPadding?: ItemTableListProps['cellPadding']; columns: ItemTableListColumnConfig[]; @@ -166,9 +514,6 @@ export const ItemTableList = ({ return distributed; }, [parsedColumns, centerContainerWidth]); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const columnWidth = (index: number, _cellProps: TableItemProps) => - calculatedColumnWidths[index]; const pinnedLeftColumnCount = parsedColumns.filter((col) => col.pinned === 'left').length; const pinnedRightColumnCount = parsedColumns.filter((col) => col.pinned === 'right').length; @@ -602,80 +947,6 @@ export const ItemTableList = ({ ], ); - const PinnedRowCell = useCallback( - (cellProps: CellComponentProps & TableItemProps) => { - return ( - - ); - }, - [pinnedLeftColumnCount, CellComponent], - ); - - const PinnedColumnCell = useCallback( - (cellProps: CellComponentProps & TableItemProps) => { - return ; - }, - [pinnedRowCount, CellComponent], - ); - - const PinnedRightColumnCell = useCallback( - (cellProps: CellComponentProps & TableItemProps) => { - return ( - - ); - }, - [pinnedLeftColumnCount, pinnedRowCount, totalColumnCount, CellComponent], - ); - - const PinnedRightIntersectionCell = useCallback( - (cellProps: CellComponentProps & TableItemProps) => { - return ( - - ); - }, - [pinnedLeftColumnCount, totalColumnCount, CellComponent], - ); - - const RowCell = useCallback( - (cellProps: CellComponentProps) => { - return ( - - ); - }, - [pinnedLeftColumnCount, pinnedRowCount, CellComponent], - ); - - const itemProps: TableItemProps = { - cellPadding, - columns: parsedColumns, - data: enableHeader ? [null, ...data] : data, - enableAlternateRowColors, - enableExpansion, - enableHeader, - enableHorizontalBorders, - enableRowHoverHighlight, - enableSelection, - enableVerticalBorders, - getRowHeight, - internalState, - itemType, - size, - }; - const isInitialScrollPositionSet = useRef(false); useEffect(() => { @@ -718,186 +989,37 @@ export const ItemTableList = ({ return (
-
-
0).reduce( - (a, _, i) => a + columnWidth(i, itemProps), - 0, - )}px`, - }} - > - {!!(pinnedLeftColumnCount || pinnedRowCount) && ( -
0, - ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`, - }} - > - - {enableHeader &&
} -
- )} - {!!pinnedLeftColumnCount && ( -
- { - return getRowHeight(index + pinnedRowCount, cellProps); - }} - /> -
- )} -
-
- {!!pinnedRowCount && ( -
0, - ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`, - } as React.CSSProperties - } - > - { - return columnWidth(index + pinnedLeftColumnCount, cellProps); - }} - rowCount={Array.from({ length: pinnedRowCount }, () => 0).length} - rowHeight={getRowHeight} - /> - {enableHeader &&
} -
- )} -
- { - return columnWidth(index + pinnedLeftColumnCount, cellProps); - }} - onCellsRendered={handleOnCellsRendered} - rowCount={totalRowCount} - rowHeight={(index, cellProps) => { - return getRowHeight(index + pinnedRowCount, cellProps); - }} - /> - {pinnedLeftColumnCount > 0 && showLeftShadow && ( -
- )} - {pinnedRightColumnCount > 0 && showRightShadow && ( -
- )} -
-
- {!!pinnedRightColumnCount && ( -
0, - ).reduce( - (a, _, i) => - a + - columnWidth( - i + pinnedLeftColumnCount + totalColumnCount, - itemProps, - ), - 0, - )}px`, - }} - > - {!!(pinnedRightColumnCount || pinnedRowCount) && ( -
0, - ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`, - }} - > - { - return columnWidth( - index + pinnedLeftColumnCount + totalColumnCount, - cellProps, - ); - }} - rowCount={pinnedRowCount} - rowHeight={getRowHeight} - /> - {enableHeader && ( -
- )} -
- )} -
- { - return columnWidth( - index + pinnedLeftColumnCount + totalColumnCount, - cellProps, - ); - }} - rowCount={totalRowCount} - rowHeight={(index, cellProps) => { - return getRowHeight(index + pinnedRowCount, cellProps); - }} - /> -
-
- )} -
+ {hasExpanded && (