@hexclave/dashboard-ui-components 1.0.49 → 1.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/components/data-grid/data-grid.js +7 -5
  2. package/dist/components/data-grid/data-grid.js.map +1 -1
  3. package/dist/components/data-grid/data-grid.test.js +36 -3
  4. package/dist/components/data-grid/data-grid.test.js.map +1 -1
  5. package/dist/components/data-grid/use-data-source.js +56 -22
  6. package/dist/components/data-grid/use-data-source.js.map +1 -1
  7. package/dist/components/data-grid/use-data-source.test.d.ts +1 -0
  8. package/dist/components/data-grid/use-data-source.test.js +276 -0
  9. package/dist/components/data-grid/use-data-source.test.js.map +1 -0
  10. package/dist/dashboard-ui-components.global.js +74 -28
  11. package/dist/dashboard-ui-components.global.js.map +2 -2
  12. package/dist/data-grid-AJi1TNDa.d.ts.map +1 -1
  13. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
  14. package/dist/esm/components/data-grid/data-grid.js +7 -5
  15. package/dist/esm/components/data-grid/data-grid.js.map +1 -1
  16. package/dist/esm/components/data-grid/data-grid.test.js +36 -3
  17. package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
  18. package/dist/esm/components/data-grid/use-data-source.d.ts.map +1 -1
  19. package/dist/esm/components/data-grid/use-data-source.js +56 -22
  20. package/dist/esm/components/data-grid/use-data-source.js.map +1 -1
  21. package/dist/esm/components/data-grid/use-data-source.test.d.ts +1 -0
  22. package/dist/esm/components/data-grid/use-data-source.test.js +277 -0
  23. package/dist/esm/components/data-grid/use-data-source.test.js.map +1 -0
  24. package/dist/use-data-source-BVFynerX.d.ts.map +1 -1
  25. package/package.json +3 -3
  26. package/src/components/data-grid/data-grid.test.tsx +46 -3
  27. package/src/components/data-grid/data-grid.tsx +22 -5
  28. package/src/components/data-grid/use-data-source.test.tsx +289 -0
  29. package/src/components/data-grid/use-data-source.ts +120 -18
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let react = require("react");
3
+ let _hexclave_shared_dist_utils_promises = require("@hexclave/shared/dist/utils/promises");
3
4
  let __state_js = require("./state.js");
4
5
  //#region src/components/data-grid/use-data-source.ts
