diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx index b7aae385f..3d115c4c1 100644 --- a/src/renderer/components/item-card/item-card.tsx +++ b/src/renderer/components/item-card/item-card.tsx @@ -122,7 +122,7 @@ const CompactItemCard = ({ const [showControls, setShowControls] = useState(false); const isSelected = data && internalState && typeof data === 'object' && 'id' in data - ? internalState.isSelected((data as any).id) + ? internalState.isSelected(internalState.extractRowId(data) || '') : false; if (data) { @@ -226,7 +226,7 @@ const DefaultItemCard = ({ const [showControls, setShowControls] = useState(false); const isSelected = data && internalState && typeof data === 'object' && 'id' in data - ? internalState.isSelected((data as any).id) + ? internalState.isSelected(internalState.extractRowId(data) || '') : false; if (data) { @@ -331,7 +331,7 @@ const PosterItemCard = ({ const [showControls, setShowControls] = useState(false); const isSelected = data && internalState && typeof data === 'object' && 'id' in data - ? internalState.isSelected((data as any).id) + ? internalState.isSelected(internalState.extractRowId(data) || '') : false; const { isDragging: isDraggingLocal, ref } = useDragDrop({ diff --git a/src/renderer/components/item-list/helpers/extract-row-id.ts b/src/renderer/components/item-list/helpers/extract-row-id.ts new file mode 100644 index 000000000..40fdfebcf --- /dev/null +++ b/src/renderer/components/item-list/helpers/extract-row-id.ts @@ -0,0 +1,28 @@ +/** + * Creates a function to extract row ID from an item based on the getRowId configuration. + * + * @param getRowId - Either a string property name, a function that extracts the ID, or undefined to use default 'id' property + * @returns A function that extracts the row ID from an item + */ +export const createExtractRowId = ( + getRowId?: ((item: unknown) => string) | string, +): ((item: unknown) => string | undefined) => { + return (item: unknown): string | undefined => { + if (!item || typeof item !== 'object') { + return undefined; + } + + if (getRowId === undefined) { + // Default behavior: use 'id' property + return (item as any).id; + } + + if (typeof getRowId === 'string') { + // getRowId is a property name + return (item as any)[getRowId]; + } + + // getRowId is a function + return getRowId(item); + }; +}; diff --git a/src/renderer/components/item-list/helpers/get-dragged-items.ts b/src/renderer/components/item-list/helpers/get-dragged-items.ts index 27a96ff73..563b1ec6c 100644 --- a/src/renderer/components/item-list/helpers/get-dragged-items.ts +++ b/src/renderer/components/item-list/helpers/get-dragged-items.ts @@ -46,12 +46,18 @@ export const getDraggedItems = ( return []; } + const rowId = internalState.extractRowId(data); + + if (!rowId) { + return []; + } + const draggedItem = data as ItemListStateItemWithRequiredProperties; const previouslySelected = internalState.getSelected(); const isDraggingSelectedItem = previouslySelected.some((selected) => { if (hasRequiredDragProperties(selected)) { - return selected.id === data.id; + return internalState.extractRowId(selected) === rowId; } return false; }); diff --git a/src/renderer/components/item-list/helpers/item-list-controls.ts b/src/renderer/components/item-list/helpers/item-list-controls.ts index 638fcd910..098d02827 100644 --- a/src/renderer/components/item-list/helpers/item-list-controls.ts +++ b/src/renderer/components/item-list/helpers/item-list-controls.ts @@ -15,12 +15,16 @@ export const useDefaultItemListControls = () => { return; } - // Use the full item instead of converting to minimal + // Extract rowId from the item + const rowId = internalState.extractRowId(item); + if (!rowId) return; + + // Use the item directly (rowId is separate, used only as key in state) const itemListItem = item as ItemListStateItemWithRequiredProperties; // Check if ctrl/cmd key is held for multi-selection if (event.ctrlKey || event.metaKey) { - const isCurrentlySelected = internalState.isSelected(item.id); + const isCurrentlySelected = internalState.isSelected(rowId); if (isCurrentlySelected) { // Remove this item from selection @@ -31,8 +35,7 @@ export const useDefaultItemListControls = () => { ): selectedItem is ItemListStateItemWithRequiredProperties => typeof selectedItem === 'object' && selectedItem !== null && - 'id' in selectedItem && - (selectedItem as any).id !== item.id, + internalState.extractRowId(selectedItem) !== rowId, ); internalState.setSelected(filteredSelected); } else { @@ -58,19 +61,18 @@ export const useDefaultItemListControls = () => { if ( lastSelectedItem && typeof lastSelectedItem === 'object' && - lastSelectedItem !== null && - 'id' in lastSelectedItem + lastSelectedItem !== null ) { // Get the data array from internalState const data = internalState.getData(); // Filter out null/undefined values (e.g., header row) - const validData = data.filter( - (d) => d && typeof d === 'object' && 'id' in d, - ); + const validData = data.filter((d) => d && typeof d === 'object'); // Find the indices of the last selected item and current item - const lastIndex = internalState.findItemIndex((lastSelectedItem as any).id); - const currentIndex = internalState.findItemIndex(item.id); + const lastRowId = internalState.extractRowId(lastSelectedItem); + if (!lastRowId) return; + const lastIndex = internalState.findItemIndex(lastRowId); + const currentIndex = internalState.findItemIndex(rowId); if (lastIndex !== -1 && currentIndex !== -1) { // Create range selection - select ALL items in the range @@ -83,13 +85,15 @@ export const useDefaultItemListControls = () => { if ( rangeItem && typeof rangeItem === 'object' && - 'id' in rangeItem && '_serverId' in rangeItem && 'itemType' in rangeItem ) { - rangeItems.push( - rangeItem as ItemListStateItemWithRequiredProperties, - ); + const rangeRowId = internalState.extractRowId(rangeItem); + if (rangeRowId) { + rangeItems.push( + rangeItem as ItemListStateItemWithRequiredProperties, + ); + } } } @@ -104,9 +108,12 @@ export const useDefaultItemListControls = () => { ), ]; rangeItems.forEach((rangeItem) => { + const rangeRowId = internalState.extractRowId(rangeItem); if ( + rangeRowId && !newSelected.some( - (selected) => (selected as any).id === rangeItem.id, + (selected) => + internalState.extractRowId(selected) === rangeRowId, ) ) { newSelected.push(rangeItem); @@ -126,8 +133,7 @@ export const useDefaultItemListControls = () => { selectedItems.length === 1 && typeof selectedItems[0] === 'object' && selectedItems[0] !== null && - 'id' in selectedItems[0] && - (selectedItems[0] as any).id === item.id; + internalState.extractRowId(selectedItems[0]) === rowId; if (isOnlySelected) { internalState.clearSelected(); @@ -146,9 +152,14 @@ export const useDefaultItemListControls = () => { return; } - return internalState?.toggleExpanded( - item as ItemListStateItemWithRequiredProperties, - ); + // Extract rowId from the item + const rowId = internalState.extractRowId(item); + if (!rowId) return; + + // Use the item directly (rowId is separate, used only as key in state) + const itemListItem = item as ItemListStateItemWithRequiredProperties; + + return internalState?.toggleExpanded(itemListItem); }, onFavorite: ({ diff --git a/src/renderer/components/item-list/helpers/item-list-reducer-utils.ts b/src/renderer/components/item-list/helpers/item-list-reducer-utils.ts index ee1cafa1c..4011845e0 100644 --- a/src/renderer/components/item-list/helpers/item-list-reducer-utils.ts +++ b/src/renderer/components/item-list/helpers/item-list-reducer-utils.ts @@ -21,27 +21,47 @@ export const itemGridActions = { type: 'CLEAR_SELECTED', }), - setDragging: (items: ItemListStateItemWithRequiredProperties[]): ItemListAction => ({ + setDragging: ( + items: ItemListStateItemWithRequiredProperties[], + extractRowId: (item: unknown) => string | undefined, + ): ItemListAction => ({ + extractRowId, payload: items, type: 'SET_DRAGGING', }), - setExpanded: (items: ItemListStateItemWithRequiredProperties[]): ItemListAction => ({ + setExpanded: ( + items: ItemListStateItemWithRequiredProperties[], + extractRowId: (item: unknown) => string | undefined, + ): ItemListAction => ({ + extractRowId, payload: items, type: 'SET_EXPANDED', }), - setSelected: (items: ItemListStateItemWithRequiredProperties[]): ItemListAction => ({ + setSelected: ( + items: ItemListStateItemWithRequiredProperties[], + extractRowId: (item: unknown) => string | undefined, + ): ItemListAction => ({ + extractRowId, payload: items, type: 'SET_SELECTED', }), - toggleExpanded: (item: ItemListStateItemWithRequiredProperties): ItemListAction => ({ + toggleExpanded: ( + item: ItemListStateItemWithRequiredProperties, + extractRowId: (item: unknown) => string | undefined, + ): ItemListAction => ({ + extractRowId, payload: item, type: 'TOGGLE_EXPANDED', }), - toggleSelected: (item: ItemListStateItemWithRequiredProperties): ItemListAction => ({ + toggleSelected: ( + item: ItemListStateItemWithRequiredProperties, + extractRowId: (item: unknown) => string | undefined, + ): ItemListAction => ({ + extractRowId, payload: item, type: 'TOGGLE_SELECTED', }), @@ -104,16 +124,16 @@ export const itemGridSelectors = { return state.selected.size > 0; }, - isDragging: (state: ItemListState, itemId: string): boolean => { - return state.dragging.has(itemId); + isDragging: (state: ItemListState, rowId: string): boolean => { + return state.dragging.has(rowId); }, - isExpanded: (state: ItemListState, itemId: string): boolean => { - return state.expanded.has(itemId); + isExpanded: (state: ItemListState, rowId: string): boolean => { + return state.expanded.has(rowId); }, - isSelected: (state: ItemListState, itemId: string): boolean => { - return state.selected.has(itemId); + isSelected: (state: ItemListState, rowId: string): boolean => { + return state.selected.has(rowId); }, }; @@ -121,15 +141,15 @@ export const itemListUtils = { /** * Check if all items in a list are selected */ - areAllSelected: (state: ItemListState, itemIds: string[]): boolean => { - return itemIds.every((id) => state.selected.has(id)); + areAllSelected: (state: ItemListState, rowIds: string[]): boolean => { + return rowIds.every((id) => state.selected.has(id)); }, /** * Check if any items in a list are selected */ - areAnySelected: (state: ItemListState, itemIds: string[]): boolean => { - return itemIds.some((id) => state.selected.has(id)); + areAnySelected: (state: ItemListState, rowIds: string[]): boolean => { + return rowIds.some((id) => state.selected.has(id)); }, /** @@ -152,9 +172,15 @@ export const itemListUtils = { toggleAllExpanded: ( items: ItemListStateItemWithRequiredProperties[], currentState: ItemListState, + extractRowId: (item: unknown) => string | undefined, ): ItemListAction => { - const allExpanded = items.every((item) => currentState.expanded.has(item.id)); - return allExpanded ? itemGridActions.clearExpanded() : itemGridActions.setExpanded(items); + const allExpanded = items.every((item) => { + const rowId = extractRowId(item); + return rowId ? currentState.expanded.has(rowId) : false; + }); + return allExpanded + ? itemGridActions.clearExpanded() + : itemGridActions.setExpanded(items, extractRowId); }, /** @@ -163,8 +189,14 @@ export const itemListUtils = { toggleAllSelected: ( items: ItemListStateItemWithRequiredProperties[], currentState: ItemListState, + extractRowId: (item: unknown) => string | undefined, ): ItemListAction => { - const allSelected = items.every((item) => currentState.selected.has(item.id)); - return allSelected ? itemGridActions.clearSelected() : itemGridActions.setSelected(items); + const allSelected = items.every((item) => { + const rowId = extractRowId(item); + return rowId ? currentState.selected.has(rowId) : false; + }); + return allSelected + ? itemGridActions.clearSelected() + : itemGridActions.setSelected(items, extractRowId); }, }; diff --git a/src/renderer/components/item-list/helpers/item-list-state.ts b/src/renderer/components/item-list/helpers/item-list-state.ts index f066a7b3c..9e3542bf5 100644 --- a/src/renderer/components/item-list/helpers/item-list-state.ts +++ b/src/renderer/components/item-list/helpers/item-list-state.ts @@ -4,11 +4,31 @@ import { itemGridSelectors } from '/@/renderer/components/item-list/helpers/item import { LibraryItem } from '/@/shared/types/domain-types'; export type ItemListAction = - | { payload: ItemListStateItemWithRequiredProperties; type: 'TOGGLE_EXPANDED' } - | { payload: ItemListStateItemWithRequiredProperties; type: 'TOGGLE_SELECTED' } - | { payload: ItemListStateItemWithRequiredProperties[]; type: 'SET_DRAGGING' } - | { payload: ItemListStateItemWithRequiredProperties[]; type: 'SET_EXPANDED' } - | { payload: ItemListStateItemWithRequiredProperties[]; type: 'SET_SELECTED' } + | { + extractRowId: (item: unknown) => string | undefined; + payload: ItemListStateItemWithRequiredProperties; + type: 'TOGGLE_EXPANDED'; + } + | { + extractRowId: (item: unknown) => string | undefined; + payload: ItemListStateItemWithRequiredProperties; + type: 'TOGGLE_SELECTED'; + } + | { + extractRowId: (item: unknown) => string | undefined; + payload: ItemListStateItemWithRequiredProperties[]; + type: 'SET_DRAGGING'; + } + | { + extractRowId: (item: unknown) => string | undefined; + payload: ItemListStateItemWithRequiredProperties[]; + type: 'SET_EXPANDED'; + } + | { + extractRowId: (item: unknown) => string | undefined; + payload: ItemListStateItemWithRequiredProperties[]; + type: 'SET_SELECTED'; + } | { type: 'CLEAR_ALL' } | { type: 'CLEAR_DRAGGING' } | { type: 'CLEAR_EXPANDED' } @@ -29,7 +49,8 @@ export interface ItemListStateActions { clearDragging: () => void; clearExpanded: () => void; clearSelected: () => void; - findItemIndex: (itemId: string) => number; + extractRowId: (item: unknown) => string | undefined; + findItemIndex: (rowId: string) => number; getData: () => unknown[]; getDragging: () => unknown[]; getDraggingIds: () => string[]; @@ -41,9 +62,9 @@ export interface ItemListStateActions { hasDragging: () => boolean; hasExpanded: () => boolean; hasSelected: () => boolean; - isDragging: (itemId: string) => boolean; - isExpanded: (itemId: string) => boolean; - isSelected: (itemId: string) => boolean; + isDragging: (rowId: string) => boolean; + isExpanded: (rowId: string) => boolean; + isSelected: (rowId: string) => boolean; setDragging: (items: ItemListStateItemWithRequiredProperties[]) => void; setExpanded: (items: ItemListStateItemWithRequiredProperties[]) => void; setSelected: (items: ItemListStateItemWithRequiredProperties[]) => void; @@ -110,8 +131,11 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I const newDraggingItems = new Map(); action.payload.forEach((item) => { - newDragging.add(item.id); - newDraggingItems.set(item.id, item); + const rowId = action.extractRowId(item); + if (rowId) { + newDragging.add(rowId); + newDraggingItems.set(rowId, item); + } }); return { @@ -128,8 +152,11 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I if (action.payload.length > 0) { const firstItem = action.payload[0]; - newExpanded.add(firstItem.id); - newExpandedItems.set(firstItem.id, firstItem); + const rowId = action.extractRowId(firstItem); + if (rowId) { + newExpanded.add(rowId); + newExpandedItems.set(rowId, firstItem); + } } return { @@ -145,8 +172,11 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I const newSelectedItems = new Map(); action.payload.forEach((item) => { - newSelected.add(item.id); - newSelectedItems.set(item.id, item); + const rowId = action.extractRowId(item); + if (rowId) { + newSelected.add(rowId); + newSelectedItems.set(rowId, item); + } }); return { @@ -161,13 +191,18 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I const newExpanded = new Set(); const newExpandedItems = new Map(); + const rowId = action.extractRowId(action.payload); + if (!rowId) { + return state; + } + // If the item is already expanded, collapse it - if (state.expanded.has(action.payload.id)) { + if (state.expanded.has(rowId)) { // Item is expanded, so collapse it (leave sets empty) } else { // Item is not expanded, so expand it (clear others first for single expansion) - newExpanded.add(action.payload.id); - newExpandedItems.set(action.payload.id, action.payload); + newExpanded.add(rowId); + newExpandedItems.set(rowId, action.payload); } return { @@ -182,12 +217,17 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I const newSelected = new Set(state.selected); const newSelectedItems = new Map(state.selectedItems); - if (newSelected.has(action.payload.id)) { - newSelected.delete(action.payload.id); - newSelectedItems.delete(action.payload.id); + const rowId = action.extractRowId(action.payload); + if (!rowId) { + return state; + } + + if (newSelected.has(rowId)) { + newSelected.delete(rowId); + newSelectedItems.delete(rowId); } else { - newSelected.add(action.payload.id); - newSelectedItems.set(action.payload.id, action.payload); + newSelected.add(rowId); + newSelectedItems.set(rowId, action.payload); } return { @@ -213,39 +253,91 @@ export const initialItemListState: ItemListState = { version: 0, }; -export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActions => { +export const useItemListState = ( + getDataFn?: () => unknown[], + extractRowId?: (item: unknown) => string | undefined, +): ItemListStateActions => { const [state, dispatch] = useReducer(itemListReducer, initialItemListState); - const setExpanded = useCallback((items: ItemListStateItemWithRequiredProperties[]) => { - dispatch({ payload: items, type: 'SET_EXPANDED' }); - }, []); + const extractRowIdFn = useCallback( + (item: unknown) => { + if (extractRowId) { + return extractRowId(item); + } + // Fallback to id if extractRowId is not provided + if (item && typeof item === 'object' && 'id' in item) { + return (item as any).id; + } + return undefined; + }, + [extractRowId], + ); - const setDragging = useCallback((items: ItemListStateItemWithRequiredProperties[]) => { - dispatch({ payload: items, type: 'SET_DRAGGING' }); - }, []); + const setExpanded = useCallback( + (items: ItemListStateItemWithRequiredProperties[]) => { + dispatch({ + extractRowId: extractRowIdFn, + payload: items, + type: 'SET_EXPANDED', + }); + }, + [extractRowIdFn], + ); - const setSelected = useCallback((items: ItemListStateItemWithRequiredProperties[]) => { - dispatch({ payload: items, type: 'SET_SELECTED' }); - }, []); + const setDragging = useCallback( + (items: ItemListStateItemWithRequiredProperties[]) => { + dispatch({ + extractRowId: extractRowIdFn, + payload: items, + type: 'SET_DRAGGING', + }); + }, + [extractRowIdFn], + ); - const toggleExpanded = useCallback((item: ItemListStateItemWithRequiredProperties) => { - dispatch({ payload: item, type: 'TOGGLE_EXPANDED' }); - }, []); + const setSelected = useCallback( + (items: ItemListStateItemWithRequiredProperties[]) => { + dispatch({ + extractRowId: extractRowIdFn, + payload: items, + type: 'SET_SELECTED', + }); + }, + [extractRowIdFn], + ); - const toggleSelected = useCallback((item: ItemListStateItemWithRequiredProperties) => { - dispatch({ payload: item, type: 'TOGGLE_SELECTED' }); - }, []); + const toggleExpanded = useCallback( + (item: ItemListStateItemWithRequiredProperties) => { + dispatch({ + extractRowId: extractRowIdFn, + payload: item, + type: 'TOGGLE_EXPANDED', + }); + }, + [extractRowIdFn], + ); + + const toggleSelected = useCallback( + (item: ItemListStateItemWithRequiredProperties) => { + dispatch({ + extractRowId: extractRowIdFn, + payload: item, + type: 'TOGGLE_SELECTED', + }); + }, + [extractRowIdFn], + ); const isExpanded = useCallback( - (itemId: string) => { - return itemGridSelectors.isExpanded(state, itemId); + (rowId: string) => { + return itemGridSelectors.isExpanded(state, rowId); }, [state], ); const isSelected = useCallback( - (itemId: string) => { - return itemGridSelectors.isSelected(state, itemId); + (rowId: string) => { + return itemGridSelectors.isSelected(state, rowId); }, [state], ); @@ -307,8 +399,8 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi }, [state]); const isDragging = useCallback( - (itemId: string) => { - return itemGridSelectors.isDragging(state, itemId); + (rowId: string) => { + return itemGridSelectors.isDragging(state, rowId); }, [state], ); @@ -318,13 +410,17 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi }, [getDataFn]); const findItemIndex = useCallback( - (itemId: string) => { + (rowId: string) => { const data = getDataFn ? getDataFn() : []; // Filter out null/undefined values (e.g., header row) - const validData = data.filter((d) => d && typeof d === 'object' && 'id' in d); - return validData.findIndex((d) => (d as any).id === itemId); + const validData = data.filter((d) => d && typeof d === 'object'); + if (!extractRowId) { + // Fallback to id if extractRowId is not provided + return validData.findIndex((d) => (d as any).id === rowId); + } + return validData.findIndex((d) => extractRowId(d) === rowId); }, - [getDataFn], + [getDataFn, extractRowId], ); return useMemo( @@ -333,6 +429,7 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi clearDragging, clearExpanded, clearSelected, + extractRowId: extractRowIdFn, findItemIndex, getData, getDragging, @@ -359,6 +456,7 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi clearDragging, clearExpanded, clearSelected, + extractRowIdFn, findItemIndex, getData, getDragging, 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 f616a3999..d026464fd 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 @@ -34,10 +34,11 @@ import { } from '/@/renderer/components/item-card/item-card'; import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded-list-container'; import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item'; +import { createExtractRowId } from '/@/renderer/components/item-list/helpers/extract-row-id'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { ItemListStateActions, - ItemListStateItem, + ItemListStateItemWithRequiredProperties, useItemListState, } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types'; @@ -249,6 +250,7 @@ export interface ItemGridListProps { enableExpansion?: boolean; enableSelection?: boolean; gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; + getRowId?: ((item: unknown) => string) | string; initialTop?: number; itemsPerRow?: number; itemType: LibraryItem; @@ -264,6 +266,7 @@ export const ItemGridList = ({ enableExpansion = true, enableSelection = true, gap = 'sm', + getRowId, initialTop, itemsPerRow, itemType, @@ -284,7 +287,9 @@ export const ItemGridList = ({ return data; }, [data]); - const internalState = useItemListState(getDataFn); + const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]); + + const internalState = useItemListState(getDataFn, extractRowId); const [initialize] = useOverlayScrollbars({ defer: false, @@ -372,13 +377,13 @@ export const ItemGridList = ({ if (selected.length > 0) { const lastSelected = selected[selected.length - 1]; - currentIndex = data.findIndex( - (d: any) => - d && - typeof d === 'object' && - 'id' in d && - d.id === (lastSelected as any).id, - ); + const lastRowId = internalState.extractRowId(lastSelected); + if (lastRowId) { + currentIndex = data.findIndex((d: any) => { + const rowId = internalState.extractRowId(d); + return rowId === lastRowId; + }); + } } // Calculate grid position @@ -474,85 +479,87 @@ export const ItemGridList = ({ if (lastSelectedItem) { // Find the indices of the last selected item and new item - const lastIndex = data.findIndex( - (d: any) => - d && - typeof d === 'object' && - 'id' in d && - d.id === (lastSelectedItem as any).id, - ); + const lastRowId = internalState.extractRowId(lastSelectedItem); + if (!lastRowId) return; + + const lastIndex = data.findIndex((d: any) => { + const rowId = internalState.extractRowId(d); + return rowId === lastRowId; + }); if (lastIndex !== -1 && newIndex !== -1) { // Create range selection from last selected to new position const startIndex = Math.min(lastIndex, newIndex); const stopIndex = Math.max(lastIndex, newIndex); - const rangeItems: ItemListStateItem[] = []; + const rangeItems: ItemListStateItemWithRequiredProperties[] = []; for (let i = startIndex; i <= stopIndex; i++) { const rangeItem = data[i]; if ( rangeItem && typeof rangeItem === 'object' && - 'id' in rangeItem && - 'serverId' in rangeItem + '_serverId' in rangeItem && + 'itemType' in rangeItem && + internalState.extractRowId(rangeItem) ) { - rangeItems.push({ - _serverId: (rangeItem as any).serverId, - id: (rangeItem as any).id, - itemType, - }); + rangeItems.push( + rangeItem as ItemListStateItemWithRequiredProperties, + ); } } // Add range items to selection (matching shift+click behavior) const currentSelected = internalState.getSelected(); - const newSelected = [...currentSelected]; + const newSelected: ItemListStateItemWithRequiredProperties[] = [ + ...currentSelected.filter( + (item): item is ItemListStateItemWithRequiredProperties => + typeof item === 'object' && item !== null, + ), + ]; rangeItems.forEach((rangeItem) => { + const rangeRowId = internalState.extractRowId(rangeItem); if ( - !newSelected.some((selected: any) => selected.id === rangeItem.id) + rangeRowId && + !newSelected.some( + (selected) => + internalState.extractRowId(selected) === rangeRowId, + ) ) { newSelected.push(rangeItem); } }); // Ensure the last item in selection is the item at newIndex for incremental extension - const newItemListItem: ItemListStateItem = { - _serverId: newItem.serverId, - id: newItem.id, - itemType, - }; - // Remove the new item from its current position if it exists - const filteredSelected = newSelected.filter( - (item: any) => item.id !== newItemListItem.id, - ); - // Add it at the end so it becomes the last selected item - filteredSelected.push(newItemListItem); - internalState.setSelected(filteredSelected as any); + const newItemListItem = newItem as ItemListStateItemWithRequiredProperties; + const newItemRowId = internalState.extractRowId(newItemListItem); + if (newItemRowId) { + // Remove the new item from its current position if it exists + const filteredSelected = newSelected.filter( + (item) => internalState.extractRowId(item) !== newItemRowId, + ); + // Add it at the end so it becomes the last selected item + filteredSelected.push(newItemListItem); + internalState.setSelected(filteredSelected); + } } } else { // No previous selection, just select the new item - internalState.setSelected([ - { - _serverId: newItem.serverId, - id: newItem.id, - itemType, - }, - ]); + const newItemListItem = newItem as ItemListStateItemWithRequiredProperties; + if (internalState.extractRowId(newItemListItem)) { + internalState.setSelected([newItemListItem]); + } } } else { // Without Shift: select only the new item - internalState.setSelected([ - { - _serverId: newItem.serverId, - id: newItem.id, - itemType, - }, - ]); + const newItemListItem = newItem as ItemListStateItemWithRequiredProperties; + if (internalState.extractRowId(newItemListItem)) { + internalState.setSelected([newItemListItem]); + } } scrollToIndex(newIndex); }, - [data, enableSelection, internalState, itemType, tableMeta, scrollToIndex], + [data, enableSelection, internalState, tableMeta, scrollToIndex], ); const imperativeHandle: ItemListHandle = useMemo(() => { 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 c1d4d7a4a..f8e8f3629 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 @@ -335,7 +335,7 @@ export const TableColumnTextContainer = ( const item = isDataRow ? props.data[props.rowIndex] : null; const isSelected = item && typeof item === 'object' && 'id' in item - ? props.internalState.isSelected((item as any).id) + ? props.internalState.isSelected(props.internalState.extractRowId(item) || '') : false; const isDragging = props.isDragging ?? false; @@ -503,7 +503,7 @@ export const TableColumnContainer = ( const item = isDataRow ? props.data[props.rowIndex] : null; const isSelected = item && typeof item === 'object' && 'id' in item - ? props.internalState.isSelected((item as any).id) + ? props.internalState.isSelected(props.internalState.extractRowId(item) || '') : false; const isDragging = props.isDragging ?? false; 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 a3feb8071..723928ccf 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 @@ -23,6 +23,7 @@ import styles from './item-table-list.module.css'; import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded-list-container'; import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item'; +import { createExtractRowId } from '/@/renderer/components/item-list/helpers/extract-row-id'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { ItemListStateActions, @@ -70,7 +71,9 @@ const hasRequiredStateItemProperties = ( '_serverId' in item && typeof (item as any)._serverId === 'string' && 'itemType' in item && - typeof (item as any).itemType === 'string' + typeof (item as any).itemType === 'string' && + 'rowId' in item && + typeof (item as any).rowId === 'string' ); }; @@ -498,6 +501,7 @@ interface ItemTableListProps { enableRowHoverHighlight?: boolean; enableSelection?: boolean; enableVerticalBorders?: boolean; + getRowId?: ((item: unknown) => string) | string; headerHeight?: number; initialTop?: { behavior?: 'auto' | 'smooth'; @@ -527,6 +531,7 @@ export const ItemTableList = ({ enableRowHoverHighlight = true, enableSelection = true, enableVerticalBorders = false, + getRowId, headerHeight = 40, initialTop, itemType, @@ -1086,10 +1091,29 @@ export const ItemTableList = ({ return enableHeader ? [null, ...data] : data; }, [data, enableHeader]); - const internalState = useItemListState(getDataFn); + const extractRowId = useMemo(() => createExtractRowId(getRowId), [getRowId]); + + const internalState = useItemListState(getDataFn, extractRowId); const hasExpanded = internalState.hasExpanded(); + // Helper function to get ItemListStateItemWithRequiredProperties (rowId is separate, not part of item) + const getStateItem = useCallback( + (item: any): ItemListStateItemWithRequiredProperties | null => { + if (!hasRequiredItemProperties(item)) return null; + if ( + typeof item === 'object' && + item !== null && + '_serverId' in item && + 'itemType' in item + ) { + return item as ItemListStateItemWithRequiredProperties; + } + return null; + }, + [], + ); + const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (!enableSelection) return; @@ -1127,9 +1151,11 @@ export const ItemTableList = ({ if (lastSelectedItem) { // Find the indices of the last selected item and new item - const lastIndex = data.findIndex( - (d) => hasRequiredItemProperties(d) && d.id === lastSelectedItem.id, - ); + const lastRowId = lastSelectedItem.rowId; + const lastIndex = data.findIndex((d) => { + const rowId = extractRowId(d); + return rowId === lastRowId; + }); if (lastIndex !== -1 && newIndex !== -1) { // Create range selection from last selected to new position @@ -1139,12 +1165,9 @@ export const ItemTableList = ({ const rangeItems: ItemListStateItemWithRequiredProperties[] = []; for (let i = startIndex; i <= stopIndex; i++) { const rangeItem = data[i]; - if (hasRequiredItemProperties(rangeItem)) { - rangeItems.push({ - _serverId: rangeItem.serverId, - id: rangeItem.id, - itemType, - } as ItemListStateItemWithRequiredProperties); + const stateItem = getStateItem(rangeItem); + if (stateItem && extractRowId(stateItem)) { + rangeItems.push(stateItem); } } @@ -1157,21 +1180,24 @@ export const ItemTableList = ({ ...validSelected, ]; rangeItems.forEach((rangeItem) => { - if (!newSelected.some((selected) => selected.id === rangeItem.id)) { + const rangeRowId = extractRowId(rangeItem); + if ( + rangeRowId && + !newSelected.some( + (selected) => extractRowId(selected) === rangeRowId, + ) + ) { newSelected.push(rangeItem); } }); // Ensure the last item in selection is the item at newIndex for incremental extension - if (hasRequiredItemProperties(newItem)) { - const newItemListItem: ItemListStateItemWithRequiredProperties = { - _serverId: newItem.serverId, - id: newItem.id, - itemType, - } as ItemListStateItemWithRequiredProperties; + const newItemListItem = getStateItem(newItem); + if (newItemListItem && extractRowId(newItemListItem)) { + const newItemRowId = extractRowId(newItemListItem); // Remove the new item from its current position if it exists const filteredSelected = newSelected.filter( - (item) => item.id !== newItemListItem.id, + (item) => extractRowId(item) !== newItemRowId, ); // Add it at the end so it becomes the last selected item filteredSelected.push(newItemListItem); @@ -1180,26 +1206,16 @@ export const ItemTableList = ({ } } else { // No previous selection, just select the new item - if (hasRequiredItemProperties(newItem)) { - internalState.setSelected([ - { - _serverId: newItem.serverId, - id: newItem.id, - itemType, - } as ItemListStateItemWithRequiredProperties, - ]); + const newItemListItem = getStateItem(newItem); + if (newItemListItem && extractRowId(newItemListItem)) { + internalState.setSelected([newItemListItem]); } } } else { // Without Shift: select only the new item - if (hasRequiredItemProperties(newItem)) { - internalState.setSelected([ - { - _serverId: newItem.serverId, - id: newItem.id, - itemType, - } as ItemListStateItemWithRequiredProperties, - ]); + const newItemListItem = getStateItem(newItem); + if (newItemListItem && extractRowId(newItemListItem)) { + internalState.setSelected([newItemListItem]); } } @@ -1210,9 +1226,10 @@ export const ItemTableList = ({ data, enableSelection, internalState, - itemType, calculateScrollTopForIndex, scrollToTableOffset, + extractRowId, + getStateItem, ], );