@marktiderman/genesis-ui 2.9.0 → 2.10.0

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.
@@ -0,0 +1,763 @@
1
+ "use client";
2
+ import { useResourcePage } from './chunk-RKPS7ERS.js';
3
+ import { useResourceUrlState } from './chunk-DIV4BYXX.js';
4
+ import { useViewSettings } from './chunk-OZ7ZAYCS.js';
5
+ import { toast } from './chunk-4RAZJ6LL.js';
6
+ import { defaultNavigate } from './chunk-WGMEVX3Y.js';
7
+ import { useKeyboardNavigation } from './chunk-YJ6MPS6U.js';
8
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
9
+ import { useResource, useOne, useCapabilities } from '@marktiderman/genesis-core/hooks';
10
+
11
+ // src/components/data/undo-toast.ts
12
+ var DONE = {
13
+ create: "Created.",
14
+ update: "Saved.",
15
+ delete: "Deleted."
16
+ };
17
+ function showUndoToast(write) {
18
+ toast(DONE[write.kind], {
19
+ duration: write.delayMs,
20
+ action: {
21
+ label: "Undo",
22
+ onClick: () => {
23
+ void write.undo().then((undone) => {
24
+ if (!undone && write.strategy === "delay") {
25
+ toast("Already saved. It can no longer be undone.");
26
+ }
27
+ });
28
+ }
29
+ }
30
+ });
31
+ }
32
+ function showWriteErrorToast(error) {
33
+ toast.error(error.message || "The change could not be saved.");
34
+ }
35
+ function writeModeOptions(resourceOptions, mutationMode) {
36
+ const mode = resourceOptions?.mutationMode ?? mutationMode ?? "pessimistic";
37
+ return {
38
+ mutationMode: mode,
39
+ onUndoable: resourceOptions?.onUndoable ?? showUndoToast,
40
+ onError: resourceOptions?.onError ?? (mode === "pessimistic" ? void 0 : showWriteErrorToast)
41
+ };
42
+ }
43
+ function useResourceTableModel(props) {
44
+ const {
45
+ resource: resourceName,
46
+ data: dataProp,
47
+ total: totalProp,
48
+ pagination = "client",
49
+ clientRowLimit = 1e3,
50
+ title,
51
+ subtitle,
52
+ icon,
53
+ columns: columnsProp,
54
+ getKey: getKeyProp,
55
+ detail = "panel",
56
+ detailWidth = "md",
57
+ titleField: titleFieldProp,
58
+ subtitleField: subtitleFieldProp,
59
+ getTitle,
60
+ getSubtitle,
61
+ renderDetail: renderDetailProp,
62
+ detailFields,
63
+ statusFilter,
64
+ searchFields,
65
+ searchPlaceholder,
66
+ initialFilters,
67
+ onFiltersChange,
68
+ filters: filtersProp,
69
+ page: pageProp,
70
+ perPage: perPageProp,
71
+ onPageChange,
72
+ onPerPageChange,
73
+ onSortChange,
74
+ onSearchChange,
75
+ loading: loadingProp,
76
+ error: errorProp,
77
+ errorMessage,
78
+ onRetry: onRetryProp,
79
+ savedViews,
80
+ onLoadView,
81
+ onSaveView,
82
+ onDeleteView,
83
+ canSaveViews,
84
+ columnVisibility,
85
+ columnVisibilityKey,
86
+ resizableColumns,
87
+ resizeKey,
88
+ stickyHeader,
89
+ stickyFirstColumn,
90
+ pageSizeOptions,
91
+ onCreate,
92
+ onEdit,
93
+ onDelete,
94
+ can,
95
+ canEditItem,
96
+ createLabel = "New",
97
+ canEdit,
98
+ canDelete,
99
+ allowDelete,
100
+ confirmDelete,
101
+ onDeleted,
102
+ secondaryActions,
103
+ allowCreate,
104
+ allowEdit,
105
+ formFields,
106
+ renderForm: renderFormProp,
107
+ formMode,
108
+ formSteps,
109
+ formLayout: formLayoutProp,
110
+ mutationMode: mutationModeProp,
111
+ bulkActions,
112
+ renderCard: renderCardProp,
113
+ openOnCardClick = false,
114
+ renderTable: renderTableProp,
115
+ gridCols: gridColsProp,
116
+ keyboardNavigation = true,
117
+ capabilities,
118
+ toolbar,
119
+ emptyState,
120
+ emptyIcon,
121
+ emptyMessage,
122
+ emptyDescription,
123
+ header: headerMode = "page",
124
+ resourceOptions,
125
+ urlState: urlStateProp,
126
+ defaultView = "table",
127
+ onNavigate = defaultNavigate,
128
+ viewSettingsKey: viewSettingsKeyProp,
129
+ slots,
130
+ slotProps,
131
+ className
132
+ } = props;
133
+ const isClientMode = !!resourceName;
134
+ const resourceServerPaging = isClientMode && pagination === "server";
135
+ const url = useResourceUrlState(
136
+ urlStateProp ? "setSearchParams" in urlStateProp ? { searchParams: urlStateProp } : urlStateProp : void 0
137
+ );
138
+ const [urlSeed] = useState(() => {
139
+ if (!url) return null;
140
+ const sortable = !url.sort || !columnsProp ? true : columnsProp.some(
141
+ (col) => typeof col === "string" ? col === url.sort.field : col.key === url.sort.field && col.sortable !== false
142
+ );
143
+ return { q: url.q, sort: sortable ? url.sort : null, page: url.page };
144
+ });
145
+ const urlPeek = detail === "panel" || detail === "modal" ? url : void 0;
146
+ const urlSortKey = urlSeed?.sort ? `${urlSeed.sort.field}:${urlSeed.sort.order}` : "";
147
+ const isServerControlled = resourceServerPaging || pageProp !== void 0 || perPageProp !== void 0 || onPageChange !== void 0 || onPerPageChange !== void 0 || onSortChange !== void 0 || onSearchChange !== void 0;
148
+ const viewSettingsKey = viewSettingsKeyProp ?? resourceName ?? title.toLowerCase().replace(/\s+/g, "-");
149
+ const { settings, updateSetting } = useViewSettings(viewSettingsKey);
150
+ const serverSearchFields = resourceOptions?.searchFields ?? searchFields;
151
+ const resourceHook = useResource(
152
+ resourceName ?? "__noop__",
153
+ isClientMode ? {
154
+ ...resourceOptions,
155
+ // Client paging fetches everything it can in one request; server
156
+ // paging starts at the user's stored page size. An explicit
157
+ // `defaultPerPage` wins over both.
158
+ defaultPerPage: resourceOptions?.defaultPerPage ?? (resourceServerPaging ? perPageProp ?? settings.pageSize : clientRowLimit),
159
+ searchFields: resourceServerPaging ? serverSearchFields : resourceOptions?.searchFields,
160
+ // Paging, searching and sorting change the query key; keep the
161
+ // current page on screen while the next one loads.
162
+ keepPreviousData: resourceOptions?.keepPreviousData ?? resourceServerPaging,
163
+ // Start where the URL says, so restoring a link doesn't push a
164
+ // search or sort that resets the page to 1.
165
+ ...resourceServerPaging && urlSeed ? {
166
+ defaultSearch: urlSeed.q,
167
+ ...urlSeed.page ? { defaultPage: urlSeed.page } : {},
168
+ ...urlSeed.sort ? { defaultSort: urlSeed.sort } : {}
169
+ } : {},
170
+ ...writeModeOptions(resourceOptions, mutationModeProp)
171
+ } : { enabled: false }
172
+ );
173
+ const effectivePageSize = resourceServerPaging ? resourceHook.params.pagination?.perPage ?? settings.pageSize : perPageProp ?? settings.pageSize;
174
+ const displayData = isClientMode ? resourceHook.list.data?.data ?? [] : dataProp ?? [];
175
+ const displayTotal = isClientMode ? resourceHook.list.data?.total ?? 0 : totalProp ?? 0;
176
+ const isLoading = isClientMode ? resourceHook.list.isLoading : loadingProp ?? false;
177
+ const hasError = isClientMode ? resourceHook.list.isError : errorProp ?? false;
178
+ const page = useResourcePage({
179
+ resource: resourceName,
180
+ data: displayData,
181
+ total: displayTotal,
182
+ columns: columnsProp,
183
+ statusFilter,
184
+ searchFields,
185
+ initialFilters: initialFilters ?? (urlSeed && (urlSeed.q || urlSeed.sort) ? [
186
+ ...urlSeed.q ? [{ field: "__search__", operator: "contains", value: urlSeed.q }] : [],
187
+ ...urlSeed.sort ? [{ field: "__sort__", operator: "eq", value: urlSortKey }] : []
188
+ ] : void 0),
189
+ onFiltersChange,
190
+ defaultView,
191
+ // Each page remembers its own table/cards choice, under the same key as
192
+ // its density and page size — not one key shared by every data page.
193
+ viewKey: viewSettingsKey,
194
+ detail,
195
+ titleField: titleFieldProp,
196
+ subtitleField: subtitleFieldProp,
197
+ controlled: isServerControlled
198
+ });
199
+ const {
200
+ setSearch: setResourceSearch,
201
+ setSort: setResourceSort,
202
+ setFilters: setResourceFilters
203
+ } = resourceHook;
204
+ const pushedSearchRef = useRef(resourceServerPaging ? urlSeed?.q ?? "" : "");
205
+ const searchEffectRanRef = useRef(false);
206
+ useEffect(() => {
207
+ if (!resourceServerPaging) return;
208
+ const firstRun = !searchEffectRanRef.current;
209
+ searchEffectRanRef.current = true;
210
+ if (page.search === pushedSearchRef.current) return;
211
+ const push = () => {
212
+ pushedSearchRef.current = page.search;
213
+ setResourceSearch(page.search);
214
+ };
215
+ if (firstRun || page.search === "") {
216
+ push();
217
+ return;
218
+ }
219
+ const timer = setTimeout(push, 300);
220
+ return () => clearTimeout(timer);
221
+ }, [resourceServerPaging, page.search, setResourceSearch]);
222
+ const defaultSort = resourceOptions?.defaultSort;
223
+ const pushedSortRef = useRef(resourceServerPaging ? urlSortKey : "");
224
+ useEffect(() => {
225
+ if (!resourceServerPaging) return;
226
+ const key = page.sort ? `${page.sort.field}:${page.sort.order}` : "";
227
+ if (key === pushedSortRef.current) return;
228
+ pushedSortRef.current = key;
229
+ setResourceSort(page.sort ?? defaultSort);
230
+ }, [resourceServerPaging, page.sort, defaultSort, setResourceSort]);
231
+ const statusField = statusFilter ? typeof statusFilter === "string" ? statusFilter : statusFilter.field : void 0;
232
+ const defaultFilters = resourceOptions?.defaultFilters;
233
+ const pushedStatusRef = useRef("");
234
+ useEffect(() => {
235
+ if (!resourceServerPaging || !statusField) return;
236
+ const key = JSON.stringify([page.statusValue, defaultFilters ?? null]);
237
+ if (key === pushedStatusRef.current) return;
238
+ pushedStatusRef.current = key;
239
+ setResourceFilters([
240
+ ...defaultFilters ?? [],
241
+ ...page.statusValue.length > 0 ? [{ field: statusField, operator: "in", value: page.statusValue }] : []
242
+ ]);
243
+ }, [
244
+ resourceServerPaging,
245
+ statusField,
246
+ page.statusValue,
247
+ defaultFilters,
248
+ setResourceFilters
249
+ ]);
250
+ const clientTruncated = isClientMode && !resourceServerPaging && !isLoading && !hasError && displayTotal > displayData.length;
251
+ const warnedTruncatedRef = useRef(false);
252
+ useEffect(() => {
253
+ if (!clientTruncated || warnedTruncatedRef.current) return;
254
+ warnedTruncatedRef.current = true;
255
+ console.warn(
256
+ `ResourcePage: "${resourceName}" has ${displayTotal} rows but only ${displayData.length} were fetched. Pass \`pagination="server"\` to page through all of them, or raise \`clientRowLimit\` (no higher than the provider's \`maxPerPage\`, which refuses a larger page).`
257
+ );
258
+ }, [clientTruncated, resourceName, displayTotal, displayData.length]);
259
+ const actions = useMemo(() => {
260
+ if (isClientMode) {
261
+ return {
262
+ update: resourceHook.update,
263
+ remove: resourceHook.remove,
264
+ refetch: () => resourceHook.list.refetch()
265
+ };
266
+ }
267
+ return {
268
+ update: async () => {
269
+ throw new Error(
270
+ "ResourceActions.update not available in server-data mode"
271
+ );
272
+ },
273
+ remove: async () => {
274
+ throw new Error(
275
+ "ResourceActions.remove not available in server-data mode"
276
+ );
277
+ },
278
+ refetch: () => {
279
+ }
280
+ };
281
+ }, [
282
+ isClientMode,
283
+ resourceHook.update,
284
+ resourceHook.remove,
285
+ resourceHook.list
286
+ ]);
287
+ const keyOf = useCallback(
288
+ (item) => getKeyProp ? getKeyProp(item) : String(item.id ?? ""),
289
+ [getKeyProp]
290
+ );
291
+ const snapshotKey = page.selectedItem ? keyOf(page.selectedItem) : null;
292
+ const selectedKey = urlPeek ? urlPeek.detailId : snapshotKey;
293
+ const snapshot = page.selectedItem && snapshotKey === selectedKey ? page.selectedItem : null;
294
+ const rowOrSnapshot = !selectedKey ? snapshot : displayData.find((row) => keyOf(row) === selectedKey) ?? snapshot;
295
+ const linkedId = urlPeek && selectedKey && !rowOrSnapshot ? selectedKey : null;
296
+ const linked = useOne(resourceName ?? "__noop__", linkedId, {
297
+ enabled: isClientMode && !!linkedId
298
+ });
299
+ const selectedItem = rowOrSnapshot ?? (linkedId && linked.data ? linked.data : null);
300
+ const linkMissing = !!linkedId && !selectedItem && !isLoading && (isClientMode ? linked.isError : !isServerControlled);
301
+ const detailOpen = urlPeek ? selectedItem !== null || linkMissing : page.detailOpen;
302
+ const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
303
+ const handleToggleSelect = useCallback((id) => {
304
+ setSelectedIds((prev) => {
305
+ const next = new Set(prev);
306
+ if (next.has(id)) next.delete(id);
307
+ else next.add(id);
308
+ return next;
309
+ });
310
+ }, []);
311
+ const handleToggleAll = useCallback(() => {
312
+ setSelectedIds((prev) => {
313
+ if (prev.size === page.data.length) return /* @__PURE__ */ new Set();
314
+ return new Set(page.data.map(keyOf));
315
+ });
316
+ }, [page.data, keyOf]);
317
+ const handleClearSelection = useCallback(() => {
318
+ setSelectedIds(/* @__PURE__ */ new Set());
319
+ }, []);
320
+ const [formOpen, setFormOpen] = useState(false);
321
+ const [formAction, setFormAction] = useState("create");
322
+ const [formItem, setFormItem] = useState(null);
323
+ const useBuiltInCreate = allowCreate && !onCreate && isClientMode;
324
+ const useBuiltInEdit = allowEdit && !onEdit && isClientMode;
325
+ const handleCreate = useCallback(() => {
326
+ if (useBuiltInCreate) {
327
+ setFormAction("create");
328
+ setFormItem(null);
329
+ setFormOpen(true);
330
+ } else if (onCreate) {
331
+ onCreate();
332
+ }
333
+ }, [useBuiltInCreate, onCreate]);
334
+ const allows = useCallback(
335
+ (action, item) => {
336
+ if (can && !can(action, item)) return false;
337
+ if (!item) return true;
338
+ if (canEditItem && !canEditItem(item)) return false;
339
+ if (action === "edit" && canEdit && !canEdit(item)) return false;
340
+ if (action === "delete" && canDelete && !canDelete(item)) return false;
341
+ return true;
342
+ },
343
+ [can, canEditItem, canEdit, canDelete]
344
+ );
345
+ const handleEdit = useCallback(
346
+ (item) => {
347
+ if (!allows("edit", item)) return;
348
+ if (useBuiltInEdit) {
349
+ setFormAction("edit");
350
+ setFormItem(item);
351
+ setFormOpen(true);
352
+ } else if (onEdit) {
353
+ onEdit(item);
354
+ }
355
+ },
356
+ [useBuiltInEdit, onEdit, allows]
357
+ );
358
+ const closeDetail = useCallback(() => {
359
+ page.setDetailOpen(false);
360
+ page.setSelectedItem(null);
361
+ urlPeek?.setDetailId(null);
362
+ }, [page, urlPeek]);
363
+ const useBuiltInDelete = !!allowDelete && !onDelete && isClientMode;
364
+ const deleteConfirmed = confirmDelete === void 0 ? useBuiltInDelete : !!confirmDelete;
365
+ const deleteCopy = typeof confirmDelete === "object" ? confirmDelete : void 0;
366
+ const [pendingDelete, setPendingDelete] = useState(null);
367
+ const [deleteOpen, setDeleteOpen] = useState(false);
368
+ const { remove: removeRecord } = resourceHook;
369
+ const { data: providerCaps } = useCapabilities(resourceName, {
370
+ enabled: useBuiltInDelete && deleteConfirmed
371
+ });
372
+ const deleteKind = providerCaps?.softDelete ? "recycle" : "permanent";
373
+ const performDelete = useCallback(
374
+ async (item) => {
375
+ if (useBuiltInDelete) {
376
+ await removeRecord(
377
+ getKeyProp ? keyOf(item) : item.id
378
+ );
379
+ } else if (onDelete) {
380
+ await onDelete(item);
381
+ }
382
+ closeDetail();
383
+ onDeleted?.(item);
384
+ },
385
+ [
386
+ useBuiltInDelete,
387
+ removeRecord,
388
+ getKeyProp,
389
+ keyOf,
390
+ onDelete,
391
+ closeDetail,
392
+ onDeleted
393
+ ]
394
+ );
395
+ const deleteHandler = !(useBuiltInDelete || onDelete) ? void 0 : deleteConfirmed ? (item) => {
396
+ setPendingDelete(item);
397
+ setDeleteOpen(true);
398
+ } : useBuiltInDelete ? (item) => {
399
+ performDelete(item).catch((err) => {
400
+ if (!resourceOptions?.onError) {
401
+ toast.error(err instanceof Error ? err.message : "The delete failed.");
402
+ }
403
+ });
404
+ } : (
405
+ // Unconfirmed `onDelete` keeps its original contract exactly:
406
+ // called straight from the peek, nothing closed for it.
407
+ (item) => {
408
+ const report = (err) => {
409
+ const error = err instanceof Error ? err : new Error(String(err));
410
+ if (resourceOptions?.onError) {
411
+ try {
412
+ resourceOptions.onError(error);
413
+ return;
414
+ } catch {
415
+ }
416
+ }
417
+ toast.error(error.message || "The delete failed.");
418
+ };
419
+ try {
420
+ Promise.resolve(onDelete?.(item)).catch(report);
421
+ } catch (err) {
422
+ report(err);
423
+ }
424
+ }
425
+ );
426
+ const selectedEditable = !!selectedItem && allows("edit", selectedItem);
427
+ const selectedDeletable = !!selectedItem && allows("delete", selectedItem);
428
+ const tableColumns = useMemo(() => {
429
+ const base = isClientMode ? page.columns.map((col) => ({
430
+ key: col.key,
431
+ header: col.header ?? col.key,
432
+ sortable: col.sortable ?? true,
433
+ hideBelow: col.hideBelow,
434
+ width: col.width,
435
+ minWidth: col.minWidth,
436
+ defaultVisible: col.defaultVisible,
437
+ editable: col.editable,
438
+ accessor: col.accessor,
439
+ render: col.render ? (item, value) => col.render(item, value, actions) : void 0
440
+ })) : page.tableColumns;
441
+ if (!isServerControlled) return base;
442
+ return base.map((col) => ({ ...col, sortable: false }));
443
+ }, [
444
+ isClientMode,
445
+ isServerControlled,
446
+ page.columns,
447
+ page.tableColumns,
448
+ actions
449
+ ]);
450
+ const [tableTieBreaks, setTableTieBreaks] = useState(null);
451
+ const pageSort = page.sort;
452
+ const setPageSort = page.setSort;
453
+ const tableSort = useMemo(() => {
454
+ if (!pageSort) return [];
455
+ const primary = { key: pageSort.field, dir: pageSort.order };
456
+ const rest = tableTieBreaks && tableTieBreaks.primary === `${primary.key}:${primary.dir}` ? tableTieBreaks.rest : [];
457
+ return [primary, ...rest].filter((s) => tableColumns.some((c) => c.key === s.key));
458
+ }, [pageSort, tableTieBreaks, tableColumns]);
459
+ const handleTableSort = useCallback(
460
+ (next) => {
461
+ const [primary, ...rest] = next;
462
+ setTableTieBreaks(primary ? { primary: `${primary.key}:${primary.dir}`, rest } : null);
463
+ setPageSort(primary ? { field: primary.key, order: primary.dir } : null);
464
+ },
465
+ [setPageSort]
466
+ );
467
+ const pagerPage = resourceServerPaging ? resourceHook.params.pagination?.page ?? 1 : pageProp;
468
+ const pagerPerPage = resourceServerPaging ? resourceHook.params.pagination?.perPage : perPageProp;
469
+ const pagerTotal = resourceServerPaging ? displayTotal : totalProp;
470
+ const pagerOnPageChange = resourceServerPaging ? resourceHook.setPage : onPageChange;
471
+ const currentPage = pagerPage ?? 1;
472
+ const writeUrl = url?.write;
473
+ useEffect(() => {
474
+ writeUrl?.({
475
+ q: page.search,
476
+ sort: page.sort,
477
+ ...resourceServerPaging ? { page: currentPage } : {}
478
+ });
479
+ }, [writeUrl, page.search, page.sort, currentPage, resourceServerPaging]);
480
+ const handleSearchChange = useCallback(
481
+ (value) => {
482
+ page.filterConfig.onSearchChange(value);
483
+ if (!isServerControlled) return;
484
+ onSearchChange?.(value);
485
+ if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
486
+ },
487
+ [
488
+ page.filterConfig,
489
+ isServerControlled,
490
+ onSearchChange,
491
+ onPageChange,
492
+ resourceServerPaging,
493
+ currentPage
494
+ ]
495
+ );
496
+ const handleSortSelect = useCallback(
497
+ (value) => {
498
+ page.filterConfig.onSortChange(value);
499
+ if (!isServerControlled) return;
500
+ const [field, order] = value.split(":");
501
+ onSortChange?.(
502
+ field && (order === "asc" || order === "desc") ? { field, order } : null
503
+ );
504
+ if (!resourceServerPaging) onPageChange?.(1);
505
+ },
506
+ [
507
+ page.filterConfig,
508
+ isServerControlled,
509
+ onSortChange,
510
+ onPageChange,
511
+ resourceServerPaging
512
+ ]
513
+ );
514
+ const handleClearFilters = useCallback(() => {
515
+ page.clearFilters();
516
+ if (!isServerControlled) return;
517
+ onSearchChange?.("");
518
+ onSortChange?.(null);
519
+ if (!resourceServerPaging) onPageChange?.(1);
520
+ }, [
521
+ page,
522
+ isServerControlled,
523
+ onSearchChange,
524
+ onSortChange,
525
+ onPageChange,
526
+ resourceServerPaging
527
+ ]);
528
+ const handleStatusChange = useCallback(
529
+ (values) => {
530
+ page.filterConfig.statusChips?.onChange(values);
531
+ if (!isServerControlled) return;
532
+ if (currentPage !== 1 && !resourceServerPaging) onPageChange?.(1);
533
+ },
534
+ [
535
+ page.filterConfig,
536
+ isServerControlled,
537
+ onPageChange,
538
+ resourceServerPaging,
539
+ currentPage
540
+ ]
541
+ );
542
+ const statusChipsConfig = useMemo(() => {
543
+ const base = page.filterConfig.statusChips;
544
+ if (!base) return void 0;
545
+ return { ...base, onChange: handleStatusChange };
546
+ }, [page.filterConfig.statusChips, handleStatusChange]);
547
+ const handlePageSizeChange = useCallback(
548
+ (size) => {
549
+ updateSetting("pageSize", size);
550
+ onPerPageChange?.(size);
551
+ if (resourceServerPaging) resourceHook.setPerPage(size);
552
+ else if (isServerControlled) onPageChange?.(1);
553
+ },
554
+ [
555
+ updateSetting,
556
+ onPerPageChange,
557
+ resourceServerPaging,
558
+ resourceHook.setPerPage,
559
+ isServerControlled,
560
+ onPageChange
561
+ ]
562
+ );
563
+ const missingPerPage = isServerControlled && !!pagerOnPageChange && pagerPerPage === void 0;
564
+ const missingTotal = isServerControlled && !!pagerOnPageChange && pagerTotal === void 0;
565
+ const pagerMisconfigured = missingPerPage || missingTotal;
566
+ const missingPagerProps = [
567
+ ...missingPerPage ? ["perPage"] : [],
568
+ ...missingTotal ? ["total"] : []
569
+ ];
570
+ const loggedMissingPerPageRef = useRef(false);
571
+ useEffect(() => {
572
+ if (!pagerMisconfigured) return;
573
+ if (loggedMissingPerPageRef.current) return;
574
+ loggedMissingPerPageRef.current = true;
575
+ console.error(
576
+ `ResourcePage: ${missingPagerProps.map((name) => `\`${name}\``).join(" and ")} ${missingPagerProps.length > 1 ? "are" : "is"} required when \`onPageChange\` is supplied.`
577
+ );
578
+ }, [pagerMisconfigured]);
579
+ const pagerPageSize = pagerPerPage ?? effectivePageSize;
580
+ const totalPages = Math.max(
581
+ 1,
582
+ Math.ceil(displayTotal / Math.max(1, pagerPageSize))
583
+ );
584
+ const rangeFrom = page.data.length === 0 ? 0 : (currentPage - 1) * pagerPageSize + 1;
585
+ const rangeTo = rangeFrom === 0 ? 0 : rangeFrom + page.data.length - 1;
586
+ const { setPage: setResourcePage } = resourceHook;
587
+ const settledPage = !isLoading && !hasError && !resourceHook.list.isPlaceholderData;
588
+ useEffect(() => {
589
+ if (!resourceServerPaging || !settledPage) return;
590
+ if (currentPage > totalPages) setResourcePage(totalPages);
591
+ }, [resourceServerPaging, settledPage, currentPage, totalPages, setResourcePage]);
592
+ const titleOf = (item) => getTitle ? getTitle(item) : String(item[page.titleField] ?? "");
593
+ const detailTitle = selectedItem ? titleOf(selectedItem) : "";
594
+ const detailSubtitle = !selectedItem ? void 0 : getSubtitle ? getSubtitle(selectedItem) : page.subtitleField ? String(
595
+ selectedItem[page.subtitleField] ?? ""
596
+ ) : void 0;
597
+ const handleRowClick = (item) => {
598
+ if (detail === "route") {
599
+ const id = item.id;
600
+ if (resourceName && id) {
601
+ onNavigate(`/${resourceName}/${id}`);
602
+ }
603
+ return;
604
+ }
605
+ page.handleRowClick(item);
606
+ urlPeek?.setDetailId(keyOf(item));
607
+ };
608
+ const hasBulkSelect = !!bulkActions && bulkActions.length > 0;
609
+ const overlayOpen = detailOpen || formOpen || deleteOpen;
610
+ const keyboardNav = useKeyboardNavigation({
611
+ itemCount: page.data.length,
612
+ enabled: keyboardNavigation && page.viewMode !== "table",
613
+ overlayOpen,
614
+ onOpen: (index) => {
615
+ const item = page.data[index];
616
+ if (item) handleRowClick(item);
617
+ },
618
+ onToggleSelect: hasBulkSelect ? (index) => {
619
+ const item = page.data[index];
620
+ if (item) {
621
+ handleToggleSelect(keyOf(item));
622
+ }
623
+ } : void 0,
624
+ // Supplied only while there is an open detail to close — the hook
625
+ // suppresses Backspace's default whenever this is present, so leaving it
626
+ // wired up permanently would swallow the browser/app's Backspace on
627
+ // every grid/list page (including detail="none" and detail="route",
628
+ // which never open one). `detailOpen` is only ever true for the panel
629
+ // and modal modes, so it is the precise precondition here.
630
+ onBack: detailOpen ? closeDetail : void 0
631
+ });
632
+ return {
633
+ actions,
634
+ allows,
635
+ clientTruncated,
636
+ closeDetail,
637
+ currentPage,
638
+ deleteConfirmed,
639
+ deleteCopy,
640
+ deleteHandler,
641
+ deleteKind,
642
+ deleteOpen,
643
+ detailOpen,
644
+ detailSubtitle,
645
+ detailTitle,
646
+ displayData,
647
+ displayTotal,
648
+ effectivePageSize,
649
+ formAction,
650
+ formItem,
651
+ formOpen,
652
+ handleClearFilters,
653
+ handleClearSelection,
654
+ handleCreate,
655
+ handleEdit,
656
+ handlePageSizeChange,
657
+ handleRowClick,
658
+ handleSearchChange,
659
+ handleSortSelect,
660
+ handleTableSort,
661
+ handleToggleAll,
662
+ handleToggleSelect,
663
+ hasError,
664
+ isClientMode,
665
+ isLoading,
666
+ isServerControlled,
667
+ keyOf,
668
+ keyboardNav,
669
+ linkMissing,
670
+ linked,
671
+ missingPagerProps,
672
+ page,
673
+ pagerMisconfigured,
674
+ pagerOnPageChange,
675
+ pendingDelete,
676
+ performDelete,
677
+ rangeFrom,
678
+ rangeTo,
679
+ resourceHook,
680
+ resourceServerPaging,
681
+ selectedDeletable,
682
+ selectedEditable,
683
+ selectedIds,
684
+ selectedItem,
685
+ serverSearchFields,
686
+ setDeleteOpen,
687
+ setFormOpen,
688
+ settings,
689
+ statusChipsConfig,
690
+ tableColumns,
691
+ tableSort,
692
+ titleOf,
693
+ totalPages,
694
+ updateSetting,
695
+ useBuiltInCreate,
696
+ useBuiltInDelete,
697
+ useBuiltInEdit
698
+ };
699
+ }
700
+ function useResourceTable(props) {
701
+ const m = useResourceTableModel(props);
702
+ const { pendingDelete, performDelete } = m;
703
+ const confirm = useCallback(
704
+ () => pendingDelete ? performDelete(pendingDelete) : Promise.resolve(),
705
+ [pendingDelete, performDelete]
706
+ );
707
+ return {
708
+ rows: m.page.data,
709
+ total: m.page.total,
710
+ loading: m.isLoading,
711
+ error: m.hasError,
712
+ truncated: m.clientTruncated,
713
+ columns: m.tableColumns,
714
+ actions: m.actions,
715
+ getKey: m.keyOf,
716
+ getTitle: m.titleOf,
717
+ can: m.allows,
718
+ search: m.page.search,
719
+ setSearch: m.handleSearchChange,
720
+ sort: m.tableSort,
721
+ setSort: m.handleTableSort,
722
+ statusFilter: m.statusChipsConfig,
723
+ hasActiveFilters: m.page.hasActiveFilters,
724
+ clearFilters: m.handleClearFilters,
725
+ page: m.currentPage,
726
+ pageCount: m.totalPages,
727
+ pageSize: m.effectivePageSize,
728
+ setPageSize: m.handlePageSizeChange,
729
+ setPage: m.pagerOnPageChange,
730
+ rangeFrom: m.rangeFrom,
731
+ rangeTo: m.rangeTo,
732
+ selectedIds: m.selectedIds,
733
+ toggleSelected: m.handleToggleSelect,
734
+ toggleAll: m.handleToggleAll,
735
+ clearSelection: m.handleClearSelection,
736
+ openItem: m.selectedItem,
737
+ detailOpen: m.detailOpen,
738
+ openRow: m.handleRowClick,
739
+ closeDetail: m.closeDetail,
740
+ openItemMissing: m.linkMissing,
741
+ create: m.useBuiltInCreate || props.onCreate ? m.handleCreate : void 0,
742
+ edit: m.handleEdit,
743
+ form: {
744
+ open: m.formOpen,
745
+ setOpen: m.setFormOpen,
746
+ action: m.formAction,
747
+ item: m.formItem
748
+ },
749
+ remove: m.deleteHandler,
750
+ deleteConfirm: {
751
+ open: m.deleteOpen,
752
+ setOpen: m.setDeleteOpen,
753
+ item: pendingDelete,
754
+ confirm,
755
+ kind: m.deleteKind,
756
+ copy: m.deleteCopy
757
+ }
758
+ };
759
+ }
760
+
761
+ export { showUndoToast, showWriteErrorToast, useResourceTable, useResourceTableModel };
762
+ //# sourceMappingURL=chunk-C7PQ54JI.js.map
763
+ //# sourceMappingURL=chunk-C7PQ54JI.js.map