feat(pagination): add pagination state management and cursor handling
This commit is contained in:
@@ -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} />
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
Reference in New Issue
Block a user