@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.
- package/SKILL.md +17 -0
- package/dist/{AppShell-LXQykEqe.d.ts → AppShell-BVr4V0UK.d.ts} +1 -110
- package/dist/PageHeader-V9lVkZtF.d.ts +114 -0
- package/dist/{chunk-S7NXD37O.js → chunk-25XEXYGC.js} +4 -4
- package/dist/chunk-25XEXYGC.js.map +1 -0
- package/dist/chunk-C7PQ54JI.js +763 -0
- package/dist/chunk-C7PQ54JI.js.map +1 -0
- package/dist/chunk-I4NBE2KL.js +16 -0
- package/dist/chunk-I4NBE2KL.js.map +1 -0
- package/dist/components/data/index.d.ts +42 -1012
- package/dist/components/data/index.js +411 -704
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/index.d.ts +2 -1
- package/dist/components/patterns/index.js +2 -1
- package/dist/components/patterns/sortable-list.js +2 -1
- package/dist/hooks/index.d.ts +7 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/use-resource-page.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/{use-resource-page-BDnnCssE.d.ts → use-resource-page-DaJzuq3M.d.ts} +1 -1
- package/dist/use-resource-table-DCVf_lEG.d.ts +1113 -0
- package/package.json +2 -2
- package/dist/chunk-S7NXD37O.js.map +0 -1
|
@@ -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
|