@chelone/core 0.6.0 → 0.9.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 (255) hide show
  1. package/dist/assistant/assistant-session.d.ts +6 -0
  2. package/dist/assistant/assistant-session.js +204 -181
  3. package/dist/assistant/echo-transport.js +26 -29
  4. package/dist/assistant/http-transport.d.ts +10 -0
  5. package/dist/assistant/http-transport.js +98 -0
  6. package/dist/assistant/types.d.ts +1 -7
  7. package/dist/auth/access-control.d.ts +13 -5
  8. package/dist/auth/access-control.js +25 -21
  9. package/dist/auth/auth-options.d.ts +23 -17
  10. package/dist/auth/auth-options.js +82 -48
  11. package/dist/auth/permission-matrix.d.ts +12 -0
  12. package/dist/auth/permission-matrix.js +67 -0
  13. package/dist/auth/types.d.ts +14 -4
  14. package/dist/block-editor/block-document.d.ts +64 -0
  15. package/dist/block-editor/block-document.js +147 -0
  16. package/dist/block-editor/data-blocks.d.ts +63 -0
  17. package/dist/block-editor/data-blocks.js +117 -0
  18. package/dist/chart/chart-data.js +46 -44
  19. package/dist/command-menu/hotkey.js +58 -47
  20. package/dist/command-menu/recents.d.ts +4 -0
  21. package/dist/command-menu/recents.js +12 -11
  22. package/dist/command-menu/registry.d.ts +9 -5
  23. package/dist/command-menu/registry.js +36 -46
  24. package/dist/controller/calendar-dates.d.ts +12 -0
  25. package/dist/controller/calendar-dates.js +94 -113
  26. package/dist/controller/calendar-events.js +93 -88
  27. package/dist/controller/client-list-controller.d.ts +2 -2
  28. package/dist/controller/client-list-controller.js +251 -254
  29. package/dist/controller/data-table-columns.d.ts +2 -0
  30. package/dist/controller/data-table-columns.js +50 -56
  31. package/dist/controller/deep-equal.d.ts +1 -0
  32. package/dist/controller/deep-equal.js +14 -0
  33. package/dist/controller/filter-toggle.js +36 -44
  34. package/dist/controller/in-place-editor.d.ts +43 -0
  35. package/dist/controller/in-place-editor.js +43 -0
  36. package/dist/controller/infinite-list-controller.js +219 -189
  37. package/dist/controller/inspector.d.ts +55 -0
  38. package/dist/controller/inspector.js +70 -0
  39. package/dist/controller/job-monitor.d.ts +10 -4
  40. package/dist/controller/job-monitor.js +46 -47
  41. package/dist/controller/kanban-board.js +45 -24
  42. package/dist/controller/list-controller.d.ts +8 -5
  43. package/dist/controller/list-controller.js +447 -400
  44. package/dist/controller/list-params.js +96 -87
  45. package/dist/controller/pivot-grid.d.ts +4 -4
  46. package/dist/controller/pivot-grid.js +89 -95
  47. package/dist/controller/prev-next.d.ts +2 -2
  48. package/dist/controller/prev-next.js +74 -54
  49. package/dist/controller/record-controllers.d.ts +14 -6
  50. package/dist/controller/record-controllers.js +288 -257
  51. package/dist/controller/record-diff.d.ts +21 -0
  52. package/dist/controller/record-diff.js +50 -0
  53. package/dist/controller/record-timeline.d.ts +5 -5
  54. package/dist/controller/record-timeline.js +42 -30
  55. package/dist/controller/record-tree.js +57 -47
  56. package/dist/controller/record-workflow.d.ts +38 -16
  57. package/dist/controller/record-workflow.js +85 -64
  58. package/dist/controller/reference-many-controller.d.ts +2 -0
  59. package/dist/controller/reference-many-controller.js +199 -192
  60. package/dist/controller/reference-one-controller.d.ts +1 -1
  61. package/dist/controller/reference-one-controller.js +63 -56
  62. package/dist/controller/saved-queries.js +104 -91
  63. package/dist/controller/translate-fallback.js +10 -6
  64. package/dist/csv/export.d.ts +4 -3
  65. package/dist/csv/export.js +43 -40
  66. package/dist/csv/import-controller.d.ts +4 -0
  67. package/dist/csv/import-controller.js +189 -169
  68. package/dist/csv/match-headers.js +29 -22
  69. package/dist/csv/parse-with.js +60 -54
  70. package/dist/csv/run-import.js +69 -72
  71. package/dist/csv/validate-rows.d.ts +1 -1
  72. package/dist/csv/validate-rows.js +65 -52
  73. package/dist/data/cache-ops.d.ts +5 -4
  74. package/dist/data/cache-ops.js +208 -188
  75. package/dist/data/combine-data-providers.js +25 -23
  76. package/dist/data/data-provider-log.d.ts +45 -0
  77. package/dist/data/data-provider-log.js +93 -0
  78. package/dist/data/errors.js +14 -10
  79. package/dist/data/fake-provider.d.ts +6 -3
  80. package/dist/data/fake-provider.js +135 -114
  81. package/dist/data/fetch-utils.js +61 -44
  82. package/dist/data/filter-branch.d.ts +5 -0
  83. package/dist/data/filter-branch.js +10 -0
  84. package/dist/data/get-many-aggregate.js +84 -76
  85. package/dist/data/http-error.js +17 -12
  86. package/dist/data/keys.js +50 -36
  87. package/dist/data/mutation-options.d.ts +14 -9
  88. package/dist/data/mutation-options.js +93 -109
  89. package/dist/data/query-options.js +73 -68
  90. package/dist/data/remove-empty.d.ts +3 -0
  91. package/dist/data/remove-empty.js +11 -0
  92. package/dist/data/undoable.d.ts +2 -2
  93. package/dist/data/undoable.js +190 -174
  94. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  95. package/dist/data/with-lifecycle-callbacks.js +66 -52
  96. package/dist/environment/application-update-store.js +58 -55
  97. package/dist/environment/offline-store.js +21 -21
  98. package/dist/form/array-binding.d.ts +30 -11
  99. package/dist/form/array-binding.js +184 -166
  100. package/dist/form/choice-ids.d.ts +7 -0
  101. package/dist/form/choice-ids.js +11 -0
  102. package/dist/form/choices-controller.d.ts +22 -2
  103. package/dist/form/choices-controller.js +315 -275
  104. package/dist/form/date-format.d.ts +38 -0
  105. package/dist/form/date-format.js +89 -0
  106. package/dist/form/dirty-values.d.ts +2 -2
  107. package/dist/form/dirty-values.js +22 -11
  108. package/dist/form/duration.d.ts +22 -0
  109. package/dist/form/duration.js +49 -0
  110. package/dist/form/edit-create-form.d.ts +12 -5
  111. package/dist/form/edit-create-form.js +243 -188
  112. package/dist/form/event-value.d.ts +6 -0
  113. package/dist/form/event-value.js +17 -0
  114. package/dist/form/field-error.d.ts +14 -0
  115. package/dist/form/field-error.js +28 -0
  116. package/dist/form/field-name.d.ts +20 -0
  117. package/dist/form/field-name.js +6 -0
  118. package/dist/form/filter-form-values.js +63 -49
  119. package/dist/form/form-controller.d.ts +9 -1
  120. package/dist/form/form-controller.js +165 -160
  121. package/dist/form/form-groups.d.ts +4 -3
  122. package/dist/form/form-groups.js +79 -79
  123. package/dist/form/form-handle.d.ts +175 -0
  124. package/dist/form/form-handle.js +68 -0
  125. package/dist/form/input-binding.d.ts +56 -31
  126. package/dist/form/input-binding.js +171 -118
  127. package/dist/form/input-values.d.ts +140 -0
  128. package/dist/form/input-values.js +206 -0
  129. package/dist/form/json-text.d.ts +11 -0
  130. package/dist/form/json-text.js +62 -37
  131. package/dist/form/save-pipeline.d.ts +6 -8
  132. package/dist/form/save-pipeline.js +91 -82
  133. package/dist/form/set-form-values.d.ts +6 -0
  134. package/dist/form/set-form-values.js +10 -0
  135. package/dist/form/source.js +97 -65
  136. package/dist/form/text-of.d.ts +2 -0
  137. package/dist/form/text-of.js +7 -0
  138. package/dist/form/validators.d.ts +58 -19
  139. package/dist/form/validators.js +97 -71
  140. package/dist/geo/bbox.d.ts +5 -4
  141. package/dist/geo/bbox.js +69 -48
  142. package/dist/geo/bounds.d.ts +14 -3
  143. package/dist/geo/bounds.js +98 -68
  144. package/dist/geo/constraints.js +88 -93
  145. package/dist/geo/coordinates.d.ts +10 -0
  146. package/dist/geo/coordinates.js +44 -23
  147. package/dist/geo/editor.d.ts +13 -13
  148. package/dist/geo/editor.js +176 -153
  149. package/dist/geo/geocoding.d.ts +4 -4
  150. package/dist/geo/geocoding.js +38 -41
  151. package/dist/geo/geojson.d.ts +12 -74
  152. package/dist/geo/geojson.js +52 -40
  153. package/dist/geo/geometry-math.d.ts +4 -4
  154. package/dist/geo/geometry-math.js +100 -93
  155. package/dist/geo/index.d.ts +9 -8
  156. package/dist/geo/map-layers.d.ts +42 -7
  157. package/dist/geo/map-layers.js +88 -62
  158. package/dist/geo/map-style.d.ts +12 -0
  159. package/dist/geo/map-style.js +43 -32
  160. package/dist/geo/messages.js +48 -45
  161. package/dist/geo/nominatim.js +88 -80
  162. package/dist/geo/osm-presets.d.ts +4 -3
  163. package/dist/geo/osm-presets.js +108 -86
  164. package/dist/geo/osm-tags.js +14 -12
  165. package/dist/geo/overpass.d.ts +14 -2
  166. package/dist/geo/overpass.js +83 -90
  167. package/dist/geo/shape-value.d.ts +9 -5
  168. package/dist/geo/shape-value.js +169 -146
  169. package/dist/geo/snap-to-roads.d.ts +3 -3
  170. package/dist/geo/snap-to-roads.js +55 -50
  171. package/dist/geo/throttle.js +36 -39
  172. package/dist/guards.d.ts +44 -0
  173. package/dist/guards.js +77 -0
  174. package/dist/i18n/default-messages.js +34 -29
  175. package/dist/i18n/english-messages.d.ts +172 -0
  176. package/dist/i18n/english-messages.js +359 -189
  177. package/dist/i18n/i18n.d.ts +2 -2
  178. package/dist/i18n/i18n.js +47 -60
  179. package/dist/i18n/inflection.d.ts +34 -0
  180. package/dist/i18n/inflection.js +82 -0
  181. package/dist/i18n/messages.d.ts +10 -2
  182. package/dist/i18n/messages.js +17 -4
  183. package/dist/i18n/missing-keys.d.ts +4 -0
  184. package/dist/i18n/missing-keys.js +70 -62
  185. package/dist/i18n/translatable.d.ts +16 -1
  186. package/dist/i18n/translatable.js +40 -47
  187. package/dist/index.d.ts +87 -39
  188. package/dist/index.js +54 -301
  189. package/dist/json/deep-equal.d.ts +8 -0
  190. package/dist/json/deep-equal.js +24 -0
  191. package/dist/json/strip-json-comments.js +38 -42
  192. package/dist/noop.d.ts +2 -0
  193. package/dist/noop.js +4 -0
  194. package/dist/notification/error-message.d.ts +2 -0
  195. package/dist/notification/error-message.js +10 -0
  196. package/dist/notification/notification-store.d.ts +3 -2
  197. package/dist/notification/notification-store.js +41 -45
  198. package/dist/onboarding-tour/placement.d.ts +2 -0
  199. package/dist/onboarding-tour/placement.js +65 -78
  200. package/dist/onboarding-tour/tour-controller.js +46 -54
  201. package/dist/realtime/add-events-for-mutations.js +75 -81
  202. package/dist/realtime/announce-lock.js +16 -13
  203. package/dist/realtime/broadcast-channel-transport.js +65 -59
  204. package/dist/realtime/copy-members.js +22 -14
  205. package/dist/realtime/in-memory-lock-provider.js +40 -48
  206. package/dist/realtime/live.js +95 -101
  207. package/dist/realtime/lock-options.js +16 -16
  208. package/dist/realtime/lock-session.js +91 -78
  209. package/dist/realtime/memory-transport.js +25 -25
  210. package/dist/realtime/presence.js +120 -122
  211. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  212. package/dist/realtime/realtime-data-provider.js +45 -42
  213. package/dist/realtime/reconnect-backoff.js +17 -0
  214. package/dist/realtime/sse-transport.d.ts +31 -0
  215. package/dist/realtime/sse-transport.js +235 -0
  216. package/dist/realtime/status-store.js +23 -28
  217. package/dist/realtime/topic-hub.js +62 -71
  218. package/dist/realtime/topics.js +10 -12
  219. package/dist/realtime/types.js +10 -12
  220. package/dist/realtime/unread-count.js +21 -21
  221. package/dist/realtime/websocket-transport.d.ts +32 -0
  222. package/dist/realtime/websocket-transport.js +306 -0
  223. package/dist/resource/describe-resource.d.ts +2 -0
  224. package/dist/resource/describe-resource.js +247 -219
  225. package/dist/resource/get-at-path.js +29 -14
  226. package/dist/resource/identifier.d.ts +18 -0
  227. package/dist/resource/identifier.js +27 -0
  228. package/dist/resource/record.d.ts +4 -0
  229. package/dist/resource/record.js +10 -0
  230. package/dist/resource/register.d.ts +7 -0
  231. package/dist/resource/registry-store.d.ts +45 -15
  232. package/dist/resource/registry-store.js +67 -60
  233. package/dist/resource/registry.d.ts +10 -0
  234. package/dist/resource/registry.js +8 -4
  235. package/dist/resource/source-path.d.ts +23 -2
  236. package/dist/rich-text/rich-text-extras.d.ts +134 -0
  237. package/dist/rich-text/rich-text-extras.js +181 -0
  238. package/dist/rich-text/rich-text-image.d.ts +124 -0
  239. package/dist/rich-text/rich-text-image.js +163 -0
  240. package/dist/rich-text/rich-text.d.ts +6 -0
  241. package/dist/rich-text/rich-text.js +125 -88
  242. package/dist/router/location.d.ts +24 -4
  243. package/dist/router/location.js +43 -18
  244. package/dist/router/match.js +47 -39
  245. package/dist/router/paths.js +40 -34
  246. package/dist/router/record-from-location.d.ts +1 -1
  247. package/dist/router/record-from-location.js +25 -17
  248. package/dist/store/adapters.js +51 -55
  249. package/dist/store/micro-store.js +57 -59
  250. package/dist/store/shallow-equal.d.ts +12 -0
  251. package/dist/store/shallow-equal.js +20 -0
  252. package/dist/store/store.js +54 -57
  253. package/dist/ui-defaults.d.ts +15 -0
  254. package/dist/ui-defaults.js +17 -0
  255. package/package.json +13 -11