5
6
  function useClientDataSource(opts) {
@@ -45,10 +46,15 @@ function useAsyncDataSource(opts) {
45
46
  const [hasMore, setHasMore] = (0, react.useState)(true);
46
47
  const [error, setError] = (0, react.useState)(null);
47
48
  const cursorRef = (0, react.useRef)(void 0);
48
- const abortRef = (0, react.useRef)(null);
49
+ const inFlightFetchRef = (0, react.useRef)(null);
50
+ const pendingLoadMoreRef = (0, react.useRef)(false);
51
+ const hasMoreRef = (0, react.useRef)(true);
52
+ const paginationModeRef = (0, react.useRef)(paginationMode);
53
+ paginationModeRef.current = paginationMode;
49
54
  const pageIndexRef = (0, react.useRef)(0);
50
55
  const hasDataRef = (0, react.useRef)(false);
51
56
  const hasMountedServerPaginationRef = (0, react.useRef)(false);
57
+ const lastCompletedNonAppendFetchKeyRef = (0, react.useRef)(null);
52
58
  const latestArgsRef = (0, react.useRef)({
53
59
  dataSource,
54
60
  getRowId,
@@ -66,57 +72,81 @@ function useAsyncDataSource(opts) {
66
72
  const sortingKey = JSON.stringify(sorting);
67
73
  const quickSearchKey = quickSearch;
68
74
  const fetchPage = (0, react.useCallback)(async (append) => {
75
+ if (append && inFlightFetchRef.current != null) return;
69
76
  const { dataSource: currentDataSource, getRowId: currentGetRowId, sorting: currentSorting, quickSearch: currentQuickSearch, pagination: currentPagination } = latestArgsRef.current;
70
- abortRef.current?.abort();
77
+ inFlightFetchRef.current?.abort();
71
78
  const controller = new AbortController();
72
- abortRef.current = controller;
79
+ inFlightFetchRef.current = controller;
80
+ const fetchKey = {
81
+ dataSource: currentDataSource,
82
+ sortingKey: JSON.stringify(currentSorting),
83
+ quickSearchKey: currentQuickSearch,
84
+ pageSize: currentPagination.pageSize
85
+ };
73
86
  if (append) setIsLoadingMore(true);
74
- else {
75
- if (hasDataRef.current) setIsRefetching(true);
76
- else setIsLoading(true);
77
- cursorRef.current = void 0;
78
- pageIndexRef.current = 0;
79
- }
87
+ else if (hasDataRef.current) setIsRefetching(true);
88
+ else setIsLoading(true);
80
89
  setError(null);
90
+ let cursor = append ? cursorRef.current : void 0;
91
+ let pageIndex = append ? pageIndexRef.current : 0;
92
+ let failed = false;
93
+ let committedResult = false;
81
94
  try {
82
95
  const gen = currentDataSource({
83
96
  sorting: currentSorting,
84
97
  quickSearch: currentQuickSearch,
85
98
  pagination: append ? {
86
- pageIndex: pageIndexRef.current,
99
+ pageIndex,
87
100
  pageSize: currentPagination.pageSize
88
101
  } : currentPagination,
89
- cursor: cursorRef.current
102
+ cursor
90
103
  });
91
104
  for await (const result of gen) {
92
105
  if (controller.signal.aborted) return;
93
106
  if (result.totalRowCount != null) setTotalRowCount(result.totalRowCount);
94
- if (result.nextCursor !== void 0) cursorRef.current = result.nextCursor;
95
- setHasMore(result.hasMore !== false);
107
+ if (result.nextCursor !== void 0) cursor = result.nextCursor;
108
+ cursorRef.current = cursor;
109
+ hasMoreRef.current = result.hasMore !== false;
110
+ setHasMore(hasMoreRef.current);
96
111
  if (append) setRows((prev) => {
97
112
  const existingIds = new Set(prev.map(currentGetRowId));
98
113
  const newRows = result.rows.filter((r) => !existingIds.has(currentGetRowId(r)));
99
114
  return [...prev, ...newRows];
100
115
  });
101
- else setRows(result.rows);
116
+ else {
117
+ lastCompletedNonAppendFetchKeyRef.current = null;
118
+ setRows(result.rows);
119
+ }
102
120
  hasDataRef.current = true;
103
- pageIndexRef.current++;
121
+ committedResult = true;
122
+ pageIndex++;
123
+ pageIndexRef.current = pageIndex;
104
124
  }
125
+ if (!append && committedResult && !controller.signal.aborted) lastCompletedNonAppendFetchKeyRef.current = fetchKey;
105
126
  } catch (err) {
106
127
  if (controller.signal.aborted) return;
107
128
  console.error("[DataGrid] Data source error:", err);
129
+ failed = true;
108
130
  setError(err instanceof Error ? err : new Error(String(err)));
109
131
  } finally {
110
- if (!controller.signal.aborted) {
132
+ if (inFlightFetchRef.current === controller) {
133
+ inFlightFetchRef.current = null;
111
134
  setIsLoading(false);
112
135
  setIsRefetching(false);
113
136
  setIsLoadingMore(false);
137
+ const shouldChainLoadMore = pendingLoadMoreRef.current && !failed && !controller.signal.aborted && hasMoreRef.current && paginationModeRef.current === "infinite";
138
+ pendingLoadMoreRef.current = false;
139
+ if (shouldChainLoadMore) (0, _hexclave_shared_dist_utils_promises.runAsynchronously)(fetchPage(true));
114
140
  }
115
141
  }
116
142
  }, []);
117
143
  (0, react.useEffect)(() => {
118
- fetchPage(false).catch(() => {});
119
- return () => abortRef.current?.abort();
144
+ if (paginationMode !== "infinite") pendingLoadMoreRef.current = false;
145
+ }, [paginationMode]);
146
+ (0, react.useEffect)(() => {
147
+ const lastCompletedKey = lastCompletedNonAppendFetchKeyRef.current;
148
+ if (!(hasDataRef.current && lastCompletedKey?.dataSource === dataSource && lastCompletedKey.sortingKey === sortingKey && lastCompletedKey.quickSearchKey === quickSearchKey && lastCompletedKey.pageSize === pagination.pageSize)) (0, _hexclave_shared_dist_utils_promises.runAsynchronously)(fetchPage(false));
149
+ return () => inFlightFetchRef.current?.abort();
120
150
  }, [
121
151
  fetchPage,
122
152
  dataSource,
@@ -133,7 +163,7 @@ function useAsyncDataSource(opts) {
133
163
  hasMountedServerPaginationRef.current = true;
134
164
  return;
135
165
  }
136
- fetchPage(false).catch(() => {});
166
+ (0, _hexclave_shared_dist_utils_promises.runAsynchronously)(fetchPage(false));
137
167
  }, [
138
168
  fetchPage,
139
169
  paginationMode,
@@ -146,16 +176,20 @@ function useAsyncDataSource(opts) {
146
176
  isRefetching,
147
177
  isLoadingMore,
148
178
  loadMore: (0, react.useCallback)(() => {
149
- if (!isLoadingMore && hasMore && paginationMode === "infinite") fetchPage(true).catch(() => {});
179
+ if (paginationMode !== "infinite" || !hasMore) return;
180
+ if (inFlightFetchRef.current != null) {
181
+ pendingLoadMoreRef.current = true;
182
+ return;
183
+ }
184
+ (0, _hexclave_shared_dist_utils_promises.runAsynchronously)(fetchPage(true));
150
185
  }, [
151
- isLoadingMore,
152
186
  hasMore,
153
187
  paginationMode,
154
188
  fetchPage
155
189
  ]),
156
190
  hasMore,
157
191
  reload: (0, react.useCallback)(() => {
158
- fetchPage(false).catch(() => {});
192
+ (0, _hexclave_shared_dist_utils_promises.runAsynchronously)(fetchPage(false));
159
193
  }, [fetchPage]),
160
194
  error
161
195
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-data-source.js","names":["defaultMatchRow"],"sources":["../../../src/components/data-grid/use-data-source.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n DataGridColumnDef,\n DataGridDataSource,\n DataGridFetchParams,\n DataGridDataPaginationMode,\n DataGridPaginationModel,\n DataGridSortModel,\n RowId,\n} from \"./types\";\nimport {\n applyQuickSearch,\n buildRowComparator,\n defaultMatchRow,\n paginateRows,\n} from \"./state\";\n\nexport type UseDataSourceResult<TRow> = {\n /** All rows currently loaded (for infinite mode, the accumulated set). */\n rows: readonly TRow[];\n /** Total row count if known. */\n totalRowCount: number | undefined;\n /** Whether the initial load is in progress (no data at all yet). */\n isLoading: boolean;\n /** Whether a background refetch is happening (data already shown). */\n isRefetching: boolean;\n /** Whether more rows are being fetched (infinite scroll). */\n isLoadingMore: boolean;\n /** Request the next page (infinite scroll). */\n loadMore: () => void;\n /** Whether there are more pages to load. */\n hasMore: boolean;\n /** Reload from scratch. */\n reload: () => void;\n /**\n * Error from the most recent async fetch, if any. Consumers should render\n * an error UI and offer a `reload()` button when this is non-null. Client\n * mode never sets this (no fetching). Cleared on next successful fetch.\n */\n error: Error | null;\n};\n\n// ─── Client-side hook ────────────────────────────────────────────────\n// Memoised so resize / selection / other unrelated state changes\n// don't recompute or create new array references.\n\nfunction useClientDataSource<TRow>(opts: {\n data: readonly TRow[];\n columns: readonly DataGridColumnDef<TRow>[];\n sorting: DataGridSortModel;\n quickSearch: string;\n matchRow: (\n row: TRow,\n query: string,\n columns: readonly DataGridColumnDef<TRow>[],\n ) => boolean;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const { data, columns, sorting, quickSearch, matchRow, pagination, paginationMode } = opts;\n\n // Stable serialised keys so useMemo only fires on real changes\n const sortingKey = JSON.stringify(sorting);\n\n const processed = useMemo(() => {\n // Quick search is applied FIRST, on the full input. If nothing is\n // typed this is a zero-cost no-op (applyQuickSearch returns the\n // original array reference). Sort and paginate operate on the\n // already-filtered set so the result counts are search-aware.\n const searched = applyQuickSearch(data, quickSearch, columns, matchRow);\n const comparator = buildRowComparator(sorting, columns);\n const sorted = comparator ? [...searched].sort(comparator) : searched;\n const totalRowCount = sorted.length;\n const paged =\n paginationMode === \"client\"\n ? paginateRows(sorted as readonly TRow[], pagination)\n : sorted;\n return { rows: paged, totalRowCount };\n }, [data, sortingKey, quickSearch, matchRow, pagination.pageIndex, pagination.pageSize, paginationMode, columns]);\n\n return useMemo(() => ({\n rows: processed.rows,\n totalRowCount: processed.totalRowCount,\n isLoading: false,\n isRefetching: false,\n isLoadingMore: false,\n loadMore: () => {},\n hasMore: false,\n reload: () => {},\n error: null,\n }), [processed]);\n}\n\n// ─── Async data source hook ──────────────────────────────────────────\n// Key behaviour: when refetching (sort change), we keep showing the old\n// rows and set `isRefetching` instead of `isLoading`. This avoids the\n// jarring flash-to-skeleton on every sort toggle.\n\nfunction useAsyncDataSource<TRow>(opts: {\n dataSource: DataGridDataSource<TRow>;\n getRowId: (row: TRow) => RowId;\n sorting: DataGridSortModel;\n quickSearch: string;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const {\n dataSource,\n getRowId,\n sorting,\n quickSearch,\n pagination,\n paginationMode,\n } = opts;\n\n const [rows, setRows] = useState<TRow[]>([]);\n const [totalRowCount, setTotalRowCount] = useState<number | undefined>(undefined);\n const [isLoading, setIsLoading] = useState(true);\n const [isRefetching, setIsRefetching] = useState(false);\n const [isLoadingMore, setIsLoadingMore] = useState(false);\n const [hasMore, setHasMore] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const cursorRef = useRef<unknown>(undefined);\n const abortRef = useRef<AbortController | null>(null);\n const pageIndexRef = useRef(0);\n const hasDataRef = useRef(false);\n const hasMountedServerPaginationRef = useRef(false);\n\n const latestArgsRef = useRef({\n dataSource,\n getRowId,\n sorting,\n quickSearch,\n pagination,\n });\n latestArgsRef.current = { dataSource, getRowId, sorting, quickSearch, pagination };\n\n const sortingKey = JSON.stringify(sorting);\n const quickSearchKey = quickSearch;\n\n const fetchPage = useCallback(\n async (append: boolean) => {\n const {\n dataSource: currentDataSource,\n getRowId: currentGetRowId,\n sorting: currentSorting,\n quickSearch: currentQuickSearch,\n pagination: currentPagination,\n } = latestArgsRef.current;\n\n abortRef.current?.abort();\n const controller = new AbortController();\n abortRef.current = controller;\n\n if (append) {\n setIsLoadingMore(true);\n } else {\n // First load → skeleton. Subsequent → subtle refetch indicator.\n if (hasDataRef.current) {\n setIsRefetching(true);\n } else {\n setIsLoading(true);\n }\n cursorRef.current = undefined;\n pageIndexRef.current = 0;\n }\n // Clear previous error at the start of a new attempt; we'll set it\n // again if this attempt fails.\n setError(null);\n\n try {\n const params: DataGridFetchParams = {\n sorting: currentSorting,\n quickSearch: currentQuickSearch,\n pagination: append\n ? { pageIndex: pageIndexRef.current, pageSize: currentPagination.pageSize }\n : currentPagination,\n cursor: cursorRef.current,\n };\n\n const gen = currentDataSource(params);\n\n for await (const result of gen) {\n if (controller.signal.aborted) return;\n\n if (result.totalRowCount != null) {\n setTotalRowCount(result.totalRowCount);\n }\n if (result.nextCursor !== undefined) {\n cursorRef.current = result.nextCursor;\n }\n setHasMore(result.hasMore !== false);\n\n if (append) {\n setRows((prev) => {\n const existingIds = new Set(prev.map(currentGetRowId));\n const newRows = result.rows.filter(\n (r) => !existingIds.has(currentGetRowId(r)),\n );\n return [...prev, ...newRows];\n });\n } else {\n setRows(result.rows);\n }\n\n hasDataRef.current = true;\n pageIndexRef.current++;\n }\n } catch (err) {\n if (controller.signal.aborted) return;\n // Surface the error on the result so consumers can render retry UI.\n // Still log to console so it's visible in dev without forcing every\n // consumer to wire up error rendering.\n // eslint-disable-next-line no-console\n console.error(\"[DataGrid] Data source error:\", err);\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n if (!controller.signal.aborted) {\n setIsLoading(false);\n setIsRefetching(false);\n setIsLoadingMore(false);\n }\n }\n },\n [],\n );\n\n useEffect(() => {\n fetchPage(false).catch(() => {});\n return () => abortRef.current?.abort();\n // Also refetches when `dataSource` identity changes — consumers encode\n // external filter state into the generator's closure, so a new\n // generator reference is the signal that the query changed.\n }, [fetchPage, dataSource, sortingKey, quickSearchKey, pagination.pageSize]);\n\n useEffect(() => {\n if (paginationMode !== \"server\") {\n hasMountedServerPaginationRef.current = false;\n return;\n }\n if (!hasMountedServerPaginationRef.current) {\n hasMountedServerPaginationRef.current = true;\n return;\n }\n fetchPage(false).catch(() => {});\n }, [fetchPage, paginationMode, pagination.pageIndex]);\n\n const loadMore = useCallback(() => {\n if (!isLoadingMore && hasMore && paginationMode === \"infinite\") {\n fetchPage(true).catch(() => {});\n }\n }, [isLoadingMore, hasMore, paginationMode, fetchPage]);\n\n const reload = useCallback(() => {\n fetchPage(false).catch(() => {});\n }, [fetchPage]);\n\n return {\n rows,\n totalRowCount,\n isLoading,\n isRefetching,\n isLoadingMore,\n loadMore,\n hasMore,\n reload,\n error,\n };\n}\n\n// ─── Noop data source (stable reference) ─────────────────────────────\nconst NOOP_DATA_SOURCE: DataGridDataSource<any> = async function* () {};\nconst NOOP_GET_ROW_ID = () => \"\";\n\n// ─── Public hook ─────────────────────────────────────────────────────\n// Both inner hooks are always called (React rules-of-hooks) but only\n// one provides the returned result.\n\n/**\n * Hook that processes raw data through the grid's sort/pagination state\n * and returns the `rows` slice ready to pass to `DataGrid`. This is the\n * only correct way to feed client-side data into a grid.\n *\n * Two modes, picked by which prop you pass:\n * - `data: TRow[]` → client-side mode. In-memory sort + paginate.\n * - `dataSource: (params) => AsyncGenerator` → server / infinite mode.\n * The generator yields pages as you scroll or change pages.\n *\n * ```tsx\n * // Client-side (most common):\n * const gridData = useDataSource({\n * data: users,\n * columns,\n * getRowId: (row) => row.id,\n * sorting: gridState.sorting,\n * quickSearch: gridState.quickSearch,\n * pagination: gridState.pagination,\n * paginationMode: \"client\",\n * });\n *\n * <DataGrid\n * columns={columns}\n * rows={gridData.rows}\n * totalRowCount={gridData.totalRowCount}\n * isLoading={gridData.isLoading}\n * state={gridState}\n * onChange={setGridState}\n * getRowId={(row) => row.id}\n * />\n * ```\n *\n * Rules:\n * - Call this hook unconditionally at the top level, before any early return.\n * - `rows` on `DataGrid` must ALWAYS be `gridData.rows`, never your raw array.\n * - For server or infinite pagination, use `dataSource` — see the\n * `DataGridDataSource` type for the generator signature.\n *\n * Quick search:\n * - Client mode (`data` prop): the hook auto-filters rows via\n * `applyQuickSearch` using a default case-insensitive substring match\n * across every column. Override with `matchRow` for custom matching\n * (fuzzy, weighted, field-specific, etc.).\n * - Async mode (`dataSource` prop): the hook passes `quickSearch` into\n * `params.quickSearch` and re-runs the generator whenever the search\n * string changes. The consumer owns the matching logic (typically by\n * folding it into a backend query). The grid performs NO client-side\n * filtering in async mode.\n */\nexport function useDataSource<TRow>(opts: {\n data?: readonly TRow[];\n dataSource?: DataGridDataSource<TRow>;\n columns: readonly DataGridColumnDef<TRow>[];\n getRowId: (row: TRow) => RowId;\n sorting: DataGridSortModel;\n /** Current quick-search text, typically `gridState.quickSearch`. */\n quickSearch: string;\n /** Override the default client-mode matcher. Ignored in async mode\n * (there the generator is the matcher). */\n matchRow?: (\n row: TRow,\n query: string,\n columns: readonly DataGridColumnDef<TRow>[],\n ) => boolean;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const {\n data,\n dataSource,\n columns,\n getRowId,\n sorting,\n quickSearch,\n matchRow = defaultMatchRow,\n pagination,\n paginationMode,\n } = opts;\n\n const isClientMode = data != null && !dataSource;\n\n if (process.env.NODE_ENV !== \"production\" && data == null && dataSource == null) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[useDataSource] neither `data` nor `dataSource` was provided — \"\n + \"the grid will render empty indefinitely. Pass one or the other.\"\n );\n }\n\n // Common footgun: consumers pass `data` as a fully-materialized array and\n // set `paginationMode: \"infinite\"` expecting the grid to page through it.\n // In client mode \"infinite\" skips `paginateRows` and returns every row;\n // `hasMore` / `loadMore` on the result are always false/no-ops. If you\n // want real paging, switch to `paginationMode: \"server\"` with a\n // `dataSource` generator. If you want in-memory slicing, use `\"client\"`.\n // If you're manually accumulating rows into `data` and driving the grid's\n // sentinel via your own `hasMore`/`onLoadMore`, this warning is a hint —\n // but current behavior (full list + external sentinel) is intentional.\n if (\n process.env.NODE_ENV !== \"production\"\n && isClientMode\n && paginationMode === \"infinite\"\n && data.length > 0\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[useDataSource] `paginationMode: \\\"infinite\\\"` with a `data` array \"\n + \"skips pagination entirely. Prefer `\\\"client\\\"` for in-memory lists \"\n + \"or `\\\"server\\\"` + a `dataSource` generator for real paging.\"\n );\n }\n\n const clientResult = useClientDataSource({\n data: data ?? [],\n columns,\n sorting,\n quickSearch,\n matchRow,\n pagination,\n paginationMode,\n });\n\n const asyncResult = useAsyncDataSource({\n dataSource: dataSource ?? NOOP_DATA_SOURCE,\n getRowId: dataSource ? getRowId : NOOP_GET_ROW_ID,\n sorting,\n quickSearch,\n pagination,\n paginationMode,\n });\n\n return isClientMode ? clientResult : asyncResult;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-data-source.js","names":["defaultMatchRow"],"sources":["../../../src/components/data-grid/use-data-source.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { runAsynchronously } from \"@hexclave/shared/dist/utils/promises\";\nimport type {\n DataGridColumnDef,\n DataGridDataSource,\n DataGridFetchParams,\n DataGridDataPaginationMode,\n DataGridPaginationModel,\n DataGridSortModel,\n RowId,\n} from \"./types\";\nimport {\n applyQuickSearch,\n buildRowComparator,\n defaultMatchRow,\n paginateRows,\n} from \"./state\";\n\nexport type UseDataSourceResult<TRow> = {\n /** All rows currently loaded (for infinite mode, the accumulated set). */\n rows: readonly TRow[];\n /** Total row count if known. */\n totalRowCount: number | undefined;\n /** Whether the initial load is in progress (no data at all yet). */\n isLoading: boolean;\n /** Whether a background refetch is happening (data already shown). */\n isRefetching: boolean;\n /** Whether more rows are being fetched (infinite scroll). */\n isLoadingMore: boolean;\n /** Request the next page (infinite scroll). */\n loadMore: () => void;\n /** Whether there are more pages to load. */\n hasMore: boolean;\n /** Reload from scratch. */\n reload: () => void;\n /**\n * Error from the most recent async fetch, if any. Consumers should render\n * an error UI and offer a `reload()` button when this is non-null. Client\n * mode never sets this (no fetching). Cleared on next successful fetch.\n */\n error: Error | null;\n};\n\n// ─── Client-side hook ────────────────────────────────────────────────\n// Memoised so resize / selection / other unrelated state changes\n// don't recompute or create new array references.\n\nfunction useClientDataSource<TRow>(opts: {\n data: readonly TRow[];\n columns: readonly DataGridColumnDef<TRow>[];\n sorting: DataGridSortModel;\n quickSearch: string;\n matchRow: (\n row: TRow,\n query: string,\n columns: readonly DataGridColumnDef<TRow>[],\n ) => boolean;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const { data, columns, sorting, quickSearch, matchRow, pagination, paginationMode } = opts;\n\n // Stable serialised keys so useMemo only fires on real changes\n const sortingKey = JSON.stringify(sorting);\n\n const processed = useMemo(() => {\n // Quick search is applied FIRST, on the full input. If nothing is\n // typed this is a zero-cost no-op (applyQuickSearch returns the\n // original array reference). Sort and paginate operate on the\n // already-filtered set so the result counts are search-aware.\n const searched = applyQuickSearch(data, quickSearch, columns, matchRow);\n const comparator = buildRowComparator(sorting, columns);\n const sorted = comparator ? [...searched].sort(comparator) : searched;\n const totalRowCount = sorted.length;\n const paged =\n paginationMode === \"client\"\n ? paginateRows(sorted as readonly TRow[], pagination)\n : sorted;\n return { rows: paged, totalRowCount };\n }, [data, sortingKey, quickSearch, matchRow, pagination.pageIndex, pagination.pageSize, paginationMode, columns]);\n\n return useMemo(() => ({\n rows: processed.rows,\n totalRowCount: processed.totalRowCount,\n isLoading: false,\n isRefetching: false,\n isLoadingMore: false,\n loadMore: () => {},\n hasMore: false,\n reload: () => {},\n error: null,\n }), [processed]);\n}\n\n// ─── Async data source hook ──────────────────────────────────────────\n// Key behaviour: when refetching (sort change), we keep showing the old\n// rows and set `isRefetching` instead of `isLoading`. This avoids the\n// jarring flash-to-skeleton on every sort toggle.\n\nfunction useAsyncDataSource<TRow>(opts: {\n dataSource: DataGridDataSource<TRow>;\n getRowId: (row: TRow) => RowId;\n sorting: DataGridSortModel;\n quickSearch: string;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const {\n dataSource,\n getRowId,\n sorting,\n quickSearch,\n pagination,\n paginationMode,\n } = opts;\n\n const [rows, setRows] = useState<TRow[]>([]);\n const [totalRowCount, setTotalRowCount] = useState<number | undefined>(undefined);\n const [isLoading, setIsLoading] = useState(true);\n const [isRefetching, setIsRefetching] = useState(false);\n const [isLoadingMore, setIsLoadingMore] = useState(false);\n const [hasMore, setHasMore] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const cursorRef = useRef<unknown>(undefined);\n const inFlightFetchRef = useRef<AbortController | null>(null);\n // The infinite-scroll sentinel's IntersectionObserver only fires on\n // intersection *changes* (by design, to avoid auto-loading the whole\n // dataset). If `loadMore` is called while another fetch is in flight we\n // can't just drop it: the sentinel won't fire again until the user scrolls\n // away and back. Remember the request and replay it once the in-flight\n // fetch settles.\n const pendingLoadMoreRef = useRef(false);\n const hasMoreRef = useRef(true);\n const paginationModeRef = useRef(paginationMode);\n paginationModeRef.current = paginationMode;\n const pageIndexRef = useRef(0);\n const hasDataRef = useRef(false);\n const hasMountedServerPaginationRef = useRef(false);\n // Effects can be replayed when a descendant suspends. Only a fetch that\n // actually delivered results is safe to reuse as a skip key.\n const lastCompletedNonAppendFetchKeyRef = useRef<{\n dataSource: DataGridDataSource<TRow>,\n sortingKey: string,\n quickSearchKey: string,\n pageSize: number,\n } | null>(null);\n\n const latestArgsRef = useRef({\n dataSource,\n getRowId,\n sorting,\n quickSearch,\n pagination,\n });\n latestArgsRef.current = { dataSource, getRowId, sorting, quickSearch, pagination };\n\n const sortingKey = JSON.stringify(sorting);\n const quickSearchKey = quickSearch;\n\n const fetchPage = useCallback(\n async (append: boolean) => {\n if (append && inFlightFetchRef.current != null) {\n return;\n }\n\n const {\n dataSource: currentDataSource,\n getRowId: currentGetRowId,\n sorting: currentSorting,\n quickSearch: currentQuickSearch,\n pagination: currentPagination,\n } = latestArgsRef.current;\n\n inFlightFetchRef.current?.abort();\n const controller = new AbortController();\n inFlightFetchRef.current = controller;\n const fetchKey = {\n dataSource: currentDataSource,\n sortingKey: JSON.stringify(currentSorting),\n quickSearchKey: currentQuickSearch,\n pageSize: currentPagination.pageSize,\n };\n\n if (append) {\n setIsLoadingMore(true);\n } else {\n // First load → skeleton. Subsequent → subtle refetch indicator.\n if (hasDataRef.current) {\n setIsRefetching(true);\n } else {\n setIsLoading(true);\n }\n }\n // Clear previous error at the start of a new attempt; we'll set it\n // again if this attempt fails.\n setError(null);\n\n // Cursor/page-index state is tracked locally and only committed to the\n // shared refs when this fetch actually delivers a result. An aborted\n // reset must not clobber the committed pagination state: if it did, a\n // later effect run that skips via `lastCompletedNonAppendFetchKeyRef`\n // would keep the old rows but leave `cursorRef === undefined`, and the\n // next loadMore would silently restart from page one.\n let cursor = append ? cursorRef.current : undefined;\n let pageIndex = append ? pageIndexRef.current : 0;\n let failed = false;\n // The abort guard lives inside the for-await body, so a generator that\n // completes with zero yields never hits it and would otherwise fall\n // through to the post-loop completion-key write. Only mark a reset\n // complete after it has actually delivered (and committed) a result.\n let committedResult = false;\n\n try {\n const params: DataGridFetchParams = {\n sorting: currentSorting,\n quickSearch: currentQuickSearch,\n pagination: append\n ? { pageIndex, pageSize: currentPagination.pageSize }\n : currentPagination,\n cursor,\n };\n\n const gen = currentDataSource(params);\n\n for await (const result of gen) {\n if (controller.signal.aborted) return;\n\n if (result.totalRowCount != null) {\n setTotalRowCount(result.totalRowCount);\n }\n if (result.nextCursor !== undefined) {\n cursor = result.nextCursor;\n }\n cursorRef.current = cursor;\n hasMoreRef.current = result.hasMore !== false;\n setHasMore(hasMoreRef.current);\n\n if (append) {\n setRows((prev) => {\n const existingIds = new Set(prev.map(currentGetRowId));\n const newRows = result.rows.filter(\n (r) => !existingIds.has(currentGetRowId(r)),\n );\n return [...prev, ...newRows];\n });\n } else {\n // The visible rows now belong to this (possibly still incomplete)\n // reset, so the previously completed fetch key no longer describes\n // them and must not allow a skip.\n lastCompletedNonAppendFetchKeyRef.current = null;\n setRows(result.rows);\n }\n\n hasDataRef.current = true;\n committedResult = true;\n pageIndex++;\n pageIndexRef.current = pageIndex;\n }\n if (!append && committedResult && !controller.signal.aborted) {\n lastCompletedNonAppendFetchKeyRef.current = fetchKey;\n }\n } catch (err) {\n if (controller.signal.aborted) return;\n // Surface the error on the result so consumers can render retry UI.\n // Still log to console so it's visible in dev without forcing every\n // consumer to wire up error rendering.\n // eslint-disable-next-line no-console\n console.error(\"[DataGrid] Data source error:\", err);\n failed = true;\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n // Only the owning fetch resets the loading flags; if this fetch was\n // replaced by a newer one, that fetch manages them instead. This also\n // covers cleanup-initiated aborts, which would otherwise leave\n // `isLoadingMore` stuck and block all future loadMore calls.\n if (inFlightFetchRef.current === controller) {\n inFlightFetchRef.current = null;\n setIsLoading(false);\n setIsRefetching(false);\n setIsLoadingMore(false);\n // Replay a loadMore that was requested (and deferred) while this\n // fetch was in flight — but only if this fetch succeeded while we\n // are still in infinite mode. Skip on failure (the append would run\n // against inconsistent state and its setError(null) would hide this\n // fetch's error), when pagination mode changed away from infinite,\n // and on unmount-cleanup aborts — `inFlightFetchRef.current ===\n // controller` with an aborted signal only happens then.\n const shouldChainLoadMore =\n pendingLoadMoreRef.current\n && !failed\n && !controller.signal.aborted\n && hasMoreRef.current\n && paginationModeRef.current === \"infinite\";\n pendingLoadMoreRef.current = false;\n if (shouldChainLoadMore) {\n runAsynchronously(fetchPage(true));\n }\n }\n }\n },\n [],\n );\n\n useEffect(() => {\n // Deferred loadMore is only meaningful for infinite scroll; drop it when\n // the consumer leaves that mode so a later settle cannot append wrongly.\n if (paginationMode !== \"infinite\") {\n pendingLoadMoreRef.current = false;\n }\n }, [paginationMode]);\n\n useEffect(() => {\n const lastCompletedKey = lastCompletedNonAppendFetchKeyRef.current;\n const isSameCompletedFetch = hasDataRef.current\n && lastCompletedKey?.dataSource === dataSource\n && lastCompletedKey.sortingKey === sortingKey\n && lastCompletedKey.quickSearchKey === quickSearchKey\n && lastCompletedKey.pageSize === pagination.pageSize;\n if (!isSameCompletedFetch) {\n runAsynchronously(fetchPage(false));\n }\n // Always return the abort cleanup (even when the fetch is skipped) so an\n // in-flight append is cancelled on unmount.\n return () => inFlightFetchRef.current?.abort();\n // Also refetches when `dataSource` identity changes — consumers encode\n // external filter state into the generator's closure, so a new\n // generator reference is the signal that the query changed.\n }, [fetchPage, dataSource, sortingKey, quickSearchKey, pagination.pageSize]);\n\n useEffect(() => {\n if (paginationMode !== \"server\") {\n hasMountedServerPaginationRef.current = false;\n return;\n }\n if (!hasMountedServerPaginationRef.current) {\n hasMountedServerPaginationRef.current = true;\n return;\n }\n runAsynchronously(fetchPage(false));\n }, [fetchPage, paginationMode, pagination.pageIndex]);\n\n const loadMore = useCallback(() => {\n if (paginationMode !== \"infinite\" || !hasMore) {\n return;\n }\n // Keep pagination single-flight. In particular, do not let the sentinel\n // start an append against a cursor that a concurrent reset is replacing.\n // Queue the request instead of dropping it (see pendingLoadMoreRef).\n if (inFlightFetchRef.current != null) {\n pendingLoadMoreRef.current = true;\n return;\n }\n runAsynchronously(fetchPage(true));\n }, [hasMore, paginationMode, fetchPage]);\n\n const reload = useCallback(() => {\n runAsynchronously(fetchPage(false));\n }, [fetchPage]);\n\n return {\n rows,\n totalRowCount,\n isLoading,\n isRefetching,\n isLoadingMore,\n loadMore,\n hasMore,\n reload,\n error,\n };\n}\n\n// ─── Noop data source (stable reference) ─────────────────────────────\nconst NOOP_DATA_SOURCE: DataGridDataSource<any> = async function* () {};\nconst NOOP_GET_ROW_ID = () => \"\";\n\n// ─── Public hook ─────────────────────────────────────────────────────\n// Both inner hooks are always called (React rules-of-hooks) but only\n// one provides the returned result.\n\n/**\n * Hook that processes raw data through the grid's sort/pagination state\n * and returns the `rows` slice ready to pass to `DataGrid`. This is the\n * only correct way to feed client-side data into a grid.\n *\n * Two modes, picked by which prop you pass:\n * - `data: TRow[]` → client-side mode. In-memory sort + paginate.\n * - `dataSource: (params) => AsyncGenerator` → server / infinite mode.\n * The generator yields pages as you scroll or change pages.\n *\n * ```tsx\n * // Client-side (most common):\n * const gridData = useDataSource({\n * data: users,\n * columns,\n * getRowId: (row) => row.id,\n * sorting: gridState.sorting,\n * quickSearch: gridState.quickSearch,\n * pagination: gridState.pagination,\n * paginationMode: \"client\",\n * });\n *\n * <DataGrid\n * columns={columns}\n * rows={gridData.rows}\n * totalRowCount={gridData.totalRowCount}\n * isLoading={gridData.isLoading}\n * state={gridState}\n * onChange={setGridState}\n * getRowId={(row) => row.id}\n * />\n * ```\n *\n * Rules:\n * - Call this hook unconditionally at the top level, before any early return.\n * - `rows` on `DataGrid` must ALWAYS be `gridData.rows`, never your raw array.\n * - For server or infinite pagination, use `dataSource` — see the\n * `DataGridDataSource` type for the generator signature.\n *\n * Quick search:\n * - Client mode (`data` prop): the hook auto-filters rows via\n * `applyQuickSearch` using a default case-insensitive substring match\n * across every column. Override with `matchRow` for custom matching\n * (fuzzy, weighted, field-specific, etc.).\n * - Async mode (`dataSource` prop): the hook passes `quickSearch` into\n * `params.quickSearch` and re-runs the generator whenever the search\n * string changes. The consumer owns the matching logic (typically by\n * folding it into a backend query). The grid performs NO client-side\n * filtering in async mode.\n */\nexport function useDataSource<TRow>(opts: {\n data?: readonly TRow[];\n dataSource?: DataGridDataSource<TRow>;\n columns: readonly DataGridColumnDef<TRow>[];\n getRowId: (row: TRow) => RowId;\n sorting: DataGridSortModel;\n /** Current quick-search text, typically `gridState.quickSearch`. */\n quickSearch: string;\n /** Override the default client-mode matcher. Ignored in async mode\n * (there the generator is the matcher). */\n matchRow?: (\n row: TRow,\n query: string,\n columns: readonly DataGridColumnDef<TRow>[],\n ) => boolean;\n pagination: DataGridPaginationModel;\n paginationMode: DataGridDataPaginationMode;\n}): UseDataSourceResult<TRow> {\n const {\n data,\n dataSource,\n columns,\n getRowId,\n sorting,\n quickSearch,\n matchRow = defaultMatchRow,\n pagination,\n paginationMode,\n } = opts;\n\n const isClientMode = data != null && !dataSource;\n\n if (process.env.NODE_ENV !== \"production\" && data == null && dataSource == null) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[useDataSource] neither `data` nor `dataSource` was provided — \"\n + \"the grid will render empty indefinitely. Pass one or the other.\"\n );\n }\n\n // Common footgun: consumers pass `data` as a fully-materialized array and\n // set `paginationMode: \"infinite\"` expecting the grid to page through it.\n // In client mode \"infinite\" skips `paginateRows` and returns every row;\n // `hasMore` / `loadMore` on the result are always false/no-ops. If you\n // want real paging, switch to `paginationMode: \"server\"` with a\n // `dataSource` generator. If you want in-memory slicing, use `\"client\"`.\n // If you're manually accumulating rows into `data` and driving the grid's\n // sentinel via your own `hasMore`/`onLoadMore`, this warning is a hint —\n // but current behavior (full list + external sentinel) is intentional.\n if (\n process.env.NODE_ENV !== \"production\"\n && isClientMode\n && paginationMode === \"infinite\"\n && data.length > 0\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[useDataSource] `paginationMode: \\\"infinite\\\"` with a `data` array \"\n + \"skips pagination entirely. Prefer `\\\"client\\\"` for in-memory lists \"\n + \"or `\\\"server\\\"` + a `dataSource` generator for real paging.\"\n );\n }\n\n const clientResult = useClientDataSource({\n data: data ?? [],\n columns,\n sorting,\n quickSearch,\n matchRow,\n pagination,\n paginationMode,\n });\n\n const asyncResult = useAsyncDataSource({\n dataSource: dataSource ?? NOOP_DATA_SOURCE,\n getRowId: dataSource ? getRowId : NOOP_GET_ROW_ID,\n sorting,\n quickSearch,\n pagination,\n paginationMode,\n });\n\n return isClientMode ? clientResult : asyncResult;\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,276 @@
1
+ let react_jsx_runtime = require("react/jsx-runtime");
2
+ let _testing_library_react = require("@testing-library/react");
3
+ let vitest = require("vitest");
4
+ let __use_data_source_js = require("./use-data-source.js");
5
+ //#region src/components/data-grid/use-data-source.test.tsx
6
+ // @vitest-environment jsdom
7
+ const columns = [{
8
+ id: "name",
9
+ header: "Name",
10
+ accessor: (row) => row.name,
11
+ width: 160
12
+ }];
13
+ function DataSourceHarness({ dataSource, paginationMode = "infinite" }) {
14
+ const gridData = (0, __use_data_source_js.useDataSource)({
15
+ dataSource,
16
+ columns,
17
+ getRowId: (row) => row.id,
18
+ sorting: [],
19
+ quickSearch: "",
20
+ pagination: {
21
+ pageIndex: 0,
22
+ pageSize: 25
23
+ },
24
+ paginationMode
25
+ });
26
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
27
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
28
+ onClick: gridData.loadMore,
29
+ children: "Load more"
30
+ }),
31
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32
+ "data-testid": "row-count",
33
+ children: gridData.rows.length
34
+ }),
35
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
36
+ "data-testid": "row-name",
37
+ children: gridData.rows[0]?.name ?? ""
38
+ }),
39
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
40
+ "data-testid": "error",
41
+ children: gridData.error?.message ?? ""
42
+ })
43
+ ] });
44
+ }
45
+ (0, vitest.afterEach)(() => {
46
+ (0, _testing_library_react.cleanup)();
47
+ });
48
+ (0, vitest.describe)("useDataSource infinite pagination", () => {
49
+ (0, vitest.it)("defers loadMore during the initial fetch and replays it once settled", async () => {
50
+ const calls = [];
51
+ let resolveInitial = () => {};
52
+ const initialFetch = new Promise((resolve) => {
53
+ resolveInitial = resolve;
54
+ });
55
+ const dataSource = async function* (params) {
56
+ calls.push({ cursor: params.cursor });
57
+ if (calls.length === 1) {
58
+ await initialFetch;
59
+ yield {
60
+ rows: [{
61
+ id: "row-1",
62
+ name: "Row 1"
63
+ }],
64
+ nextCursor: "cursor-1",
65
+ hasMore: true
66
+ };
67
+ return;
68
+ }
69
+ yield {
70
+ rows: [{
71
+ id: "row-2",
72
+ name: "Row 2"
73
+ }],
74
+ nextCursor: null,
75
+ hasMore: false
76
+ };
77
+ };
78
+ const { getByRole } = (0, _testing_library_react.render)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource }));
79
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(calls).toHaveLength(1));
80
+ _testing_library_react.fireEvent.click(getByRole("button", { name: "Load more" }));
81
+ (0, vitest.expect)(calls).toHaveLength(1);
82
+ await (0, _testing_library_react.act)(async () => {
83
+ resolveInitial();
84
+ });
85
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(calls).toHaveLength(2));
86
+ (0, vitest.expect)(calls[1]).toEqual({ cursor: "cursor-1" });
87
+ });
88
+ (0, vitest.it)("keeps the committed cursor when an aborted reset is followed by a skipped refetch", async () => {
89
+ const callsA = [];
90
+ const dataSourceA = async function* (params) {
91
+ callsA.push({ cursor: params.cursor });
92
+ if (callsA.length === 1) {
93
+ yield {
94
+ rows: [{
95
+ id: "row-1",
96
+ name: "Row 1"
97
+ }],
98
+ nextCursor: "cursor-1",
99
+ hasMore: true
100
+ };
101
+ return;
102
+ }
103
+ yield {
104
+ rows: [{
105
+ id: "row-2",
106
+ name: "Row 2"
107
+ }],
108
+ nextCursor: null,
109
+ hasMore: false
110
+ };
111
+ };
112
+ let resolveB = () => {};
113
+ const dataSourceB = async function* () {
114
+ await new Promise((resolve) => {
115
+ resolveB = resolve;
116
+ });
117
+ };
118
+ const { getByRole, getByTestId, rerender } = (0, _testing_library_react.render)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource: dataSourceA }));
119
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(getByTestId("row-count").textContent).toBe("1"));
120
+ rerender(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource: dataSourceB }));
121
+ rerender(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource: dataSourceA }));
122
+ await (0, _testing_library_react.act)(async () => {
123
+ resolveB();
124
+ });
125
+ _testing_library_react.fireEvent.click(getByRole("button", { name: "Load more" }));
126
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(getByTestId("row-count").textContent).toBe("2"));
127
+ (0, vitest.expect)(callsA).toEqual([{ cursor: void 0 }, { cursor: "cursor-1" }]);
128
+ });
129
+ (0, vitest.it)("does not treat an aborted zero-yield reset as a completed fetch", async () => {
130
+ const dataSourceA = async function* () {
131
+ yield {
132
+ rows: [{
133
+ id: "row-a",
134
+ name: "A"
135
+ }],
136
+ nextCursor: null,
137
+ hasMore: false
138
+ };
139
+ };
140
+ let bCalls = 0;
141
+ let resolveFirstB = () => {};
142
+ const dataSourceB = async function* () {
143
+ bCalls++;
144
+ if (bCalls === 1) {
145
+ await new Promise((resolve) => {
146
+ resolveFirstB = resolve;
147
+ });
148
+ return;
149
+ }
150
+ yield {
151
+ rows: [{
152
+ id: "row-b",
153
+ name: "B"
154
+ }],
155
+ nextCursor: null,
156
+ hasMore: false
157
+ };
158
+ };
159
+ const { getByTestId, rerender } = (0, _testing_library_react.render)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource: dataSourceA }));
160
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(getByTestId("row-name").textContent).toBe("A"));
161
+ rerender(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource: dataSourceB }));
162
+ rerender(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource: dataSourceA }));
163
+ await (0, _testing_library_react.act)(async () => {
164
+ resolveFirstB();
165
+ });
166
+ rerender(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource: dataSourceB }));
167
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(getByTestId("row-name").textContent).toBe("B"));
168
+ (0, vitest.expect)(bCalls).toBe(2);
169
+ });
170
+ (0, vitest.it)("does not replay a deferred loadMore after the in-flight fetch errors", async () => {
171
+ const calls = [];
172
+ let rejectInitial = () => {};
173
+ const initialFetch = new Promise((_resolve, reject) => {
174
+ rejectInitial = reject;
175
+ });
176
+ const dataSource = async function* (params) {
177
+ calls.push({ cursor: params.cursor });
178
+ await initialFetch;
179
+ yield {
180
+ rows: [],
181
+ nextCursor: null,
182
+ hasMore: false
183
+ };
184
+ };
185
+ const { getByRole, getByTestId } = (0, _testing_library_react.render)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource }));
186
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(calls).toHaveLength(1));
187
+ _testing_library_react.fireEvent.click(getByRole("button", { name: "Load more" }));
188
+ await (0, _testing_library_react.act)(async () => {
189
+ rejectInitial(/* @__PURE__ */ new Error("fetch failed"));
190
+ });
191
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(getByTestId("error").textContent).toBe("fetch failed"));
192
+ (0, vitest.expect)(calls).toHaveLength(1);
193
+ });
194
+ (0, vitest.it)("discards a deferred loadMore when pagination mode leaves infinite", async () => {
195
+ const calls = [];
196
+ let resolveInitial = () => {};
197
+ const initialFetch = new Promise((resolve) => {
198
+ resolveInitial = resolve;
199
+ });
200
+ const dataSource = async function* (params) {
201
+ calls.push({ cursor: params.cursor });
202
+ if (calls.length === 1) {
203
+ await initialFetch;
204
+ yield {
205
+ rows: [{
206
+ id: "row-1",
207
+ name: "Row 1"
208
+ }],
209
+ nextCursor: "cursor-1",
210
+ hasMore: true
211
+ };
212
+ return;
213
+ }
214
+ yield {
215
+ rows: [{
216
+ id: "row-2",
217
+ name: "Row 2"
218
+ }],
219
+ nextCursor: null,
220
+ hasMore: false
221
+ };
222
+ };
223
+ const { getByRole, rerender } = (0, _testing_library_react.render)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, {
224
+ dataSource,
225
+ paginationMode: "infinite"
226
+ }));
227
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(calls).toHaveLength(1));
228
+ _testing_library_react.fireEvent.click(getByRole("button", { name: "Load more" }));
229
+ (0, vitest.expect)(calls).toHaveLength(1);
230
+ rerender(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, {
231
+ dataSource,
232
+ paginationMode: "server"
233
+ }));
234
+ await (0, _testing_library_react.act)(async () => {
235
+ resolveInitial();
236
+ });
237
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(calls).toHaveLength(1));
238
+ await (0, _testing_library_react.act)(async () => {
239
+ await new Promise((resolve) => setTimeout(resolve, 20));
240
+ });
241
+ (0, vitest.expect)(calls).toHaveLength(1);
242
+ });
243
+ (0, vitest.it)("uses the completed page cursor for the next loadMore fetch", async () => {
244
+ const calls = [];
245
+ const dataSource = async function* (params) {
246
+ calls.push({ cursor: params.cursor });
247
+ if (calls.length === 1) {
248
+ yield {
249
+ rows: [{
250
+ id: "row-1",
251
+ name: "Row 1"
252
+ }],
253
+ nextCursor: "cursor-1",
254
+ hasMore: true
255
+ };
256
+ return;
257
+ }
258
+ yield {
259
+ rows: [{
260
+ id: "row-2",
261
+ name: "Row 2"
262
+ }],
263
+ nextCursor: null,
264
+ hasMore: false
265
+ };
266
+ };
267
+ const { getByRole, getByTestId } = (0, _testing_library_react.render)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataSourceHarness, { dataSource }));
268
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(getByTestId("row-count").textContent).toBe("1"));
269
+ _testing_library_react.fireEvent.click(getByRole("button", { name: "Load more" }));
270
+ await (0, _testing_library_react.waitFor)(() => (0, vitest.expect)(getByTestId("row-count").textContent).toBe("2"));
271
+ (0, vitest.expect)(calls).toEqual([{ cursor: void 0 }, { cursor: "cursor-1" }]);
272
+ });
273
+ });
274
+ //#endregion
275
+
276
+ //# sourceMappingURL=use-data-source.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-data-source.test.js","names":[],"sources":["../../../src/components/data-grid/use-data-source.test.tsx"],"sourcesContent":["// @vitest-environment jsdom\n\nimport { act, cleanup, fireEvent, render, waitFor } from \"@testing-library/react\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { useDataSource } from \"./use-data-source\";\nimport type { DataGridColumnDef, DataGridDataPaginationMode, DataGridDataSource } from \"./types\";\n\ntype Row = {\n id: string,\n name: string,\n};\n\nconst columns: DataGridColumnDef<Row>[] = [\n {\n id: \"name\",\n header: \"Name\",\n accessor: (row) => row.name,\n width: 160,\n },\n];\n\nfunction DataSourceHarness({\n dataSource,\n paginationMode = \"infinite\",\n}: {\n dataSource: DataGridDataSource<Row>,\n paginationMode?: DataGridDataPaginationMode,\n}) {\n const gridData = useDataSource({\n dataSource,\n columns,\n getRowId: (row) => row.id,\n sorting: [],\n quickSearch: \"\",\n pagination: { pageIndex: 0, pageSize: 25 },\n paginationMode,\n });\n\n return (\n <>\n <button onClick={gridData.loadMore}>Load more</button>\n <span data-testid=\"row-count\">{gridData.rows.length}</span>\n <span data-testid=\"row-name\">{gridData.rows[0]?.name ?? \"\"}</span>\n <span data-testid=\"error\">{gridData.error?.message ?? \"\"}</span>\n </>\n );\n}\n\nafterEach(() => {\n cleanup();\n});\n\ndescribe(\"useDataSource infinite pagination\", () => {\n it(\"defers loadMore during the initial fetch and replays it once settled\", async () => {\n const calls: Array<{ cursor: unknown }> = [];\n let resolveInitial: (value: void) => void = () => {};\n const initialFetch = new Promise<void>((resolve) => {\n resolveInitial = resolve;\n });\n const dataSource: DataGridDataSource<Row> = async function* (params) {\n calls.push({ cursor: params.cursor });\n if (calls.length === 1) {\n await initialFetch;\n yield {\n rows: [{ id: \"row-1\", name: \"Row 1\" }],\n nextCursor: \"cursor-1\",\n hasMore: true,\n };\n return;\n }\n yield {\n rows: [{ id: \"row-2\", name: \"Row 2\" }],\n nextCursor: null,\n hasMore: false,\n };\n };\n\n const { getByRole } = render(<DataSourceHarness dataSource={dataSource} />);\n await waitFor(() => expect(calls).toHaveLength(1));\n\n fireEvent.click(getByRole(\"button\", { name: \"Load more\" }));\n // Not started concurrently — queued behind the in-flight initial fetch.\n expect(calls).toHaveLength(1);\n\n await act(async () => {\n resolveInitial();\n });\n // The queued loadMore replays automatically with the completed cursor,\n // so a sentinel that fired during the fetch is not silently dropped.\n await waitFor(() => expect(calls).toHaveLength(2));\n expect(calls[1]).toEqual({ cursor: \"cursor-1\" });\n });\n\n it(\"keeps the committed cursor when an aborted reset is followed by a skipped refetch\", async () => {\n const callsA: Array<{ cursor: unknown }> = [];\n const dataSourceA: DataGridDataSource<Row> = async function* (params) {\n callsA.push({ cursor: params.cursor });\n if (callsA.length === 1) {\n yield {\n rows: [{ id: \"row-1\", name: \"Row 1\" }],\n nextCursor: \"cursor-1\",\n hasMore: true,\n };\n return;\n }\n yield {\n rows: [{ id: \"row-2\", name: \"Row 2\" }],\n nextCursor: null,\n hasMore: false,\n };\n };\n // Stays in flight until we resolve it, after the switch back to A has\n // already aborted it.\n let resolveB: (value: void) => void = () => {};\n const dataSourceB: DataGridDataSource<Row> = async function* () {\n await new Promise<void>((resolve) => {\n resolveB = resolve;\n });\n };\n\n const { getByRole, getByTestId, rerender } = render(\n <DataSourceHarness dataSource={dataSourceA} />,\n );\n await waitFor(() => expect(getByTestId(\"row-count\").textContent).toBe(\"1\"));\n\n // Unstable dataSource identity flipping away and back to a completed\n // reference: the B reset starts (and used to wipe cursor/pageIndex\n // immediately), then gets aborted; the A effect run matches the\n // completed-fetch key and skips.\n rerender(<DataSourceHarness dataSource={dataSourceB} />);\n rerender(<DataSourceHarness dataSource={dataSourceA} />);\n await act(async () => {\n resolveB();\n });\n\n fireEvent.click(getByRole(\"button\", { name: \"Load more\" }));\n await waitFor(() => expect(getByTestId(\"row-count\").textContent).toBe(\"2\"));\n // The append must continue from the committed cursor, not restart from\n // the beginning with cursor === undefined.\n expect(callsA).toEqual([{ cursor: undefined }, { cursor: \"cursor-1\" }]);\n });\n\n it(\"does not treat an aborted zero-yield reset as a completed fetch\", async () => {\n const dataSourceA: DataGridDataSource<Row> = async function* () {\n yield {\n rows: [{ id: \"row-a\", name: \"A\" }],\n nextCursor: null,\n hasMore: false,\n };\n };\n let bCalls = 0;\n let resolveFirstB: (value: void) => void = () => {};\n const dataSourceB: DataGridDataSource<Row> = async function* () {\n bCalls++;\n if (bCalls === 1) {\n // Aborted before any yield: completing this must not mark B as a\n // successful skip key, or a later render with B would keep A's rows.\n await new Promise<void>((resolve) => {\n resolveFirstB = resolve;\n });\n return;\n }\n yield {\n rows: [{ id: \"row-b\", name: \"B\" }],\n nextCursor: null,\n hasMore: false,\n };\n };\n\n const { getByTestId, rerender } = render(\n <DataSourceHarness dataSource={dataSourceA} />,\n );\n await waitFor(() => expect(getByTestId(\"row-name\").textContent).toBe(\"A\"));\n\n rerender(<DataSourceHarness dataSource={dataSourceB} />);\n rerender(<DataSourceHarness dataSource={dataSourceA} />);\n await act(async () => {\n resolveFirstB();\n });\n\n rerender(<DataSourceHarness dataSource={dataSourceB} />);\n await waitFor(() => expect(getByTestId(\"row-name\").textContent).toBe(\"B\"));\n expect(bCalls).toBe(2);\n });\n\n it(\"does not replay a deferred loadMore after the in-flight fetch errors\", async () => {\n const calls: Array<{ cursor: unknown }> = [];\n let rejectInitial: (err: Error) => void = () => {};\n const initialFetch = new Promise<void>((_resolve, reject) => {\n rejectInitial = reject;\n });\n const dataSource: DataGridDataSource<Row> = async function* (params) {\n calls.push({ cursor: params.cursor });\n await initialFetch;\n yield { rows: [], nextCursor: null, hasMore: false };\n };\n\n const { getByRole, getByTestId } = render(<DataSourceHarness dataSource={dataSource} />);\n await waitFor(() => expect(calls).toHaveLength(1));\n\n // Deferred while the (soon-to-fail) fetch is in flight.\n fireEvent.click(getByRole(\"button\", { name: \"Load more\" }));\n\n await act(async () => {\n rejectInitial(new Error(\"fetch failed\"));\n });\n\n // The failed fetch must surface its error and must NOT chain the deferred\n // append (which would fetch against inconsistent state and clear the\n // error again via setError(null)).\n await waitFor(() => expect(getByTestId(\"error\").textContent).toBe(\"fetch failed\"));\n expect(calls).toHaveLength(1);\n });\n\n it(\"discards a deferred loadMore when pagination mode leaves infinite\", async () => {\n const calls: Array<{ cursor: unknown }> = [];\n let resolveInitial: (value: void) => void = () => {};\n const initialFetch = new Promise<void>((resolve) => {\n resolveInitial = resolve;\n });\n const dataSource: DataGridDataSource<Row> = async function* (params) {\n calls.push({ cursor: params.cursor });\n if (calls.length === 1) {\n await initialFetch;\n yield {\n rows: [{ id: \"row-1\", name: \"Row 1\" }],\n nextCursor: \"cursor-1\",\n hasMore: true,\n };\n return;\n }\n yield {\n rows: [{ id: \"row-2\", name: \"Row 2\" }],\n nextCursor: null,\n hasMore: false,\n };\n };\n\n const { getByRole, rerender } = render(\n <DataSourceHarness dataSource={dataSource} paginationMode=\"infinite\" />,\n );\n await waitFor(() => expect(calls).toHaveLength(1));\n\n fireEvent.click(getByRole(\"button\", { name: \"Load more\" }));\n expect(calls).toHaveLength(1);\n\n rerender(<DataSourceHarness dataSource={dataSource} paginationMode=\"server\" />);\n\n await act(async () => {\n resolveInitial();\n });\n\n // Mode left infinite while the deferred loadMore was queued — do not\n // append against server pagination after the fetch settles.\n await waitFor(() => expect(calls).toHaveLength(1));\n await act(async () => {\n await new Promise((resolve) => setTimeout(resolve, 20));\n });\n expect(calls).toHaveLength(1);\n });\n\n it(\"uses the completed page cursor for the next loadMore fetch\", async () => {\n const calls: Array<{ cursor: unknown }> = [];\n const dataSource: DataGridDataSource<Row> = async function* (params) {\n calls.push({ cursor: params.cursor });\n if (calls.length === 1) {\n yield {\n rows: [{ id: \"row-1\", name: \"Row 1\" }],\n nextCursor: \"cursor-1\",\n hasMore: true,\n };\n return;\n }\n yield {\n rows: [{ id: \"row-2\", name: \"Row 2\" }],\n nextCursor: null,\n hasMore: false,\n };\n };\n\n const { getByRole, getByTestId } = render(<DataSourceHarness dataSource={dataSource} />);\n await waitFor(() => expect(getByTestId(\"row-count\").textContent).toBe(\"1\"));\n\n fireEvent.click(getByRole(\"button\", { name: \"Load more\" }));\n\n await waitFor(() => expect(getByTestId(\"row-count\").textContent).toBe(\"2\"));\n expect(calls).toEqual([{ cursor: undefined }, { cursor: \"cursor-1\" }]);\n });\n});\n"],"mappings":"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"}