@marktiderman/genesis-core 1.5.0 → 1.6.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.
Files changed (51) hide show
  1. package/README.md +143 -10
  2. package/dist/chunk-ADD2RIGA.js +108 -0
  3. package/dist/chunk-ADD2RIGA.js.map +1 -0
  4. package/dist/{chunk-IZT2O57C.js → chunk-CKGGE6WA.js} +36 -16
  5. package/dist/chunk-CKGGE6WA.js.map +1 -0
  6. package/dist/{chunk-DVW5GXWZ.js → chunk-KV6KH3L2.js} +16 -11
  7. package/dist/chunk-KV6KH3L2.js.map +1 -0
  8. package/dist/chunk-KXVHDK3Q.js +71 -0
  9. package/dist/chunk-KXVHDK3Q.js.map +1 -0
  10. package/dist/chunk-OBUBP4BY.js +556 -0
  11. package/dist/chunk-OBUBP4BY.js.map +1 -0
  12. package/dist/chunk-V3GDAELU.js +3 -0
  13. package/dist/{chunk-HT43MPKW.js.map → chunk-V3GDAELU.js.map} +1 -1
  14. package/dist/{chunk-RLY7TZUY.js → chunk-Y475Z7BM.js} +247 -22
  15. package/dist/chunk-Y475Z7BM.js.map +1 -0
  16. package/dist/{chunk-ZK5C3OOT.js → chunk-ZKMAU6NN.js} +12 -3
  17. package/dist/chunk-ZKMAU6NN.js.map +1 -0
  18. package/dist/hooks/index.d.ts +4 -2
  19. package/dist/hooks/index.js +7 -5
  20. package/dist/hooks/mutation-mode.d.ts +40 -30
  21. package/dist/hooks/mutation-mode.js +4 -1
  22. package/dist/hooks/related-keys.d.ts +2 -2
  23. package/dist/hooks/use-capabilities.d.ts +1 -1
  24. package/dist/hooks/use-infinite-resource.d.ts +50 -0
  25. package/dist/hooks/use-infinite-resource.js +6 -0
  26. package/dist/hooks/use-infinite-resource.js.map +1 -0
  27. package/dist/hooks/use-live.d.ts +27 -0
  28. package/dist/hooks/use-live.js +5 -0
  29. package/dist/hooks/use-live.js.map +1 -0
  30. package/dist/hooks/use-one.d.ts +9 -2
  31. package/dist/hooks/use-one.js +2 -1
  32. package/dist/hooks/use-resource-form.d.ts +10 -2
  33. package/dist/hooks/use-resource-form.js +5 -3
  34. package/dist/hooks/use-resource.d.ts +14 -2
  35. package/dist/hooks/use-resource.js +4 -2
  36. package/dist/index.d.ts +6 -4
  37. package/dist/index.js +9 -7
  38. package/dist/provider/index.d.ts +59 -7
  39. package/dist/provider/index.js +1 -1
  40. package/dist/{read-outcome-BQkYcV4t.d.ts → read-outcome-C_9HbXvb.d.ts} +1 -1
  41. package/dist/schema/index.d.ts +4 -4
  42. package/dist/{types-C5bjsbxz.d.ts → types-C7FJtFXV.d.ts} +20 -0
  43. package/dist/{types-Dsqqr9s7.d.ts → types-CS9UpxQS.d.ts} +73 -4
  44. package/package.json +1 -1
  45. package/dist/chunk-DVW5GXWZ.js.map +0 -1
  46. package/dist/chunk-HT43MPKW.js +0 -3
  47. package/dist/chunk-IZT2O57C.js.map +0 -1
  48. package/dist/chunk-QH4P5MI2.js +0 -278
  49. package/dist/chunk-QH4P5MI2.js.map +0 -1
  50. package/dist/chunk-RLY7TZUY.js.map +0 -1
  51. package/dist/chunk-ZK5C3OOT.js.map +0 -1
