From 141a20f04293167496ba424b93c5ccac372367dc Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 4 Apr 2026 12:34:27 -0700 Subject: [PATCH] refactor item table props --- .../cell-component-factory.tsx | 3 +- .../item-table-list/columns/date-column.tsx | 27 +- .../item-table-list/columns/title-column.tsx | 13 +- .../hooks/use-item-drag-drop-state.tsx | 462 +++++++++--------- .../item-table-list-column.tsx | 5 +- .../item-table-list-context.tsx | 30 +- .../item-table-list/item-table-list.tsx | 292 ++++------- .../item-table-list/memoized-cell-router.tsx | 21 +- 8 files changed, 395 insertions(+), 458 deletions(-) diff --git a/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx b/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx index a641edcf6..dc04a20d4 100644 --- a/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx +++ b/src/renderer/components/item-list/item-table-list/cell-component-factory.tsx @@ -20,7 +20,8 @@ export const createColumnCellComponent = ( prevProps.columnIndex === nextProps.columnIndex && prevProps.data === nextProps.data && prevProps.style === nextProps.style && - prevProps.columns === nextProps.columns + prevProps.columns === nextProps.columns && + prevProps.playlistId === nextProps.playlistId ); }, ); diff --git a/src/renderer/components/item-list/item-table-list/columns/date-column.tsx b/src/renderer/components/item-list/item-table-list/columns/date-column.tsx index 27a6c8cd1..22fb245be 100644 --- a/src/renderer/components/item-list/item-table-list/columns/date-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/date-column.tsx @@ -18,10 +18,15 @@ const DateColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; - if (typeof row === 'string' && row) { + const formattedAbsolute = useMemo( + () => (typeof row === 'string' && row ? formatDateAbsolute(row) : null), + [row], + ); + + if (formattedAbsolute) { return ( - {formatDateAbsolute(row)} + {formattedAbsolute} ); } @@ -63,6 +68,11 @@ const AbsoluteDateColumnBase = (props: ItemTableListInnerColumn) => { return null; }, [props.type, rowItem]); + const formattedIsoFallback = useMemo( + () => (typeof row === 'string' && row ? formatPartialIsoDateUTC(row) : null), + [row], + ); + if (props.type === TableColumn.RELEASE_DATE) { if (releaseDateContent) { return ( @@ -72,10 +82,10 @@ const AbsoluteDateColumnBase = (props: ItemTableListInnerColumn) => { ); } - if (typeof row === 'string' && row) { + if (formattedIsoFallback) { return ( - {formatPartialIsoDateUTC(row)} + {formattedIsoFallback} ); } @@ -96,10 +106,15 @@ const RelativeDateColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; - if (typeof row === 'string') { + const formattedRelative = useMemo(() => { + if (typeof row !== 'string') return null; + return formatDateRelative(row); + }, [row]); + + if (formattedRelative !== null) { return ( - {formatDateRelative(row)} + {formattedRelative} ); } diff --git a/src/renderer/components/item-list/item-table-list/columns/title-column.tsx b/src/renderer/components/item-list/item-table-list/columns/title-column.tsx index 9af74f149..3c98e315b 100644 --- a/src/renderer/components/item-list/item-table-list/columns/title-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/title-column.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx'; +import { useMemo } from 'react'; import { Link } from 'react-router'; import styles from './title-column.module.css'; @@ -35,8 +36,12 @@ function DefaultTitleColumn(props: ItemTableListInnerColumn) { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const row: string | undefined = rowItem?.[props.columns[props.columnIndex].id]; + const path = useMemo(() => { + if (typeof row !== 'string' || !rowItem || !(rowItem as any).id) return undefined; + return getTitlePath(props.itemType, (rowItem as any).id as string); + }, [props.itemType, row, rowItem]); + if (typeof row === 'string') { - const path = getTitlePath(props.itemType, (rowItem as any).id as string); const item = rowItem as any; const titleLinkProps = path @@ -80,8 +85,12 @@ function QueueSongTitleColumn(props: ItemTableListInnerColumn) { const song = rowItem as QueueSong; const isActive = useIsActiveRow(song?.id, song?._uniqueId); + const path = useMemo(() => { + if (typeof row !== 'string' || !rowItem || !(rowItem as any).id) return undefined; + return getTitlePath(props.itemType, (rowItem as any).id as string); + }, [props.itemType, row, rowItem]); + if (typeof row === 'string') { - const path = getTitlePath(props.itemType, (rowItem as any).id as string); const item = rowItem as any; const titleLinkProps = path diff --git a/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx b/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx index ee586cfb8..b8b89e73c 100644 --- a/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx +++ b/src/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state.tsx @@ -34,256 +34,268 @@ export const useItemDragDropState = => { const shouldEnableDrag = enableDrag && isDataRow && !!item; + const needsDropRegistration = + shouldEnableDrag && + (itemType === LibraryItem.QUEUE_SONG || itemType === LibraryItem.PLAYLIST_SONG); + const { isDraggedOver, isDragging: isDraggingLocal, ref: dragRef, } = useDragDrop({ - drag: { - getId: () => { - if (!item || !isDataRow) { - return []; - } + drag: shouldEnableDrag + ? { + getId: () => { + if (!item || !isDataRow) { + return []; + } - const draggedItems = getDraggedItems(item as any, internalState); + const draggedItems = getDraggedItems(item as any, internalState); - return draggedItems.map((draggedItem) => draggedItem.id); - }, - getItem: () => { - if (!item || !isDataRow) { - return []; - } + return draggedItems.map((draggedItem) => draggedItem.id); + }, + getItem: () => { + if (!item || !isDataRow) { + return []; + } - const draggedItems = getDraggedItems(item as any, internalState); + const draggedItems = getDraggedItems(item as any, internalState); - return draggedItems; - }, - itemType, - onDragStart: () => { - if (!item || !isDataRow) { - return; - } + return draggedItems; + }, + itemType, + onDragStart: () => { + if (!item || !isDataRow) { + return; + } - const draggedItems = getDraggedItems(item as any, internalState); - if (internalState) { - internalState.setDragging(draggedItems); - } - }, - onDrop: () => { - if (internalState) { - internalState.setDragging([]); - } - }, - operation: - itemType === LibraryItem.QUEUE_SONG - ? [DragOperation.REORDER, DragOperation.ADD] - : itemType === LibraryItem.PLAYLIST_SONG - ? [DragOperation.REORDER, DragOperation.ADD] - : [DragOperation.ADD], - target: DragTargetMap[itemType] || DragTarget.GENERIC, - }, - drop: { - canDrop: (args) => { - if (args.source.type === DragTarget.TABLE_COLUMN) { - return false; - } + const draggedItems = getDraggedItems(item as any, internalState); + if (internalState) { + internalState.setDragging(draggedItems); + } + }, + onDrop: () => { + if (internalState) { + internalState.setDragging([]); + } + }, + operation: + itemType === LibraryItem.QUEUE_SONG + ? [DragOperation.REORDER, DragOperation.ADD] + : itemType === LibraryItem.PLAYLIST_SONG + ? [DragOperation.REORDER, DragOperation.ADD] + : [DragOperation.ADD], + target: DragTargetMap[itemType] || DragTarget.GENERIC, + } + : undefined, + drop: needsDropRegistration + ? { + canDrop: (args) => { + if (args.source.type === DragTarget.TABLE_COLUMN) { + return false; + } - // Allow drops for QUEUE_SONG (queue reordering) - if (itemType === LibraryItem.QUEUE_SONG) { - return true; - } + // Allow drops for QUEUE_SONG (queue reordering) + if (itemType === LibraryItem.QUEUE_SONG) { + return true; + } - // Allow drops for PLAYLIST_SONG (playlist reordering) - // Only allow drops when drag is started from the reorder handle - if ( - itemType === LibraryItem.PLAYLIST_SONG && - args.source.itemType === LibraryItem.PLAYLIST_SONG && - args.source.metadata?.fromReorderHandle === true - ) { - return true; - } + // Allow drops for PLAYLIST_SONG (playlist reordering) + // Only allow drops when drag is started from the reorder handle + if ( + itemType === LibraryItem.PLAYLIST_SONG && + args.source.itemType === LibraryItem.PLAYLIST_SONG && + args.source.metadata?.fromReorderHandle === true + ) { + return true; + } - return false; - }, - getData: () => { - return { - id: [(item as unknown as { id: string }).id], - item: [item as unknown as unknown[]], - itemType, - type: DragTargetMap[itemType] || DragTarget.GENERIC, - }; - }, - onDrag: () => { - return; - }, - onDragLeave: () => { - return; - }, - onDrop: (args) => { - if (args.self.type === DragTarget.QUEUE_SONG) { - const sourceServerId = ( - args.source.item?.[0] as unknown as { _serverId: string } - )._serverId; + return false; + }, + getData: () => { + return { + id: [(item as unknown as { id: string }).id], + item: [item as unknown as unknown[]], + itemType, + type: DragTargetMap[itemType] || DragTarget.GENERIC, + }; + }, + onDrag: () => { + return; + }, + onDragLeave: () => { + return; + }, + onDrop: (args) => { + if (args.self.type === DragTarget.QUEUE_SONG) { + const sourceServerId = ( + args.source.item?.[0] as unknown as { _serverId: string } + )._serverId; - const sourceItemType = args.source.itemType as LibraryItem; + const sourceItemType = args.source.itemType as LibraryItem; - const droppedOnUniqueId = ( - args.self.item?.[0] as unknown as { _uniqueId: string } - )._uniqueId; + const droppedOnUniqueId = ( + args.self.item?.[0] as unknown as { _uniqueId: string } + )._uniqueId; - switch (args.source.type) { - case DragTarget.ALBUM: { - playerContext.addToQueueByFetch( - sourceServerId, - args.source.id, - sourceItemType, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - break; - } - case DragTarget.ALBUM_ARTIST: { - playerContext.addToQueueByFetch( - sourceServerId, - args.source.id, - sourceItemType, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - break; - } - case DragTarget.ARTIST: { - playerContext.addToQueueByFetch( - sourceServerId, - args.source.id, - sourceItemType, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - break; - } - case DragTarget.FOLDER: { - const items = args.source.item; + switch (args.source.type) { + case DragTarget.ALBUM: { + playerContext.addToQueueByFetch( + sourceServerId, + args.source.id, + sourceItemType, + { edge: args.edge, uniqueId: droppedOnUniqueId }, + ); + break; + } + case DragTarget.ALBUM_ARTIST: { + playerContext.addToQueueByFetch( + sourceServerId, + args.source.id, + sourceItemType, + { edge: args.edge, uniqueId: droppedOnUniqueId }, + ); + break; + } + case DragTarget.ARTIST: { + playerContext.addToQueueByFetch( + sourceServerId, + args.source.id, + sourceItemType, + { edge: args.edge, uniqueId: droppedOnUniqueId }, + ); + break; + } + case DragTarget.FOLDER: { + const items = args.source.item; - const { folders, songs } = (items || []).reduce<{ - folders: Folder[]; - songs: Song[]; - }>( - (acc, item) => { - if ((item as unknown as Song)._itemType === LibraryItem.SONG) { - acc.songs.push(item as unknown as Song); - } else if ( - (item as unknown as Folder)._itemType === LibraryItem.FOLDER - ) { - acc.folders.push(item as unknown as Folder); - } - return acc; - }, - { folders: [], songs: [] }, - ); + const { folders, songs } = (items || []).reduce<{ + folders: Folder[]; + songs: Song[]; + }>( + (acc, item) => { + if ( + (item as unknown as Song)._itemType === + LibraryItem.SONG + ) { + acc.songs.push(item as unknown as Song); + } else if ( + (item as unknown as Folder)._itemType === + LibraryItem.FOLDER + ) { + acc.folders.push(item as unknown as Folder); + } + return acc; + }, + { folders: [], songs: [] }, + ); - const folderIds = folders.map((folder) => folder.id); + const folderIds = folders.map((folder) => folder.id); - // Handle folders: fetch and add to queue - if (folderIds.length > 0) { - playerContext.addToQueueByFetch( - sourceServerId, - folderIds, - LibraryItem.FOLDER, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - } + // Handle folders: fetch and add to queue + if (folderIds.length > 0) { + playerContext.addToQueueByFetch( + sourceServerId, + folderIds, + LibraryItem.FOLDER, + { edge: args.edge, uniqueId: droppedOnUniqueId }, + ); + } - // Handle songs: add directly to queue - if (songs.length > 0) { - playerContext.addToQueueByData(songs, { - edge: args.edge, - uniqueId: droppedOnUniqueId, - }); - } + // Handle songs: add directly to queue + if (songs.length > 0) { + playerContext.addToQueueByData(songs, { + edge: args.edge, + uniqueId: droppedOnUniqueId, + }); + } - break; - } - case DragTarget.GENRE: { - playerContext.addToQueueByFetch( - sourceServerId, - args.source.id, - sourceItemType, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - break; - } - case DragTarget.PLAYLIST: { - playerContext.addToQueueByFetch( - sourceServerId, - args.source.id, - sourceItemType, - { edge: args.edge, uniqueId: droppedOnUniqueId }, - ); - break; - } - case DragTarget.QUEUE_SONG: { - const sourceItems = (args.source.item || []) as QueueSong[]; - if ( - sourceItems.length > 0 && - args.edge && - (args.edge === 'top' || args.edge === 'bottom') - ) { - playerContext.moveSelectedTo( - sourceItems, - args.edge, - droppedOnUniqueId, - ); - } - break; - } - case DragTarget.SONG: { - const sourceItems = (args.source.item || []) as Song[]; - if (sourceItems.length > 0) { - playerContext.addToQueueByData(sourceItems, { - edge: args.edge, - uniqueId: droppedOnUniqueId, - }); - } - break; - } - default: { - break; - } - } - } + break; + } + case DragTarget.GENRE: { + playerContext.addToQueueByFetch( + sourceServerId, + args.source.id, + sourceItemType, + { edge: args.edge, uniqueId: droppedOnUniqueId }, + ); + break; + } + case DragTarget.PLAYLIST: { + playerContext.addToQueueByFetch( + sourceServerId, + args.source.id, + sourceItemType, + { edge: args.edge, uniqueId: droppedOnUniqueId }, + ); + break; + } + case DragTarget.QUEUE_SONG: { + const sourceItems = (args.source.item || []) as QueueSong[]; + if ( + sourceItems.length > 0 && + args.edge && + (args.edge === 'top' || args.edge === 'bottom') + ) { + playerContext.moveSelectedTo( + sourceItems, + args.edge, + droppedOnUniqueId, + ); + } + break; + } + case DragTarget.SONG: { + const sourceItems = (args.source.item || []) as Song[]; + if (sourceItems.length > 0) { + playerContext.addToQueueByData(sourceItems, { + edge: args.edge, + uniqueId: droppedOnUniqueId, + }); + } + break; + } + default: { + break; + } + } + } - // Handle PLAYLIST_SONG reordering - // Only allow drops when drag is started from the reorder handle - if ( - args.self.itemType === LibraryItem.PLAYLIST_SONG && - args.source.itemType === LibraryItem.PLAYLIST_SONG && - args.source.metadata?.fromReorderHandle === true && - playlistId - ) { - const sourceItems = (args.source.item || []) as any[]; - const targetItem = item as any; + // Handle PLAYLIST_SONG reordering + // Only allow drops when drag is started from the reorder handle + if ( + args.self.itemType === LibraryItem.PLAYLIST_SONG && + args.source.itemType === LibraryItem.PLAYLIST_SONG && + args.source.metadata?.fromReorderHandle === true && + playlistId + ) { + const sourceItems = (args.source.item || []) as any[]; + const targetItem = item as any; - if ( - sourceItems.length > 0 && - args.edge && - (args.edge === 'top' || args.edge === 'bottom') && - targetItem - ) { - // Emit event to reorder playlist songs - eventEmitter.emit('PLAYLIST_REORDER', { - edge: args.edge, - playlistId, - sourceIds: args.source.id, - targetId: targetItem.id, - }); - } - } + if ( + sourceItems.length > 0 && + args.edge && + (args.edge === 'top' || args.edge === 'bottom') && + targetItem + ) { + // Emit event to reorder playlist songs + eventEmitter.emit('PLAYLIST_REORDER', { + edge: args.edge, + playlistId, + sourceIds: args.source.id, + targetId: targetItem.id, + }); + } + } - if (internalState) { - internalState.setDragging([]); - } + if (internalState) { + internalState.setDragging([]); + } - return; - }, - }, + return; + }, + } + : undefined, isEnabled: shouldEnableDrag, }); diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx index fe3968480..ea2a5cebf 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx @@ -19,7 +19,6 @@ import React, { useRef, useState, } from 'react'; -import { useParams } from 'react-router'; import { CellComponentProps } from 'react-window-v2'; import styles from './item-table-list-column.module.css'; @@ -82,7 +81,6 @@ export interface ItemTableListInnerColumn extends ItemTableListColumn { } const ItemTableListColumnBase = (props: ItemTableListColumn) => { - const { playlistId } = useParams() as { playlistId?: string }; const type = props.columnType ?? (props.columns[props.columnIndex].id as TableColumn); const isHeaderEnabled = !!props.enableHeader; @@ -135,7 +133,7 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => { item, itemType: props.itemType, playerContext: props.playerContext, - playlistId, + playlistId: props.playlistId, }); const controls = props.controls; @@ -362,6 +360,7 @@ export const ItemTableListColumn = memo(ItemTableListColumnBase, (prevProps, nex prevProps.enableColumnResize === nextProps.enableColumnResize && prevProps.enableColumnReorder === nextProps.enableColumnReorder && prevProps.cellPadding === nextProps.cellPadding && + prevProps.playlistId === nextProps.playlistId && prevItem === nextItem ); }); diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-context.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-context.tsx index 4cf4db435..eca4ebc92 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-context.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list-context.tsx @@ -1,31 +1,51 @@ +import type { ReactElement } from 'react'; + import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import { useSyncExternalStore } from 'react'; +import type { TableItemProps } from './item-table-list'; + import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemControls, ItemTableListColumnConfig } from '/@/renderer/components/item-list/types'; import { PlayerContext } from '/@/renderer/features/player/context/player-context'; import { LibraryItem } from '/@/shared/types/domain-types'; -/** - * Stage A/B: Provide table-scoped config + external stores so churny values can update - * without forcing `cellProps` identity changes (and therefore without rerendering every visible cell). - */ - export type ItemTableListConfig = { cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; columns: ItemTableListColumnConfig[]; controls: ItemControls; + enableAlternateRowColors: boolean; + enableColumnReorder: boolean; + enableColumnResize: boolean; + enableDrag: boolean; + enableExpansion: boolean; enableHeader: boolean; + enableHorizontalBorders: boolean; enableRowHoverHighlight: boolean; enableSelection: boolean; + enableVerticalBorders: boolean; + getRowHeight: (index: number, cellProps: TableItemProps) => number; + groups?: ItemTableListGroupHeader[]; internalState: ItemListStateActions; itemType: LibraryItem; playerContext: PlayerContext; + playlistId?: string; size: 'compact' | 'default' | 'large'; startRowIndex?: number; tableId: string; }; +export type ItemTableListGroupHeader = { + itemCount: number; + render: (props: { + data: unknown[]; + groupIndex: number; + index: number; + internalState: ItemListStateActions; + startDataIndex: number; + }) => ReactElement; +}; + const ItemTableListConfigContext = createContext(null); export const ItemTableListConfigProvider = ({ 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 4bf34fea3..cdcf953c6 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 @@ -15,6 +15,7 @@ import React, { useRef, useState, } from 'react'; +import { useParams } from 'react-router'; import { type CellComponentProps, Grid } from 'react-window-v2'; import styles from './item-table-list.module.css'; @@ -43,6 +44,7 @@ import { useTableRowModel } from '/@/renderer/components/item-list/item-table-li import { useTableScrollToIndex } from '/@/renderer/components/item-list/item-table-list/hooks/use-table-scroll-to-index'; import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { + type ItemTableListConfig, ItemTableListConfigProvider, ItemTableListStoreProvider, } from '/@/renderer/components/item-list/item-table-list/item-table-list-context'; @@ -104,27 +106,11 @@ export enum TableItemSize { interface VirtualizedTableGridProps { calculatedColumnWidths: number[]; CellComponent: JSXElementConstructor>; - cellPadding: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; - controls: ItemControls; data: unknown[]; dataWithGroups: (null | unknown)[]; - enableAlternateRowColors: boolean; - enableColumnReorder: boolean; - enableColumnResize: boolean; - enableDrag?: boolean; - enableExpansion: boolean; - enableHeader: boolean; - enableHorizontalBorders: boolean; - enableRowHoverHighlight: boolean; enableScrollShadow: boolean; - enableSelection: boolean; - enableVerticalBorders: boolean; getItem?: (index: number) => undefined | unknown; - getRowHeight: (index: number, cellProps: TableItemProps) => number; - groups?: TableGroupHeader[]; headerHeight: number; - internalState: ItemListStateActions; - itemType: LibraryItem; mergedRowRef: React.Ref; onRangeChanged?: ItemTableListProps['onRangeChanged']; parsedColumns: ReturnType; @@ -134,13 +120,10 @@ interface VirtualizedTableGridProps { pinnedRightColumnRef: React.RefObject; pinnedRowCount: number; pinnedRowRef: React.RefObject; - playerContext: PlayerContext; showLeftShadow: boolean; showRightShadow: boolean; showTopShadow: boolean; - size: 'compact' | 'default' | 'large'; - startRowIndex?: number; - tableId: string; + tableConfig: ItemTableListConfig; totalColumnCount: number; totalRowCount: number; } @@ -148,27 +131,11 @@ interface VirtualizedTableGridProps { const VirtualizedTableGrid = ({ calculatedColumnWidths, CellComponent, - cellPadding, - controls, data, dataWithGroups, - enableAlternateRowColors, - enableColumnReorder, - enableColumnResize, - enableDrag, - enableExpansion, - enableHeader, - enableHorizontalBorders, - enableRowHoverHighlight, enableScrollShadow, - enableSelection, - enableVerticalBorders, getItem, - getRowHeight, - groups, headerHeight, - internalState, - itemType, mergedRowRef, onRangeChanged, parsedColumns, @@ -178,16 +145,14 @@ const VirtualizedTableGrid = ({ pinnedRightColumnRef, pinnedRowCount, pinnedRowRef, - playerContext, showLeftShadow, showRightShadow, showTopShadow, - size, - startRowIndex, - tableId, + tableConfig, totalColumnCount, totalRowCount, }: VirtualizedTableGridProps) => { + const { enableHeader, enableRowHoverHighlight, getRowHeight, groups } = tableConfig; const hoverDelegateRef = useRef(null); useRowInteractionDelegate({ @@ -345,35 +310,7 @@ const VirtualizedTableGrid = ({ ], ); - const stableConfigProps = useMemo( - () => ({ - cellPadding, - columns: parsedColumns, - controls, - enableHeader, - getRowHeight, - hasAlbumGroupColumn: parsedColumns.some((col) => col.id === TableColumn.ALBUM_GROUP), - internalState, - itemType, - playerContext, - size, - tableId, - }), - [ - cellPadding, - parsedColumns, - controls, - enableHeader, - getRowHeight, - internalState, - itemType, - playerContext, - size, - tableId, - ], - ); - - const dynamicDataProps = useMemo( + const gridOnlyProps = useMemo( () => ({ calculatedColumnWidths, data: dataWithGroups, @@ -381,11 +318,11 @@ const VirtualizedTableGrid = ({ getGroupRenderData, getRowItem, groupHeaderInfoByRowIndex, + hasAlbumGroupColumn: parsedColumns.some((col) => col.id === TableColumn.ALBUM_GROUP), pinnedLeftColumnCount, pinnedLeftColumnWidths, pinnedRightColumnCount, pinnedRightColumnWidths, - startRowIndex, }), [ calculatedColumnWidths, @@ -394,50 +331,68 @@ const VirtualizedTableGrid = ({ getAdjustedRowIndex, getGroupRenderData, groupHeaderInfoByRowIndex, + parsedColumns, pinnedLeftColumnCount, pinnedLeftColumnWidths, pinnedRightColumnCount, pinnedRightColumnWidths, - startRowIndex, - ], - ); - - const featureFlags = useMemo( - () => ({ - enableAlternateRowColors, - enableColumnReorder, - enableColumnResize, - enableDrag, - enableExpansion, - enableHorizontalBorders, - enableRowHoverHighlight, - enableSelection, - enableVerticalBorders, - groups, - }), - [ - enableAlternateRowColors, - enableColumnReorder, - enableColumnResize, - enableDrag, - enableExpansion, - enableHorizontalBorders, - enableRowHoverHighlight, - enableSelection, - enableVerticalBorders, - groups, ], ); const itemProps: TableItemProps = useMemo( () => ({ - ...stableConfigProps, - ...dynamicDataProps, - ...featureFlags, + cellPadding: tableConfig.cellPadding, + columns: tableConfig.columns, + controls: tableConfig.controls, + enableAlternateRowColors: tableConfig.enableAlternateRowColors, + enableColumnReorder: tableConfig.enableColumnReorder, + enableColumnResize: tableConfig.enableColumnResize, + enableDrag: tableConfig.enableDrag, + enableExpansion: tableConfig.enableExpansion, + enableHeader: tableConfig.enableHeader, + enableHorizontalBorders: tableConfig.enableHorizontalBorders, + enableRowHoverHighlight: tableConfig.enableRowHoverHighlight, + enableSelection: tableConfig.enableSelection, + enableVerticalBorders: tableConfig.enableVerticalBorders, + getRowHeight: tableConfig.getRowHeight, + groups: tableConfig.groups, + internalState: tableConfig.internalState, + itemType: tableConfig.itemType, + playerContext: tableConfig.playerContext, + playlistId: tableConfig.playlistId, + size: tableConfig.size, + startRowIndex: tableConfig.startRowIndex, + tableId: tableConfig.tableId, + ...gridOnlyProps, }), - [stableConfigProps, dynamicDataProps, featureFlags], + [gridOnlyProps, tableConfig], ); + const pinnedLeftGridMinWidthPx = useMemo(() => { + let sum = 0; + for (let i = 0; i < pinnedLeftColumnCount; i++) { + sum += calculatedColumnWidths[i] ?? 0; + } + return sum; + }, [calculatedColumnWidths, pinnedLeftColumnCount]); + + const pinnedRightGridMinWidthPx = useMemo(() => { + let sum = 0; + const start = pinnedLeftColumnCount + totalColumnCount; + for (let i = 0; i < pinnedRightColumnCount; i++) { + sum += calculatedColumnWidths[start + i] ?? 0; + } + return sum; + }, [calculatedColumnWidths, pinnedLeftColumnCount, pinnedRightColumnCount, totalColumnCount]); + + const pinnedRowsMinHeightPx = useMemo(() => { + let sum = 0; + for (let i = 0; i < pinnedRowCount; i++) { + sum += getRowHeight(i, itemProps); + } + return sum; + }, [getRowHeight, itemProps, pinnedRowCount]); + const PinnedRowCell = useCallback( (cellProps: CellComponentProps & TableItemProps) => { return ( @@ -447,16 +402,14 @@ const VirtualizedTableGrid = ({ /> ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [pinnedLeftColumnCount, CellComponent, featureFlags, calculatedColumnWidths], + [pinnedLeftColumnCount, CellComponent], ); const PinnedColumnCell = useCallback( (cellProps: CellComponentProps & TableItemProps) => { return ; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [pinnedRowCount, CellComponent, featureFlags, calculatedColumnWidths], + [pinnedRowCount, CellComponent], ); const PinnedRightColumnCell = useCallback( @@ -469,15 +422,7 @@ const VirtualizedTableGrid = ({ /> ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - pinnedLeftColumnCount, - pinnedRowCount, - totalColumnCount, - CellComponent, - featureFlags, - calculatedColumnWidths, - ], + [pinnedLeftColumnCount, pinnedRowCount, totalColumnCount, CellComponent], ); const PinnedRightIntersectionCell = useCallback( @@ -489,14 +434,7 @@ const VirtualizedTableGrid = ({ /> ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - pinnedLeftColumnCount, - totalColumnCount, - CellComponent, - featureFlags, - calculatedColumnWidths, - ], + [pinnedLeftColumnCount, totalColumnCount, CellComponent], ); const RowCell = useCallback( @@ -509,14 +447,7 @@ const VirtualizedTableGrid = ({ /> ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - pinnedLeftColumnCount, - pinnedRowCount, - CellComponent, - featureFlags, - calculatedColumnWidths, - ], + [pinnedLeftColumnCount, pinnedRowCount, CellComponent], ); const handleOnCellsRendered = useCallback( @@ -541,10 +472,7 @@ const VirtualizedTableGrid = ({ style={ { '--header-height': `${headerHeight}px`, - minWidth: `${Array.from({ length: pinnedLeftColumnCount }, () => 0).reduce( - (a, _, i) => a + columnWidth(i), - 0, - )}px`, + minWidth: `${pinnedLeftGridMinWidthPx}px`, } as React.CSSProperties } > @@ -554,10 +482,7 @@ const VirtualizedTableGrid = ({ [styles.withHeader]: enableHeader, })} style={{ - minHeight: `${Array.from({ length: pinnedRowCount }, () => 0).reduce( - (a, _, i) => a + getRowHeight(i, itemProps), - 0, - )}px`, + minHeight: `${pinnedRowsMinHeightPx}px`, overflow: 'hidden', }} > @@ -611,10 +536,7 @@ const VirtualizedTableGrid = ({ style={ { '--header-height': `${headerHeight}px`, - minHeight: `${Array.from( - { length: pinnedRowCount }, - () => 0, - ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`, + minHeight: `${pinnedRowsMinHeightPx}px`, overflow: 'hidden', } as React.CSSProperties } @@ -627,7 +549,7 @@ const VirtualizedTableGrid = ({ columnWidth={(index) => { return columnWidth(index + pinnedLeftColumnCount); }} - rowCount={Array.from({ length: pinnedRowCount }, () => 0).length} + rowCount={pinnedRowCount} rowHeight={getRowHeight} /> @@ -660,14 +582,7 @@ const VirtualizedTableGrid = ({ style={ { '--header-height': `${headerHeight}px`, - minWidth: `${Array.from( - { length: pinnedRightColumnCount }, - () => 0, - ).reduce( - (a, _, i) => - a + columnWidth(i + pinnedLeftColumnCount + totalColumnCount), - 0, - )}px`, + minWidth: `${pinnedRightGridMinWidthPx}px`, } as React.CSSProperties } > @@ -677,10 +592,7 @@ const VirtualizedTableGrid = ({ [styles.withHeader]: enableHeader, })} style={{ - minHeight: `${Array.from( - { length: pinnedRowCount }, - () => 0, - ).reduce((a, _, i) => a + getRowHeight(i, itemProps), 0)}px`, + minHeight: `${pinnedRowsMinHeightPx}px`, overflow: 'hidden', }} > @@ -739,27 +651,12 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next prevProps.calculatedColumnWidths, nextProps.calculatedColumnWidths, ) && - prevProps.cellPadding === nextProps.cellPadding && - prevProps.controls === nextProps.controls && + prevProps.tableConfig === nextProps.tableConfig && prevProps.data === nextProps.data && prevProps.dataWithGroups === nextProps.dataWithGroups && - prevProps.enableAlternateRowColors === nextProps.enableAlternateRowColors && - prevProps.enableColumnReorder === nextProps.enableColumnReorder && - prevProps.enableColumnResize === nextProps.enableColumnResize && - prevProps.enableDrag === nextProps.enableDrag && - prevProps.enableExpansion === nextProps.enableExpansion && - prevProps.enableHeader === nextProps.enableHeader && - prevProps.enableHorizontalBorders === nextProps.enableHorizontalBorders && - prevProps.enableRowHoverHighlight === nextProps.enableRowHoverHighlight && prevProps.enableScrollShadow === nextProps.enableScrollShadow && - prevProps.enableSelection === nextProps.enableSelection && - prevProps.enableVerticalBorders === nextProps.enableVerticalBorders && prevProps.getItem === nextProps.getItem && - prevProps.getRowHeight === nextProps.getRowHeight && - prevProps.groups === nextProps.groups && prevProps.headerHeight === nextProps.headerHeight && - prevProps.internalState === nextProps.internalState && - prevProps.itemType === nextProps.itemType && prevProps.mergedRowRef === nextProps.mergedRowRef && prevProps.onRangeChanged === nextProps.onRangeChanged && prevProps.parsedColumns === nextProps.parsedColumns && @@ -769,13 +666,9 @@ const MemoizedVirtualizedTableGrid = memo(VirtualizedTableGrid, (prevProps, next prevProps.pinnedRightColumnRef === nextProps.pinnedRightColumnRef && prevProps.pinnedRowCount === nextProps.pinnedRowCount && prevProps.pinnedRowRef === nextProps.pinnedRowRef && - prevProps.playerContext === nextProps.playerContext && prevProps.showLeftShadow === nextProps.showLeftShadow && prevProps.showRightShadow === nextProps.showRightShadow && prevProps.showTopShadow === nextProps.showTopShadow && - prevProps.size === nextProps.size && - prevProps.startRowIndex === nextProps.startRowIndex && - prevProps.tableId === nextProps.tableId && prevProps.totalColumnCount === nextProps.totalColumnCount && prevProps.totalRowCount === nextProps.totalRowCount && prevProps.CellComponent === nextProps.CellComponent @@ -828,6 +721,7 @@ export interface TableItemProps { pinnedRightColumnCount?: number; pinnedRightColumnWidths?: number[]; playerContext: PlayerContext; + playlistId?: string; size?: ItemTableListProps['size']; startRowIndex?: number; tableId: string; @@ -1309,6 +1203,7 @@ const BaseItemTableList = ({ size = 'default', startRowIndex, }: ItemTableListProps) => { + const { playlistId: routePlaylistId } = useParams() as { playlistId?: string }; const tableId = useId(); const baseItemCount = itemCount ?? data.length; const totalItemCount = enableHeader ? baseItemCount + 1 : baseItemCount; @@ -1574,6 +1469,7 @@ const BaseItemTableList = ({ pinnedLeftColumnCount + totalColumnCount, ), playerContext, + playlistId: routePlaylistId, size, tableId, }), @@ -1599,6 +1495,7 @@ const BaseItemTableList = ({ pinnedLeftColumnCount, pinnedRightColumnCount, playerContext, + routePlaylistId, size, tableId, totalColumnCount, @@ -1612,17 +1509,27 @@ const BaseItemTableList = ({ itemType, }); - const tableConfigValue = useMemo( + const tableConfigValue = useMemo( () => ({ cellPadding, columns: parsedColumns, controls, + enableAlternateRowColors, + enableColumnReorder: !!onColumnReordered, + enableColumnResize: !!onColumnResized, + enableDrag, + enableExpansion, enableHeader, + enableHorizontalBorders, enableRowHoverHighlight, enableSelection, + enableVerticalBorders, + getRowHeight, + groups, internalState, itemType, playerContext, + playlistId: routePlaylistId, size, startRowIndex, tableId, @@ -1631,12 +1538,22 @@ const BaseItemTableList = ({ cellPadding, parsedColumns, controls, + enableAlternateRowColors, + onColumnReordered, + onColumnResized, + enableDrag, + enableExpansion, enableHeader, + enableHorizontalBorders, enableRowHoverHighlight, enableSelection, + enableVerticalBorders, + getRowHeight, + groups, internalState, itemType, playerContext, + routePlaylistId, size, startRowIndex, tableId, @@ -1707,27 +1624,11 @@ const BaseItemTableList = ({ diff --git a/src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx b/src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx index 61a2aa154..9431c5353 100644 --- a/src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx +++ b/src/renderer/components/item-list/item-table-list/memoized-cell-router.tsx @@ -1,4 +1,4 @@ -import React, { memo, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { CellComponentProps } from 'react-window-v2'; import { createColumnCellComponents } from './cell-component-factory'; @@ -24,24 +24,7 @@ const MemoizedCellRouterBase = (props: MemoizedCellRouterProps) => { return ; }; -export const MemoizedCellRouter = memo(MemoizedCellRouterBase, (prevProps, nextProps) => { - return ( - prevProps.rowIndex === nextProps.rowIndex && - prevProps.columnIndex === nextProps.columnIndex && - prevProps.data === nextProps.data && - prevProps.columns === nextProps.columns && - prevProps.columnCellComponents === nextProps.columnCellComponents && - prevProps.size === nextProps.size && - prevProps.enableAlternateRowColors === nextProps.enableAlternateRowColors && - prevProps.enableHorizontalBorders === nextProps.enableHorizontalBorders && - prevProps.enableVerticalBorders === nextProps.enableVerticalBorders && - prevProps.enableRowHoverHighlight === nextProps.enableRowHoverHighlight && - prevProps.enableSelection === nextProps.enableSelection && - prevProps.enableColumnResize === nextProps.enableColumnResize && - prevProps.enableColumnReorder === nextProps.enableColumnReorder && - prevProps.cellPadding === nextProps.cellPadding - ); -}); +export const MemoizedCellRouter = MemoizedCellRouterBase; export const useColumnCellComponents = ( columns: TableColumn[],