feat(pagination): add pagination state management and cursor handling

This commit is contained in:
2026-02-15 22:24:38 +02:00
parent 7244bd33fc
commit 391450f615
6 changed files with 97 additions and 72 deletions

View File

@@ -158,6 +158,10 @@ const meta = {
control: 'object', control: 'object',
description: 'Griddy column ID to API column name mapping', description: 'Griddy column ID to API column name mapping',
}, },
cursorField: {
control: 'text',
description: 'Field to extract cursor from (default: "id")',
},
debounceMs: { debounceMs: {
control: { max: 2000, min: 0, step: 50, type: 'range' }, control: { max: 2000, min: 0, step: 50, type: 'range' },
description: 'Filter change debounce in ms', description: 'Filter change debounce in ms',
@@ -166,6 +170,11 @@ const meta = {
control: 'text', control: 'text',
description: 'Database entity/table name', description: 'Database entity/table name',
}, },
mode: {
control: 'inline-radio',
description: 'Pagination mode: cursor (infinite scroll) or offset (page controls)',
options: ['cursor', 'offset'],
},
schema: { schema: {
control: 'text', control: 'text',
description: 'Database schema name', description: 'Database schema name',
@@ -193,7 +202,7 @@ export const ResolveSpec: Story = {
baseUrl: 'https://utils.btsys.tech/api', baseUrl: 'https://utils.btsys.tech/api',
}, },
render: (args) => <ResolveSpecAdapterStory {...args} />, render: (args) => <ResolveSpecAdapterStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
/** HeaderSpec adapter — same as ResolveSpec but uses HeaderSpecClient */ /** HeaderSpec adapter — same as ResolveSpec but uses HeaderSpecClient */
@@ -209,7 +218,7 @@ export const HeaderSpec: Story = {
token: ' 773EB99C-F625-4E99-9DB9-CDDA7CA17639', token: ' 773EB99C-F625-4E99-9DB9-CDDA7CA17639',
}, },
render: (args) => <HeaderSpecAdapterStory {...args} />, render: (args) => <HeaderSpecAdapterStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
/** ResolveSpec with column mapping — remaps Griddy column IDs to different API column names */ /** ResolveSpec with column mapping — remaps Griddy column IDs to different API column names */
@@ -222,7 +231,7 @@ export const WithColumnMap: Story = {
name: 'full_name', name: 'full_name',
}, },
}, },
render: (args) => <ResolveSpecAdapterStory {...args} />, render: (args) => <ResolveSpecAdapterStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
/** ResolveSpec with custom debounce — slower debounce for expensive queries */ /** ResolveSpec with custom debounce — slower debounce for expensive queries */
@@ -230,7 +239,7 @@ export const WithCustomDebounce: Story = {
args: { args: {
debounceMs: 1000, debounceMs: 1000,
}, },
render: (args) => <ResolveSpecAdapterStory {...args} />, render: (args) => <ResolveSpecAdapterStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
/** ResolveSpec with autoFetch disabled — data only loads on manual refetch */ /** ResolveSpec with autoFetch disabled — data only loads on manual refetch */
@@ -238,7 +247,7 @@ export const ManualFetchOnly: Story = {
args: { args: {
autoFetch: false, autoFetch: false,
}, },
render: (args) => <ResolveSpecAdapterStory {...args} />, render: (args) => <ResolveSpecAdapterStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
/** ResolveSpec with default options merged into every request */ /** ResolveSpec with default options merged into every request */
@@ -249,7 +258,7 @@ export const WithDefaultOptions: Story = {
sort: [{ column: 'name', direction: 'asc' }], sort: [{ column: 'name', direction: 'asc' }],
}, },
}, },
render: (args) => <ResolveSpecAdapterStory {...args} />, render: (args) => <ResolveSpecAdapterStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
// ─── Cursor / Infinite Scroll Stories ──────────────────────────────────────── // ─── Cursor / Infinite Scroll Stories ────────────────────────────────────────
@@ -280,7 +289,7 @@ function HeaderSpecInfiniteScrollStory(props: AdapterConfig) {
manualFiltering manualFiltering
manualSorting manualSorting
> >
<HeaderSpecAdapter ref={adapterRef} {...props} mode="cursor" /> <HeaderSpecAdapter cursorField="id" ref={adapterRef} {...props} mode="cursor" />
</Griddy> </Griddy>
</Box> </Box>
); );
@@ -361,7 +370,7 @@ export const WithInfiniteScroll: Story = {
baseUrl: 'https://utils.btsys.tech/api', baseUrl: 'https://utils.btsys.tech/api',
}, },
render: (args) => <InfiniteScrollStory {...args} />, render: (args) => <InfiniteScrollStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
/** ResolveSpec with explicit cursor pagination config */ /** ResolveSpec with explicit cursor pagination config */
@@ -370,7 +379,7 @@ export const WithCursorPagination: Story = {
cursorField: 'id', cursorField: 'id',
pageSize: 50, pageSize: 50,
}, },
render: (args) => <InfiniteScrollStory {...args} />, render: (args) => <InfiniteScrollStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
/** ResolveSpec with offset pagination controls */ /** ResolveSpec with offset pagination controls */
@@ -378,7 +387,7 @@ export const WithOffsetPagination: Story = {
args: { args: {
pageSize: 25, pageSize: 25,
}, },
render: (args) => <OffsetPaginationStory {...args} />, render: (args) => <OffsetPaginationStory baseUrl={''} entity={''} schema={''} {...args} />,
}; };
/** HeaderSpec adapter with cursor-based infinite scroll */ /** HeaderSpec adapter with cursor-based infinite scroll */
@@ -389,5 +398,7 @@ export const HeaderSpecInfiniteScroll: Story = {
token: ' 773EB99C-F625-4E99-9DB9-CDDA7CA17639', token: ' 773EB99C-F625-4E99-9DB9-CDDA7CA17639',
}, },
render: (args) => <HeaderSpecInfiniteScrollStory {...args} />, render: (args) => (
<HeaderSpecInfiniteScrollStory baseUrl={''} entity={''} schema={''} {...args} />
),
}; };

