@marktiderman/genesis-core 1.4.0 → 1.5.1
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/README.md +102 -0
- package/dist/{chunk-IHWWJUEN.js → chunk-3JSFTLDV.js} +147 -39
- package/dist/chunk-3JSFTLDV.js.map +1 -0
- package/dist/{chunk-6MBACQYA.js → chunk-5NHR6ZXS.js} +10 -2
- package/dist/chunk-5NHR6ZXS.js.map +1 -0
- package/dist/{chunk-P4OIIRQS.js → chunk-6Q477CHB.js} +72 -26
- package/dist/chunk-6Q477CHB.js.map +1 -0
- package/dist/chunk-BQOJX67X.js +31 -0
- package/dist/chunk-BQOJX67X.js.map +1 -0
- package/dist/{chunk-NW26Z3LZ.js → chunk-E5VYFAQO.js} +212 -37
- package/dist/chunk-E5VYFAQO.js.map +1 -0
- package/dist/chunk-F3BE7NGM.js +520 -0
- package/dist/chunk-F3BE7NGM.js.map +1 -0
- package/dist/chunk-HT43MPKW.js +3 -0
- package/dist/{chunk-EL7YGOTY.js.map → chunk-HT43MPKW.js.map} +1 -1
- package/dist/chunk-HUNGCTRA.js +33 -0
- package/dist/chunk-HUNGCTRA.js.map +1 -0
- package/dist/chunk-XQO4OH2N.js +32 -0
- package/dist/chunk-XQO4OH2N.js.map +1 -0
- package/dist/hooks/index.d.ts +5 -1
- package/dist/hooks/index.js +8 -4
- package/dist/hooks/mutation-mode.d.ts +119 -0
- package/dist/hooks/mutation-mode.js +6 -0
- package/dist/hooks/mutation-mode.js.map +1 -0
- package/dist/hooks/related-keys.d.ts +2 -1
- package/dist/hooks/use-capabilities.d.ts +21 -0
- package/dist/hooks/use-capabilities.js +5 -0
- package/dist/hooks/use-capabilities.js.map +1 -0
- package/dist/hooks/use-one.d.ts +2 -1
- package/dist/hooks/use-resource-form.d.ts +43 -2
- package/dist/hooks/use-resource-form.js +6 -2
- package/dist/hooks/use-resource.d.ts +29 -5
- package/dist/hooks/use-resource.js +3 -1
- package/dist/hooks/use-unsaved-changes-guard.d.ts +33 -0
- package/dist/hooks/use-unsaved-changes-guard.js +3 -0
- package/dist/hooks/use-unsaved-changes-guard.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +10 -6
- package/dist/provider/index.d.ts +81 -5
- package/dist/provider/index.js +4 -2
- package/dist/read-outcome-kgNhfuST.d.ts +88 -0
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +2 -1
- package/dist/{types-zILrlHuV.d.ts → types-5cuflvha.d.ts} +20 -279
- package/dist/types-DDOmG9hj.d.ts +292 -0
- package/package.json +1 -1
- package/dist/chunk-6MBACQYA.js.map +0 -1
- package/dist/chunk-EL7YGOTY.js +0 -3
- package/dist/chunk-IHWWJUEN.js.map +0 -1
- package/dist/chunk-NW26Z3LZ.js.map +0 -1
- package/dist/chunk-P4OIIRQS.js.map +0 -1
- package/dist/write-outcome-BE3Tk6bA.d.ts +0 -47
package/README.md
CHANGED
|
@@ -140,3 +140,105 @@ try {
|
|
|
140
140
|
the root and `@marktiderman/genesis-core/schema`. Check with
|
|
141
141
|
`isDataWriteError(error)` rather than `instanceof`, which fails when two copies
|
|
142
142
|
of the package are installed.
|
|
143
|
+
|
|
144
|
+
### Field-level errors
|
|
145
|
+
|
|
146
|
+
A `DataWriteError` may carry `fields`: field (column) name → a message safe to
|
|
147
|
+
show by that input. `useResourceForm` puts each one on its field
|
|
148
|
+
(`setError(field, { type: "server" })`), focuses the first, and sets
|
|
149
|
+
`errors.root` to the error's message as the summary; a field the form does not
|
|
150
|
+
show appears only in the summary.
|
|
151
|
+
|
|
152
|
+
`createSupabaseProvider` fills `fields` where Postgres says which column it
|
|
153
|
+
means (`fieldErrorsFromPostgrest`), and replaces the error's message with a
|
|
154
|
+
plain sentence (the raw text stays on `cause`):
|
|
155
|
+
|
|
156
|
+
| Code | Read from | Field message | Error message |
|
|
157
|
+
| --- | --- | --- | --- |
|
|
158
|
+
| `23505` unique | `details`: `Key (email)=(…) already exists.` | "This value is already in use." (several columns: "This combination is already in use.") | "Email is already in use." |
|
|
159
|
+
| `23502` not null | `message`: `null value in column "title" …` | "This field is required." | "Title is required." |
|
|
160
|
+
| `23503` foreign key (insert/update) | `details`: `Key (owner_id)=(…) is not present in table …` | "The selected item doesn't exist." | "Owner Id: the selected item doesn't exist." |
|
|
161
|
+
|
|
162
|
+
Only plain column names are taken from the store's text; the values it echoes
|
|
163
|
+
and constraint names never reach a message. A unique index on one column in a
|
|
164
|
+
simple expression (`lower(email::text)`) maps to that column; a unique
|
|
165
|
+
violation whose columns cannot be read still gets the safe sentence "This value
|
|
166
|
+
is already in use." with no field. Anything else (check constraints) is left
|
|
167
|
+
unmapped. Your own provider or `prepare` rule can
|
|
168
|
+
throw `new DataWriteError("invalid", "…", { fields: { slug: "Pick another slug." } })`.
|
|
169
|
+
|
|
170
|
+
## Mutation modes
|
|
171
|
+
|
|
172
|
+
`useResource` (its `create`, `update`, `remove`) and `useResourceForm` take
|
|
173
|
+
`mutationMode`:
|
|
174
|
+
|
|
175
|
+
| Mode | Cache | Provider | Returned promise / `onSuccess` |
|
|
176
|
+
| --- | --- | --- | --- |
|
|
177
|
+
| `"pessimistic"` (default) | refetched after the write | at once | after the write |
|
|
178
|
+
| `"optimistic"` | patched at once; put back if the write fails | at once | at once |
|
|
179
|
+
| `"undoable"` | patched at once; put back on undo or failure | after `undoDelayMs` (default 5000) unless undone | at once |
|
|
180
|
+
|
|
181
|
+
- **Failures** in `optimistic` / `undoable` mode go to `onError` (the caller
|
|
182
|
+
was already told it worked, and the cache has been put back). Show them
|
|
183
|
+
there. `useResourceForm` also puts them on the form, if it is still open.
|
|
184
|
+
- **The undo control is yours.** Core has no UI: `onUndoable(write)` is called
|
|
185
|
+
with `{ resource, kind, id, strategy, delayMs, undo(), commit() }`.
|
|
186
|
+
genesis-ui's `ResourceForm` and `ResourcePage` show a sonner toast with an
|
|
187
|
+
Undo button.
|
|
188
|
+
- **A provider that serves `undo`** (`capabilities().undo === true` and an
|
|
189
|
+
`undo` method) gets an `undoable` write at once; `undo()` then calls
|
|
190
|
+
`provider.undo(resource, { auditId })` with the audit id the write returned
|
|
191
|
+
(`strategy: "provider"`). Without an audit id no undo is offered.
|
|
192
|
+
- **Undo outlives the form.** Pending writes belong to the `QueryClient`, not
|
|
193
|
+
to the hook: a form or page that unmounts during the delay (a dialog that
|
|
194
|
+
closes on success, a route change) leaves its writes waiting, and the undo
|
|
195
|
+
toast still works. They are sent when the delay ends.
|
|
196
|
+
- **Closing the tab sends.** On `beforeunload` and `pagehide`, waiting writes
|
|
197
|
+
are sent at once with `keepalive: true` in their params (`CreateParams`,
|
|
198
|
+
`UpdateParams`, `DeleteParams`). A fetch-based provider should pass it to
|
|
199
|
+
`fetch` and report `keepaliveWrites: true` in its capabilities. The
|
|
200
|
+
"Leave site?" prompt shows only while a write is being sent without that
|
|
201
|
+
guarantee (the built-in Supabase provider cannot pass keepalive through
|
|
202
|
+
supabase-js, so it prompts while a write is in flight).
|
|
203
|
+
- **Writes on one record compose.** They are sent in the order they were
|
|
204
|
+
made, one after another; sending a later one (its delay ended, `commit()`)
|
|
205
|
+
first sends the earlier ones still waiting, which can then no longer be
|
|
206
|
+
undone. Undoing (or a failure of) an earlier edit takes only its own fields
|
|
207
|
+
out of the cache, keeping later pending edits shown. A later pending edit
|
|
208
|
+
that kept the earlier edit's values unchanged (a form opened from the
|
|
209
|
+
patched cache) has them put back before it is sent, so it never sends the
|
|
210
|
+
undone values; one whose values are not row-shaped (`toFormValues`) cannot
|
|
211
|
+
be fixed, so it is dropped and reported through `onError`.
|
|
212
|
+
- **What is patched:** an update merges the values into the row in every
|
|
213
|
+
cached list (`[resource, "list", …]`) and detail (`[resource, "detail", id,
|
|
214
|
+
…]`); a delete removes the row and lowers `total`. A create patches nothing
|
|
215
|
+
(its id and place in a sorted list are the server's), so `optimistic` /
|
|
216
|
+
`undoable` only change when it is sent and when the caller moves on. A
|
|
217
|
+
rollback restores only the row it patched, so concurrent writes on other
|
|
218
|
+
rows survive.
|
|
219
|
+
- **Refetches keep the patch.** While a write is pending, its patch is
|
|
220
|
+
re-applied to any `[resource, "list", …]` or `[resource, "detail", …]` data
|
|
221
|
+
that lands in the cache: a refetch, a window-focus refetch, a new page,
|
|
222
|
+
filter or search. A deleted row stays gone and an edit stays shown until the
|
|
223
|
+
write settles; then the server's rows win. A fetch already in flight is not
|
|
224
|
+
cancelled. Each write refetches `[resource]` when it settles.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const tasks = useResource("tasks", {
|
|
228
|
+
mutationMode: "undoable",
|
|
229
|
+
onUndoable: (write) => toast("Deleted.", { action: { label: "Undo", onClick: () => void write.undo() } }),
|
|
230
|
+
onError: (error) => toast.error(error.message),
|
|
231
|
+
});
|
|
232
|
+
await tasks.remove(id); // resolves at once; sent in 5 s unless undone
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Unsaved changes
|
|
236
|
+
|
|
237
|
+
`useResourceForm({ warnWhenUnsaved: true })` (default off) warns while the
|
|
238
|
+
form is dirty: the browser's prompt on closing or reloading the tab, and
|
|
239
|
+
`confirmLeave()` for in-app navigation. It returns `isDirty` (false again once
|
|
240
|
+
a save succeeds or is handed off) and `confirmLeave()` (true when it is fine to
|
|
241
|
+
leave; asks with `window.confirm` otherwise). For other state,
|
|
242
|
+
`useUnsavedChangesGuard({ when, message, confirm })` gives the same
|
|
243
|
+
`confirmLeave` and `beforeunload` handling. Neither depends on a router; see
|
|
244
|
+
genesis-ui's SKILL.md "Unsaved changes" for React Router and TanStack Router.
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
import { useUnsavedChangesGuard } from './chunk-XQO4OH2N.js';
|
|
2
|
+
import { isDataWriteError } from './chunk-5NHR6ZXS.js';
|
|
1
3
|
import { titleCase } from './chunk-RYEXWQFD.js';
|
|
2
|
-
import {
|
|
4
|
+
import { sameValue, cachedRecord, executeWrite, invalidateResource } from './chunk-F3BE7NGM.js';
|
|
3
5
|
import { relatedKeys } from './chunk-7NUL32W7.js';
|
|
6
|
+
import { useOne } from './chunk-ZK5C3OOT.js';
|
|
4
7
|
import { useDataProvider, useStorage } from './chunk-GGSXSI6Z.js';
|
|
5
|
-
import { useMemo, useEffect, useCallback } from 'react';
|
|
8
|
+
import { useMemo, useEffect, useState, useRef, useCallback } from 'react';
|
|
6
9
|
import { useForm } from 'react-hook-form';
|
|
7
10
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
8
11
|
import { z } from 'zod';
|
|
9
12
|
import { useQueryClient } from '@tanstack/react-query';
|
|
10
13
|
|
|
14
|
+
function structuredCloneSafe(value) {
|
|
15
|
+
try {
|
|
16
|
+
return typeof structuredClone === "function" ? structuredClone(value) : value;
|
|
17
|
+
} catch {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
11
21
|
function inferFieldType(value) {
|
|
12
22
|
if (typeof value === "boolean") return "switch";
|
|
13
23
|
if (typeof value === "number") return "number";
|
|
@@ -234,7 +244,12 @@ function useResourceForm(options) {
|
|
|
234
244
|
autoPlaceholder = true,
|
|
235
245
|
validationMode,
|
|
236
246
|
autosave,
|
|
237
|
-
invalidates
|
|
247
|
+
invalidates,
|
|
248
|
+
warnWhenUnsaved = false,
|
|
249
|
+
mutationMode = "pessimistic",
|
|
250
|
+
undoDelayMs,
|
|
251
|
+
onUndoable,
|
|
252
|
+
visible = true
|
|
238
253
|
} = options;
|
|
239
254
|
const autosaveEnabled = !!autosave;
|
|
240
255
|
const debounceMs = typeof autosave === "object" ? autosave.debounceMs ?? 1e3 : 1e3;
|
|
@@ -320,54 +335,145 @@ function useResourceForm(options) {
|
|
|
320
335
|
subscription.unsubscribe();
|
|
321
336
|
};
|
|
322
337
|
}, [autosaveEnabled, resource, action, form, debounceMs, storage]);
|
|
338
|
+
const rhfDirty = form.formState.isDirty;
|
|
339
|
+
const [savedClean, setSavedClean] = useState(false);
|
|
340
|
+
const savedCleanRef = useRef(false);
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
const subscription = form.watch((_values, info) => {
|
|
343
|
+
if (!info.name) return;
|
|
344
|
+
savedCleanRef.current = false;
|
|
345
|
+
setSavedClean(false);
|
|
346
|
+
});
|
|
347
|
+
return () => subscription.unsubscribe();
|
|
348
|
+
}, [form]);
|
|
349
|
+
const isDirty = rhfDirty && !savedClean;
|
|
350
|
+
const warn = !!warnWhenUnsaved;
|
|
351
|
+
const { confirmLeave: guardConfirm } = useUnsavedChangesGuard({
|
|
352
|
+
when: warn && isDirty,
|
|
353
|
+
message: typeof warnWhenUnsaved === "object" ? warnWhenUnsaved.message : void 0
|
|
354
|
+
});
|
|
355
|
+
const confirmLeave = useCallback(
|
|
356
|
+
// The ref, not state: a router blocker may ask in the same tick as the
|
|
357
|
+
// save's onSuccess navigates, before a re-render.
|
|
358
|
+
() => !warn || savedCleanRef.current ? true : guardConfirm(),
|
|
359
|
+
[warn, guardConfirm]
|
|
360
|
+
);
|
|
361
|
+
const markSaved = useCallback(() => {
|
|
362
|
+
savedCleanRef.current = true;
|
|
363
|
+
setSavedClean(true);
|
|
364
|
+
}, []);
|
|
365
|
+
const mountedRef = useRef(true);
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
mountedRef.current = true;
|
|
368
|
+
return () => {
|
|
369
|
+
mountedRef.current = false;
|
|
370
|
+
};
|
|
371
|
+
}, []);
|
|
372
|
+
const visibleRef = useRef(visible);
|
|
373
|
+
visibleRef.current = visible;
|
|
374
|
+
const showError = useCallback(
|
|
375
|
+
(error) => {
|
|
376
|
+
const fieldErrors = isDataWriteError(error) ? error.fields : void 0;
|
|
377
|
+
const known = new Set(resolvedFields.map((f) => f.key));
|
|
378
|
+
let first;
|
|
379
|
+
if (fieldErrors) {
|
|
380
|
+
for (const [field, message] of Object.entries(fieldErrors)) {
|
|
381
|
+
if (!known.has(field)) continue;
|
|
382
|
+
form.setError(field, { type: "server", message });
|
|
383
|
+
first ??= field;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
form.setError("root", { type: "server", message: error.message });
|
|
387
|
+
if (first) {
|
|
388
|
+
try {
|
|
389
|
+
form.setFocus(first);
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
[form, resolvedFields]
|
|
395
|
+
);
|
|
396
|
+
const reportError = useCallback(
|
|
397
|
+
(error, handedOff) => {
|
|
398
|
+
const onForm = !handedOff || mountedRef.current && visibleRef.current && sameValue(form.getValues(), handedOff);
|
|
399
|
+
if (onForm) {
|
|
400
|
+
savedCleanRef.current = false;
|
|
401
|
+
setSavedClean(false);
|
|
402
|
+
showError(error);
|
|
403
|
+
}
|
|
404
|
+
if (onError) {
|
|
405
|
+
onError(error);
|
|
406
|
+
} else {
|
|
407
|
+
console.error(`[useResourceForm] ${action} failed for "${resource}":`, error);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
[form, showError, onError, action, resource]
|
|
411
|
+
);
|
|
323
412
|
const handleSubmit = useCallback(
|
|
324
413
|
async (formData) => {
|
|
414
|
+
const shown = structuredCloneSafe(form.getValues());
|
|
325
415
|
try {
|
|
326
|
-
let result;
|
|
327
416
|
let recordId;
|
|
328
|
-
if (action === "
|
|
329
|
-
const res = await provider.create(resource, { data: formData });
|
|
330
|
-
result = res.data;
|
|
331
|
-
} else {
|
|
417
|
+
if (action === "edit") {
|
|
332
418
|
recordId = id ?? String(effectiveData?.id ?? "");
|
|
333
419
|
if (!recordId) {
|
|
334
420
|
throw new Error("Cannot update record: no ID available. Provide an id prop or ensure the record has an id field.");
|
|
335
421
|
}
|
|
336
|
-
const res = await provider.update(resource, { id: recordId, data: formData });
|
|
337
|
-
result = res.data;
|
|
338
422
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
} catch (err) {
|
|
343
|
-
console.warn(`[useResourceForm] Draft cleanup failed for "${resource}":`, err instanceof Error ? err.message : String(err));
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
await queryClient.invalidateQueries({ queryKey: [resource] });
|
|
347
|
-
const related = relatedKeys(
|
|
423
|
+
const before = recordId != null ? cachedRecord(queryClient, resource, recordId) : void 0;
|
|
424
|
+
const outcome = await executeWrite({
|
|
425
|
+
queryClient,
|
|
348
426
|
provider,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
427
|
+
resource,
|
|
428
|
+
kind: action === "create" ? "create" : "update",
|
|
429
|
+
id: recordId,
|
|
430
|
+
// The cache holds rows; patch it only when the form's values are
|
|
431
|
+
// row-shaped (no provider.toFormValues reshaping this record).
|
|
432
|
+
data: formSource === effectiveData ? formData : void 0,
|
|
433
|
+
mode: mutationMode,
|
|
434
|
+
undoDelayMs,
|
|
435
|
+
onUndoable,
|
|
436
|
+
send: async (ctx) => {
|
|
437
|
+
const keepalive = ctx.keepalive ? { keepalive: true } : {};
|
|
438
|
+
const res = action === "create" ? await provider.create(resource, { data: formData, ...keepalive }) : await provider.update(resource, { id: recordId, data: ctx.data ?? formData, ...keepalive });
|
|
439
|
+
return { record: res.data, audit: res.audit };
|
|
354
440
|
},
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
441
|
+
committed: async (result) => {
|
|
442
|
+
if (autosaveEnabled && action === "create") {
|
|
443
|
+
try {
|
|
444
|
+
storage.removeItem(`genesis-draft:${resource}:new`);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
console.warn(`[useResourceForm] Draft cleanup failed for "${resource}":`, err instanceof Error ? err.message : String(err));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
await invalidateResource(queryClient, resource);
|
|
450
|
+
const related = relatedKeys(
|
|
451
|
+
provider,
|
|
452
|
+
{
|
|
453
|
+
resource,
|
|
454
|
+
kind: action === "create" ? "create" : "update",
|
|
455
|
+
id: recordId ?? result.id,
|
|
456
|
+
record: result
|
|
457
|
+
},
|
|
458
|
+
invalidates
|
|
459
|
+
);
|
|
460
|
+
await Promise.all(related.map((queryKey) => queryClient.invalidateQueries({ queryKey: [...queryKey] })));
|
|
461
|
+
},
|
|
462
|
+
failedLater: (error) => reportError(error, shown)
|
|
463
|
+
});
|
|
464
|
+
markSaved();
|
|
465
|
+
if (outcome.status === "committed") {
|
|
466
|
+
onSuccess?.(outcome.record);
|
|
364
467
|
} else {
|
|
365
|
-
|
|
468
|
+
onSuccess?.(
|
|
469
|
+
action === "edit" ? { ...before ?? effectiveData ?? {}, ...formData, id: recordId } : { ...formData }
|
|
470
|
+
);
|
|
366
471
|
}
|
|
472
|
+
} catch (err) {
|
|
473
|
+
reportError(err instanceof Error ? err : new Error(String(err)));
|
|
367
474
|
}
|
|
368
475
|
},
|
|
369
|
-
|
|
370
|
-
[action, provider, resource, id, effectiveData, queryClient, onSuccess, onError, autosaveEnabled, form, storage, invalidates]
|
|
476
|
+
[form, action, provider, resource, id, effectiveData, formSource, queryClient, onSuccess, autosaveEnabled, storage, invalidates, mutationMode, undoDelayMs, onUndoable, reportError, markSaved]
|
|
371
477
|
);
|
|
372
478
|
const onSubmit = useCallback(
|
|
373
479
|
(e) => form.handleSubmit(handleSubmit)(e),
|
|
@@ -380,10 +486,12 @@ function useResourceForm(options) {
|
|
|
380
486
|
onSubmit,
|
|
381
487
|
handleSave: handleSubmit,
|
|
382
488
|
isSaving,
|
|
383
|
-
isLoading
|
|
489
|
+
isLoading,
|
|
490
|
+
isDirty,
|
|
491
|
+
confirmLeave
|
|
384
492
|
};
|
|
385
493
|
}
|
|
386
494
|
|
|
387
495
|
export { buildArrayItemDefaults, useResourceForm };
|
|
388
|
-
//# sourceMappingURL=chunk-
|
|
389
|
-
//# sourceMappingURL=chunk-
|
|
496
|
+
//# sourceMappingURL=chunk-3JSFTLDV.js.map
|
|
497
|
+
//# sourceMappingURL=chunk-3JSFTLDV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-resource-form.ts"],"names":[],"mappings":"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","file":"chunk-3JSFTLDV.js","sourcesContent":["import { useMemo, useCallback, useEffect, useRef, useState } from \"react\";\nimport { useForm, type UseFormReturn, type Resolver } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport { useOne } from \"./use-one\";\nimport { useDataProvider, useStorage } from \"../provider/context\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { titleCase } from \"../utils\";\nimport type { ResourceFormFieldDef, FieldType } from \"../form-types\";\nimport type { BaseRecord, InvalidationKey } from \"../provider/types\";\nimport { relatedKeys } from \"./related-keys\";\nimport { isDataWriteError } from \"../schema/write-outcome\";\nimport {\n cachedRecord,\n executeWrite,\n invalidateResource,\n sameValue,\n type MutationModeOptions,\n} from \"./mutation-mode\";\nimport { useUnsavedChangesGuard } from \"./use-unsaved-changes-guard\";\n\n/** A deep copy where the platform allows one (form values are plain data). */\nfunction structuredCloneSafe<T>(value: T): T {\n try {\n return typeof structuredClone === \"function\" ? structuredClone(value) : value;\n } catch {\n return value;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface UseResourceFormOptions extends MutationModeOptions {\n resource: string;\n action: \"create\" | \"edit\";\n id?: string;\n initialData?: Record<string, unknown>;\n fields?: ResourceFormFieldDef[];\n /** Full dataset used to auto-detect select fields from unique values. */\n listData?: Record<string, unknown>[];\n /**\n * The form is done. After the save in `pessimistic` mode (with the stored\n * row); at once in `optimistic` and `undoable` modes (with the submitted\n * values over the record, and no `id` for a create).\n */\n onSuccess?: (data: Record<string, unknown>) => void;\n /**\n * The save failed. In `optimistic` and `undoable` modes the form may be\n * gone by then, so show the error from here (genesis-ui's ResourceForm\n * toasts it when no `onError` is given).\n */\n onError?: (error: Error) => void;\n /**\n * Warn before unsaved changes are lost. Default false. While the form is\n * dirty, closing or reloading the tab shows the browser's prompt, and\n * `confirmLeave()` asks before in-app navigation (wire it to a router\n * blocker; see SKILL.md \"Unsaved changes\").\n */\n warnWhenUnsaved?: boolean | { message?: string };\n /**\n * Whether the form is on screen. Default true. Pass false while it is\n * hidden but still mounted (a closed dialog). An optimistic or undoable\n * save that fails later is put back on the form (field errors, unsaved\n * again) only while the form is mounted, visible and still holds the\n * values it saved; otherwise the failure only reaches `onError`.\n */\n visible?: boolean;\n /**\n * Pre-fill create form defaults from URL query params: a param named like a\n * field (`?status=draft`) becomes that field's starting value. Default true.\n * Pass false for a form that should not read the page's URL, e.g. one\n * opened from a list whose own filters live in the query string.\n */\n prefillFromUrl?: boolean;\n /**\n * Generate a placeholder (\"Enter name\", \"name@example.com\") for a field\n * whose `placeholder` is unset. Default true. A placeholder that only\n * repeats the label adds noise without information, so `ResourceForm`\n * turns this off unless asked.\n */\n autoPlaceholder?: boolean;\n /** Validation trigger mode. Default \"onBlur\". */\n validationMode?: \"onBlur\" | \"onChange\" | \"onSubmit\";\n /** Enable autosave drafts to localStorage for create mode. */\n autosave?: boolean | { debounceMs?: number };\n /**\n * Other query keys to invalidate after a save, on top of `[resource]` and\n * the provider's own `invalidates`.\n */\n invalidates?: InvalidationKey[];\n}\n\nexport interface UseResourceFormReturn {\n form: UseFormReturn<Record<string, unknown>>;\n resolvedFields: ResourceFormFieldDef[];\n onSubmit: (e?: { preventDefault?: () => void }) => Promise<void>;\n /** Raw save handler — use with form.handleSubmit(handleSave, onInvalid). */\n handleSave: (data: Record<string, unknown>) => Promise<void>;\n isSaving: boolean;\n isLoading: boolean;\n /**\n * The form has changes that have not been saved. False again once a save\n * succeeds (or, in optimistic / undoable modes, is handed off).\n */\n isDirty: boolean;\n /**\n * For in-app navigation: true when it is fine to leave (nothing unsaved,\n * `warnWhenUnsaved` off, or the user confirmed). Asks with\n * `window.confirm` otherwise.\n */\n confirmLeave: () => boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Infer a field type from a value */\nfunction inferFieldType(value: unknown): FieldType {\n if (typeof value === \"boolean\") return \"switch\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"string\") {\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(value)) return \"date\";\n if (value.includes(\"@\") && value.includes(\".\")) return \"email\";\n if (/^https?:\\/\\//.test(value)) return \"url\";\n if (value.length > 100) return \"textarea\";\n }\n return \"text\";\n}\n\n/** Try to infer a select field from unique values across the full dataset */\nfunction inferSelectFromData(\n key: string,\n allData: Record<string, unknown>[] | undefined,\n): { type: \"select\"; options: { value: string; label: string }[] } | null {\n if (!allData || allData.length === 0) return null;\n const uniqueValues = new Set<string>();\n for (const row of allData) {\n const v = row[key];\n if (typeof v === \"string\" && v.length > 0) uniqueValues.add(v);\n }\n if (uniqueValues.size > 0 && uniqueValues.size <= 10) {\n return {\n type: \"select\" as const,\n options: Array.from(uniqueValues)\n .sort()\n .map((v) => ({ value: v, label: v })),\n };\n }\n return null;\n}\n\n/** Generate a sensible placeholder for an auto-inferred field */\nfunction defaultPlaceholder(key: string, type: FieldType): string {\n switch (type) {\n case \"email\": return \"name@example.com\";\n case \"url\": return \"https://...\";\n case \"number\": return \"0\";\n case \"date\": return \"YYYY-MM-DD\";\n case \"textarea\": return `Enter ${titleCase(key).toLowerCase()}...`;\n default: return `Enter ${titleCase(key).toLowerCase()}`;\n }\n}\n\n/** Resolve fields from data keys when none provided */\nfunction resolveFields(\n fields: ResourceFormFieldDef[] | undefined,\n data: Record<string, unknown> | undefined,\n allData?: Record<string, unknown>[],\n autoPlaceholder = true,\n): ResourceFormFieldDef[] {\n const placeholderFor = (key: string, type: FieldType): string | undefined =>\n autoPlaceholder ? defaultPlaceholder(key, type) : undefined;\n if (fields && fields.length > 0) {\n return fields.map((f) => {\n const inferred = f.type ?? (data ? inferFieldType(data[f.key]) : \"text\");\n return {\n ...f,\n label: f.label ?? titleCase(f.key),\n type: inferred,\n placeholder: f.placeholder ?? placeholderFor(f.key, inferred),\n required: f.required ?? (inferred !== \"switch\" && inferred !== \"checkbox\" && inferred !== \"hidden\"),\n };\n });\n }\n\n if (!data) return [];\n\n return Object.keys(data)\n .filter((key) => key !== \"id\" && key !== \"created_at\" && key !== \"updated_at\")\n .map((key) => {\n // Check for select-like field first (string with few unique values)\n if (typeof data[key] === \"string\") {\n const selectInfo = inferSelectFromData(key, allData);\n if (selectInfo) {\n return {\n key,\n label: titleCase(key),\n type: selectInfo.type,\n options: selectInfo.options,\n placeholder: autoPlaceholder\n ? `Select ${titleCase(key).toLowerCase()}...`\n : undefined,\n required: true,\n };\n }\n }\n\n const inferred = inferFieldType(data[key]);\n return {\n key,\n label: titleCase(key),\n type: inferred,\n placeholder: placeholderFor(key, inferred),\n required: inferred !== \"switch\" && inferred !== \"checkbox\" && inferred !== \"hidden\",\n };\n });\n}\n\n/** Build a zod schema from resolved fields */\nfunction buildSchema(fields: ResourceFormFieldDef[]) {\n const shape: Record<string, z.ZodTypeAny> = {};\n\n for (const field of fields) {\n const fieldType = field.type ?? \"text\";\n\n if (field.hidden || fieldType === \"hidden\") continue;\n\n // File/image fields store File objects — z.any() is intentional here because\n // the File API shape varies by platform (browser File, React Native asset, etc.)\n // and form libraries may wrap files differently.\n if (fieldType === \"file\" || fieldType === \"image\") {\n shape[field.key] = field.required !== false ? z.unknown().refine((v) => v != null, { message: `${field.label ?? titleCase(field.key)} is required` }) : z.unknown().optional();\n continue;\n }\n\n switch (fieldType) {\n case \"number\": {\n let schema = z.coerce.number({ message: \"Must be a number\" });\n if (field.min != null) schema = schema.min(field.min);\n if (field.max != null) schema = schema.max(field.max);\n shape[field.key] = field.required !== false\n ? schema\n : schema.optional();\n break;\n }\n case \"array\": {\n if (field.arrayFields) {\n const itemShape: Record<string, z.ZodTypeAny> = {};\n for (const sub of field.arrayFields) {\n const subType = sub.type ?? \"text\";\n if (subType === \"number\") {\n let s = z.coerce.number({ message: \"Must be a number\" });\n if (sub.min != null) s = s.min(sub.min);\n if (sub.max != null) s = s.max(sub.max);\n itemShape[sub.key] = sub.required !== false ? s : s.optional();\n } else if (subType === \"switch\" || subType === \"checkbox\") {\n itemShape[sub.key] = z.boolean().optional();\n } else {\n let s = z.string();\n if (sub.required !== false) {\n s = s.min(1, `${sub.label ?? titleCase(sub.key)} is required`);\n }\n if (subType === \"email\") s = s.email(\"Invalid email address\");\n if (subType === \"url\") s = s.url(\"Invalid URL\");\n if (sub.maxLength != null) {\n s = s.max(\n sub.maxLength,\n `${sub.label ?? titleCase(sub.key)} must be ${sub.maxLength} characters or fewer`,\n );\n }\n itemShape[sub.key] = sub.required === false ? s.optional().or(z.literal(\"\")) : s;\n }\n }\n let arraySchema: z.ZodTypeAny = z.array(z.object(itemShape));\n if (field.arrayMin != null) arraySchema = (arraySchema as z.ZodArray<z.ZodTypeAny>).min(field.arrayMin, `Minimum ${field.arrayMin} items required`);\n if (field.arrayMax != null) arraySchema = (arraySchema as z.ZodArray<z.ZodTypeAny>).max(field.arrayMax, `Maximum ${field.arrayMax} items allowed`);\n shape[field.key] = arraySchema;\n }\n break;\n }\n case \"switch\":\n case \"checkbox\": {\n shape[field.key] = z.boolean().optional();\n break;\n }\n default: {\n // text, email, url, textarea, date, select\n let schema = z.string();\n if (field.required !== false) {\n schema = schema.min(1, `${field.label ?? titleCase(field.key)} is required`);\n }\n if (fieldType === \"email\") {\n schema = schema.email(\"Invalid email address\");\n }\n if (fieldType === \"url\") {\n schema = schema.url(\"Invalid URL\");\n }\n if (field.maxLength != null) {\n schema = schema.max(\n field.maxLength,\n `${field.label ?? titleCase(field.key)} must be ${field.maxLength} characters or fewer`,\n );\n }\n shape[field.key] = field.required === false\n ? schema.optional().or(z.literal(\"\"))\n : schema;\n break;\n }\n }\n }\n\n return z.object(shape).passthrough();\n}\n\n/** Build default values for a single array item from its sub-field definitions */\nexport function buildArrayItemDefaults(\n arrayFields: ResourceFormFieldDef[],\n): Record<string, unknown> {\n const defaults: Record<string, unknown> = {};\n for (const sub of arrayFields) {\n const subType = sub.type ?? \"text\";\n if (subType === \"number\") defaults[sub.key] = undefined;\n else if (subType === \"switch\" || subType === \"checkbox\") defaults[sub.key] = false;\n else defaults[sub.key] = sub.defaultValue ?? \"\";\n }\n return defaults;\n}\n\n/** Build default values from fields and existing data */\nfunction buildDefaults(\n fields: ResourceFormFieldDef[],\n data: Record<string, unknown> | undefined,\n action: \"create\" | \"edit\",\n prefillFromUrl: boolean,\n): Record<string, unknown> {\n const defaults: Record<string, unknown> = {};\n for (const field of fields) {\n if (data && field.key in data) {\n defaults[field.key] = data[field.key];\n } else if (field.defaultValue !== undefined) {\n defaults[field.key] = field.defaultValue;\n } else {\n const fieldType = field.type ?? \"text\";\n switch (fieldType) {\n case \"array\":\n defaults[field.key] = field.arrayMin\n ? Array.from({ length: field.arrayMin }, () =>\n buildArrayItemDefaults(field.arrayFields ?? []),\n )\n : [];\n break;\n case \"number\":\n defaults[field.key] = undefined;\n break;\n case \"switch\":\n case \"checkbox\":\n defaults[field.key] = false;\n break;\n default:\n defaults[field.key] = \"\";\n break;\n }\n }\n }\n\n // URL pre-fill for create mode\n if (typeof window !== \"undefined\" && typeof window.location !== \"undefined\" && typeof window.location.search === \"string\" && action === \"create\" && prefillFromUrl !== false) {\n const params = new URLSearchParams(window.location.search);\n for (const field of fields) {\n const val = params.get(field.key);\n if (val != null) {\n defaults[field.key] = field.type === \"number\" ? Number(val) : val;\n }\n }\n }\n\n return defaults;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useResourceForm(options: UseResourceFormOptions): UseResourceFormReturn {\n const {\n resource,\n action,\n id,\n initialData,\n fields: fieldsProp,\n listData,\n onSuccess,\n onError,\n prefillFromUrl = true,\n autoPlaceholder = true,\n validationMode,\n autosave,\n invalidates,\n warnWhenUnsaved = false,\n mutationMode = \"pessimistic\",\n undoDelayMs,\n onUndoable,\n visible = true,\n } = options;\n\n const autosaveEnabled = !!autosave;\n const debounceMs = typeof autosave === \"object\" ? autosave.debounceMs ?? 1000 : 1000;\n\n const provider = useDataProvider();\n const storage = useStorage();\n const queryClient = useQueryClient();\n\n // Fetch record for edit mode when no initialData\n const needsFetch = action === \"edit\" && !initialData && !!id;\n const fetchResult = useOne(\n resource,\n needsFetch ? id : undefined,\n { enabled: needsFetch },\n );\n\n const fetchedData = fetchResult.data ?? undefined;\n const effectiveData = initialData ?? fetchedData;\n const isLoading = needsFetch && fetchResult.isLoading;\n\n // In edit mode the form starts from the provider's form shape of the row\n // (`DataProvider.toFormValues`), for a resource whose form differs from\n // its table. The record id still comes from the row itself.\n const formSource = useMemo(\n () =>\n action === \"edit\" && effectiveData && provider.toFormValues\n ? provider.toFormValues(resource, effectiveData)\n : effectiveData,\n [action, effectiveData, provider, resource],\n );\n\n // Resolve fields\n const resolvedFields = useMemo(\n () => resolveFields(fieldsProp, formSource, listData, autoPlaceholder),\n [fieldsProp, formSource, listData, autoPlaceholder],\n );\n\n // Build schema\n const schema = useMemo(\n () => buildSchema(resolvedFields),\n [resolvedFields],\n );\n\n // Build defaults (with optional draft restoration)\n const defaultValues = useMemo(() => {\n const defaults = buildDefaults(resolvedFields, formSource, action, prefillFromUrl);\n if (autosaveEnabled && action === \"create\") {\n const key = `genesis-draft:${resource}:new`;\n const saved = storage.getItem(key);\n if (saved) {\n try {\n const draft = JSON.parse(saved);\n if (draft && typeof draft === \"object\" && !Array.isArray(draft)) {\n // Only restore keys that exist in the field definitions\n const validKeys = new Set(resolvedFields.map((f) => f.key));\n for (const k of Object.keys(draft)) {\n if (validKeys.has(k)) {\n defaults[k] = draft[k];\n }\n }\n } else {\n console.warn(`[useResourceForm] Draft for \"${resource}\" has unexpected shape, removing`);\n storage.removeItem(key);\n }\n } catch {\n console.warn(`[useResourceForm] Corrupt draft for \"${resource}\", removing`);\n storage.removeItem(key);\n }\n }\n }\n return defaults;\n }, [resolvedFields, formSource, action, prefillFromUrl, autosaveEnabled, resource, storage]);\n\n // Form\n // Cast the resolver because zod v4's passthrough() produces `unknown` for input,\n // but react-hook-form expects FieldValues (Record<string, any>).\n const resolver = useMemo(\n () => zodResolver(schema) as unknown as Resolver<Record<string, unknown>>,\n [schema],\n );\n\n const form = useForm<Record<string, unknown>>({\n resolver,\n defaultValues,\n values: action === \"edit\" ? defaultValues : undefined,\n mode: validationMode ?? \"onBlur\",\n reValidateMode: \"onChange\",\n shouldUnregister: true,\n });\n\n // Autosave effect — persist form values to storage for create mode\n useEffect(() => {\n if (!autosaveEnabled || action !== \"create\") return;\n const key = `genesis-draft:${resource}:new`;\n let timer: ReturnType<typeof setTimeout>;\n\n const subscription = form.watch((values) => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n try {\n storage.setItem(key, JSON.stringify(values));\n } catch (err) {\n console.warn(`[useResourceForm] Autosave failed for \"${resource}\":`, err instanceof Error ? err.message : String(err));\n }\n }, debounceMs);\n });\n\n return () => {\n clearTimeout(timer);\n subscription.unsubscribe();\n };\n }, [autosaveEnabled, resource, action, form, debounceMs, storage]);\n\n // Unsaved changes: RHF's isDirty, cleared by a successful save until the\n // next edit (a create form keeps its values after saving, so RHF alone\n // would still call it dirty).\n const rhfDirty = form.formState.isDirty;\n const [savedClean, setSavedClean] = useState(false);\n const savedCleanRef = useRef(false);\n useEffect(() => {\n const subscription = form.watch((_values, info) => {\n // `watch` also reports resets (no field name); only an edit dirties.\n if (!info.name) return;\n savedCleanRef.current = false;\n setSavedClean(false);\n });\n return () => subscription.unsubscribe();\n }, [form]);\n const isDirty = rhfDirty && !savedClean;\n const warn = !!warnWhenUnsaved;\n const { confirmLeave: guardConfirm } = useUnsavedChangesGuard({\n when: warn && isDirty,\n message: typeof warnWhenUnsaved === \"object\" ? warnWhenUnsaved.message : undefined,\n });\n const confirmLeave = useCallback(\n // The ref, not state: a router blocker may ask in the same tick as the\n // save's onSuccess navigates, before a re-render.\n () => (!warn || savedCleanRef.current ? true : guardConfirm()),\n [warn, guardConfirm],\n );\n const markSaved = useCallback(() => {\n savedCleanRef.current = true;\n setSavedClean(true);\n }, []);\n\n // A save that fails after it was handed off only comes back to the form\n // while the user can still see it.\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n const visibleRef = useRef(visible);\n visibleRef.current = visible;\n\n /** A failed save, on the form: per-field messages and a root summary. */\n const showError = useCallback(\n (error: Error) => {\n const fieldErrors = isDataWriteError(error) ? error.fields : undefined;\n const known = new Set(resolvedFields.map((f) => f.key));\n let first: string | undefined;\n if (fieldErrors) {\n for (const [field, message] of Object.entries(fieldErrors)) {\n if (!known.has(field)) continue;\n form.setError(field, { type: \"server\", message });\n first ??= field;\n }\n }\n form.setError(\"root\", { type: \"server\", message: error.message });\n if (first) {\n try {\n form.setFocus(first);\n } catch {\n // Not rendered (hidden or custom renderForm): the summary still says it.\n }\n }\n },\n [form, resolvedFields],\n );\n\n const reportError = useCallback(\n (error: Error, handedOff?: Record<string, unknown>) => {\n // A save handed off optimistically and then refused is unsaved again,\n // if the form still shows what it saved; a closed, hidden or reset\n // form is left alone.\n const onForm =\n !handedOff ||\n (mountedRef.current && visibleRef.current && sameValue(form.getValues(), handedOff));\n if (onForm) {\n savedCleanRef.current = false;\n setSavedClean(false);\n showError(error);\n }\n if (onError) {\n onError(error);\n } else {\n console.error(`[useResourceForm] ${action} failed for \"${resource}\":`, error);\n }\n },\n [form, showError, onError, action, resource],\n );\n\n // Submit handler\n const handleSubmit = useCallback(\n async (formData: Record<string, unknown>) => {\n // What the form shows as this save goes out (raw input values, before\n // any resolver coercion), to tell later whether it still does.\n const shown = structuredCloneSafe(form.getValues());\n try {\n let recordId: string | number | undefined;\n if (action === \"edit\") {\n recordId = id ?? String((effectiveData as Record<string, unknown>)?.id ?? \"\");\n if (!recordId) {\n throw new Error(\"Cannot update record: no ID available. Provide an id prop or ensure the record has an id field.\");\n }\n }\n const before = recordId != null ? cachedRecord(queryClient, resource, recordId) : undefined;\n const outcome = await executeWrite<BaseRecord>({\n queryClient,\n provider,\n resource,\n kind: action === \"create\" ? \"create\" : \"update\",\n id: recordId,\n // The cache holds rows; patch it only when the form's values are\n // row-shaped (no provider.toFormValues reshaping this record).\n data: formSource === effectiveData ? formData : undefined,\n mode: mutationMode,\n undoDelayMs,\n onUndoable,\n send: async (ctx) => {\n const keepalive = ctx.keepalive ? { keepalive: true } : {};\n const res =\n action === \"create\"\n ? await provider.create(resource, { data: formData, ...keepalive })\n : await provider.update(resource, { id: recordId!, data: ctx.data ?? formData, ...keepalive });\n return { record: res.data, audit: res.audit };\n },\n committed: async (result) => {\n // Clear draft on successful save\n if (autosaveEnabled && action === \"create\") {\n try {\n storage.removeItem(`genesis-draft:${resource}:new`);\n } catch (err) {\n console.warn(`[useResourceForm] Draft cleanup failed for \"${resource}\":`, err instanceof Error ? err.message : String(err));\n }\n }\n // Invalidate queries to refresh data: the resource's own, then any\n // related keys the provider or the caller named.\n await invalidateResource(queryClient, resource);\n const related = relatedKeys(\n provider,\n {\n resource,\n kind: action === \"create\" ? \"create\" : \"update\",\n id: (recordId ?? result.id) as string | number,\n record: result,\n },\n invalidates,\n );\n await Promise.all(related.map((queryKey) => queryClient.invalidateQueries({ queryKey: [...queryKey] })));\n },\n failedLater: (error) => reportError(error, shown),\n });\n markSaved();\n if (outcome.status === \"committed\") {\n onSuccess?.(outcome.record);\n } else {\n onSuccess?.(\n action === \"edit\"\n ? { ...(before ?? effectiveData ?? {}), ...formData, id: recordId }\n : { ...formData },\n );\n }\n } catch (err) {\n reportError(err instanceof Error ? err : new Error(String(err)));\n }\n },\n [form, action, provider, resource, id, effectiveData, formSource, queryClient, onSuccess, autosaveEnabled, storage, invalidates, mutationMode, undoDelayMs, onUndoable, reportError, markSaved],\n );\n\n const onSubmit = useCallback(\n (e?: { preventDefault?: () => void }) => form.handleSubmit(handleSubmit)(e as React.BaseSyntheticEvent),\n [form, handleSubmit],\n );\n\n const isSaving = form.formState.isSubmitting;\n\n return {\n form,\n resolvedFields,\n onSubmit,\n handleSave: handleSubmit,\n isSaving,\n isLoading,\n isDirty,\n confirmLeave,\n };\n}\n"]}
|
|
@@ -5,6 +5,11 @@ var DataWriteError = class extends Error {
|
|
|
5
5
|
currentVersion;
|
|
6
6
|
/** The store's own error code, when it gave one. */
|
|
7
7
|
code;
|
|
8
|
+
/**
|
|
9
|
+
* Per-field messages (field name → safe message), when the store said which
|
|
10
|
+
* values it rejected. See `DataWriteErrorOptions.fields`.
|
|
11
|
+
*/
|
|
12
|
+
fields;
|
|
8
13
|
constructor(outcome, message, options = {}) {
|
|
9
14
|
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
10
15
|
this.name = "DataWriteError";
|
|
@@ -12,6 +17,9 @@ var DataWriteError = class extends Error {
|
|
|
12
17
|
this.current = options.current;
|
|
13
18
|
this.currentVersion = options.currentVersion;
|
|
14
19
|
if (options.code !== void 0) this.code = options.code;
|
|
20
|
+
if (options.fields && Object.keys(options.fields).length > 0) {
|
|
21
|
+
this.fields = Object.freeze({ ...options.fields });
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
24
|
};
|
|
17
25
|
function isDataWriteError(error) {
|
|
@@ -19,5 +27,5 @@ function isDataWriteError(error) {
|
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export { DataWriteError, isDataWriteError };
|
|
22
|
-
//# sourceMappingURL=chunk-
|
|
23
|
-
//# sourceMappingURL=chunk-
|
|
30
|
+
//# sourceMappingURL=chunk-5NHR6ZXS.js.map
|
|
31
|
+
//# sourceMappingURL=chunk-5NHR6ZXS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/write-outcome.ts"],"names":[],"mappings":";AAuDO,IAAM,cAAA,GAAN,cAA6B,KAAA,CAAM;AAAA,EAC/B,OAAA;AAAA,EACA,OAAA;AAAA,EACA,cAAA;AAAA;AAAA,EAEA,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA,MAAA;AAAA,EAET,WAAA,CAAY,OAAA,EAAuB,OAAA,EAAiB,OAAA,GAAiC,EAAC,EAAG;AACvF,IAAA,KAAA,CAAM,OAAA,EAAS,QAAQ,KAAA,KAAU,MAAA,GAAY,SAAY,EAAE,KAAA,EAAO,OAAA,CAAQ,KAAA,EAAO,CAAA;AACjF,IAAA,IAAA,CAAK,IAAA,GAAO,gBAAA;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AACf,IAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,OAAA;AACvB,IAAA,IAAA,CAAK,iBAAiB,OAAA,CAAQ,cAAA;AAC9B,IAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,MAAA,EAAW,IAAA,CAAK,OAAO,OAAA,CAAQ,IAAA;AACpD,IAAA,IAAI,OAAA,CAAQ,UAAU,MAAA,CAAO,IAAA,CAAK,QAAQ,MAAM,CAAA,CAAE,SAAS,CAAA,EAAG;AAC5D,MAAA,IAAA,CAAK,SAAS,MAAA,CAAO,MAAA,CAAO,EAAE,GAAG,OAAA,CAAQ,QAAQ,CAAA;AAAA,IACnD;AAAA,EACF;AACF;AAMO,SAAS,iBAAiB,KAAA,EAAyC;AACxE,EAAA,OACE,iBAAiB,KAAA,IACjB,KAAA,CAAM,SAAS,gBAAA,IACf,OAAQ,MAAgC,OAAA,KAAY,QAAA;AAExD","file":"chunk-5NHR6ZXS.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Why a write did not save (DATA-STUDIO.md \"Write safety\")\n//\n// A write that changes nothing is never reported as \"permission denied\" by\n// assumption. The provider re-reads the row and says what actually happened.\n// ---------------------------------------------------------------------------\n\nimport type { DataRow } from \"./types\";\n\n/**\n * - `conflict`: someone else changed the row since it was read. `current`\n * carries the row as it is now.\n * - `gone`: the row was deleted, or is no longer visible to this user. The\n * two cannot be told apart without a privileged look at the table, which\n * would show this user a fact about a row they cannot see.\n * - `forbidden`: the row is visible and unchanged, but the app or the\n * database does not let this user make this change. Only reported when\n * that is proven, never assumed from \"zero rows changed\".\n * - `invalid`: the store rejected the value (type, constraint, not-null).\n * - `audit_failed`: the audit entry could not be written, so the edit was\n * rolled back with it.\n * - `unauthenticated`: no signed-in user, or the session expired.\n * - `error`: anything else. The edit did not commit.\n */\nexport type WriteOutcome =\n | \"conflict\"\n | \"gone\"\n | \"forbidden\"\n | \"invalid\"\n | \"audit_failed\"\n | \"unauthenticated\"\n | \"error\";\n\nexport interface DataWriteErrorOptions {\n /** The row as it is now, for a `conflict`. Only fields the user may read. */\n current?: DataRow;\n /** The row's current version token, for a `conflict`. */\n currentVersion?: string | null;\n /**\n * The store's own error code, when it gave one (a Postgres SQLSTATE such as\n * `23505`, or a PostgREST code such as `PGRST301`).\n */\n code?: string;\n /**\n * Per-field messages, keyed by field (column) name, for a write the store\n * rejected because of particular values. Each message is safe to show next\n * to its input: plain words, never the store's raw text or the value.\n * `useResourceForm` puts each one on its field.\n */\n fields?: Record<string, string>;\n /** The underlying error, kept for logging. */\n cause?: unknown;\n}\n\n/** Thrown by a provider when a write did not commit. */\nexport class DataWriteError extends Error {\n readonly outcome: WriteOutcome;\n readonly current?: DataRow;\n readonly currentVersion?: string | null;\n /** The store's own error code, when it gave one. */\n readonly code?: string;\n /**\n * Per-field messages (field name → safe message), when the store said which\n * values it rejected. See `DataWriteErrorOptions.fields`.\n */\n readonly fields?: Readonly<Record<string, string>>;\n\n constructor(outcome: WriteOutcome, message: string, options: DataWriteErrorOptions = {}) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"DataWriteError\";\n this.outcome = outcome;\n this.current = options.current;\n this.currentVersion = options.currentVersion;\n if (options.code !== undefined) this.code = options.code;\n if (options.fields && Object.keys(options.fields).length > 0) {\n this.fields = Object.freeze({ ...options.fields });\n }\n }\n}\n\n/**\n * `instanceof` fails when two copies of this package are installed, so check\n * the shape instead.\n */\nexport function isDataWriteError(error: unknown): error is DataWriteError {\n return (\n error instanceof Error &&\n error.name === \"DataWriteError\" &&\n typeof (error as { outcome?: unknown }).outcome === \"string\"\n );\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { invalidateResource, executeWrite, cachedRecord } from './chunk-F3BE7NGM.js';
|
|
1
2
|
import { relatedKeys } from './chunk-7NUL32W7.js';
|
|
2
3
|
import { useDataProvider } from './chunk-GGSXSI6Z.js';
|
|
3
4
|
import { useState, useCallback, useMemo } from 'react';
|
|
@@ -20,6 +21,8 @@ function useResource(resource, options = {}) {
|
|
|
20
21
|
defaultSort,
|
|
21
22
|
defaultFilters,
|
|
22
23
|
defaultPerPage = 25,
|
|
24
|
+
defaultPage = 1,
|
|
25
|
+
defaultSearch = "",
|
|
23
26
|
select,
|
|
24
27
|
searchFields: searchFieldsOption,
|
|
25
28
|
enabled = true,
|
|
@@ -28,15 +31,18 @@ function useResource(resource, options = {}) {
|
|
|
28
31
|
onCreateSuccess,
|
|
29
32
|
onUpdateSuccess,
|
|
30
33
|
onDeleteSuccess,
|
|
31
|
-
onError
|
|
34
|
+
onError,
|
|
35
|
+
mutationMode = "pessimistic",
|
|
36
|
+
undoDelayMs,
|
|
37
|
+
onUndoable
|
|
32
38
|
} = options;
|
|
33
39
|
const provider = useDataProvider();
|
|
34
40
|
const queryClient = useQueryClient();
|
|
35
41
|
const [sort, setSortRaw] = useState(defaultSort);
|
|
36
42
|
const [filters, setFiltersRaw] = useState(defaultFilters ?? []);
|
|
37
|
-
const [page, setPage] = useState(1);
|
|
43
|
+
const [page, setPage] = useState(Math.max(1, Math.floor(defaultPage)));
|
|
38
44
|
const [perPage, setPerPageRaw] = useState(defaultPerPage);
|
|
39
|
-
const [searchQuery, setSearchQuery] = useState(
|
|
45
|
+
const [searchQuery, setSearchQuery] = useState(defaultSearch);
|
|
40
46
|
const defaultFiltersKey = stableFilterKey(defaultFilters);
|
|
41
47
|
const [syncedFiltersKey, setSyncedFiltersKey] = useState(defaultFiltersKey);
|
|
42
48
|
if (defaultFiltersKey !== syncedFiltersKey) {
|
|
@@ -84,7 +90,7 @@ function useResource(resource, options = {}) {
|
|
|
84
90
|
});
|
|
85
91
|
const invalidateAfter = useCallback(
|
|
86
92
|
(event) => {
|
|
87
|
-
void queryClient
|
|
93
|
+
void invalidateResource(queryClient, resource);
|
|
88
94
|
for (const queryKey of relatedKeys(provider, event, invalidates)) {
|
|
89
95
|
void queryClient.invalidateQueries({ queryKey: [...queryKey] });
|
|
90
96
|
}
|
|
@@ -97,37 +103,77 @@ function useResource(resource, options = {}) {
|
|
|
97
103
|
},
|
|
98
104
|
[resource]
|
|
99
105
|
);
|
|
106
|
+
const reportError = onError ?? defaultOnError;
|
|
107
|
+
const modeArgs = {
|
|
108
|
+
queryClient,
|
|
109
|
+
provider,
|
|
110
|
+
resource,
|
|
111
|
+
mode: mutationMode,
|
|
112
|
+
undoDelayMs,
|
|
113
|
+
onUndoable,
|
|
114
|
+
failedLater: reportError
|
|
115
|
+
};
|
|
100
116
|
const createMutation = useMutation({
|
|
101
117
|
mutationFn: async (data) => {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
118
|
+
const outcome = await executeWrite({
|
|
119
|
+
...modeArgs,
|
|
120
|
+
kind: "create",
|
|
121
|
+
data,
|
|
122
|
+
send: async ({ keepalive }) => {
|
|
123
|
+
const result = await provider.create(resource, { data, ...keepalive ? { keepalive } : {} });
|
|
124
|
+
return { record: result.data, audit: result.audit };
|
|
125
|
+
},
|
|
126
|
+
committed: (record) => {
|
|
127
|
+
invalidateAfter({ resource, kind: "create", id: record.id, record });
|
|
128
|
+
onCreateSuccess?.(record);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return outcome.status === "committed" ? outcome.record : data;
|
|
108
132
|
},
|
|
109
|
-
onError:
|
|
133
|
+
onError: reportError
|
|
110
134
|
});
|
|
111
135
|
const updateMutation = useMutation({
|
|
112
136
|
mutationFn: async ({ id, data }) => {
|
|
113
|
-
const
|
|
114
|
-
|
|
137
|
+
const before = cachedRecord(queryClient, resource, id);
|
|
138
|
+
const outcome = await executeWrite({
|
|
139
|
+
...modeArgs,
|
|
140
|
+
kind: "update",
|
|
141
|
+
id,
|
|
142
|
+
data,
|
|
143
|
+
send: async (ctx) => {
|
|
144
|
+
const result = await provider.update(resource, {
|
|
145
|
+
id,
|
|
146
|
+
data: ctx.data ?? data,
|
|
147
|
+
...ctx.keepalive ? { keepalive: true } : {}
|
|
148
|
+
});
|
|
149
|
+
return { record: result.data, audit: result.audit };
|
|
150
|
+
},
|
|
151
|
+
committed: (record) => {
|
|
152
|
+
invalidateAfter({ resource, kind: "update", id, record });
|
|
153
|
+
onUpdateSuccess?.(record);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return outcome.status === "committed" ? outcome.record : { ...before ?? {}, ...data, id: before?.id ?? id };
|
|
115
157
|
},
|
|
116
|
-
|
|
117
|
-
invalidateAfter({ resource, kind: "update", id, record: data });
|
|
118
|
-
onUpdateSuccess?.(data);
|
|
119
|
-
},
|
|
120
|
-
onError: onError ?? defaultOnError
|
|
158
|
+
onError: reportError
|
|
121
159
|
});
|
|
122
160
|
const deleteMutation = useMutation({
|
|
123
161
|
mutationFn: async (id) => {
|
|
124
|
-
await
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
162
|
+
await executeWrite({
|
|
163
|
+
...modeArgs,
|
|
164
|
+
kind: "delete",
|
|
165
|
+
id,
|
|
166
|
+
send: async ({ keepalive }) => {
|
|
167
|
+
const result = await provider.deleteOne(resource, { id, ...keepalive ? { keepalive } : {} });
|
|
168
|
+
return { record: void 0, audit: result?.audit };
|
|
169
|
+
},
|
|
170
|
+
committed: () => {
|
|
171
|
+
invalidateAfter({ resource, kind: "delete", id });
|
|
172
|
+
onDeleteSuccess?.();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
129
175
|
},
|
|
130
|
-
onError:
|
|
176
|
+
onError: reportError
|
|
131
177
|
});
|
|
132
178
|
const create = useCallback(
|
|
133
179
|
(data) => createMutation.mutateAsync(data),
|
|
@@ -161,5 +207,5 @@ function useResource(resource, options = {}) {
|
|
|
161
207
|
}
|
|
162
208
|
|
|
163
209
|
export { useResource };
|
|
164
|
-
//# sourceMappingURL=chunk-
|
|
165
|
-
//# sourceMappingURL=chunk-
|
|
210
|
+
//# sourceMappingURL=chunk-6Q477CHB.js.map
|
|
211
|
+
//# sourceMappingURL=chunk-6Q477CHB.js.map
|