* Implement flat mode to transform flat data with parentId to nested structure. * Introduce lazy mode for on-demand loading of children. * Update tree rendering logic to accommodate new modes. * Enhance tree cell expansion logic to support lazy loading. * Add dark mode styles for improved UI experience. * Create comprehensive end-to-end tests for tree functionality.
176 lines
6.1 KiB
TypeScript
176 lines
6.1 KiB
TypeScript
import type { Table } from '@tanstack/react-table';
|
|
import type {
|
|
ColumnFiltersState,
|
|
ColumnPinningState,
|
|
RowSelectionState,
|
|
SortingState,
|
|
} from '@tanstack/react-table';
|
|
import type { Virtualizer } from '@tanstack/react-virtual';
|
|
|
|
import { createSyncStore } from '@warkypublic/zustandsyncstore';
|
|
|
|
import type {
|
|
AdvancedSearchConfig,
|
|
DataAdapter,
|
|
GriddyColumn,
|
|
GriddyProps,
|
|
GriddyUIState,
|
|
GroupingConfig,
|
|
InfiniteScrollConfig,
|
|
PaginationConfig,
|
|
SearchConfig,
|
|
SelectionConfig,
|
|
TreeConfig,
|
|
} from './types';
|
|
|
|
// ─── Store State ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Full store state: UI state + synced props + internal references.
|
|
* Props from GriddyProps are synced automatically via createSyncStore's $sync.
|
|
* Fields from GriddyProps must be declared here so TypeScript can see them.
|
|
*/
|
|
export interface GriddyStoreState extends GriddyUIState {
|
|
_scrollRef: HTMLDivElement | null;
|
|
// ─── Internal refs (set imperatively) ───
|
|
_table: null | Table<any>;
|
|
_virtualizer: null | Virtualizer<HTMLDivElement, Element>;
|
|
advancedSearch?: AdvancedSearchConfig;
|
|
// ─── Adapter Actions ───
|
|
appendData: (data: any[]) => void;
|
|
className?: string;
|
|
columnFilters?: ColumnFiltersState;
|
|
columnPinning?: ColumnPinningState;
|
|
columns?: GriddyColumn<any>[];
|
|
data?: any[];
|
|
dataAdapter?: DataAdapter<any>;
|
|
dataCount?: number;
|
|
// ─── Error State ───
|
|
error: Error | null;
|
|
exportFilename?: string;
|
|
filterPresets?: boolean;
|
|
getRowId?: (row: any, index: number) => string;
|
|
grouping?: GroupingConfig;
|
|
height?: number | string;
|
|
infiniteScroll?: InfiniteScrollConfig;
|
|
isLoading?: boolean;
|
|
keyboardNavigation?: boolean;
|
|
manualFiltering?: boolean;
|
|
manualSorting?: boolean;
|
|
onColumnFiltersChange?: (filters: ColumnFiltersState) => void;
|
|
onColumnPinningChange?: (pinning: ColumnPinningState) => void;
|
|
onEditCommit?: (rowId: string, columnId: string, value: unknown) => Promise<void> | void;
|
|
onError?: (error: Error) => void;
|
|
onRowSelectionChange?: (selection: RowSelectionState) => void;
|
|
onSortingChange?: (sorting: SortingState) => void;
|
|
overscan?: number;
|
|
pagination?: PaginationConfig;
|
|
paginationState?: { pageIndex: number; pageSize: number };
|
|
persistenceKey?: string;
|
|
rowHeight?: number;
|
|
|
|
rowSelection?: RowSelectionState;
|
|
search?: SearchConfig;
|
|
selection?: SelectionConfig;
|
|
setData: (data: any[]) => void;
|
|
setDataCount: (count: number) => void;
|
|
setError: (error: Error | null) => void;
|
|
|
|
setInfiniteScroll: (config: InfiniteScrollConfig | undefined) => void;
|
|
setIsLoading: (loading: boolean) => void;
|
|
setPaginationState: (state: { pageIndex: number; pageSize: number }) => void;
|
|
setScrollRef: (el: HTMLDivElement | null) => void;
|
|
// ─── Internal ref setters ───
|
|
setTable: (table: Table<any>) => void;
|
|
setTreeChildrenCache: (nodeId: string, children: any[]) => void;
|
|
|
|
setTreeLoadingNode: (nodeId: string, loading: boolean) => void;
|
|
setVirtualizer: (virtualizer: Virtualizer<HTMLDivElement, Element>) => void;
|
|
showToolbar?: boolean;
|
|
sorting?: SortingState;
|
|
// ─── Tree/Hierarchical Data ───
|
|
tree?: TreeConfig<any>;
|
|
treeChildrenCache: Map<string, any[]>;
|
|
treeLoadingNodes: Set<string>;
|
|
// ─── Synced from GriddyProps (written by $sync) ───
|
|
uniqueId?: string;
|
|
}
|
|
|
|
// ─── Create Store ────────────────────────────────────────────────────────────
|
|
|
|
export const { Provider: GriddyProvider, useStore: useGriddyStore } = createSyncStore<
|
|
GriddyStoreState,
|
|
GriddyProps<any>
|
|
>((set, get) => ({
|
|
_scrollRef: null,
|
|
// ─── Internal Refs ───
|
|
_table: null,
|
|
|
|
_virtualizer: null,
|
|
appendData: (data) => set((state) => ({ data: [...(state.data ?? []), ...data] })),
|
|
error: null,
|
|
focusedColumnId: null,
|
|
// ─── Focus State ───
|
|
focusedRowIndex: null,
|
|
|
|
// ─── Mode State ───
|
|
isEditing: false,
|
|
|
|
isSearchOpen: false,
|
|
isSelecting: false,
|
|
moveFocus: (direction, amount) => {
|
|
const { focusedRowIndex, totalRows } = get();
|
|
const current = focusedRowIndex ?? 0;
|
|
const delta = direction === 'down' ? amount : -amount;
|
|
const next = Math.max(0, Math.min(current + delta, totalRows - 1));
|
|
set({ focusedRowIndex: next });
|
|
},
|
|
|
|
moveFocusToEnd: () => {
|
|
const { totalRows } = get();
|
|
set({ focusedRowIndex: Math.max(0, totalRows - 1) });
|
|
},
|
|
moveFocusToStart: () => set({ focusedRowIndex: 0 }),
|
|
setData: (data) => set({ data }),
|
|
setDataCount: (count) => set({ dataCount: count }),
|
|
setEditing: (editing) => set({ isEditing: editing }),
|
|
setError: (error) => set({ error }),
|
|
setFocusedColumn: (id) => set({ focusedColumnId: id }),
|
|
// ─── Actions ───
|
|
setFocusedRow: (index) => set({ focusedRowIndex: index }),
|
|
setInfiniteScroll: (config) => set({ infiniteScroll: config }),
|
|
setIsLoading: (loading) => set({ isLoading: loading }),
|
|
setPaginationState: (state) => set({ paginationState: state }),
|
|
setScrollRef: (el) => set({ _scrollRef: el }),
|
|
|
|
setSearchOpen: (open) => set({ isSearchOpen: open }),
|
|
|
|
setSelecting: (selecting) => set({ isSelecting: selecting }),
|
|
// ─── Internal Ref Setters ───
|
|
setTable: (table) => set({ _table: table }),
|
|
|
|
setTotalRows: (count) => set({ totalRows: count }),
|
|
setTreeChildrenCache: (nodeId, children) =>
|
|
set((state) => {
|
|
const newMap = new Map(state.treeChildrenCache);
|
|
newMap.set(nodeId, children);
|
|
return { treeChildrenCache: newMap };
|
|
}),
|
|
setTreeLoadingNode: (nodeId, loading) =>
|
|
set((state) => {
|
|
const newSet = new Set(state.treeLoadingNodes);
|
|
if (loading) {
|
|
newSet.add(nodeId);
|
|
} else {
|
|
newSet.delete(nodeId);
|
|
}
|
|
return { treeLoadingNodes: newSet };
|
|
}),
|
|
setVirtualizer: (virtualizer) => set({ _virtualizer: virtualizer }),
|
|
// ─── Row Count ───
|
|
totalRows: 0,
|
|
treeChildrenCache: new Map(),
|
|
// ─── Tree State ───
|
|
treeLoadingNodes: new Set(),
|
|
}));
|