View File

@@ -28,22 +28,19 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
const sorting = useGriddyStore((s) => s.sorting ?? []); const sorting = useGriddyStore((s) => s.sorting ?? []);
const columnFilters = useGriddyStore((s) => s.columnFilters ?? []); const columnFilters = useGriddyStore((s) => s.columnFilters ?? []);
const pagination = useGriddyStore((s) => s.pagination); const pagination = useGriddyStore((s) => s.pagination);
const _setData = useGriddyStore((s) => s.setData); const paginationState = useGriddyStore((s) => s.paginationState);
const setData = useGriddyStore((s) => s.setData);
const appendData = useGriddyStore((s) => s.appendData); const appendData = useGriddyStore((s) => s.appendData);
const setDataCount = useGriddyStore((s) => s.setDataCount); const setDataCount = useGriddyStore((s) => s.setDataCount);
const setIsLoading = useGriddyStore((s) => s.setIsLoading); const setIsLoading = useGriddyStore((s) => s.setIsLoading);
const setError = useGriddyStore((s) => s.setError); const setError = useGriddyStore((s) => s.setError);
const setInfiniteScroll = useGriddyStore((s) => s.setInfiniteScroll); const setInfiniteScroll = useGriddyStore((s) => s.setInfiniteScroll);
const setData = (data: any[]) => {
console.log('Set Data', data);
_setData(data);
};
const clientRef = useRef(getHeaderSpecClient({ baseUrl, token })); const clientRef = useRef(getHeaderSpecClient({ baseUrl, token }));
const debounceRef = useRef<null | ReturnType<typeof setTimeout>>(null); const debounceRef = useRef<null | ReturnType<typeof setTimeout>>(null);
const mountedRef = useRef(true); const mountedRef = useRef(true);
// Cursor state (only used in cursor mode) // Infinite scroll state (cursor mode)
const cursorRef = useRef<null | string>(null); const cursorRef = useRef<null | string>(null);
const hasMoreRef = useRef(true); const hasMoreRef = useRef(true);
const [cursorLoading, setCursorLoading] = useState(false); const [cursorLoading, setCursorLoading] = useState(false);
@@ -67,14 +64,15 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
setError(null); setError(null);
try { try {
const paginationState = pagination?.enabled // Fall back to config when store hasn't synced yet (initial render)
? { pageIndex: 0, pageSize: pagination.pageSize } const effectivePagination =
: undefined; paginationState ??
(pagination?.enabled ? { pageIndex: 0, pageSize: pagination.pageSize } : undefined);
const options = buildOptions( const options = buildOptions(
sorting, sorting,
columnFilters, columnFilters,
paginationState, effectivePagination,
columnMap, columnMap,
defaultOptions defaultOptions
); );
@@ -84,13 +82,11 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
if (customOperators) options.customOperators = customOperators; if (customOperators) options.customOperators = customOperators;
const response = await clientRef.current.read(schema, entity, undefined, options); const response = await clientRef.current.read(schema, entity, undefined, options);
if (!mountedRef.current) return; if (!mountedRef.current) return;
console.log('Fetch data (offset mode) Res', {
response,
});
if (response.success) { if (response.success) {
setData(Array.isArray(response.data) ? response.data : [response.data]); const rows = Array.isArray(response.data) ? response.data : [response.data];
setData(rows);
if (response.metadata?.total != null) { if (response.metadata?.total != null) {
setDataCount(response.metadata.total); setDataCount(response.metadata.total);
} }
@@ -110,6 +106,7 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
sorting, sorting,
columnFilters, columnFilters,
pagination, pagination,
paginationState,
columnMap, columnMap,
defaultOptions, defaultOptions,
preload, preload,
@@ -123,7 +120,7 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
setError, setError,
]); ]);
// ─── Cursor mode fetch ─── // ─── Cursor mode fetch (uses cursor_forward only) ───
const fetchCursorPage = useCallback( const fetchCursorPage = useCallback(
async (cursor: null | string, isAppend: boolean) => { async (cursor: null | string, isAppend: boolean) => {
if (!mountedRef.current) return; if (!mountedRef.current) return;
@@ -148,7 +145,7 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
if (computedColumns) options.computedColumns = computedColumns; if (computedColumns) options.computedColumns = computedColumns;
if (customOperators) options.customOperators = customOperators; if (customOperators) options.customOperators = customOperators;
const cursorOptions = applyCursor(options, cursor, pageSize); const cursorOptions = applyCursor(options, pageSize, cursor);
const response = await clientRef.current.read(schema, entity, undefined, cursorOptions); const response = await clientRef.current.read(schema, entity, undefined, cursorOptions);
if (!mountedRef.current) return; if (!mountedRef.current) return;
@@ -162,18 +159,28 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
setData(rows); setData(rows);
} }
if (response.metadata?.total != null) { if (response.metadata?.total) {
setDataCount(response.metadata.total); setDataCount(response.metadata.total);
} else if (response.metadata?.count) {
setDataCount(response.metadata.count);
} }
// Extract cursor from last row // Extract cursor from last row
if (rows.length > 0) { if (rows.length > 0) {
const lastRow = rows[rows.length - 1]; const lastRow = rows[rows.length - 1];
cursorRef.current = if (lastRow?.[cursorField] == null) {
lastRow?.[cursorField] != null ? String(lastRow[cursorField]) : null; hasMoreRef.current = false;
setError(
new Error(
`Cursor field "${cursorField}" not found in response data. ` +
`Set cursorField to match your data's primary key, or use mode="offset".`
)
);
return;
}
cursorRef.current = String(lastRow[cursorField]);
} }
// Determine hasMore
hasMoreRef.current = rows.length >= pageSize; hasMoreRef.current = rows.length >= pageSize;
} else if (response.error) { } else if (response.error) {
setError(new Error(response.error.message ?? 'Request failed')); setError(new Error(response.error.message ?? 'Request failed'));
@@ -213,13 +220,11 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
); );
const fetchNextPage = useCallback(() => { const fetchNextPage = useCallback(() => {
console.log('Fetch next page', { hasMore: hasMoreRef.current, cursorLoading });
if (!hasMoreRef.current || cursorLoading) return; if (!hasMoreRef.current || cursorLoading) return;
fetchCursorPage(cursorRef.current, true); return fetchCursorPage(cursorRef.current, true);
}, [cursorLoading, fetchCursorPage]); }, [cursorLoading, fetchCursorPage]);
const resetAndFetch = useCallback(async () => { const resetAndFetch = useCallback(async () => {
console.log('Reset and fetch', { hasMore: hasMoreRef.current, cursorLoading });
cursorRef.current = null; cursorRef.current = null;
hasMoreRef.current = true; hasMoreRef.current = true;
await fetchCursorPage(null, false); await fetchCursorPage(null, false);
@@ -230,7 +235,6 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
// ─── Infinite scroll config sync (cursor mode only) ─── // ─── Infinite scroll config sync (cursor mode only) ───
useEffect(() => { useEffect(() => {
// Skip infinite scroll if not in cursor mode OR if pagination is explicitly enabled
if (mode !== 'cursor' || pagination?.enabled) { if (mode !== 'cursor' || pagination?.enabled) {
setInfiniteScroll(undefined); setInfiniteScroll(undefined);
return; return;
@@ -245,7 +249,6 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
}); });
}, [mode, pagination?.enabled, cursorLoading, fetchNextPage, setInfiniteScroll]); }, [mode, pagination?.enabled, cursorLoading, fetchNextPage, setInfiniteScroll]);
// Cleanup infinite scroll on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
setInfiniteScroll(undefined); setInfiniteScroll(undefined);
@@ -256,7 +259,6 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
const initialFetchDone = useRef(false); const initialFetchDone = useRef(false);
useEffect(() => { useEffect(() => {
console.log('Auto-fetch effect', { autoFetch, initialFetchDone: initialFetchDone.current });
if (autoFetch && !initialFetchDone.current) { if (autoFetch && !initialFetchDone.current) {
initialFetchDone.current = true; initialFetchDone.current = true;
fetchData(); fetchData();
@@ -268,7 +270,7 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
const depsKey = const depsKey =
mode === 'cursor' mode === 'cursor'
? JSON.stringify({ columnFilters, sorting }) ? JSON.stringify({ columnFilters, sorting })
: JSON.stringify({ columnFilters, pagination, sorting }); : JSON.stringify({ columnFilters, paginationState, sorting });
if (prevDepsRef.current === null) { if (prevDepsRef.current === null) {
prevDepsRef.current = depsKey; prevDepsRef.current = depsKey;
@@ -284,16 +286,8 @@ export const HeaderSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
return () => { return () => {
if (debounceRef.current) clearTimeout(debounceRef.current); if (debounceRef.current) clearTimeout(debounceRef.current);
}; };
}, [sorting, columnFilters, pagination, debounceMs, fetchData, mode]); }, [sorting, columnFilters, paginationState, debounceMs, fetchData, mode]);
console.log('Render HeaderSpecAdapter', {
sorting,
columnFilters,
pagination,
cursor: cursorRef.current,
hasMore: hasMoreRef.current,
cursorLoading,
});
return null; return null;
} }
); );

View File

@@ -28,6 +28,7 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
const sorting = useGriddyStore((s) => s.sorting ?? []); const sorting = useGriddyStore((s) => s.sorting ?? []);
const columnFilters = useGriddyStore((s) => s.columnFilters ?? []); const columnFilters = useGriddyStore((s) => s.columnFilters ?? []);
const pagination = useGriddyStore((s) => s.pagination); const pagination = useGriddyStore((s) => s.pagination);
const paginationState = useGriddyStore((s) => s.paginationState);
const setData = useGriddyStore((s) => s.setData); const setData = useGriddyStore((s) => s.setData);
const appendData = useGriddyStore((s) => s.appendData); const appendData = useGriddyStore((s) => s.appendData);
const setDataCount = useGriddyStore((s) => s.setDataCount); const setDataCount = useGriddyStore((s) => s.setDataCount);
@@ -39,12 +40,11 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
const debounceRef = useRef<null | ReturnType<typeof setTimeout>>(null); const debounceRef = useRef<null | ReturnType<typeof setTimeout>>(null);
const mountedRef = useRef(true); const mountedRef = useRef(true);
// Cursor state (only used in cursor mode) // Infinite scroll state (cursor mode)
const cursorRef = useRef<null | string>(null); const cursorRef = useRef<null | string>(null);
const hasMoreRef = useRef(true); const hasMoreRef = useRef(true);
const [cursorLoading, setCursorLoading] = useState(false); const [cursorLoading, setCursorLoading] = useState(false);
// Update client if baseUrl/token changes
useEffect(() => { useEffect(() => {
clientRef.current = getResolveSpecClient({ baseUrl, token }); clientRef.current = getResolveSpecClient({ baseUrl, token });
}, [baseUrl, token]); }, [baseUrl, token]);
@@ -64,14 +64,14 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
setError(null); setError(null);
try { try {
const paginationState = pagination?.enabled // Fall back to config when store hasn't synced yet (initial render)
? { pageIndex: 0, pageSize: pagination.pageSize } const effectivePagination = paginationState ??
: undefined; (pagination?.enabled ? { pageIndex: 0, pageSize: pagination.pageSize } : undefined);
const options = buildOptions( const options = buildOptions(
sorting, sorting,
columnFilters, columnFilters,
paginationState, effectivePagination,
columnMap, columnMap,
defaultOptions defaultOptions
); );
@@ -85,7 +85,8 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
if (!mountedRef.current) return; if (!mountedRef.current) return;
if (response.success) { if (response.success) {
setData(Array.isArray(response.data) ? response.data : [response.data]); const rows = Array.isArray(response.data) ? response.data : [response.data];
setData(rows);
if (response.metadata?.total != null) { if (response.metadata?.total != null) {
setDataCount(response.metadata.total); setDataCount(response.metadata.total);
} }
@@ -105,6 +106,7 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
sorting, sorting,
columnFilters, columnFilters,
pagination, pagination,
paginationState,
columnMap, columnMap,
defaultOptions, defaultOptions,
preload, preload,
@@ -118,7 +120,7 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
setError, setError,
]); ]);
// ─── Cursor mode fetch ─── // ─── Cursor mode fetch (uses cursor_forward only) ───
const fetchCursorPage = useCallback( const fetchCursorPage = useCallback(
async (cursor: null | string, isAppend: boolean) => { async (cursor: null | string, isAppend: boolean) => {
if (!mountedRef.current) return; if (!mountedRef.current) return;
@@ -143,7 +145,7 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
if (computedColumns) options.computedColumns = computedColumns; if (computedColumns) options.computedColumns = computedColumns;
if (customOperators) options.customOperators = customOperators; if (customOperators) options.customOperators = customOperators;
const cursorOptions = applyCursor(options, cursor, pageSize); const cursorOptions = applyCursor(options, pageSize, cursor);
const response = await clientRef.current.read(schema, entity, undefined, cursorOptions); const response = await clientRef.current.read(schema, entity, undefined, cursorOptions);
if (!mountedRef.current) return; if (!mountedRef.current) return;
@@ -157,18 +159,26 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
setData(rows); setData(rows);
} }
if (response.metadata?.total != null) { if (response.metadata?.total) {
setDataCount(response.metadata.total); setDataCount(response.metadata.total);
} else if (response.metadata?.count) {
setDataCount(response.metadata.count);
} }
// Extract cursor from last row // Extract cursor from last row
if (rows.length > 0) { if (rows.length > 0) {
const lastRow = rows[rows.length - 1]; const lastRow = rows[rows.length - 1];
cursorRef.current = if (lastRow?.[cursorField] == null) {
lastRow?.[cursorField] != null ? String(lastRow[cursorField]) : null; hasMoreRef.current = false;
setError(new Error(
`Cursor field "${cursorField}" not found in response data. ` +
`Set cursorField to match your data's primary key, or use mode="offset".`
));
return;
}
cursorRef.current = String(lastRow[cursorField]);
} }
// Determine hasMore
hasMoreRef.current = rows.length >= pageSize; hasMoreRef.current = rows.length >= pageSize;
} else if (response.error) { } else if (response.error) {
setError(new Error(response.error.message ?? 'Request failed')); setError(new Error(response.error.message ?? 'Request failed'));
@@ -209,7 +219,7 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
const fetchNextPage = useCallback(() => { const fetchNextPage = useCallback(() => {
if (!hasMoreRef.current || cursorLoading) return; if (!hasMoreRef.current || cursorLoading) return;
fetchCursorPage(cursorRef.current, true); return fetchCursorPage(cursorRef.current, true);
}, [cursorLoading, fetchCursorPage]); }, [cursorLoading, fetchCursorPage]);
const resetAndFetch = useCallback(async () => { const resetAndFetch = useCallback(async () => {
@@ -223,7 +233,6 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
// ─── Infinite scroll config sync (cursor mode only) ─── // ─── Infinite scroll config sync (cursor mode only) ───
useEffect(() => { useEffect(() => {
// Skip infinite scroll if not in cursor mode OR if pagination is explicitly enabled
if (mode !== 'cursor' || pagination?.enabled) { if (mode !== 'cursor' || pagination?.enabled) {
setInfiniteScroll(undefined); setInfiniteScroll(undefined);
return; return;
@@ -238,7 +247,6 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
}); });
}, [mode, pagination?.enabled, cursorLoading, fetchNextPage, setInfiniteScroll]); }, [mode, pagination?.enabled, cursorLoading, fetchNextPage, setInfiniteScroll]);
// Cleanup infinite scroll on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
setInfiniteScroll(undefined); setInfiniteScroll(undefined);
@@ -247,7 +255,6 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
useImperativeHandle(ref, () => ({ refetch: fetchData }), [fetchData]); useImperativeHandle(ref, () => ({ refetch: fetchData }), [fetchData]);
// Auto-fetch on mount
const initialFetchDone = useRef(false); const initialFetchDone = useRef(false);
useEffect(() => { useEffect(() => {
if (autoFetch && !initialFetchDone.current) { if (autoFetch && !initialFetchDone.current) {
@@ -256,13 +263,12 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
} }
}, [autoFetch, fetchData]); }, [autoFetch, fetchData]);
// Debounced re-fetch on state changes (skip initial)
const prevDepsRef = useRef<null | string>(null); const prevDepsRef = useRef<null | string>(null);
useEffect(() => { useEffect(() => {
const depsKey = const depsKey =
mode === 'cursor' mode === 'cursor'
? JSON.stringify({ columnFilters, sorting }) ? JSON.stringify({ columnFilters, sorting })
: JSON.stringify({ columnFilters, pagination, sorting }); : JSON.stringify({ columnFilters, paginationState, sorting });
if (prevDepsRef.current === null) { if (prevDepsRef.current === null) {
prevDepsRef.current = depsKey; prevDepsRef.current = depsKey;
@@ -278,7 +284,7 @@ export const ResolveSpecAdapter = forwardRef<AdapterRef, AdapterConfig>(
return () => { return () => {
if (debounceRef.current) clearTimeout(debounceRef.current); if (debounceRef.current) clearTimeout(debounceRef.current);
}; };
}, [sorting, columnFilters, pagination, debounceMs, fetchData, mode]); }, [sorting, columnFilters, paginationState, debounceMs, fetchData, mode]);
return null; return null;
} }

View File

@@ -25,7 +25,7 @@ const OPERATOR_MAP: Record<string, string> = {
startsWith: 'startswith', startsWith: 'startswith',
}; };
export function applyCursor(opts: Options, cursor: null | string, limit: number): Options { export function applyCursor(opts: Options, limit: number, cursor?: null | string): Options {
const result = { ...opts, limit }; const result = { ...opts, limit };
if (cursor) { if (cursor) {
result.cursor_forward = cursor; result.cursor_forward = cursor;

View File

@@ -91,6 +91,7 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
const manualFiltering = useGriddyStore((s) => s.manualFiltering); const manualFiltering = useGriddyStore((s) => s.manualFiltering);
const dataCount = useGriddyStore((s) => s.dataCount); const dataCount = useGriddyStore((s) => s.dataCount);
const setTable = useGriddyStore((s) => s.setTable); const setTable = useGriddyStore((s) => s.setTable);
const setPaginationState = useGriddyStore((s) => s.setPaginationState);
const setVirtualizer = useGriddyStore((s) => s.setVirtualizer); const setVirtualizer = useGriddyStore((s) => s.setVirtualizer);
const setScrollRef = useGriddyStore((s) => s.setScrollRef); const setScrollRef = useGriddyStore((s) => s.setScrollRef);
const setFocusedRow = useGriddyStore((s) => s.setFocusedRow); const setFocusedRow = useGriddyStore((s) => s.setFocusedRow);
@@ -153,6 +154,13 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
}); });
}; };
// Sync pagination state to store so adapters can read pageIndex/pageSize
useEffect(() => {
if (paginationConfig?.enabled) {
setPaginationState(internalPagination);
}
}, [paginationConfig?.enabled, internalPagination, setPaginationState]);
// Resolve controlled vs uncontrolled // Resolve controlled vs uncontrolled
const sorting = controlledSorting ?? internalSorting; const sorting = controlledSorting ?? internalSorting;
const setSorting = onSortingChange ?? setInternalSorting; const setSorting = onSortingChange ?? setInternalSorting;
@@ -188,6 +196,7 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
getRowId: (getRowId as any) ?? ((_, index) => String(index)), getRowId: (getRowId as any) ?? ((_, index) => String(index)),
getSortedRowModel: manualSorting ? undefined : getSortedRowModel(), getSortedRowModel: manualSorting ? undefined : getSortedRowModel(),
manualFiltering: manualFiltering ?? false, manualFiltering: manualFiltering ?? false,
manualPagination: paginationConfig?.type === 'offset',
manualSorting: manualSorting ?? false, manualSorting: manualSorting ?? false,
onColumnFiltersChange: setColumnFilters as any, onColumnFiltersChange: setColumnFilters as any,
onColumnOrderChange: setColumnOrder, onColumnOrderChange: setColumnOrder,
@@ -212,7 +221,9 @@ function GriddyInner<T>({ tableRef }: { tableRef: Ref<GriddyRef<T>> }) {
sorting, sorting,
...(paginationConfig?.enabled ? { pagination: internalPagination } : {}), ...(paginationConfig?.enabled ? { pagination: internalPagination } : {}),
}, },
...(paginationConfig?.enabled ? { getPaginationRowModel: getPaginationRowModel() } : {}), ...(paginationConfig?.enabled && paginationConfig.type !== 'offset'
? { getPaginationRowModel: getPaginationRowModel() }
: {}),
columnResizeMode: 'onChange', columnResizeMode: 'onChange',
}); });