@@ -1,56 +1,70 @@
1
- const resolveFilterValue = (formValues, key, filter) => {
2
- const formValue = formValues[key];
3
- if (formValue === void 0 || formValue === null || Array.isArray(formValue) || formValue instanceof Date) {
4
- return key in filter ? filter[key] : "";
5
- }
6
- if (typeof formValue === "object") {
7
- const nestedFilter = typeof filter[key] === "object" && filter[key] !== null ? filter[key] : {};
8
- const inner = formValue;
9
- const rebuilt = {};
10
- for (const innerKey of Object.keys(inner)) {
11
- rebuilt[innerKey] = resolveFilterValue(inner, innerKey, nestedFilter);
12
- }
13
- if (Object.keys(rebuilt).length === 0) return "";
14
- return rebuilt;
15
- }
16
- return key in filter ? filter[key] : "";
1
+ import { isFilterBranch } from "../data/filter-branch.js";
2
+ import { isRecord } from "../guards.js";
3
+ // src/form/filter-form-values.ts
4
+ /**
5
+ * Pure helpers a live filter form uses to keep its values and the list filter
6
+ * in sync. Shared by the React and Vue adapters so the merge and reflect rules
7
+ * live once.
8
+ */
9
+ var resolveFilterValue = (formValues, key, filter) => {
10
+ const formValue = formValues[key];
11
+ if (formValue === void 0 || formValue === null || Array.isArray(formValue) || formValue instanceof Date) return key in filter ? filter[key] : "";
12
+ if (isRecord(formValue)) {
13
+ const heldFilter = filter[key];
14
+ const nestedFilter = isRecord(heldFilter) ? heldFilter : {};
15
+ const rebuilt = {};
16
+ for (const innerKey of Object.keys(formValue)) rebuilt[innerKey] = resolveFilterValue(formValue, innerKey, nestedFilter);
17
+ if (Object.keys(rebuilt).length === 0) return "";
18
+ return rebuilt;
19
+ }
20
+ return key in filter ? filter[key] : "";
17
21
  };
22
+ /**
23
+ * Rebuild form values from the incoming filter so EVERY current form key is
24
+ * present with a defined value (an empty string when the filter lacks it).
25
+ *
26
+ * form-core (like RHF) re-applies a field's CURRENT value when `reset(values)`
27
+ * omits that key — which would silently ignore an external filter clear. So a
28
+ * reset must carry a value for every mounted input. This never produces
29
+ * `undefined`/`null` for a present form key.
30
+ */
18
31
  function getFilterFormValues(formValues, filter) {
19
- const result = {};
20
- for (const key of Object.keys(formValues)) {
21
- result[key] = resolveFilterValue(formValues, key, filter);
22
- }
23
- return result;
32
+ const result = {};
33
+ for (const key of Object.keys(formValues)) result[key] = resolveFilterValue(formValues, key, filter);
34
+ return result;
24
35
  }
36
+ /**
37
+ * The form shape the external-reflect must cover: every key that is either a
38
+ * current form value, a REGISTERED field (form-core tracks these in `fieldMeta`
39
+ * even when unset), or present on the incoming filter. Existing values are
40
+ * carried through; registered-but-unset / filter-only keys seed to `undefined`
41
+ * so {@link getFilterFormValues} resolves them from the filter (fallback `''`).
42
+ */
25
43
  function filterFormReflectShape(values, fieldMeta, filter) {
26
- const shape = { ...values };
27
- for (const key of Object.keys(fieldMeta)) {
28
- const top = key.split(/[.[]/, 1)[0];
29
- if (top && !(top in shape)) shape[top] = void 0;
30
- }
31
- for (const key of Object.keys(filter)) {
32
- if (!(key in shape)) shape[key] = void 0;
33
- }
34
- return shape;
44
+ const shape = { ...values };
45
+ for (const key of Object.keys(fieldMeta)) {
46
+ const top = key.split(/[.[]/, 1)[0];
47
+ if (top && !(top in shape)) shape[top] = void 0;
48
+ }
49
+ for (const key of Object.keys(filter)) if (!(key in shape)) shape[key] = void 0;
50
+ return shape;
35
51
  }
52
+ /**
53
+ * Merge form values OVER the existing filter, one level deep, with arrays
54
+ * REPLACED (never element-merged) — matching ra's mergeObjNotArray. The result
55
+ * is a fresh object (no mutation of the inputs).
56
+ */
36
57
  function mergeFilterValues(filter, values) {
37
- const merged = { ...filter };
38
- for (const key of Object.keys(values)) {
39
- const next = values[key];
40
- const prev = merged[key];
41
- if (next !== null && typeof next === "object" && !Array.isArray(next) && !(next instanceof Date) && prev !== null && typeof prev === "object" && !Array.isArray(prev) && !(prev instanceof Date)) {
42
- merged[key] = {
43
- ...prev,
44
- ...next
45
- };
46
- } else {
47
- merged[key] = next;
48
- }
49
- }
50
- return merged;
58
+ const merged = { ...filter };
59
+ for (const key of Object.keys(values)) {
60
+ const next = values[key];
61
+ const prev = merged[key];
62
+ if (isFilterBranch(next) && isFilterBranch(prev)) merged[key] = {
63
+ ...prev,
64
+ ...next
65
+ };
66
+ else merged[key] = next;
67
+ }
68
+ return merged;
51
69
  }
52
- export {
53
- filterFormReflectShape,
54
- getFilterFormValues,
55
- mergeFilterValues
56
- };
70
+ export { filterFormReflectShape, getFilterFormValues, mergeFilterValues };
@@ -1,5 +1,7 @@
1
1
  import { FormApi, type FormAsyncValidateOrFn, type FormOptions, type FormValidateOrFn, type StandardSchemaV1 } from "@tanstack/form-core";
2
+ import { type TranslateParamValues } from "../i18n/messages.js";
2
3
  import type { NotificationStore } from "../notification/notification-store.js";
4
+ import { type FormHandle } from "./form-handle.js";
3
5
  /** A sync validator slot of the wrapped FormApi: fn, Standard Schema, or absent. */
4
6
  export type SyncValidator<TRecord> = FormValidateOrFn<TRecord> | undefined;
5
7
  /** An async validator slot of the wrapped FormApi. */
@@ -39,7 +41,7 @@ export type FieldErrors = Record<string, string | string[]>;
39
41
  */
40
42
  export interface TranslatableMessage {
41
43
  message: string;
42
- args?: Record<string, unknown>;
44
+ args?: TranslateParamValues;
43
45
  }
44
46
  /**
45
47
  * Form-level validate fn (ra contract): receives ALL values, returns a map of
@@ -115,6 +117,12 @@ export interface FormController<TRecord> {
115
117
  * and advanced imperative use.
116
118
  */
117
119
  form: CheloneFormApi<TRecord>;
120
+ /**
121
+ * The same form as a {@link FormHandle}: `source` paths typed by `TRecord`,
122
+ * values typed at each path, and the record-erased `ambient` form a context
123
+ * can carry.
124
+ */
125
+ handle: FormHandle<TRecord>;
118
126
  /**
119
127
  * Refcounted subscription (uSES-compatible): registers `listener` with
120
128
  * `form.store` and mounts the form on the 0 -> 1 transition. Returns an
@@ -1,168 +1,173 @@
1
- import { revalidateLogic, FormApi } from "@tanstack/form-core";
2
- import { composeFormValidator, isThenable, toFormErrorMapValue, sanitizeEmptyValues } from "./save-pipeline.js";
1
+ import { hasProperty, isThenable } from "../guards.js";
2
+ import { pickTranslateParamValues } from "../i18n/messages.js";
3
+ import { ERROR_MAP_PRIORITY } from "./field-error.js";
4
+ import { createFormHandle } from "./form-handle.js";
5
+ import { composeFormValidator, sanitizeEmptyValues, toFormErrorMapValue } from "./save-pipeline.js";
6
+ import { FormApi, revalidateLogic } from "@tanstack/form-core";
7
+ // src/form/form-controller.ts
3
8
  function createFormController(options) {
4
- const userFormOptions = options.formOptions;
5
- let serverErrorsApplied = false;
6
- const notifyInvalid = (root) => {
7
- const display = displayableRoot(root);
8
- if (display === void 0) {
9
- options.notifications?.notify("chelone.message.invalid_form", {
10
- type: "error"
11
- });
12
- } else if (typeof display === "string") {
13
- options.notifications?.notify(display, { type: "error" });
14
- } else {
15
- options.notifications?.notify(display.message, {
16
- type: "error",
17
- messageArgs: display.args
18
- });
19
- }
20
- };
21
- const dynamicValidator = composeFormValidator({
22
- schema: options.schema,
23
- validate: options.validate,
24
- userValidator: userFormOptions?.validators?.onDynamic
25
- });
26
- const onSubmit = async ({ value }) => {
27
- if (options.validate) {
28
- const pending = options.validate(value);
29
- const result = isThenable(pending) ? await pending : pending;
30
- const mapped2 = toFormErrorMapValue(result);
31
- if (mapped2) {
32
- form.setErrorMap({ onSubmit: mapped2 });
33
- serverErrorsApplied = true;
34
- notifyInvalid(mapped2.form);
35
- return;
36
- }
37
- }
38
- const data = options.sanitizeEmptyValues ? sanitizeEmptyValues(value, options.getRecord?.()) : value;
39
- const fieldErrors = await options.save(data);
40
- const mapped = toFormErrorMapValue(fieldErrors);
41
- if (mapped) {
42
- form.setErrorMap({ onSubmit: mapped });
43
- serverErrorsApplied = true;
44
- notifyInvalid(mapped.form);
45
- return;
46
- }
47
- if (options.resetOnSuccess !== false) form.reset(value);
48
- await options.onSuccess?.(data);
49
- };
50
- const onSubmitInvalid = (props) => {
51
- notifyInvalid(firstFormLevelError(form.store.state.errorMap));
52
- userFormOptions?.onSubmitInvalid?.(props);
53
- };
54
- const formOptions = {
55
- ...userFormOptions,
56
- defaultValues: options.defaultValues,
57
- validators: dynamicValidator ? { ...userFormOptions?.validators, onDynamic: dynamicValidator } : userFormOptions?.validators,
58
- // revalidateLogic routes the onDynamic slots: run on submit, re-run on
59
- // change once a submission failed. ALWAYS set (not only when the form
60
- // has its own dynamic validator): FIELD bindings (T3) wire their
61
- // validators into the field onDynamic slots, and those route through
62
- // `form.options.validationLogic` — under form-core's default logic they
63
- // would never run (probed on 1.33). A user-supplied validationLogic wins.
64
- validationLogic: userFormOptions?.validationLogic ?? revalidateLogic(),
65
- onSubmit,
66
- onSubmitInvalid
67
- };
68
- const form = new FormApi(formOptions);
69
- let destroyed = false;
70
- let unmountForm;
71
- const subscriptions = /* @__PURE__ */ new Set();
72
- const detach = () => {
73
- const unmount = unmountForm;
74
- unmountForm = void 0;
75
- unmount?.();
76
- };
77
- return {
78
- form,
79
- subscribe(listener) {
80
- if (destroyed) return () => {
81
- };
82
- const storeSubscription = form.store.subscribe(listener);
83
- const subscription = {
84
- off: () => storeSubscription.unsubscribe()
85
- };
86
- const isFirst = subscriptions.size === 0;
87
- subscriptions.add(subscription);
88
- if (isFirst) {
89
- unmountForm = form.mount();
90
- }
91
- return () => {
92
- if (!subscriptions.delete(subscription)) return;
93
- subscription.off();
94
- if (subscriptions.size === 0 && !destroyed) detach();
95
- };
96
- },
97
- getSnapshot: () => form.store.state,
98
- async submit() {
99
- if (destroyed) return;
100
- if (serverErrorsApplied) {
101
- form.setErrorMap({ onSubmit: { form: void 0, fields: {} } });
102
- serverErrorsApplied = false;
103
- }
104
- try {
105
- await form.handleSubmit();
106
- } catch (error) {
107
- options.notifications?.notify("chelone.notification.http_error", {
108
- type: "error",
109
- messageArgs: { _: String(error) }
110
- });
111
- }
112
- },
113
- destroy() {
114
- if (destroyed) return;
115
- destroyed = true;
116
- detach();
117
- for (const subscription of subscriptions) subscription.off();
118
- subscriptions.clear();
119
- }
120
- };
9
+ const userFormOptions = options.formOptions;
10
+ /**
11
+ * True while the `onSubmit` error-map key holds errors WE wrote (server
12
+ * fieldErrors / async validate results). They make `canSubmit` false, and
13
+ * `handleSubmit` dead-ends on that gate BEFORE any validator could clear
14
+ * them (probed on form-core 1.33) — so `submit()` clears the channel first,
15
+ * letting an unchanged resubmit re-reach the server.
16
+ */
17
+ let serverErrorsApplied = false;
18
+ /** Emits the invalid-form notification, preferring a displayable root message. */
19
+ const notifyInvalid = (root) => {
20
+ const display = displayableRoot(root);
21
+ if (display === void 0) options.notifications?.notify("chelone.message.invalid_form", { type: "error" });
22
+ else if (typeof display === "string") options.notifications?.notify(display, { type: "error" });
23
+ else options.notifications?.notify(display.message, {
24
+ type: "error",
25
+ messageArgs: display.args
26
+ });
27
+ };
28
+ const dynamicValidator = composeFormValidator({
29
+ schema: options.schema,
30
+ validate: options.validate,
31
+ userValidator: userFormOptions?.validators?.onDynamic
32
+ });
33
+ const onSubmit = async ({ value }) => {
34
+ if (options.validate) {
35
+ const pending = options.validate(value);
36
+ const result = isThenable(pending) ? await pending : pending;
37
+ const mapped = toFormErrorMapValue(result);
38
+ if (mapped) {
39
+ form.setErrorMap({ onSubmit: mapped });
40
+ serverErrorsApplied = true;
41
+ notifyInvalid(mapped.form);
42
+ return;
43
+ }
44
+ }
45
+ const data = options.sanitizeEmptyValues ? sanitizeEmptyValues(value, options.getRecord?.()) : value;
46
+ const fieldErrors = await options.save(data);
47
+ const mapped = toFormErrorMapValue(fieldErrors);
48
+ if (mapped) {
49
+ form.setErrorMap({ onSubmit: mapped });
50
+ serverErrorsApplied = true;
51
+ notifyInvalid(mapped.form);
52
+ return;
53
+ }
54
+ if (options.resetOnSuccess !== false) form.reset(value);
55
+ await options.onSuccess?.(data);
56
+ };
57
+ const onSubmitInvalid = (props) => {
58
+ notifyInvalid(firstFormLevelError(form.store.state.errorMap));
59
+ userFormOptions?.onSubmitInvalid?.(props);
60
+ };
61
+ const formOptions = {
62
+ ...userFormOptions,
63
+ defaultValues: options.defaultValues,
64
+ validators: dynamicValidator ? {
65
+ ...userFormOptions?.validators,
66
+ onDynamic: dynamicValidator
67
+ } : userFormOptions?.validators,
68
+ validationLogic: userFormOptions?.validationLogic ?? revalidateLogic(),
69
+ onSubmit,
70
+ onSubmitInvalid
71
+ };
72
+ const form = new FormApi(formOptions);
73
+ let destroyed = false;
74
+ /** Non-undefined exactly while attached (mount cleanup). */
75
+ let unmountForm;
76
+ const subscriptions = /* @__PURE__ */ new Set();
77
+ /** Runs the mount cleanup at most once per attach cycle. */
78
+ const detach = () => {
79
+ const unmount = unmountForm;
80
+ unmountForm = void 0;
81
+ unmount?.();
82
+ };
83
+ return {
84
+ form,
85
+ handle: createFormHandle(form),
86
+ subscribe(listener) {
87
+ if (destroyed) return () => {};
88
+ const storeSubscription = form.store.subscribe(listener);
89
+ const subscription = { off: () => storeSubscription.unsubscribe() };
90
+ const isFirst = subscriptions.size === 0;
91
+ subscriptions.add(subscription);
92
+ if (isFirst) unmountForm = form.mount();
93
+ return () => {
94
+ if (!subscriptions.delete(subscription)) return;
95
+ subscription.off();
96
+ if (subscriptions.size === 0 && !destroyed) detach();
97
+ };
98
+ },
99
+ getSnapshot: () => form.store.state,
100
+ async submit() {
101
+ if (destroyed || form.state.isSubmitting) return;
102
+ if (serverErrorsApplied) {
103
+ form.setErrorMap({ onSubmit: {
104
+ form: void 0,
105
+ fields: {}
106
+ } });
107
+ serverErrorsApplied = false;
108
+ }
109
+ try {
110
+ await form.handleSubmit();
111
+ } catch (error) {
112
+ options.notifications?.notify("chelone.notification.http_error", {
113
+ type: "error",
114
+ messageArgs: { _: String(error) }
115
+ });
116
+ }
117
+ },
118
+ destroy() {
119
+ if (destroyed) return;
120
+ destroyed = true;
121
+ detach();
122
+ for (const subscription of subscriptions) subscription.off();
123
+ subscriptions.clear();
124
+ }
125
+ };
121
126
  }
127
+ /**
128
+ * Filters an error down to something a NOTIFICATION can display: strings
129
+ * (and the first entry of arrays) pass, objects with a string `message`
130
+ * normalize to a translatable payload, everything else — schema path-record
131
+ * objects, issue lists addressed at fields — yields `undefined` (those are
132
+ * field concerns, the generic invalid_form key covers the notification).
133
+ */
122
134
  function displayableRoot(error) {
123
- if (typeof error === "string") return error || void 0;
124
- if (Array.isArray(error)) return displayableRoot(error[0]);
125
- if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
126
- return normalizeFormError(error);
127
- }
128
- return void 0;
135
+ if (typeof error === "string") return error || void 0;
136
+ if (Array.isArray(error)) return displayableRoot(error[0]);
137
+ if (hasProperty(error, "message") && typeof error.message === "string") return normalizeFormError(error);
129
138
  }
139
+ /** First displayable form-level error across the error-map keys, submit-first. */
130
140
  function firstFormLevelError(errorMap) {
131
- const map = errorMap;
132
- for (const key of [
133
- "onSubmit",
134
- "onDynamic",
135
- "onChange",
136
- "onBlur",
137
- "onMount",
138
- "onServer"
139
- ]) {
140
- const candidate = displayableRoot(map[key]);
141
- if (candidate !== void 0) return candidate;
142
- }
143
- return void 0;
141
+ for (const key of ERROR_MAP_PRIORITY) {
142
+ const candidate = displayableRoot(errorMap[key]);
143
+ if (candidate !== void 0) return candidate;
144
+ }
144
145
  }
146
+ /**
147
+ * Boundary normalizer for form-core's `unknown`-typed errors: strings pass
148
+ * through, ra translatable payloads (`{message, args}`) are preserved,
149
+ * Standard Schema issues (and anything else with a string `message`,
150
+ * including `Error`s) collapse to their message, arrays surface their first
151
+ * entry (schema validators emit `StandardSchemaV1Issue[]`), nullish/empty
152
+ * input yields `undefined`, and everything else is `String()`-ed.
153
+ */
145
154
  function normalizeFormError(error) {
146
- if (error == null) return void 0;
147
- if (typeof error === "string") return error;
148
- if (Array.isArray(error)) {
149
- return error.length > 0 ? normalizeFormError(error[0]) : void 0;
150
- }
151
- if (typeof error === "object" && "message" in error) {
152
- const { message } = error;
153
- if (typeof message === "string") {
154
- if ("args" in error) {
155
- const { args } = error;
156
- if (typeof args === "object" && args !== null && !Array.isArray(args)) {
157
- return { message, args };
158
- }
159
- }
160
- return message;
161
- }
162
- }
163
- return String(error);
155
+ if (error == null) return void 0;
156
+ if (typeof error === "string") return error;
157
+ if (Array.isArray(error)) return error.length > 0 ? normalizeFormError(error[0]) : void 0;
158
+ if (typeof error === "object" && "message" in error) {
159
+ const { message } = error;
160
+ if (typeof message === "string") {
161
+ if ("args" in error) {
162
+ const args = pickTranslateParamValues(error.args);
163
+ if (args) return {
164
+ message,
165
+ args
166
+ };
167
+ }
168
+ return message;
169
+ }
170
+ }
171
+ return String(error);
164
172
  }
165
- export {
166
- createFormController,
167
- normalizeFormError
168
- };
173
+ export { createFormController, normalizeFormError };
@@ -13,7 +13,8 @@
13
13
  * Vanilla + SSR-safe: no `window`, no React, no router/form library import at
14
14
  * runtime — mirrors the resource-registry / MicroStore notification pattern.
15
15
  */
16
- import { type TranslatableMessage } from "./form-controller.js";
16
+ import type { ValidationErrorMap } from "@tanstack/form-core";
17
+ import type { TranslatableMessage } from "./form-controller.js";
17
18
  /** Called on every membership mutation (a member added or removed). */
18
19
  export type FormGroupsListener = () => void;
19
20
  /** The vanilla form-group membership registry contract. */
@@ -47,8 +48,8 @@ export interface GroupFieldMeta {
47
48
  isDirty: boolean;
48
49
  isTouched: boolean;
49
50
  isValidating: boolean;
50
- /** Cause-keyed validation errors (`onSubmit`/`onDynamic`/…) — `unknown` values. */
51
- errorMap: Record<string, unknown>;
51
+ /** form-core's cause-keyed validation errors (`onSubmit`/`onDynamic`/…). */
52
+ errorMap: ValidationErrorMap;
52
53
  }
53
54
  /** The aggregated group state a badge/toolbar renders. */
54
55
  export interface GroupState {