@cosmicdrift/kumiko-headless 0.328.0 → 0.329.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 (108) hide show
  1. package/dist/apex/css.d.ts +3 -0
  2. package/{src/apex/css.ts → dist/apex/css.js} +11 -24
  3. package/dist/apex/index.d.ts +229 -0
  4. package/dist/apex/index.js +360 -0
  5. package/dist/apex/lightbox.d.ts +4 -0
  6. package/{src/apex/lightbox.ts → dist/apex/lightbox.js} +0 -4
  7. package/dist/contracts/asset.d.ts +15 -0
  8. package/dist/contracts/asset.js +17 -0
  9. package/dist/contracts/index.d.ts +3 -0
  10. package/dist/contracts/index.js +1 -0
  11. package/dist/contracts/locale.d.ts +7 -0
  12. package/dist/contracts/locale.js +11 -0
  13. package/dist/contracts/primitives.d.ts +93 -0
  14. package/dist/contracts/primitives.js +21 -0
  15. package/dist/dispatcher/index.d.ts +2 -0
  16. package/dist/dispatcher/index.js +1 -0
  17. package/dist/dispatcher/types.d.ts +86 -0
  18. package/dist/dispatcher/types.js +34 -0
  19. package/dist/form/form-controller.d.ts +2 -0
  20. package/dist/form/form-controller.js +404 -0
  21. package/dist/form/index.d.ts +3 -0
  22. package/dist/form/index.js +2 -0
  23. package/dist/form/types.d.ts +71 -0
  24. package/dist/form/types.js +1 -0
  25. package/dist/form/zod-bridge.d.ts +5 -0
  26. package/{src/form/zod-bridge.ts → dist/form/zod-bridge.js} +52 -58
  27. package/dist/format/escape.d.ts +5 -0
  28. package/{src/format/escape.ts → dist/format/escape.js} +43 -47
  29. package/dist/format/html-template.d.ts +9 -0
  30. package/dist/format/html-template.js +39 -0
  31. package/dist/format/index.d.ts +10 -0
  32. package/dist/format/index.js +192 -0
  33. package/dist/format/money.d.ts +1 -0
  34. package/{src/format/money.ts → dist/format/money.js} +6 -5
  35. package/dist/index.d.ts +14 -0
  36. package/dist/index.js +9 -0
  37. package/dist/locale-routing/index.d.ts +39 -0
  38. package/dist/locale-routing/index.js +143 -0
  39. package/dist/nav/index.d.ts +2 -0
  40. package/dist/nav/index.js +1 -0
  41. package/dist/nav/resolve.d.ts +2 -0
  42. package/dist/nav/resolve.js +58 -0
  43. package/dist/nav/types.d.ts +26 -0
  44. package/dist/nav/types.js +1 -0
  45. package/dist/store/create-store.d.ts +2 -0
  46. package/{src/store/create-store.ts → dist/store/create-store.js} +21 -23
  47. package/dist/store/equality.d.ts +1 -0
  48. package/{src/store/equality.ts → dist/store/equality.js} +19 -17
  49. package/dist/store/index.d.ts +3 -0
  50. package/dist/store/index.js +2 -0
  51. package/dist/store/types.d.ts +7 -0
  52. package/dist/store/types.js +10 -0
  53. package/dist/view-model/edit.d.ts +10 -0
  54. package/dist/view-model/edit.js +392 -0
  55. package/dist/view-model/embedded-list.d.ts +27 -0
  56. package/dist/view-model/embedded-list.js +63 -0
  57. package/dist/view-model/index.d.ts +7 -0
  58. package/dist/view-model/index.js +3 -0
  59. package/dist/view-model/list.d.ts +16 -0
  60. package/dist/view-model/list.js +201 -0
  61. package/dist/view-model/types.d.ts +300 -0
  62. package/dist/view-model/types.js +1 -0
  63. package/package.json +31 -12
  64. package/src/apex/__tests__/lightbox.test.ts +0 -20
  65. package/src/apex/__tests__/page-head.test.ts +0 -63
  66. package/src/apex/__tests__/render.test.ts +0 -401
  67. package/src/apex/index.ts +0 -667
  68. package/src/contracts/__tests__/contracts.test.ts +0 -123
  69. package/src/contracts/__tests__/field-issue-compat.test.ts +0 -19
  70. package/src/contracts/asset.ts +0 -48
  71. package/src/contracts/index.ts +0 -23
  72. package/src/contracts/locale.ts +0 -33
  73. package/src/contracts/primitives.ts +0 -158
  74. package/src/dispatcher/__tests__/contract.test.ts +0 -163
  75. package/src/dispatcher/index.ts +0 -16
  76. package/src/dispatcher/types.ts +0 -217
  77. package/src/form/__tests__/conditional-fields.test.ts +0 -177
  78. package/src/form/__tests__/form-controller.test.ts +0 -231
  79. package/src/form/__tests__/submit.test.ts +0 -575
  80. package/src/form/__tests__/validation.test.ts +0 -274
  81. package/src/form/form-controller.ts +0 -458
  82. package/src/form/index.ts +0 -15
  83. package/src/form/types.ts +0 -297
  84. package/src/format/__tests__/escape.test.ts +0 -79
  85. package/src/format/__tests__/format.test.ts +0 -285
  86. package/src/format/__tests__/html-template.test.ts +0 -47
  87. package/src/format/html-template.ts +0 -49
  88. package/src/format/index.ts +0 -223
  89. package/src/index.ts +0 -118
  90. package/src/locale-routing/__tests__/locale-routing.test.ts +0 -276
  91. package/src/locale-routing/index.ts +0 -218
  92. package/src/nav/__tests__/resolve.test.ts +0 -279
  93. package/src/nav/index.ts +0 -8
  94. package/src/nav/resolve.ts +0 -60
  95. package/src/nav/types.ts +0 -70
  96. package/src/store/__tests__/create-store.test.ts +0 -139
  97. package/src/store/__tests__/equality.test.ts +0 -66
  98. package/src/store/index.ts +0 -3
  99. package/src/store/types.ts +0 -27
  100. package/src/view-model/__tests__/edit.test.ts +0 -1072
  101. package/src/view-model/__tests__/embedded-list.test.ts +0 -96
  102. package/src/view-model/__tests__/field-labels.test.ts +0 -62
  103. package/src/view-model/__tests__/list.test.ts +0 -439
  104. package/src/view-model/edit.ts +0 -645
  105. package/src/view-model/embedded-list.ts +0 -89
  106. package/src/view-model/index.ts +0 -43
  107. package/src/view-model/list.ts +0 -280
  108. package/src/view-model/types.ts +0 -400