View File

@@ -64,18 +64,20 @@ export interface GriddyStoreState extends GriddyUIState {
onSortingChange?: (sorting: SortingState) => void; onSortingChange?: (sorting: SortingState) => void;
overscan?: number; overscan?: number;
pagination?: PaginationConfig; pagination?: PaginationConfig;
paginationState?: { pageIndex: number; pageSize: number };
persistenceKey?: string; persistenceKey?: string;
rowHeight?: number; rowHeight?: number;
rowSelection?: RowSelectionState;
rowSelection?: RowSelectionState;
search?: SearchConfig; search?: SearchConfig;
selection?: SelectionConfig; selection?: SelectionConfig;
setData: (data: any[]) => void; setData: (data: any[]) => void;
setDataCount: (count: number) => void; setDataCount: (count: number) => void;
setError: (error: Error | null) => void; setError: (error: Error | null) => void;
setInfiniteScroll: (config: InfiniteScrollConfig | undefined) => void;
setInfiniteScroll: (config: InfiniteScrollConfig | undefined) => void;
setIsLoading: (loading: boolean) => void; setIsLoading: (loading: boolean) => void;
setPaginationState: (state: { pageIndex: number; pageSize: number }) => void;
setScrollRef: (el: HTMLDivElement | null) => void; setScrollRef: (el: HTMLDivElement | null) => void;
// ─── Internal ref setters ─── // ─── Internal ref setters ───
setTable: (table: Table<any>) => void; setTable: (table: Table<any>) => void;
@@ -131,6 +133,7 @@ export const { Provider: GriddyProvider, useStore: useGriddyStore } = createSync
setFocusedRow: (index) => set({ focusedRowIndex: index }), setFocusedRow: (index) => set({ focusedRowIndex: index }),
setInfiniteScroll: (config) => set({ infiniteScroll: config }), setInfiniteScroll: (config) => set({ infiniteScroll: config }),
setIsLoading: (loading) => set({ isLoading: loading }), setIsLoading: (loading) => set({ isLoading: loading }),
setPaginationState: (state) => set({ paginationState: state }),
setScrollRef: (el) => set({ _scrollRef: el }), setScrollRef: (el) => set({ _scrollRef: el }),
setSearchOpen: (open) => set({ isSearchOpen: open }), setSearchOpen: (open) => set({ isSearchOpen: open }),