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