@@ -1,458 +0,0 @@
1
- import type { FieldIssue } from "../dispatcher";
2
- import { createStore } from "../store";
3
- import type {
4
- FieldConditions,
5
- FieldConditionValue,
6
- FieldState,
7
- FormController,
8
- FormControllerOptions,
9
- FormSnapshot,
10
- FormValues,
11
- SubmitResult,
12
- } from "./types";
13
- import { groupIssuesByPath, zodErrorToFieldIssues } from "./zod-bridge";
14
-
15
- // Paths without the prefix (hook-level errors, "(root)") pass through unchanged.
16
- function stripServerFieldPathPrefix(
17
- issues: readonly FieldIssue[],
18
- prefix: string,
19
- ): readonly FieldIssue[] {
20
- return issues.map((issue) =>
21
- issue.path.startsWith(prefix) ? { ...issue, path: issue.path.slice(prefix.length) } : issue,
22
- );
23
- }
24
-
25
- // Resolve one condition-value (boolean | predicate) against current values + ctx.
26
- // `undefined` means "condition not declared"; the caller substitutes the
27
- // field-default (visible:true, readonly:false, required:false).
28
- function evalCondition<TValues extends FormValues, TCtx>(
29
- condition: FieldConditionValue<TValues, TCtx> | undefined,
30
- fallback: boolean,
31
- values: TValues,
32
- ctx: TCtx,
33
- ): boolean {
34
- if (condition === undefined) return fallback;
35
- if (typeof condition === "boolean") return condition;
36
- return condition(values, ctx);
37
- }
38
-
39
- function computeFieldStates<TValues extends FormValues, TCtx>(
40
- rules: Readonly<Record<string, FieldConditions<TValues, TCtx>>> | undefined,
41
- values: TValues,
42
- ctx: TCtx,
43
- ): Record<string, FieldState> {
44
- if (!rules) return {};
45
- const out: Record<string, FieldState> = {};
46
- for (const [fieldKey, rule] of Object.entries(rules)) {
47
- out[fieldKey] = {
48
- visible: evalCondition(rule.visible, true, values, ctx),
49
- readonly: evalCondition(rule.readonly, false, values, ctx),
50
- required: evalCondition(rule.required, false, values, ctx),
51
- };
52
- }
53
- return out;
54
- }
55
-
56
- // Reference equality only. For primitive-valued fields this is exact; for
57
- // object/array fields it treats "same reference" as "unchanged" — callers
58
- // who mutate in place instead of swapping references (which is bad form
59
- // anyway, but common in ad-hoc tests) would miss a change. Kumiko's
60
- // form-controller contract is that mutators build new references.
61
- // Object.is is chosen over === so NaN !== NaN doesn't mark a field dirty
62
- // after you re-type the same number back in.
63
- function valuesDiff<TValues extends FormValues>(
64
- current: TValues,
65
- initial: TValues,
66
- ): Partial<TValues> {
67
- const out: Partial<TValues> = {};
68
- // Iterate over BOTH sides — a field that existed on initial but was
69
- // deleted from current (via `setValues({ foo: undefined })`) still
70
- // counts as a change.
71
- // FormValues<T> erlaubt kein keyof-T-Iteration (T ist generic). Cast
72
- // zu Record<string, unknown> für dynamic-key-Inspection.
73
- const cur = current as Record<string, unknown>; // @cast-boundary form-values
74
- const ini = initial as Record<string, unknown>; // @cast-boundary form-values
75
- const o = out as Record<string, unknown>; // @cast-boundary form-values
76
- const keys = new Set<string>([...Object.keys(cur), ...Object.keys(ini)]);
77
- for (const key of keys) {
78
- const a = cur[key];
79
- const b = ini[key];
80
- if (!Object.is(a, b)) {
81
- o[key] = a;
82
- }
83
- }
84
- return out;
85
- }
86
-
87
- // buildInitialValues seeds untouched optional fields with "" for controlled
88
- // inputs; a `.optional()` server schema accepts undefined but not "".
89
- // String-only: number/money seed to 0 and boolean seeds to false, and an
90
- // untouched optional field of those types still submits that seed instead
91
- // of being omitted — no validation error surfaces it, unlike the "" case.
92
- // Fixing that needs real per-field seed-tracking (which value was actually
93
- // user-set vs. defaulted), not just a wider strip predicate here.
94
- function stripUntouchedEmptyStrings<TValues extends FormValues>(
95
- values: TValues,
96
- initial: TValues,
97
- forceIncludeEmpty: ReadonlySet<string>,
98
- ): TValues {
99
- const v = values as Record<string, unknown>; // @cast-boundary form-values
100
- const ini = initial as Record<string, unknown>; // @cast-boundary form-values
101
- let out: Record<string, unknown> | undefined;
102
- for (const key of Object.keys(v)) {
103
- // Value-based seed strip, except keys previously submitted as "" after a
104
- // real clear — those must survive post-rebase submits (#1858).
105
- if (!forceIncludeEmpty.has(key) && v[key] === "" && ini[key] === "") {
106
- out ??= { ...v };
107
- delete out[key];
108
- }
109
- }
110
- return (out ?? v) as TValues;
111
- }
112
-
113
- // Shallow-freeze so accidental mutations on the snapshot (e.g. a test
114
- // writing `snapshot.values.title = "x"`) throw in strict mode instead of
115
- // silently diverging from the controller's internal state. Deep-freeze
116
- // would be safer but expensive — keeping it shallow matches React's own
117
- // immutability expectations and leaves room for sub-controllers to manage
118
- // their own sub-trees without double-frozen parents blocking them.
119
- function freezeSnapshot<TValues extends FormValues>(
120
- snapshot: FormSnapshot<TValues>,
121
- ): FormSnapshot<TValues> {
122
- return Object.freeze(snapshot);
123
- }
124
-
125
- export function createFormController<TValues extends FormValues, TCtx = unknown>(
126
- options: FormControllerOptions<TValues, TCtx>,
127
- ): FormController<TValues> {
128
- // Shallow copy so mutating the input after creation doesn't bleed into
129
- // the controller's internal state. `initial` stays fixed across the
130
- // controller's lifetime unless rebase() replaces it.
131
- let values: TValues = { ...options.initial };
132
- let initial: TValues = { ...options.initial };
133
- let errors: Readonly<Record<string, readonly FieldIssue[]>> = Object.freeze({});
134
- // Keys last submitted as "" (intentional clear). Survives rebase so a later
135
- // submit of other fields does not drop them (#1858).
136
- const forceIncludeEmpty = new Set<string>();
137
- // ctx is cast-held as TCtx; `undefined` is valid when callers don't
138
- // declare conditional predicates that depend on it.
139
- let ctx: TCtx = (options.ctx as TCtx) ?? (undefined as TCtx);
140
-
141
- // Snapshot lives in a Store — same reference across getSnapshot() calls
142
- // until a mutator invalidates it via setState. React's useSyncExternalStore
143
- // compares snapshot identity to decide on a re-render; the Store holds the
144
- // identity stable until invalidate() swaps it for a fresh build.
145
- const snapshotStore = createStore<FormSnapshot<TValues>>(buildSnapshot());
146
-
147
- // In-flight submit tracker. When a submit() is pending, a second call
148
- // awaits the same promise instead of firing a parallel write — avoids
149
- // double-submission on double-click and keeps the rebase semantics
150
- // coherent (rebasing twice in quick succession would mis-align the
151
- // baseline with what the server actually saw).
152
- let submitInFlight: Promise<unknown> | null = null;
153
-
154
- function buildSnapshot(): FormSnapshot<TValues> {
155
- const changes = valuesDiff(values, initial);
156
- const isDirty = Object.keys(changes).length > 0;
157
- const fields = Object.freeze(computeFieldStates(options.fields, values, ctx));
158
- return freezeSnapshot({
159
- values,
160
- initial,
161
- changes,
162
- isDirty,
163
- isUnchanged: !isDirty,
164
- errors,
165
- fields,
166
- });
167
- }
168
-
169
- function invalidate() {
170
- snapshotStore.setState(buildSnapshot());
171
- }
172
-
173
- // Local shared implementations so submit() can call validate/rebase
174
- // without the this-in-object-literal dance. Both also expose themselves
175
- // as methods on the returned controller.
176
- // Clears/replaces errors for a scoped run without touching fields outside
177
- // the scope — a step-2 validate() that comes back clean must not wipe
178
- // step-1's still-unresolved errors from an earlier submit() (or vice
179
- // versa when step-2 newly has issues).
180
- function replaceScopedErrors(
181
- scopeSet: Set<string> | undefined,
182
- grouped: Readonly<Record<string, readonly FieldIssue[]>>,
183
- ): void {
184
- if (scopeSet === undefined) {
185
- errors = Object.freeze(grouped);
186
- // skip: unscoped replace — wholesale overwrite, nothing left to merge
187
- return;
188
- }
189
- const merged: Record<string, readonly FieldIssue[]> = {};
190
- for (const [path, issues] of Object.entries(errors)) {
191
- if (!scopeSet.has(path)) merged[path] = issues;
192
- }
193
- Object.assign(merged, grouped);
194
- errors = Object.freeze(merged);
195
- }
196
-
197
- function runValidate(
198
- scope?: readonly string[],
199
- opts?: { readonly includeRoot?: boolean },
200
- ): boolean {
201
- // See the validate() doc comment in types.ts for the scope contract.
202
- // Normalize nested scope entries (`address.city` → `address`) so wizard
203
- // callers that pass dotted paths still match issue root segments (#1898).
204
- const scopeSet =
205
- scope === undefined ? undefined : new Set(scope.map((s) => s.split(".")[0] ?? s));
206
- const includeRoot = opts?.includeRoot === true;
207
- if (!options.schema) {
208
- if (Object.keys(errors).length > 0) {
209
- replaceScopedErrors(scopeSet, {});
210
- invalidate();
211
- }
212
- return true;
213
- }
214
- const fieldStates = computeFieldStates(options.fields, values, ctx);
215
- const parsed = options.schema.safeParse(values);
216
- if (parsed.success) {
217
- if (Object.keys(errors).length > 0) {
218
- replaceScopedErrors(scopeSet, {});
219
- invalidate();
220
- }
221
- return true;
222
- }
223
- const hiddenFields = new Set<string>();
224
- for (const [fieldKey, state] of Object.entries(fieldStates)) {
225
- if (!state.visible) hiddenFields.add(fieldKey);
226
- }
227
- const allIssues = zodErrorToFieldIssues(parsed.error);
228
- const relevantIssues = allIssues.filter((issue) => {
229
- const rootField = issue.path.split(".")[0] ?? "";
230
- if (hiddenFields.has(rootField)) return false;
231
- // Root-level .refine() issues use path "(root)". Wizard step validate()
232
- // still drops them (#1885); submit() passes includeRoot so RenderEdit
233
- // fields={…} cannot silently skip cross-field rules (#1907).
234
- const isRootIssue = issue.path === "(root)";
235
- if (scopeSet && !(includeRoot && isRootIssue) && !scopeSet.has(rootField)) {
236
- return false;
237
- }
238
- return true;
239
- });
240
- if (relevantIssues.length === 0) {
241
- if (Object.keys(errors).length > 0) {
242
- replaceScopedErrors(scopeSet, {});
243
- invalidate();
244
- }
245
- return true;
246
- }
247
- replaceScopedErrors(scopeSet, groupIssuesByPath(relevantIssues));
248
- invalidate();
249
- return false;
250
- }
251
-
252
- function runRebase(): void {
253
- initial = { ...values };
254
- if (Object.keys(errors).length > 0) errors = Object.freeze({});
255
- invalidate();
256
- }
257
-
258
- // Stale-submit-safe rebase. Difference to runRebase: the baseline is
259
- // taken from the values that were actually SENT to the server, not
260
- // from `values` at the moment the server replied. If the user typed
261
- // into a field while the submit was in flight, those edits stay as
262
- // dirty changes after the call — they never made it to the server, so
263
- // pretending they did (via `initial = { ...values }`) would mask unsaved
264
- // input. The race shows up as "user edits a field during a 2s save, sees
265
- // it as saved, closes the tab — the edit is lost". This path keeps
266
- // `values` untouched; only `initial` shifts to the submitted snapshot.
267
- function runRebaseToSnapshot(snapped: TValues): void {
268
- initial = { ...snapped };
269
- if (Object.keys(errors).length > 0) errors = Object.freeze({});
270
- invalidate();
271
- }
272
-
273
- return {
274
- getSnapshot: snapshotStore.getSnapshot,
275
- subscribe: snapshotStore.subscribe,
276
- setField(key, value) {
277
- // skip: value unchanged, avoid notify/re-render on identical set
278
- // avoids a notify + re-render for "setField with same value" which
279
- // happens a lot in controlled inputs on every keystroke of an
280
- // untouched field.
281
- if (Object.is(values[key], value)) return;
282
- values = { ...values, [key]: value };
283
- invalidate();
284
- },
285
- setValues(partial) {
286
- // skip: partial matches current values, avoid no-op notify
287
- // partial that matches current values shouldn't fire listeners.
288
- let changed = false;
289
- const v = values as Record<string, unknown>; // @cast-boundary form-values
290
- const p = partial as Record<string, unknown>; // @cast-boundary form-values
291
- for (const k of Object.keys(p)) {
292
- if (!Object.is(v[k], p[k])) {
293
- changed = true;
294
- break;
295
- }
296
- }
297
- // skip: no key in partial actually changed, avoid no-op notify
298
- if (!changed) return;
299
- values = { ...values, ...partial };
300
- invalidate();
301
- },
302
- clearErrors(path) {
303
- if (path === undefined) {
304
- // skip: no errors present, avoid no-op notify
305
- if (Object.keys(errors).length === 0) return;
306
- errors = Object.freeze({});
307
- } else {
308
- // skip: path has no error entry, avoid no-op notify
309
- if (!(path in errors)) return;
310
- const next: Record<string, readonly FieldIssue[]> = { ...errors };
311
- delete next[path];
312
- errors = Object.freeze(next);
313
- }
314
- invalidate();
315
- },
316
- setErrors(nextErrors) {
317
- errors = Object.freeze({ ...nextErrors });
318
- invalidate();
319
- },
320
- validate: runValidate,
321
- reset() {
322
- const alreadyClean = !snapshotStore.getSnapshot().isDirty && Object.keys(errors).length === 0;
323
- // skip: already at baseline with no errors, no-op reset
324
- if (alreadyClean) return;
325
- values = { ...initial };
326
- errors = Object.freeze({});
327
- forceIncludeEmpty.clear();
328
- invalidate();
329
- },
330
- rebase: runRebase,
331
- setCtx(nextCtx) {
332
- // Cast back to TCtx: setCtx's public signature takes unknown so
333
- // callers don't need to plumb generics. If conditions depend on ctx
334
- // and callers pass the wrong shape, the predicate throws at
335
- // evaluation time — which is the same fail mode as any other
336
- // callback contract violation.
337
- ctx = nextCtx as TCtx;
338
- invalidate();
339
- },
340
- async submit<TData = unknown>(): Promise<SubmitResult<TData>> {
341
- const submitCfg = options.submit;
342
- if (!submitCfg) {
343
- throw new Error(
344
- "createFormController: submit() called without a `submit` config. Configure `{ dispatcher, type }` on the controller or drive dispatching manually.",
345
- );
346
- }
347
-
348
- // Concurrent-submit guard: a double-click (two invocations before
349
- // the first network call returned) would otherwise fire two writes
350
- // AND rebase twice — compounding with the stale-submit race below.
351
- // Serialize: subsequent calls await the in-flight promise. Same
352
- // pattern the server-side event-dispatcher uses (inFlightTurns).
353
- if (submitInFlight) return submitInFlight as Promise<SubmitResult<TData>>;
354
-
355
- if (!runValidate(submitCfg.validateScope, { includeRoot: true })) {
356
- return { validationBlocked: true, isSuccess: false };
357
- }
358
-
359
- const payloadMode = submitCfg.payloadMode ?? "values";
360
- // Capture the whole snapshot AT submit-time. The user may keep
361
- // typing while the network call is in flight; on success we rebase
362
- // ONLY to the values that were actually sent, leaving any
363
- // subsequent edits as a fresh dirty delta. Without this, an edit
364
- // during the await would be swallowed into the new baseline and
365
- // the user would see it as "saved" despite the server never seeing
366
- // it. Same snapshot is fed to buildPayload — a custom transformer
367
- // (nested-write case) sees exactly what submit() sees.
368
- const submittedSnapshot = snapshotStore.getSnapshot();
369
- const submittedValues = submittedSnapshot.values;
370
-
371
- // buildPayload wins over payloadMode when both are set.
372
- let payload: unknown;
373
- if (submitCfg.buildPayload) {
374
- payload = submitCfg.buildPayload(submittedSnapshot);
375
- } else if (payloadMode === "changes") {
376
- if (submittedSnapshot.isUnchanged) {
377
- return {
378
- validationBlocked: false,
379
- isSuccess: true,
380
- isNoOp: true,
381
- data: submittedValues as unknown as TData,
382
- };
383
- }
384
- payload = submittedSnapshot.changes;
385
- } else if (submitCfg.stripEmptySeeds === false) {
386
- payload = submittedValues;
387
- } else {
388
- payload = stripUntouchedEmptyStrings(
389
- submittedValues,
390
- submittedSnapshot.initial,
391
- forceIncludeEmpty,
392
- );
393
- }
394
-
395
- // Only enforced here, right before the write actually happens — a
396
- // form that never reaches this point (blocked by validation, or a
397
- // clean payloadMode "changes" no-op) must not throw just because no
398
- // dispatcher was ever wired up.
399
- const dispatcher = submitCfg.dispatcher;
400
- if (!dispatcher) {
401
- // Return SubmitResult (not throw) so RenderEdit's banner path shows
402
- // the misconfiguration instead of an unhandled rejection on save.
403
- return {
404
- validationBlocked: false,
405
- isSuccess: false,
406
- isNoOp: false,
407
- error: {
408
- code: "misconfigured",
409
- httpStatus: 500,
410
- i18nKey: "errors.internal",
411
- message:
412
- "createFormController: submit() called without a dispatcher (dispatcher was absent when the form mounted). Pass `submit.dispatcher` explicitly, or mount a <DispatcherProvider> above this form.",
413
- },
414
- };
415
- }
416
-
417
- const runWrite = async (): Promise<SubmitResult<TData>> => {
418
- const result = await dispatcher.write<TData>(submitCfg.type, payload);
419
-
420
- if (result.isSuccess) {
421
- // Remember intentional empty clears so post-rebase submits keep them (#1858).
422
- if (payload && typeof payload === "object" && !Array.isArray(payload)) {
423
- for (const [k, v] of Object.entries(payload as Record<string, unknown>)) {
424
- if (v === "") forceIncludeEmpty.add(k);
425
- else forceIncludeEmpty.delete(k);
426
- }
427
- }
428
- // Rebase to the SNAPSHOT, not to the current values — see
429
- // runRebaseToSnapshot comment for why.
430
- runRebaseToSnapshot(submittedValues);
431
- return { validationBlocked: false, isSuccess: true, isNoOp: false, data: result.data };
432
- }
433
-
434
- const serverFields = result.error.details?.fields;
435
- if (serverFields && serverFields.length > 0) {
436
- const prefix = submitCfg.serverFieldPathPrefix;
437
- const mappedFields = prefix
438
- ? stripServerFieldPathPrefix(serverFields, prefix)
439
- : serverFields;
440
- errors = Object.freeze(groupIssuesByPath(mappedFields));
441
- invalidate();
442
- const error = prefix
443
- ? { ...result.error, details: { ...result.error.details, fields: mappedFields } }
444
- : result.error;
445
- return { validationBlocked: false, isSuccess: false, isNoOp: false, error };
446
- }
447
- return { validationBlocked: false, isSuccess: false, isNoOp: false, error: result.error };
448
- };
449
-
450
- submitInFlight = runWrite();
451
- try {
452
- return await (submitInFlight as Promise<SubmitResult<TData>>);
453
- } finally {
454
- submitInFlight = null;
455
- }
456
- },
457
- };
458
- }
package/src/form/index.ts DELETED
@@ -1,15 +0,0 @@
1
- export { createFormController } from "./form-controller";
2
- export type {
3
- FieldConditionPredicate,
4
- FieldConditions,
5
- FieldConditionValue,
6
- FieldState,
7
- FormController,
8
- FormControllerOptions,
9
- FormSnapshot,
10
- FormValues,
11
- SubmitConfig,
12
- SubmitPayloadMode,
13
- SubmitResult,
14
- } from "./types";
15
- export { groupIssuesByPath, I18N_KEY_PARAM, zodErrorToFieldIssues } from "./zod-bridge";