package/README.md CHANGED
@@ -101,6 +101,7 @@ const provider = createSupabaseProvider(supabase, {
101
101
  | `toFormValues(row)` | Row → edit-form values, for a form shaped differently from its table. Served as `DataProvider.toFormValues`, which `useResourceForm` applies in edit mode. The reverse is `prepare`. |
102
102
  | `invalidates` | Other query keys a write makes stale, as a list or `(event) => keys`. Served as `DataProvider.invalidates`, which `useResource` and `useResourceForm` (and so `ResourcePage` and `ResourceForm`) invalidate after their writes, on top of `[resource]`. |
103
103
  | `onWrite(event)` | Called after every committed create, update and delete, including writes made straight through the provider. Awaited; if it throws, the error is logged and the write is still reported saved. |
104
+ | `realtime` / `resources[r].realtime` | Serve `subscribe` through Supabase Realtime, for `live` lists and records. Off by default: `true`, a list of resources, or `(resource) => boolean`; a resource's own `realtime` wins. See [Live updates](#live-updates-realtime). |
104
105
 
105
106
  Both hooks also take an `invalidates` option for keys one screen cares about:
106
107
  `useResource("tasks", { invalidates: [["me"]] })`.
@@ -160,8 +161,11 @@ plain sentence (the raw text stays on `cause`):
160
161
  | `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
 
162
163
  Only plain column names are taken from the store's text; the values it echoes
163
- and constraint names never reach a message. Anything else (expression indexes,
164
- check constraints) is left unmapped. Your own provider or `prepare` rule can
164
+ and constraint names never reach a message. A unique index on one column in a
165
+ simple expression (`lower(email::text)`) maps to that column; a unique
166
+ violation whose columns cannot be read still gets the safe sentence "This value
167
+ is already in use." with no field. Anything else (check constraints) is left
168
+ unmapped. Your own provider or `prepare` rule can
165
169
  throw `new DataWriteError("invalid", "…", { fields: { slug: "Pick another slug." } })`.
166
170
 
167
171
  ## Mutation modes
@@ -186,10 +190,26 @@ throw `new DataWriteError("invalid", "…", { fields: { slug: "Pick another slug
186
190
  `undo` method) gets an `undoable` write at once; `undo()` then calls
187
191
  `provider.undo(resource, { auditId })` with the audit id the write returned
188
192
  (`strategy: "provider"`). Without an audit id no undo is offered.
189
- - **Leaving sends.** When the hook unmounts during the delay (the form closes,
190
- the route changes), waiting writes are sent at once, as React Admin does.
191
- Closing or reloading the tab also sends them, and holds the page with the
192
- browser's prompt so the requests can finish.
193
+ - **Undo outlives the form.** Pending writes belong to the `QueryClient`, not
194
+ to the hook: a form or page that unmounts during the delay (a dialog that
195
+ closes on success, a route change) leaves its writes waiting, and the undo
196
+ toast still works. They are sent when the delay ends.
197
+ - **Closing the tab sends.** On `beforeunload` and `pagehide`, waiting writes
198
+ are sent at once with `keepalive: true` in their params (`CreateParams`,
199
+ `UpdateParams`, `DeleteParams`). A fetch-based provider should pass it to
200
+ `fetch` and report `keepaliveWrites: true` in its capabilities. The
201
+ "Leave site?" prompt shows only while a write is being sent without that
202
+ guarantee (the built-in Supabase provider cannot pass keepalive through
203
+ supabase-js, so it prompts while a write is in flight).
204
+ - **Writes on one record compose.** They are sent in the order they were
205
+ made, one after another; sending a later one (its delay ended, `commit()`)
206
+ first sends the earlier ones still waiting, which can then no longer be
207
+ undone. Undoing (or a failure of) an earlier edit takes only its own fields
208
+ out of the cache, keeping later pending edits shown. A later pending edit
209
+ that kept the earlier edit's values unchanged (a form opened from the
210
+ patched cache) has them put back before it is sent, so it never sends the
211
+ undone values; one whose values are not row-shaped (`toFormValues`) cannot
212
+ be fixed, so it is dropped and reported through `onError`.
193
213
  - **What is patched:** an update merges the values into the row in every
194
214
  cached list (`[resource, "list", …]`) and detail (`[resource, "detail", id,
195
215
  …]`); a delete removes the row and lowers `total`. A create patches nothing
@@ -197,10 +217,12 @@ throw `new DataWriteError("invalid", "…", { fields: { slug: "Pick another slug
197
217
  `undoable` only change when it is sent and when the caller moves on. A
198
218
  rollback restores only the row it patched, so concurrent writes on other
199
219
  rows survive.
200
- - **Refetching waits.** While an optimistic or undoable write on a resource is
201
- pending, the hooks hold off invalidating `[resource]`; the last write to
202
- settle refetches. Other code that invalidates the resource (or a window-focus
203
- refetch) during the delay shows the server's rows until then.
220
+ - **Refetches keep the patch.** While a write is pending, its patch is
221
+ re-applied to any `[resource, "list", …]` or `[resource, "detail", …]` data
222
+ that lands in the cache: a refetch, a window-focus refetch, a new page,
223
+ filter or search. A deleted row stays gone and an edit stays shown until the
224
+ write settles; then the server's rows win. A fetch already in flight is not
225
+ cancelled. Each write refetches `[resource]` when it settles.
204
226
 
205
227
  ```ts
206
228
  const tasks = useResource("tasks", {
@@ -211,6 +233,117 @@ const tasks = useResource("tasks", {
211
233
  await tasks.remove(id); // resolves at once; sent in 5 s unless undone
212
234
  ```
213
235
 
236
+ ## Cursor paging
237
+
238
+ For long lists that grow with "Load more", page by cursor (keyset) instead of
239
+ offset: pages stay right while rows are added or removed, and a deep page
240
+ costs no more than the first.
241
+
242
+ ```ts
243
+ const tasks = useInfiniteResource("tasks", {
244
+ perPage: 50,
245
+ defaultSort: { field: "due_at", order: "asc" },
246
+ });
247
+ tasks.rows; // every loaded row
248
+ tasks.total; // from the first page's count
249
+ tasks.hasNextPage && tasks.fetchNextPage();
250
+ ```
251
+
252
+ - **The contract.** `getList` with `pagination: { page, perPage, cursor }`,
253
+ where `cursor` is `null` for the first page and the previous page's
254
+ `nextCursor` after that. A provider that serves it
255
+ (`capabilities().cursorPagination`) returns `nextCursor` (`null` on the
256
+ last page) and may return `total: -1` after the first page (not counted).
257
+ A provider that doesn't ignores `cursor` and pages by `page`;
258
+ `useInfiniteResource` then pages by offset until `total` rows are loaded.
259
+ - **Supabase.**
260
+ - It orders by the sort column and then the `orderKey` (default `id`) and
261
+ fetches one row more than the page to tell whether there is a next
262
+ page. A page is at most `maxPerPage - 1` rows, so that extra row fits
263
+ under PostgREST's `max-rows`.
264
+ - The cursor (opaque, base64 JSON) holds the last row's sort and key
265
+ values and the sort it was made for. A cursor from another sort is
266
+ refused.
267
+ - The keyset filter handles ascending and descending sorts, ties (broken
268
+ by the key) and null sort values, matching Postgres's default null
269
+ placement (last when ascending, first when descending, pinned in the
270
+ query).
271
+ - It refuses, with an `invalid` `DataReadError` whose `cause` says why:
272
+ `orderKey: false`, a sort on an embedded field (`author.name`), a sort
273
+ column with non-scalar values, and a `select` that leaves out the sort
274
+ or key column.
275
+ - Only the first page is counted.
276
+ - **Writes and pending patches.** Pages live under
277
+ `[resource, "infinite", params]`, so writes (which invalidate `[resource]`)
278
+ refetch them. Optimistic and undoable patches are applied to every loaded
279
+ page and survive refetches, like lists.
280
+ - **A row shows once.** A row that moved into a later page while the user
281
+ scrolled (its sort value changed, or rows were added before it) is shown
282
+ where it first appeared, not twice.
283
+
284
+ ## Live updates (realtime)
285
+
286
+ `useResource(resource, { live: true })`, `useOne(resource, id, { live: true })`
287
+ and genesis-ui's `<ResourcePage live />` keep what is on screen current while
288
+ they are mounted. They subscribe through the provider's optional
289
+ `subscribe(resource, params, callback)` and, when it reports a change,
290
+ refetch the resource's cached reads (`useResource`: every `[resource, …]`
291
+ query; `useOne`: that record, and only for its own id).
292
+
293
+ - **Events are hints, never data.** The callback gets `{ type: "created" |
294
+ "updated" | "deleted", id? }`, or `{ type: "changed" }` when changes may have
295
+ been missed (after a reconnect). Nothing from an event is written to the
296
+ cache: rows always come from the provider's normal read, under its RLS.
297
+ - **One refetch per burst.** Events within `debounceMs` (default 250) of each
298
+ other cause one refetch: `live: { debounceMs: 1000 }`. A steady stream
299
+ still refetches at least every `4 × debounceMs`.
300
+ - **Pending writes stay applied.** A refetch while an optimistic or undoable
301
+ write is pending gets that write's patch re-applied (see
302
+ [Mutation modes](#mutation-modes)), so a row deleted with Undo stays hidden
303
+ and an edit stays shown. Your own committed writes come back as events too;
304
+ the extra refetch is harmless and is not filtered out.
305
+ - **A hidden tab waits.** While the page is hidden, reads are only marked
306
+ stale; they refetch when it is shown again (with TanStack Query's default
307
+ `refetchOnWindowFocus`) or when next used.
308
+ - **Without `subscribe`**, or for a resource the provider doesn't watch,
309
+ `live` does nothing. `capabilities(resource).realtime` says whether it
310
+ will work.
311
+
312
+ ### Supabase setup
313
+
314
+ ```ts
315
+ const provider = createSupabaseProvider(supabase, {
316
+ resources: { tasks: { realtime: true } }, // or realtime: true | ["tasks"] for several
317
+ });
318
+ ```
319
+
320
+ ```sql
321
+ -- 1. Publish the table's changes to Realtime.
322
+ alter publication supabase_realtime add table public.tasks;
323
+ -- 2. Keep RLS on: Realtime only sends an insert or update to a subscriber
324
+ -- whose SELECT policies allow the new row.
325
+ alter table public.tasks enable row level security;
326
+ ```
327
+
328
+ - The provider opens one `postgres_changes` channel per subscription and
329
+ removes it on unsubscribe. `useOne`'s channel is not filtered on the
330
+ server (Realtime never delivers deletes through a filter); its id is
331
+ matched on arrival. A
332
+ resource named `schema.table` listens in that schema; otherwise `public`.
333
+ - **What leaks, and what doesn't.** Insert and update events are checked
334
+ against RLS by Realtime. **Delete events are not:** every subscriber to the
335
+ table receives the deleted row's primary key (with RLS on, Supabase sends
336
+ only the key, even under `REPLICA IDENTITY FULL`). The provider passes on only the id, and the hooks only refetch,
337
+ so a user never sees a row their read doesn't return. Don't enable
338
+ realtime on a table whose ids are themselves sensitive.
339
+ - **Cost.** Each mounted live list or record is one Realtime channel, and
340
+ each change refetches every live list of that table for every subscriber
341
+ (debounced per client). Realtime checks RLS once per subscriber per
342
+ change, so busy tables with many viewers multiply database load. Enable it
343
+ where people watch shared data change, not on every table.
344
+ - `capabilities(resource).realtime` is true only for resources with realtime
345
+ on and a client with `channel()` (any `@supabase/supabase-js` client).
346
+
214
347
  ## Unsaved changes
215
348
 
216
349
  `useResourceForm({ warnWhenUnsaved: true })` (default off) warns while the
@@ -0,0 +1,108 @@
1
+ import { useLiveInvalidation } from './chunk-KXVHDK3Q.js';
2
+ import { useDataProvider } from './chunk-GGSXSI6Z.js';
3
+ import { useState, useCallback, useMemo } from 'react';
4
+ import { useInfiniteQuery } from '@tanstack/react-query';
5
+
6
+ function filterKey(filters) {
7
+ return JSON.stringify(
8
+ (filters ?? []).map(
9
+ (f) => Object.fromEntries(Object.entries(f).sort(([a], [b]) => a.localeCompare(b)))
10
+ )
11
+ );
12
+ }
13
+ function useInfiniteResource(resource, options = {}) {
14
+ const {
15
+ defaultSort,
16
+ defaultFilters,
17
+ perPage = 25,
18
+ defaultSearch = "",
19
+ select,
20
+ searchFields,
21
+ enabled = true,
22
+ live
23
+ } = options;
24
+ const provider = useDataProvider();
25
+ const [sort, setSort] = useState(defaultSort);
26
+ const [filters, setFiltersRaw] = useState(defaultFilters ?? []);
27
+ const [search, setSearch] = useState(defaultSearch);
28
+ const defaultKey = filterKey(defaultFilters);
29
+ const [syncedKey, setSyncedKey] = useState(defaultKey);
30
+ if (defaultKey !== syncedKey) {
31
+ setSyncedKey(defaultKey);
32
+ setFiltersRaw(defaultFilters ?? []);
33
+ }
34
+ const setFilters = useCallback((next) => setFiltersRaw(next), []);
35
+ const params = useMemo(() => {
36
+ const p = {
37
+ perPage,
38
+ sort,
39
+ filters: filters.length > 0 ? filters : void 0,
40
+ select
41
+ };
42
+ if (search && searchFields && searchFields.length > 0) {
43
+ p.search = { query: search, fields: searchFields };
44
+ }
45
+ return p;
46
+ }, [perPage, sort, filters, select, search, searchFields]);
47
+ const query = useInfiniteQuery({
48
+ queryKey: [resource, "infinite", params],
49
+ initialPageParam: { page: 1, cursor: null },
50
+ queryFn: ({ pageParam }) => {
51
+ const { perPage: size, ...rest } = params;
52
+ return provider.getList(resource, {
53
+ ...rest,
54
+ pagination: { page: pageParam.page, perPage: size, cursor: pageParam.cursor }
55
+ });
56
+ },
57
+ getNextPageParam: (last, pages, lastParam) => {
58
+ if (last.nextCursor !== void 0) {
59
+ return last.nextCursor ? { page: lastParam.page + 1, cursor: last.nextCursor } : void 0;
60
+ }
61
+ const loaded = pages.reduce((n, p) => n + p.data.length, 0);
62
+ const total2 = pages[0]?.total ?? 0;
63
+ if (last.data.length < params.perPage || loaded >= total2) return void 0;
64
+ return { page: lastParam.page + 1, cursor: null };
65
+ },
66
+ enabled
67
+ });
68
+ useLiveInvalidation(resource, live, enabled, { filters: params.filters }, [resource]);
69
+ const rows = useMemo(() => {
70
+ const seen = /* @__PURE__ */ new Set();
71
+ const out = [];
72
+ for (const page of query.data?.pages ?? []) {
73
+ for (const row of page.data) {
74
+ const id = row?.id;
75
+ if (id != null) {
76
+ const key = String(id);
77
+ if (seen.has(key)) continue;
78
+ seen.add(key);
79
+ }
80
+ out.push(row);
81
+ }
82
+ }
83
+ return out;
84
+ }, [query.data]);
85
+ const firstTotal = query.data?.pages[0]?.total;
86
+ const total = firstTotal === void 0 || firstTotal < 0 ? void 0 : firstTotal;
87
+ const { fetchNextPage: fetchNext, hasNextPage, isFetchingNextPage } = query;
88
+ const fetchNextPage = useCallback(() => {
89
+ if (!hasNextPage || isFetchingNextPage) return;
90
+ void fetchNext();
91
+ }, [fetchNext, hasNextPage, isFetchingNextPage]);
92
+ return {
93
+ query,
94
+ rows,
95
+ total,
96
+ fetchNextPage,
97
+ hasNextPage: !!hasNextPage,
98
+ isFetchingNextPage,
99
+ params,
100
+ setSort,
101
+ setFilters,
102
+ setSearch
103
+ };
104
+ }
105
+
106
+ export { useInfiniteResource };
107
+ //# sourceMappingURL=chunk-ADD2RIGA.js.map
108
+ //# sourceMappingURL=chunk-ADD2RIGA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/use-infinite-resource.ts"],"names":["total"],"mappings":"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","file":"chunk-ADD2RIGA.js","sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport {\n useInfiniteQuery,\n type InfiniteData,\n type UseInfiniteQueryResult,\n} from \"@tanstack/react-query\";\nimport { useDataProvider } from \"../provider/context\";\nimport type {\n BaseRecord,\n FilterParam,\n ListParams,\n ListResult,\n SortParam,\n} from \"../provider/types\";\nimport { useLiveInvalidation, type LiveOptions } from \"./use-live\";\n\n// ---------------------------------------------------------------------------\n// useInfiniteResource — a list that grows with \"Load more\", by cursor.\n//\n// Asks for the first page with `cursor: null`. A provider that serves cursor\n// paging answers with `nextCursor` and is paged by keyset from then on; one\n// that doesn't answers without it and is paged by `page` (offset) until\n// `total` rows are loaded. Pages live under `[resource, \"infinite\", params]`,\n// so writes on the resource (which invalidate `[resource]`) refetch them,\n// and pending optimistic and undoable writes are patched into every page.\n// ---------------------------------------------------------------------------\n\nexport interface UseInfiniteResourceOptions {\n defaultSort?: SortParam;\n /** Re-synced when its value changes, like `useResource`'s. */\n defaultFilters?: FilterParam[];\n /** Rows per page. Default 25. */\n perPage?: number;\n /** The search to start with. Default \"\". */\n defaultSearch?: string;\n /** Supabase select/join syntax. Must include the sort and key columns. */\n select?: string;\n /** Fields to search across when `setSearch` is called. */\n searchFields?: string[];\n /** Disable fetching. */\n enabled?: boolean;\n /** Refetch the loaded pages when the provider reports a change. See `UseResourceOptions.live`. */\n live?: boolean | LiveOptions;\n}\n\n/** Where one page starts: its offset page, and its cursor (`null` for the first). */\nexport interface InfinitePageParam {\n page: number;\n cursor: string | null;\n}\n\nexport interface UseInfiniteResourceReturn<T> {\n /** The TanStack infinite query. */\n query: UseInfiniteQueryResult<InfiniteData<ListResult<T>, InfinitePageParam>>;\n /** Every loaded row, in order. */\n rows: T[];\n /** Rows matching the filters and search, from the first page's count. */\n total: number | undefined;\n /** Loads the next page. Does nothing while one is loading or none is left. */\n fetchNextPage: () => void;\n hasNextPage: boolean;\n isFetchingNextPage: boolean;\n /** The params every page is asked with (without its page and cursor). */\n params: Omit<ListParams, \"pagination\"> & { perPage: number };\n setSort: (sort: SortParam | undefined) => void;\n setFilters: (filters: FilterParam[]) => void;\n setSearch: (query: string) => void;\n}\n\nfunction filterKey(filters: FilterParam[] | undefined): string {\n return JSON.stringify(\n (filters ?? []).map((f) =>\n Object.fromEntries(Object.entries(f).sort(([a], [b]) => a.localeCompare(b))),\n ),\n );\n}\n\nexport function useInfiniteResource<T extends BaseRecord = BaseRecord>(\n resource: string,\n options: UseInfiniteResourceOptions = {},\n): UseInfiniteResourceReturn<T> {\n const {\n defaultSort,\n defaultFilters,\n perPage = 25,\n defaultSearch = \"\",\n select,\n searchFields,\n enabled = true,\n live,\n } = options;\n const provider = useDataProvider();\n\n const [sort, setSort] = useState<SortParam | undefined>(defaultSort);\n const [filters, setFiltersRaw] = useState<FilterParam[]>(defaultFilters ?? []);\n const [search, setSearch] = useState(defaultSearch);\n\n // Follow `defaultFilters` by value, during render (see useResource).\n const defaultKey = filterKey(defaultFilters);\n const [syncedKey, setSyncedKey] = useState(defaultKey);\n if (defaultKey !== syncedKey) {\n setSyncedKey(defaultKey);\n setFiltersRaw(defaultFilters ?? []);\n }\n\n const setFilters = useCallback((next: FilterParam[]) => setFiltersRaw(next), []);\n\n const params = useMemo(() => {\n const p: Omit<ListParams, \"pagination\"> & { perPage: number } = {\n perPage,\n sort,\n filters: filters.length > 0 ? filters : undefined,\n select,\n };\n if (search && searchFields && searchFields.length > 0) {\n p.search = { query: search, fields: searchFields };\n }\n return p;\n }, [perPage, sort, filters, select, search, searchFields]);\n\n const query = useInfiniteQuery<\n ListResult<T>,\n Error,\n InfiniteData<ListResult<T>, InfinitePageParam>,\n readonly unknown[],\n InfinitePageParam\n >({\n queryKey: [resource, \"infinite\", params],\n initialPageParam: { page: 1, cursor: null },\n queryFn: ({ pageParam }) => {\n const { perPage: size, ...rest } = params;\n return provider.getList<T>(resource, {\n ...rest,\n pagination: { page: pageParam.page, perPage: size, cursor: pageParam.cursor },\n });\n },\n getNextPageParam: (last, pages, lastParam) => {\n if (last.nextCursor !== undefined) {\n // Cursor mode: the provider says whether there is more.\n return last.nextCursor ? { page: lastParam.page + 1, cursor: last.nextCursor } : undefined;\n }\n // Offset mode: more while fewer than `total` rows are loaded and the\n // last page was full.\n const loaded = pages.reduce((n, p) => n + p.data.length, 0);\n const total = pages[0]?.total ?? 0;\n if (last.data.length < params.perPage || loaded >= total) return undefined;\n return { page: lastParam.page + 1, cursor: null };\n },\n enabled,\n });\n\n useLiveInvalidation(resource, live, enabled, { filters: params.filters }, [resource]);\n\n // A row can move between pages while the user scrolls (its sort value\n // changed, or rows before it were added): show it once, where it first appears.\n const rows = useMemo(() => {\n const seen = new Set<string>();\n const out: T[] = [];\n for (const page of query.data?.pages ?? []) {\n for (const row of page.data) {\n const id = row?.id;\n if (id != null) {\n const key = String(id);\n if (seen.has(key)) continue;\n seen.add(key);\n }\n out.push(row);\n }\n }\n return out;\n }, [query.data]);\n const firstTotal = query.data?.pages[0]?.total;\n const total = firstTotal === undefined || firstTotal < 0 ? undefined : firstTotal;\n\n const { fetchNextPage: fetchNext, hasNextPage, isFetchingNextPage } = query;\n const fetchNextPage = useCallback(() => {\n if (!hasNextPage || isFetchingNextPage) return;\n void fetchNext();\n }, [fetchNext, hasNextPage, isFetchingNextPage]);\n\n return {\n query,\n rows,\n total,\n fetchNextPage,\n hasNextPage: !!hasNextPage,\n isFetchingNextPage,\n params,\n setSort,\n setFilters,\n setSearch,\n };\n}\n"]}
@@ -1,9 +1,9 @@
1
+ import { useOne } from './chunk-ZKMAU6NN.js';
1
2
  import { useUnsavedChangesGuard } from './chunk-XQO4OH2N.js';
2
3
  import { isDataWriteError } from './chunk-5NHR6ZXS.js';
3
4
  import { titleCase } from './chunk-RYEXWQFD.js';
4
- import { useDelayedWrites, cachedRecord, executeWrite, invalidateResource } from './chunk-QH4P5MI2.js';
5
+ import { sameValue, cachedRecord, executeWrite, invalidateResource } from './chunk-OBUBP4BY.js';
5
6
  import { relatedKeys } from './chunk-7NUL32W7.js';
6
- import { useOne } from './chunk-ZK5C3OOT.js';
7
7
  import { useDataProvider, useStorage } from './chunk-GGSXSI6Z.js';
8
8
  import { useMemo, useEffect, useState, useRef, useCallback } from 'react';
9
9
  import { useForm } from 'react-hook-form';
@@ -11,6 +11,13 @@ import { zodResolver } from '@hookform/resolvers/zod';
11
11
  import { z } from 'zod';
12
12
  import { useQueryClient } from '@tanstack/react-query';
13
13
 
14
+ function structuredCloneSafe(value) {
15
+ try {
16
+ return typeof structuredClone === "function" ? structuredClone(value) : value;
17
+ } catch {
18
+ return value;
19
+ }
20
+ }
14
21
  function inferFieldType(value) {
15
22
  if (typeof value === "boolean") return "switch";
16
23
  if (typeof value === "number") return "number";
@@ -241,7 +248,8 @@ function useResourceForm(options) {
241
248
  warnWhenUnsaved = false,
242
249
  mutationMode = "pessimistic",
243
250
  undoDelayMs,
244
- onUndoable
251
+ onUndoable,
252
+ visible = true
245
253
  } = options;
246
254
  const autosaveEnabled = !!autosave;
247
255
  const debounceMs = typeof autosave === "object" ? autosave.debounceMs ?? 1e3 : 1e3;
@@ -354,7 +362,15 @@ function useResourceForm(options) {
354
362
  savedCleanRef.current = true;
355
363
  setSavedClean(true);
356
364
  }, []);
357
- const delayed = useDelayedWrites();
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;
358
374
  const showError = useCallback(
359
375
  (error) => {
360
376
  const fieldErrors = isDataWriteError(error) ? error.fields : void 0;
@@ -378,20 +394,24 @@ function useResourceForm(options) {
378
394
  [form, resolvedFields]
379
395
  );
380
396
  const reportError = useCallback(
381
- (error) => {
382
- savedCleanRef.current = false;
383
- setSavedClean(false);
384
- showError(error);
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
+ }
385
404
  if (onError) {
386
405
  onError(error);
387
406
  } else {
388
407
  console.error(`[useResourceForm] ${action} failed for "${resource}":`, error);
389
408
  }
390
409
  },
391
- [showError, onError, action, resource]
410
+ [form, showError, onError, action, resource]
392
411
  );
393
412
  const handleSubmit = useCallback(
394
413
  async (formData) => {
414
+ const shown = structuredCloneSafe(form.getValues());
395
415
  try {
396
416
  let recordId;
397
417
  if (action === "edit") {
@@ -413,9 +433,9 @@ function useResourceForm(options) {
413
433
  mode: mutationMode,
414
434
  undoDelayMs,
415
435
  onUndoable,
416
- delayed,
417
- send: async () => {
418
- const res = action === "create" ? await provider.create(resource, { data: formData }) : await provider.update(resource, { id: recordId, data: formData });
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 });
419
439
  return { record: res.data, audit: res.audit };
420
440
  },
421
441
  committed: async (result) => {
@@ -439,7 +459,7 @@ function useResourceForm(options) {
439
459
  );
440
460
  await Promise.all(related.map((queryKey) => queryClient.invalidateQueries({ queryKey: [...queryKey] })));
441
461
  },
442
- failedLater: reportError
462
+ failedLater: (error) => reportError(error, shown)
443
463
  });
444
464
  markSaved();
445
465
  if (outcome.status === "committed") {
@@ -453,7 +473,7 @@ function useResourceForm(options) {
453
473
  reportError(err instanceof Error ? err : new Error(String(err)));
454
474
  }
455
475
  },
456
- [action, provider, resource, id, effectiveData, formSource, queryClient, onSuccess, autosaveEnabled, storage, invalidates, mutationMode, undoDelayMs, onUndoable, delayed, reportError, markSaved]
476
+ [form, action, provider, resource, id, effectiveData, formSource, queryClient, onSuccess, autosaveEnabled, storage, invalidates, mutationMode, undoDelayMs, onUndoable, reportError, markSaved]
457
477
  );
458
478
  const onSubmit = useCallback(
459
479
  (e) => form.handleSubmit(handleSubmit)(e),
@@ -473,5 +493,5 @@ function useResourceForm(options) {
473
493
  }
474
494
 
475
495
  export { buildArrayItemDefaults, useResourceForm };
476
- //# sourceMappingURL=chunk-IZT2O57C.js.map
477
- //# sourceMappingURL=chunk-IZT2O57C.js.map
496
+ //# sourceMappingURL=chunk-CKGGE6WA.js.map
497
+ //# sourceMappingURL=chunk-CKGGE6WA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/use-resource-form.ts"],"names":[],"mappings":"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","file":"chunk-CKGGE6WA.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"]}
@@ -1,5 +1,6 @@
1
- import { invalidateResource, useDelayedWrites, executeWrite, cachedRecord } from './chunk-QH4P5MI2.js';
1
+ import { invalidateResource, executeWrite, cachedRecord } from './chunk-OBUBP4BY.js';
2
2
  import { relatedKeys } from './chunk-7NUL32W7.js';
3
+ import { useLiveInvalidation } from './chunk-KXVHDK3Q.js';
3
4
  import { useDataProvider } from './chunk-GGSXSI6Z.js';
4
5
  import { useState, useCallback, useMemo } from 'react';
5
6
  import { useQueryClient, useQuery, keepPreviousData, useMutation } from '@tanstack/react-query';
@@ -28,6 +29,7 @@ function useResource(resource, options = {}) {
28
29
  enabled = true,
29
30
  invalidates,
30
31
  keepPreviousData: keepPreviousData$1 = false,
32
+ live,
31
33
  onCreateSuccess,
32
34
  onUpdateSuccess,
33
35
  onDeleteSuccess,
@@ -88,6 +90,7 @@ function useResource(resource, options = {}) {
88
90
  enabled,
89
91
  placeholderData: keepPreviousData$1 ? keepPreviousData : void 0
90
92
  });
93
+ useLiveInvalidation(resource, live, enabled, { filters: params.filters }, [resource]);
91
94
  const invalidateAfter = useCallback(
92
95
  (event) => {
93
96
  void invalidateResource(queryClient, resource);
@@ -104,7 +107,6 @@ function useResource(resource, options = {}) {
104
107
  [resource]
105
108
  );
106
109
  const reportError = onError ?? defaultOnError;
107
- const delayed = useDelayedWrites();
108
110
  const modeArgs = {
109
111
  queryClient,
110
112
  provider,
@@ -112,7 +114,6 @@ function useResource(resource, options = {}) {
112
114
  mode: mutationMode,
113
115
  undoDelayMs,
114
116
  onUndoable,
115
- delayed,
116
117
  failedLater: reportError
117
118
  };
118
119
  const createMutation = useMutation({
@@ -121,8 +122,8 @@ function useResource(resource, options = {}) {
121
122
  ...modeArgs,
122
123
  kind: "create",
123
124
  data,
124
- send: async () => {
125
- const result = await provider.create(resource, { data });
125
+ send: async ({ keepalive }) => {
126
+ const result = await provider.create(resource, { data, ...keepalive ? { keepalive } : {} });
126
127
  return { record: result.data, audit: result.audit };
127
128
  },
128
129
  committed: (record) => {
@@ -142,8 +143,12 @@ function useResource(resource, options = {}) {
142
143
  kind: "update",
143
144
  id,
144
145
  data,
145
- send: async () => {
146
- const result = await provider.update(resource, { id, data });
146
+ send: async (ctx) => {
147
+ const result = await provider.update(resource, {
148
+ id,
149
+ data: ctx.data ?? data,
150
+ ...ctx.keepalive ? { keepalive: true } : {}
151
+ });
147
152
  return { record: result.data, audit: result.audit };
148
153
  },
149
154
  committed: (record) => {
@@ -161,8 +166,8 @@ function useResource(resource, options = {}) {
161
166
  ...modeArgs,
162
167
  kind: "delete",
163
168
  id,
164
- send: async () => {
165
- const result = await provider.deleteOne(resource, { id });
169
+ send: async ({ keepalive }) => {
170
+ const result = await provider.deleteOne(resource, { id, ...keepalive ? { keepalive } : {} });
166
171
  return { record: void 0, audit: result?.audit };
167
172
  },
168
173
  committed: () => {
@@ -205,5 +210,5 @@ function useResource(resource, options = {}) {
205
210
  }
206
211
 
207
212
  export { useResource };
208
- //# sourceMappingURL=chunk-DVW5GXWZ.js.map
209
- //# sourceMappingURL=chunk-DVW5GXWZ.js.map
213
+ //# sourceMappingURL=chunk-KV6KH3L2.js.map
214
+ //# sourceMappingURL=chunk-KV6KH3L2.js.map