@entifix/react-integration 0.1.1 → 0.1.3

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 (42) hide show
  1. package/README.md +4 -3
  2. package/dist/hooks/use-entity-form/use-entity-form.d.ts.map +1 -1
  3. package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts +11 -2
  4. package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts.map +1 -1
  5. package/dist/hooks/use-entity-form/use-entity-form.helpers.js +15 -14
  6. package/dist/hooks/use-entity-form/use-entity-form.helpers.js.map +1 -1
  7. package/dist/hooks/use-entity-form/use-entity-form.js +20 -8
  8. package/dist/hooks/use-entity-form/use-entity-form.js.map +1 -1
  9. package/dist/hooks/use-entity-form/use-entity-form.types.d.ts +4 -4
  10. package/dist/hooks/use-entity-form/use-entity-form.types.js.map +1 -1
  11. package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts +1 -1
  12. package/dist/hooks/use-entity-link-source/use-entity-link-source.js +1 -1
  13. package/dist/hooks/use-entity-link-source/use-entity-link-source.js.map +1 -1
  14. package/dist/hooks/use-wizard/use-wizard.types.d.ts +2 -2
  15. package/dist/hooks/use-wizard/use-wizard.types.js.map +1 -1
  16. package/dist/reactive/use-transaction-settlement.d.ts +1 -1
  17. package/dist/reactive/use-transaction-settlement.js.map +1 -1
  18. package/dist/url/entity-source.d.ts +9 -0
  19. package/dist/url/entity-source.d.ts.map +1 -0
  20. package/dist/url/entity-source.js +8 -0
  21. package/dist/url/entity-source.js.map +1 -0
  22. package/dist/url/url-filter.d.ts +40 -0
  23. package/dist/url/url-filter.d.ts.map +1 -0
  24. package/dist/url/url-filter.js +65 -0
  25. package/dist/url/url-filter.js.map +1 -0
  26. package/dist/url/url-query.d.ts +83 -0
  27. package/dist/url/url-query.d.ts.map +1 -0
  28. package/dist/url/url-query.js +122 -0
  29. package/dist/url/url-query.js.map +1 -0
  30. package/dist/url/url-state.d.ts +11 -0
  31. package/dist/url/url-state.d.ts.map +1 -0
  32. package/dist/url/url-state.js +40 -0
  33. package/dist/url/url-state.js.map +1 -0
  34. package/dist/url/use-entity-load.d.ts +29 -0
  35. package/dist/url/use-entity-load.d.ts.map +1 -0
  36. package/dist/url/use-entity-load.js +67 -0
  37. package/dist/url/use-entity-load.js.map +1 -0
  38. package/dist/url.d.ts +11 -0
  39. package/dist/url.d.ts.map +1 -0
  40. package/dist/url.js +11 -0
  41. package/dist/url.js.map +1 -0
  42. package/package.json +11 -8
package/README.md CHANGED
@@ -12,9 +12,10 @@ pnpm add @entifix/react-integration @tanstack/react-form @tanstack/react-query e
12
12
 
13
13
  ## Entry points
14
14
 
15
- | Import | What it holds |
16
- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | `@entifix/react-integration` | `EntifixQueryProvider`, `useEntityRecord`, `useEntityForm`, `useEntityMutation`, `useWizard`, `useReactiveInvalidation`, `useTransactionSettlement` |
15
+ | Import | What it holds |
16
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `@entifix/react-integration` | `EntifixQueryProvider`, `useEntityRecord`, `useEntityForm`, `useEntityMutation`, `useWizard`, `useReactiveInvalidation`, `useTransactionSettlement` |
18
+ | `@entifix/react-integration/url` | `defineUrlQuery`, `useUrlState`, `useEntityLoad`, `useUrlFilter`: a filter kept in the query string, answered by `loadAllUCFactory` over an `EntitySource`. No TanStack |
18
19
 
19
20
  ---
20
21
 
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-form.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,MAAM,EAIZ,MAAM,eAAe,CAAC;AAWvB,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,EACpD,iBAAiB,EACjB,MAAM,EACN,KAAK,EACL,MAAM,EACN,MAAM,EACN,QAAQ,EACR,kBAAkB,EAClB,YAAY,EACZ,QAAQ,GACT,EAAE,oBAAoB,CAAC,OAAO,CAAC,GAAG,mBAAmB,CAyLrD"}
1
+ {"version":3,"file":"use-entity-form.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,MAAM,EAKZ,MAAM,eAAe,CAAC;AAWvB,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,EACpD,iBAAiB,EACjB,MAAM,EACN,KAAK,EACL,MAAM,EACN,MAAM,EACN,QAAQ,EACR,kBAAkB,EAClB,YAAY,EACZ,QAAQ,GACT,EAAE,oBAAoB,CAAC,OAAO,CAAC,GAAG,mBAAmB,CAuMrD"}
@@ -72,6 +72,13 @@ export declare function readFieldErrors(fieldMeta: Partial<Record<string, FieldE
72
72
  * sentence cannot be assembled from a label plus a hardcoded suffix: word order
73
73
  * differs between locales.
74
74
  */
75
+ /**
76
+ * How a message names a field. The default is the descriptor's `label`, which is
77
+ * the accessor's `label` option or its humanized member name — never translated.
78
+ * A host with a catalog resolves `labelKey` instead, so the message names the
79
+ * field in the words its input and column header already use.
80
+ */
81
+ export type EntityFieldLabeler = (descriptor: EntityFieldDescriptor) => string;
75
82
  export interface EntityDraftMessages {
76
83
  required(field: string): string;
77
84
  number(field: string): string;
@@ -87,7 +94,7 @@ export interface EntityDraftMessages {
87
94
  * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}
88
95
  * is what layers a schema and the caller's own rules over this.
89
96
  */
90
- export declare function validateEntityDraft(descriptors: readonly EntityFieldDescriptor[], values: EntityDraft, messages: EntityDraftMessages): Record<string, string>;
97
+ export declare function validateEntityDraft(descriptors: readonly EntityFieldDescriptor[], values: EntityDraft, messages: EntityDraftMessages, labelFor?: EntityFieldLabeler): Record<string, string>;
91
98
  /**
92
99
  * Runs a Standard Schema over the draft and sorts its issues into per-field
93
100
  * messages plus a form-level one (an issue with no path — a cross-field rule).
@@ -110,6 +117,8 @@ export interface ComposeEntityFormErrorsOptions {
110
117
  descriptors: readonly EntityFieldDescriptor[];
111
118
  values: EntityDraft;
112
119
  messages: EntityDraftMessages;
120
+ /** How a metadata message names its field. Defaults to the untranslated `label`. */
121
+ labelFor?: EntityFieldLabeler;
113
122
  schema?: StandardSchemaV1;
114
123
  translateIssue: (message: string, field: string | undefined) => string;
115
124
  validate?: (values: EntityDraft) => Record<string, string>;
@@ -119,7 +128,7 @@ export interface ComposeEntityFormErrorsOptions {
119
128
  * the entity's schema, then the caller's `validate` callback. Later sources win
120
129
  * on conflict, so a caller can always override a message it disagrees with.
121
130
  */
122
- export declare function composeEntityFormErrors({ descriptors, values, messages, schema, translateIssue, validate, }: ComposeEntityFormErrorsOptions): {
131
+ export declare function composeEntityFormErrors({ descriptors, values, messages, labelFor, schema, translateIssue, validate, }: ComposeEntityFormErrorsOptions): {
123
132
  fields: Record<string, string>;
124
133
  form?: string;
125
134
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-entity-form.helpers.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAU1B,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAC;AAwCvB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,qBAAqB,EACjC,MAAM,EAAE,OAAO,GACd,gBAAgB,CAKlB;AAqDD,4EAA4E;AAC5E,wBAAgB,eAAe,CAC7B,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,MAAM,EAAE,OAAO,GACd,WAAW,CAMb;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,WAAW,GAAG,SAAS,GACjC,WAAW,CAeb;AAED,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC,CAAC,GAC7D,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBxB;AAkHD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,WAAW,EACnB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GACrE;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAuBnD;AAED,2EAA2E;AAC3E,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC;IACvE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,WAAW,EACX,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,QAAQ,GACT,EAAE,8BAA8B,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAUA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,EAAE,mBAKzC,CAAC"}
1
+ {"version":3,"file":"use-entity-form.helpers.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAU1B,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAC;AAwCvB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,qBAAqB,EACjC,MAAM,EAAE,OAAO,GACd,gBAAgB,CAKlB;AAqDD,4EAA4E;AAC5E,wBAAgB,eAAe,CAC7B,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,MAAM,EAAE,OAAO,GACd,WAAW,CAMb;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,WAAW,GAAG,SAAS,GACjC,WAAW,CAeb;AAED,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC,CAAC,GAC7D,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB;AAED;;;;;GAKG;AACH;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,UAAU,EAAE,qBAAqB,KAAK,MAAM,CAAC;AAI/E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,GAAE,kBAA4B,GACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BxB;AAoHD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,WAAW,EACnB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GACrE;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAuBnD;AAED,2EAA2E;AAC3E,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,oFAAoF;IACpF,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC;IACvE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,WAAW,EACX,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,cAAc,EACd,QAAQ,GACT,EAAE,8BAA8B,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAUA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,EAAE,mBAKzC,CAAC"}
@@ -162,6 +162,7 @@ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCo
162
162
  }
163
163
  return errors;
164
164
  }
165
+ const labelOf = (descriptor)=>descriptor.label;
165
166
  /**
166
167
  * Validates a draft against what the metadata implies — `required` members must
167
168
  * be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only
@@ -170,21 +171,21 @@ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCo
170
171
  *
171
172
  * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}
172
173
  * is what layers a schema and the caller's own rules over this.
173
- */ export function validateEntityDraft(descriptors, values, messages) {
174
+ */ export function validateEntityDraft(descriptors, values, messages, labelFor = labelOf) {
174
175
  const errors = {};
175
176
  for (const descriptor of descriptors){
176
177
  if (isNeverEdited(descriptor)) continue;
177
178
  if (descriptor.type === 'composition') {
178
- Object.assign(errors, validateComposition(descriptor, values, messages));
179
+ Object.assign(errors, validateComposition(descriptor, values, messages, labelFor));
179
180
  continue;
180
181
  }
181
182
  const raw = readDraftString(values, descriptor.name);
182
183
  if (descriptor.required && raw.trim() === '') {
183
- errors[descriptor.name] = messages.required(descriptor.label);
184
+ errors[descriptor.name] = messages.required(labelFor(descriptor));
184
185
  continue;
185
186
  }
186
187
  if (raw === '' || !hasCheckableFormat(descriptor)) continue;
187
- const message = formatMessage(descriptor, raw, messages);
188
+ const message = formatMessage(descriptor, raw, messages, labelFor);
188
189
  if (message !== undefined) errors[descriptor.name] = message;
189
190
  }
190
191
  return errors;
@@ -196,15 +197,15 @@ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCo
196
197
  * Extracted so a row's cell is judged by the **same** rule as a record's field.
197
198
  * Two copies is how a child's `number` member would quietly start accepting
198
199
  * something its master's would reject.
199
- */ function formatMessage(descriptor, raw, messages) {
200
+ */ function formatMessage(descriptor, raw, messages, labelFor) {
200
201
  if (descriptor.type === 'number' && Number.isNaN(Number(raw))) {
201
- return messages.number(descriptor.label);
202
+ return messages.number(labelFor(descriptor));
202
203
  }
203
204
  if (descriptor.type === 'date' && Number.isNaN(new Date(raw).getTime())) {
204
- return messages.date(descriptor.label);
205
+ return messages.date(labelFor(descriptor));
205
206
  }
206
207
  if (descriptor.type === 'enum' && descriptor.enumValues && !descriptor.enumValues.includes(raw)) {
207
- return messages.option(descriptor.label);
208
+ return messages.option(labelFor(descriptor));
208
209
  }
209
210
  return undefined;
210
211
  }
@@ -221,11 +222,11 @@ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCo
221
222
  * `items[2].quantity`, so the grid can put the message in the cell that caused
222
223
  * it. Read-only child members are skipped for the reason they always are: no
223
224
  * editor exists for them, so no rule the user could satisfy applies.
224
- */ function validateComposition(descriptor, values, messages) {
225
+ */ function validateComposition(descriptor, values, messages, labelFor) {
225
226
  const errors = {};
226
227
  const rows = readRowDrafts(values[descriptor.name]);
227
228
  if (descriptor.required && rows.length === 0) {
228
- errors[descriptor.name] = messages.required(descriptor.label);
229
+ errors[descriptor.name] = messages.required(labelFor(descriptor));
229
230
  return errors;
230
231
  }
231
232
  if (descriptor.childType === undefined) return errors;
@@ -237,11 +238,11 @@ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCo
237
238
  const raw = (_row_column_name = row[column.name]) != null ? _row_column_name : '';
238
239
  const path = rowFieldPath(descriptor.name, index, column.name);
239
240
  if (column.required && raw.trim() === '') {
240
- errors[path] = messages.required(column.label);
241
+ errors[path] = messages.required(labelFor(column));
241
242
  continue;
242
243
  }
243
244
  if (raw === '') continue;
244
- const message = formatMessage(column, raw, messages);
245
+ const message = formatMessage(column, raw, messages, labelFor);
245
246
  if (message !== undefined) errors[path] = message;
246
247
  }
247
248
  });
@@ -308,8 +309,8 @@ import { describeChildColumns, editableChildColumns, EntifixLogicError, EntityCo
308
309
  * The single composition point for a draft's errors: metadata rules first, then
309
310
  * the entity's schema, then the caller's `validate` callback. Later sources win
310
311
  * on conflict, so a caller can always override a message it disagrees with.
311
- */ export function composeEntityFormErrors({ descriptors, values, messages, schema, translateIssue, validate }) {
312
- const metadata = validateEntityDraft(descriptors, values, messages);
312
+ */ export function composeEntityFormErrors({ descriptors, values, messages, labelFor, schema, translateIssue, validate }) {
313
+ const metadata = validateEntityDraft(descriptors, values, messages, labelFor);
313
314
  const schemaIssues = schema ? readSchemaIssues(schema, values, translateIssue) : {
314
315
  fields: {},
315
316
  form: undefined
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"sourcesContent":["import {\n describeChildColumns,\n editableChildColumns,\n EntifixLogicError,\n EntityCollectionLink,\n type EntityDraft,\n type EntityDraftValue,\n type EntityFieldDescriptor,\n EntityLink,\n type EntityRowDraft,\n isRowDraftArray,\n joinFieldPath,\n readDraftString,\n readRowDrafts,\n ROW_KEY,\n rowFieldPath,\n seededRowKey,\n type StandardSchemaV1,\n type StandardSchemaV1Issue,\n} from '@entifix/core';\n\n/**\n * A member no form writes back, whatever the draft says about it — and the only\n * one is a read-only member, because it is the only one no editor may ever\n * exist for.\n *\n * `linkCollection` used to be here too, which meant `required` on a to-many\n * relation was silently unenforced: a rule the entity declared and the form\n * never applied. Whether a collection has an editor yet is a question about the\n * controls, not about what the domain insists on, so it is excluded from the\n * *format* check below instead.\n */\nfunction isNeverEdited(descriptor: EntityFieldDescriptor): boolean {\n return descriptor.readonly;\n}\n\n/**\n * A member whose *format* the metadata can judge. Both relation shapes are\n * excluded: their draft value is a foreign key (or a joined list of them), and\n * \"is this a real id\" is a question only the service can answer — but whether\n * one is present at all is checked, which is what `required` on a relation has\n * to mean.\n *\n * A `composition` is excluded here too, but it is no longer unjudged: its rows\n * are checked one at a time by {@link validateRowDrafts}, against the child's\n * own descriptors. What has no meaning is a *format* rule on the collection\n * itself — there is no string to judge, only rows.\n *\n * `scalarCollection` is deliberately *not* excluded — its comma list is real\n * text the user typed.\n */\nfunction hasCheckableFormat(descriptor: EntityFieldDescriptor): boolean {\n return (\n descriptor.type !== 'link' &&\n descriptor.type !== 'linkCollection' &&\n descriptor.type !== 'composition'\n );\n}\n\n/**\n * The value a field seeds with from a record.\n *\n * A scalar seeds as a **string**: links with their foreign key(s), dates with a\n * `yyyy-mm-dd` value a `date` input accepts, everything else stringified.\n *\n * The array branch is explicit rather than left to `String(raw)`, even though\n * the two produce the same characters today. `String(['a','b'])` is `'a,b'` by\n * way of `Array.prototype.toString`, which is an accident: it was doing the\n * work of a `scalarCollection` seed before the type existed, and\n * `reconstructEntity` was handing the same string straight back as a `string`.\n * Both halves were wrong in the same direction, so the fixed-point spec could\n * not see it. Now the join is declared here and the split is declared there,\n * and they are inverses on purpose.\n *\n * A **`composition`** is the one member that does not seed as a string, because\n * it has no lossless string form at all — its rows seed as row drafts, each\n * with a freshly minted key. That key is the only thing here the record does not\n * supply, and it is minted at seed time rather than at render so that the rows a\n * form starts with are already addressable.\n */\nexport function seedFieldValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityDraftValue {\n if (descriptor.type === 'composition') {\n return seedRowDrafts(descriptor, entity);\n }\n return seedScalarValue(descriptor, entity);\n}\n\n/** {@link seedFieldValue} for every member that round-trips through a string. */\nfunction seedScalarValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): string {\n if (entity == null) return '';\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (raw == null) return '';\n if (raw instanceof EntityLink) return raw.id == null ? '' : String(raw.id);\n if (raw instanceof EntityCollectionLink) return raw.ids.map(String).join(',');\n if (raw instanceof Date) return raw.toISOString().slice(0, 10);\n if (Array.isArray(raw)) return raw.map(String).join(',');\n return String(raw);\n}\n\n/**\n * The rows a composition member seeds with.\n *\n * Each row is walked with the child's **own** descriptors, through the same\n * `seedScalarValue` the master's members use — so a child's `date` seeds as\n * `yyyy-mm-dd` on a line exactly as it would on a record, and\n * `reconstructChild` is its inverse for the same reason `coerceFieldValue` is.\n *\n * A row arrives as a plain object as readily as an instance (a payload off the\n * wire, a fixture), which is fine: nothing here asks what class it is, matching\n * `ChildConstructor`'s rule that a child is never `instanceof`-tested.\n *\n * A member with no declared child seeds as no rows — there are no columns to\n * walk, so there is nothing a row could be.\n */\nfunction seedRowDrafts(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityRowDraft[] {\n if (entity == null || descriptor.childType === undefined) return [];\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (!Array.isArray(raw)) return [];\n\n const columns = describeChildColumns(descriptor.childType);\n\n return raw.map((row, index) => {\n // Deterministic, so re-seeding the same record produces the same draft —\n // see `seededRowKey` for the render loop the random one causes.\n const draft: EntityRowDraft = { [ROW_KEY]: seededRowKey(index) };\n for (const column of columns) {\n draft[column.name] = seedScalarValue(column, row);\n }\n return draft;\n });\n}\n\n/** Builds the initial draft for a record (or an empty one for a create). */\nexport function seedEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n entity: unknown,\n): EntityDraft {\n const draft: EntityDraft = {};\n for (const descriptor of descriptors) {\n draft[descriptor.name] = seedFieldValue(descriptor, entity);\n }\n return draft;\n}\n\n/**\n * Restores a persisted draft **over** a freshly seeded one, rather than in place\n * of it.\n *\n * A draft outlives the entity that wrote it — it sits in IndexedDB across\n * refreshes and deployments — so the two can disagree by the time it comes back.\n * Replacing the seed outright is how that disagreement becomes a bug the user\n * sees: a member added since the draft was written has no entry, reaches the form\n * engine as `undefined`, and its input flips from controlled to uncontrolled\n * mid-render. A member since removed goes the other way, surviving as a value\n * nothing renders and `reconstructEntity` cannot assign.\n *\n * So the seed decides the *keys* and the draft decides the *values*: a name the\n * entity no longer declares is dropped, and a name it declares that the draft\n * lacks keeps its seeded value.\n *\n * **A value is restored only in the shape its member can hold**, which is now\n * two shapes rather than one. A scalar takes a string; a `composition` takes a\n * readable row list. Anything else is dropped back to its seed — a member that\n * changed type between the write and the read, or a row list from a build that\n * did not carry keys, restores as the record's own value instead of as\n * something no editor can render. This is per member, so one unreadable entry\n * never costs the user the rest of the form.\n *\n * This is the shape guard at the one place the shape is actually known. The\n * store-level version check (`drafts-state.ts`) can only decide that a whole\n * generation of drafts is unreadable; it cannot know which members one entity\n * declares.\n */\nexport function restoreEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n seed: EntityDraft,\n persisted: EntityDraft | undefined,\n): EntityDraft {\n if (persisted === undefined) return seed;\n\n const restored: EntityDraft = { ...seed };\n for (const descriptor of descriptors) {\n const value = persisted[descriptor.name];\n if (value === undefined) continue;\n\n if (descriptor.type === 'composition') {\n if (isRowDraftArray(value)) restored[descriptor.name] = value;\n continue;\n }\n if (typeof value === 'string') restored[descriptor.name] = value;\n }\n return restored;\n}\n\n/** The slice of a form field's state this module reads: its error list. */\nexport interface FieldErrorMeta {\n errors?: readonly unknown[];\n}\n\n/**\n * Flattens the form engine's per-field state into the `name → message` map the\n * agnostic `EntityForm` renders. Only the first message per field survives: a\n * row shows one line, and the metadata rules already stop at the first failure.\n *\n * Kept structural rather than typed against the engine so the rest of this\n * module stays independent of which form library backs the hook.\n */\nexport function readFieldErrors(\n fieldMeta: Partial<Record<string, FieldErrorMeta | undefined>>,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const [name, meta] of Object.entries(fieldMeta)) {\n const message = meta?.errors?.find(entry => typeof entry === 'string');\n if (typeof message === 'string') errors[name] = message;\n }\n\n return errors;\n}\n\n/**\n * The four metadata-derived messages, already localized. Taken as an argument\n * rather than built here so this stays a pure function — and because the\n * sentence cannot be assembled from a label plus a hardcoded suffix: word order\n * differs between locales.\n */\nexport interface EntityDraftMessages {\n required(field: string): string;\n number(field: string): string;\n date(field: string): string;\n option(field: string): string;\n}\n\n/**\n * Validates a draft against what the metadata implies — `required` members must\n * be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only\n * members are skipped entirely; a relation is checked for presence but never\n * for format, since its draft value is a foreign key this side cannot resolve.\n *\n * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}\n * is what layers a schema and the caller's own rules over this.\n */\nexport function validateEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n messages: EntityDraftMessages,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const descriptor of descriptors) {\n if (isNeverEdited(descriptor)) continue;\n\n if (descriptor.type === 'composition') {\n Object.assign(errors, validateComposition(descriptor, values, messages));\n continue;\n }\n\n const raw = readDraftString(values, descriptor.name);\n\n if (descriptor.required && raw.trim() === '') {\n errors[descriptor.name] = messages.required(descriptor.label);\n continue;\n }\n if (raw === '' || !hasCheckableFormat(descriptor)) continue;\n\n const message = formatMessage(descriptor, raw, messages);\n if (message !== undefined) errors[descriptor.name] = message;\n }\n\n return errors;\n}\n\n/**\n * The metadata rule for one filled scalar draft value, or `undefined` when it\n * passes.\n *\n * Extracted so a row's cell is judged by the **same** rule as a record's field.\n * Two copies is how a child's `number` member would quietly start accepting\n * something its master's would reject.\n */\nfunction formatMessage(\n descriptor: EntityFieldDescriptor,\n raw: string,\n messages: EntityDraftMessages,\n): string | undefined {\n if (descriptor.type === 'number' && Number.isNaN(Number(raw))) {\n return messages.number(descriptor.label);\n }\n if (descriptor.type === 'date' && Number.isNaN(new Date(raw).getTime())) {\n return messages.date(descriptor.label);\n }\n if (\n descriptor.type === 'enum' &&\n descriptor.enumValues &&\n !descriptor.enumValues.includes(raw)\n ) {\n return messages.option(descriptor.label);\n }\n return undefined;\n}\n\n/**\n * The rules an owned collection carries, at both of its levels.\n *\n * **`required` on the collection means \"at least one row\"** — the only thing a\n * master can assert about a collection it owns, and a genuinely different fact\n * from `required` on a child member, which is per row. Conflating the two would\n * make an order with three lines, one of them blank, indistinguishable from an\n * order with none.\n *\n * A row's own members are judged against the child's descriptors and reported at\n * `items[2].quantity`, so the grid can put the message in the cell that caused\n * it. Read-only child members are skipped for the reason they always are: no\n * editor exists for them, so no rule the user could satisfy applies.\n */\nfunction validateComposition(\n descriptor: EntityFieldDescriptor,\n values: EntityDraft,\n messages: EntityDraftMessages,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n const rows = readRowDrafts(values[descriptor.name]);\n\n if (descriptor.required && rows.length === 0) {\n errors[descriptor.name] = messages.required(descriptor.label);\n return errors;\n }\n if (descriptor.childType === undefined) return errors;\n\n const columns = editableChildColumns(\n describeChildColumns(descriptor.childType),\n );\n\n rows.forEach((row, index) => {\n for (const column of columns) {\n if (isNeverEdited(column)) continue;\n\n const raw = row[column.name] ?? '';\n const path = rowFieldPath(descriptor.name, index, column.name);\n\n if (column.required && raw.trim() === '') {\n errors[path] = messages.required(column.label);\n continue;\n }\n if (raw === '') continue;\n\n const message = formatMessage(column, raw, messages);\n if (message !== undefined) errors[path] = message;\n }\n });\n\n return errors;\n}\n\n/**\n * The error-map key an issue belongs to — the **whole** path, not its head.\n *\n * It used to read `issue.path?.[0]`, which was right while every member was a\n * scalar and wrong the moment one held rows: `['items', 2, 'quantity']`\n * collapsed to `'items'`, so a rule about the third line's quantity reported\n * itself against the collection and the grid had no way to know which cell it\n * meant. Joining instead produces `items[2].quantity`, which is the key\n * `validateComposition` already writes — so a schema rule and a metadata rule\n * address the same cell the same way and `composeEntityFormErrors` can merge\n * them.\n *\n * A one-segment path still yields exactly the head, so no existing rule changes\n * meaning.\n */\nfunction issueFieldName(issue: StandardSchemaV1Issue): string | undefined {\n const path = issue.path;\n if (path == null || path.length === 0) return undefined;\n\n const segments = path.map(segment => {\n const raw =\n typeof segment === 'object' && segment !== null && 'key' in segment\n ? segment.key\n : segment;\n return typeof raw === 'number' ? raw : String(raw);\n });\n\n return joinFieldPath(segments);\n}\n\n/**\n * Runs a Standard Schema over the draft and sorts its issues into per-field\n * messages plus a form-level one (an issue with no path — a cross-field rule).\n *\n * **Schemas are authored against the string draft**, so they coerce\n * (`z.coerce.number()`) rather than expect typed values: the draft is what the\n * inputs round-trip, and the same schema then validates exactly what the user\n * sees. Only the first issue per field survives, matching how a row renders.\n *\n * Synchronous rules only. An async schema returns a Promise the form's\n * synchronous validation pass cannot wait on, so it is rejected loudly rather\n * than silently passing.\n */\nexport function readSchemaIssues(\n schema: StandardSchemaV1,\n values: EntityDraft,\n translateIssue: (message: string, field: string | undefined) => string,\n): { fields: Record<string, string>; form?: string } {\n const result = schema['~standard'].validate(values);\n\n if (result instanceof Promise) {\n throw new EntifixLogicError(\n 'useEntityForm cannot run an async schema: its validation pass is ' +\n 'synchronous. Move the asynchronous rule into the `validate` option.',\n );\n }\n if (!result.issues) return { fields: {} };\n\n const fields: Record<string, string> = {};\n let form: string | undefined;\n\n for (const issue of result.issues) {\n const name = issueFieldName(issue);\n const message = translateIssue(issue.message, name);\n\n if (name === undefined) form ??= message;\n else fields[name] ??= message;\n }\n\n return { fields, form };\n}\n\n/** Everything {@link composeEntityFormErrors} needs to judge one draft. */\nexport interface ComposeEntityFormErrorsOptions {\n descriptors: readonly EntityFieldDescriptor[];\n values: EntityDraft;\n messages: EntityDraftMessages;\n schema?: StandardSchemaV1;\n translateIssue: (message: string, field: string | undefined) => string;\n validate?: (values: EntityDraft) => Record<string, string>;\n}\n\n/**\n * The single composition point for a draft's errors: metadata rules first, then\n * the entity's schema, then the caller's `validate` callback. Later sources win\n * on conflict, so a caller can always override a message it disagrees with.\n */\nexport function composeEntityFormErrors({\n descriptors,\n values,\n messages,\n schema,\n translateIssue,\n validate,\n}: ComposeEntityFormErrorsOptions): {\n fields: Record<string, string>;\n form?: string;\n} {\n const metadata = validateEntityDraft(descriptors, values, messages);\n const schemaIssues = schema\n ? readSchemaIssues(schema, values, translateIssue)\n : { fields: {}, form: undefined };\n\n return {\n fields: { ...metadata, ...schemaIssues.fields, ...validate?.(values) },\n form: schemaIssues.form,\n };\n}\n\n/**\n * What a caller who supplies no {@link ValidationMessages} gets: the catalog key\n * itself, with the field appended.\n *\n * Honest rather than English — there is no catalog in this package to resolve\n * against — and visible enough in a form that a host notices it has wired no\n * copy. It sits beside the validator rather than in `.types.ts`, which this\n * repository keeps type-only.\n */\nexport const DEFAULT_VALIDATION_MESSAGES: EntityDraftMessages = {\n required: field => `validation.required:${field}`,\n number: field => `validation.number:${field}`,\n date: field => `validation.date:${field}`,\n option: field => `validation.option:${field}`,\n};\n"],"names":["describeChildColumns","editableChildColumns","EntifixLogicError","EntityCollectionLink","EntityLink","isRowDraftArray","joinFieldPath","readDraftString","readRowDrafts","ROW_KEY","rowFieldPath","seededRowKey","isNeverEdited","descriptor","readonly","hasCheckableFormat","type","seedFieldValue","entity","seedRowDrafts","seedScalarValue","raw","name","id","String","ids","map","join","Date","toISOString","slice","Array","isArray","childType","undefined","columns","row","index","draft","column","seedEntityDraft","descriptors","restoreEntityDraft","seed","persisted","restored","value","readFieldErrors","fieldMeta","errors","meta","Object","entries","message","find","entry","validateEntityDraft","values","messages","assign","validateComposition","required","trim","label","formatMessage","Number","isNaN","number","getTime","date","enumValues","includes","option","rows","length","forEach","path","issueFieldName","issue","segments","segment","key","readSchemaIssues","schema","translateIssue","result","validate","Promise","issues","fields","form","composeEntityFormErrors","metadata","schemaIssues","DEFAULT_VALIDATION_MESSAGES","field"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"sourcesContent":["import {\n describeChildColumns,\n editableChildColumns,\n EntifixLogicError,\n EntityCollectionLink,\n type EntityDraft,\n type EntityDraftValue,\n type EntityFieldDescriptor,\n EntityLink,\n type EntityRowDraft,\n isRowDraftArray,\n joinFieldPath,\n readDraftString,\n readRowDrafts,\n ROW_KEY,\n rowFieldPath,\n seededRowKey,\n type StandardSchemaV1,\n type StandardSchemaV1Issue,\n} from '@entifix/core';\n\n/**\n * A member no form writes back, whatever the draft says about it — and the only\n * one is a read-only member, because it is the only one no editor may ever\n * exist for.\n *\n * `linkCollection` used to be here too, which meant `required` on a to-many\n * relation was silently unenforced: a rule the entity declared and the form\n * never applied. Whether a collection has an editor yet is a question about the\n * controls, not about what the domain insists on, so it is excluded from the\n * *format* check below instead.\n */\nfunction isNeverEdited(descriptor: EntityFieldDescriptor): boolean {\n return descriptor.readonly;\n}\n\n/**\n * A member whose *format* the metadata can judge. Both relation shapes are\n * excluded: their draft value is a foreign key (or a joined list of them), and\n * \"is this a real id\" is a question only the service can answer — but whether\n * one is present at all is checked, which is what `required` on a relation has\n * to mean.\n *\n * A `composition` is excluded here too, but it is no longer unjudged: its rows\n * are checked one at a time by {@link validateRowDrafts}, against the child's\n * own descriptors. What has no meaning is a *format* rule on the collection\n * itself — there is no string to judge, only rows.\n *\n * `scalarCollection` is deliberately *not* excluded — its comma list is real\n * text the user typed.\n */\nfunction hasCheckableFormat(descriptor: EntityFieldDescriptor): boolean {\n return (\n descriptor.type !== 'link' &&\n descriptor.type !== 'linkCollection' &&\n descriptor.type !== 'composition'\n );\n}\n\n/**\n * The value a field seeds with from a record.\n *\n * A scalar seeds as a **string**: links with their foreign key(s), dates with a\n * `yyyy-mm-dd` value a `date` input accepts, everything else stringified.\n *\n * The array branch is explicit rather than left to `String(raw)`, even though\n * the two produce the same characters today. `String(['a','b'])` is `'a,b'` by\n * way of `Array.prototype.toString`, which is an accident: it was doing the\n * work of a `scalarCollection` seed before the type existed, and\n * `reconstructEntity` was handing the same string straight back as a `string`.\n * Both halves were wrong in the same direction, so the fixed-point spec could\n * not see it. Now the join is declared here and the split is declared there,\n * and they are inverses on purpose.\n *\n * A **`composition`** is the one member that does not seed as a string, because\n * it has no lossless string form at all — its rows seed as row drafts, each\n * with a freshly minted key. That key is the only thing here the record does not\n * supply, and it is minted at seed time rather than at render so that the rows a\n * form starts with are already addressable.\n */\nexport function seedFieldValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityDraftValue {\n if (descriptor.type === 'composition') {\n return seedRowDrafts(descriptor, entity);\n }\n return seedScalarValue(descriptor, entity);\n}\n\n/** {@link seedFieldValue} for every member that round-trips through a string. */\nfunction seedScalarValue(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): string {\n if (entity == null) return '';\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (raw == null) return '';\n if (raw instanceof EntityLink) return raw.id == null ? '' : String(raw.id);\n if (raw instanceof EntityCollectionLink) return raw.ids.map(String).join(',');\n if (raw instanceof Date) return raw.toISOString().slice(0, 10);\n if (Array.isArray(raw)) return raw.map(String).join(',');\n return String(raw);\n}\n\n/**\n * The rows a composition member seeds with.\n *\n * Each row is walked with the child's **own** descriptors, through the same\n * `seedScalarValue` the master's members use — so a child's `date` seeds as\n * `yyyy-mm-dd` on a line exactly as it would on a record, and\n * `reconstructChild` is its inverse for the same reason `coerceFieldValue` is.\n *\n * A row arrives as a plain object as readily as an instance (a payload off the\n * wire, a fixture), which is fine: nothing here asks what class it is, matching\n * `ChildConstructor`'s rule that a child is never `instanceof`-tested.\n *\n * A member with no declared child seeds as no rows — there are no columns to\n * walk, so there is nothing a row could be.\n */\nfunction seedRowDrafts(\n descriptor: EntityFieldDescriptor,\n entity: unknown,\n): EntityRowDraft[] {\n if (entity == null || descriptor.childType === undefined) return [];\n const raw = (entity as Record<string, unknown>)[descriptor.name];\n if (!Array.isArray(raw)) return [];\n\n const columns = describeChildColumns(descriptor.childType);\n\n return raw.map((row, index) => {\n // Deterministic, so re-seeding the same record produces the same draft —\n // see `seededRowKey` for the render loop the random one causes.\n const draft: EntityRowDraft = { [ROW_KEY]: seededRowKey(index) };\n for (const column of columns) {\n draft[column.name] = seedScalarValue(column, row);\n }\n return draft;\n });\n}\n\n/** Builds the initial draft for a record (or an empty one for a create). */\nexport function seedEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n entity: unknown,\n): EntityDraft {\n const draft: EntityDraft = {};\n for (const descriptor of descriptors) {\n draft[descriptor.name] = seedFieldValue(descriptor, entity);\n }\n return draft;\n}\n\n/**\n * Restores a persisted draft **over** a freshly seeded one, rather than in place\n * of it.\n *\n * A draft outlives the entity that wrote it — it sits in IndexedDB across\n * refreshes and deployments — so the two can disagree by the time it comes back.\n * Replacing the seed outright is how that disagreement becomes a bug the user\n * sees: a member added since the draft was written has no entry, reaches the form\n * engine as `undefined`, and its input flips from controlled to uncontrolled\n * mid-render. A member since removed goes the other way, surviving as a value\n * nothing renders and `reconstructEntity` cannot assign.\n *\n * So the seed decides the *keys* and the draft decides the *values*: a name the\n * entity no longer declares is dropped, and a name it declares that the draft\n * lacks keeps its seeded value.\n *\n * **A value is restored only in the shape its member can hold**, which is now\n * two shapes rather than one. A scalar takes a string; a `composition` takes a\n * readable row list. Anything else is dropped back to its seed — a member that\n * changed type between the write and the read, or a row list from a build that\n * did not carry keys, restores as the record's own value instead of as\n * something no editor can render. This is per member, so one unreadable entry\n * never costs the user the rest of the form.\n *\n * This is the shape guard at the one place the shape is actually known. The\n * store-level version check (`drafts-state.ts`) can only decide that a whole\n * generation of drafts is unreadable; it cannot know which members one entity\n * declares.\n */\nexport function restoreEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n seed: EntityDraft,\n persisted: EntityDraft | undefined,\n): EntityDraft {\n if (persisted === undefined) return seed;\n\n const restored: EntityDraft = { ...seed };\n for (const descriptor of descriptors) {\n const value = persisted[descriptor.name];\n if (value === undefined) continue;\n\n if (descriptor.type === 'composition') {\n if (isRowDraftArray(value)) restored[descriptor.name] = value;\n continue;\n }\n if (typeof value === 'string') restored[descriptor.name] = value;\n }\n return restored;\n}\n\n/** The slice of a form field's state this module reads: its error list. */\nexport interface FieldErrorMeta {\n errors?: readonly unknown[];\n}\n\n/**\n * Flattens the form engine's per-field state into the `name → message` map the\n * agnostic `EntityForm` renders. Only the first message per field survives: a\n * row shows one line, and the metadata rules already stop at the first failure.\n *\n * Kept structural rather than typed against the engine so the rest of this\n * module stays independent of which form library backs the hook.\n */\nexport function readFieldErrors(\n fieldMeta: Partial<Record<string, FieldErrorMeta | undefined>>,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const [name, meta] of Object.entries(fieldMeta)) {\n const message = meta?.errors?.find(entry => typeof entry === 'string');\n if (typeof message === 'string') errors[name] = message;\n }\n\n return errors;\n}\n\n/**\n * The four metadata-derived messages, already localized. Taken as an argument\n * rather than built here so this stays a pure function — and because the\n * sentence cannot be assembled from a label plus a hardcoded suffix: word order\n * differs between locales.\n */\n/**\n * How a message names a field. The default is the descriptor's `label`, which is\n * the accessor's `label` option or its humanized member name — never translated.\n * A host with a catalog resolves `labelKey` instead, so the message names the\n * field in the words its input and column header already use.\n */\nexport type EntityFieldLabeler = (descriptor: EntityFieldDescriptor) => string;\n\nconst labelOf: EntityFieldLabeler = descriptor => descriptor.label;\n\nexport interface EntityDraftMessages {\n required(field: string): string;\n number(field: string): string;\n date(field: string): string;\n option(field: string): string;\n}\n\n/**\n * Validates a draft against what the metadata implies — `required` members must\n * be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only\n * members are skipped entirely; a relation is checked for presence but never\n * for format, since its draft value is a foreign key this side cannot resolve.\n *\n * Metadata is only the first of three rule sources; {@link composeEntityFormErrors}\n * is what layers a schema and the caller's own rules over this.\n */\nexport function validateEntityDraft(\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler = labelOf,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n\n for (const descriptor of descriptors) {\n if (isNeverEdited(descriptor)) continue;\n\n if (descriptor.type === 'composition') {\n Object.assign(\n errors,\n validateComposition(descriptor, values, messages, labelFor),\n );\n continue;\n }\n\n const raw = readDraftString(values, descriptor.name);\n\n if (descriptor.required && raw.trim() === '') {\n errors[descriptor.name] = messages.required(labelFor(descriptor));\n continue;\n }\n if (raw === '' || !hasCheckableFormat(descriptor)) continue;\n\n const message = formatMessage(descriptor, raw, messages, labelFor);\n if (message !== undefined) errors[descriptor.name] = message;\n }\n\n return errors;\n}\n\n/**\n * The metadata rule for one filled scalar draft value, or `undefined` when it\n * passes.\n *\n * Extracted so a row's cell is judged by the **same** rule as a record's field.\n * Two copies is how a child's `number` member would quietly start accepting\n * something its master's would reject.\n */\nfunction formatMessage(\n descriptor: EntityFieldDescriptor,\n raw: string,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): string | undefined {\n if (descriptor.type === 'number' && Number.isNaN(Number(raw))) {\n return messages.number(labelFor(descriptor));\n }\n if (descriptor.type === 'date' && Number.isNaN(new Date(raw).getTime())) {\n return messages.date(labelFor(descriptor));\n }\n if (\n descriptor.type === 'enum' &&\n descriptor.enumValues &&\n !descriptor.enumValues.includes(raw)\n ) {\n return messages.option(labelFor(descriptor));\n }\n return undefined;\n}\n\n/**\n * The rules an owned collection carries, at both of its levels.\n *\n * **`required` on the collection means \"at least one row\"** — the only thing a\n * master can assert about a collection it owns, and a genuinely different fact\n * from `required` on a child member, which is per row. Conflating the two would\n * make an order with three lines, one of them blank, indistinguishable from an\n * order with none.\n *\n * A row's own members are judged against the child's descriptors and reported at\n * `items[2].quantity`, so the grid can put the message in the cell that caused\n * it. Read-only child members are skipped for the reason they always are: no\n * editor exists for them, so no rule the user could satisfy applies.\n */\nfunction validateComposition(\n descriptor: EntityFieldDescriptor,\n values: EntityDraft,\n messages: EntityDraftMessages,\n labelFor: EntityFieldLabeler,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n const rows = readRowDrafts(values[descriptor.name]);\n\n if (descriptor.required && rows.length === 0) {\n errors[descriptor.name] = messages.required(labelFor(descriptor));\n return errors;\n }\n if (descriptor.childType === undefined) return errors;\n\n const columns = editableChildColumns(\n describeChildColumns(descriptor.childType),\n );\n\n rows.forEach((row, index) => {\n for (const column of columns) {\n if (isNeverEdited(column)) continue;\n\n const raw = row[column.name] ?? '';\n const path = rowFieldPath(descriptor.name, index, column.name);\n\n if (column.required && raw.trim() === '') {\n errors[path] = messages.required(labelFor(column));\n continue;\n }\n if (raw === '') continue;\n\n const message = formatMessage(column, raw, messages, labelFor);\n if (message !== undefined) errors[path] = message;\n }\n });\n\n return errors;\n}\n\n/**\n * The error-map key an issue belongs to — the **whole** path, not its head.\n *\n * It used to read `issue.path?.[0]`, which was right while every member was a\n * scalar and wrong the moment one held rows: `['items', 2, 'quantity']`\n * collapsed to `'items'`, so a rule about the third line's quantity reported\n * itself against the collection and the grid had no way to know which cell it\n * meant. Joining instead produces `items[2].quantity`, which is the key\n * `validateComposition` already writes — so a schema rule and a metadata rule\n * address the same cell the same way and `composeEntityFormErrors` can merge\n * them.\n *\n * A one-segment path still yields exactly the head, so no existing rule changes\n * meaning.\n */\nfunction issueFieldName(issue: StandardSchemaV1Issue): string | undefined {\n const path = issue.path;\n if (path == null || path.length === 0) return undefined;\n\n const segments = path.map(segment => {\n const raw =\n typeof segment === 'object' && segment !== null && 'key' in segment\n ? segment.key\n : segment;\n return typeof raw === 'number' ? raw : String(raw);\n });\n\n return joinFieldPath(segments);\n}\n\n/**\n * Runs a Standard Schema over the draft and sorts its issues into per-field\n * messages plus a form-level one (an issue with no path — a cross-field rule).\n *\n * **Schemas are authored against the string draft**, so they coerce\n * (`z.coerce.number()`) rather than expect typed values: the draft is what the\n * inputs round-trip, and the same schema then validates exactly what the user\n * sees. Only the first issue per field survives, matching how a row renders.\n *\n * Synchronous rules only. An async schema returns a Promise the form's\n * synchronous validation pass cannot wait on, so it is rejected loudly rather\n * than silently passing.\n */\nexport function readSchemaIssues(\n schema: StandardSchemaV1,\n values: EntityDraft,\n translateIssue: (message: string, field: string | undefined) => string,\n): { fields: Record<string, string>; form?: string } {\n const result = schema['~standard'].validate(values);\n\n if (result instanceof Promise) {\n throw new EntifixLogicError(\n 'useEntityForm cannot run an async schema: its validation pass is ' +\n 'synchronous. Move the asynchronous rule into the `validate` option.',\n );\n }\n if (!result.issues) return { fields: {} };\n\n const fields: Record<string, string> = {};\n let form: string | undefined;\n\n for (const issue of result.issues) {\n const name = issueFieldName(issue);\n const message = translateIssue(issue.message, name);\n\n if (name === undefined) form ??= message;\n else fields[name] ??= message;\n }\n\n return { fields, form };\n}\n\n/** Everything {@link composeEntityFormErrors} needs to judge one draft. */\nexport interface ComposeEntityFormErrorsOptions {\n descriptors: readonly EntityFieldDescriptor[];\n values: EntityDraft;\n messages: EntityDraftMessages;\n /** How a metadata message names its field. Defaults to the untranslated `label`. */\n labelFor?: EntityFieldLabeler;\n schema?: StandardSchemaV1;\n translateIssue: (message: string, field: string | undefined) => string;\n validate?: (values: EntityDraft) => Record<string, string>;\n}\n\n/**\n * The single composition point for a draft's errors: metadata rules first, then\n * the entity's schema, then the caller's `validate` callback. Later sources win\n * on conflict, so a caller can always override a message it disagrees with.\n */\nexport function composeEntityFormErrors({\n descriptors,\n values,\n messages,\n labelFor,\n schema,\n translateIssue,\n validate,\n}: ComposeEntityFormErrorsOptions): {\n fields: Record<string, string>;\n form?: string;\n} {\n const metadata = validateEntityDraft(descriptors, values, messages, labelFor);\n const schemaIssues = schema\n ? readSchemaIssues(schema, values, translateIssue)\n : { fields: {}, form: undefined };\n\n return {\n fields: { ...metadata, ...schemaIssues.fields, ...validate?.(values) },\n form: schemaIssues.form,\n };\n}\n\n/**\n * What a caller who supplies no {@link ValidationMessages} gets: the catalog key\n * itself, with the field appended.\n *\n * Honest rather than English — there is no catalog in this package to resolve\n * against — and visible enough in a form that a host notices it has wired no\n * copy. It sits beside the validator rather than in `.types.ts`, which this\n * repository keeps type-only.\n */\nexport const DEFAULT_VALIDATION_MESSAGES: EntityDraftMessages = {\n required: field => `validation.required:${field}`,\n number: field => `validation.number:${field}`,\n date: field => `validation.date:${field}`,\n option: field => `validation.option:${field}`,\n};\n"],"names":["describeChildColumns","editableChildColumns","EntifixLogicError","EntityCollectionLink","EntityLink","isRowDraftArray","joinFieldPath","readDraftString","readRowDrafts","ROW_KEY","rowFieldPath","seededRowKey","isNeverEdited","descriptor","readonly","hasCheckableFormat","type","seedFieldValue","entity","seedRowDrafts","seedScalarValue","raw","name","id","String","ids","map","join","Date","toISOString","slice","Array","isArray","childType","undefined","columns","row","index","draft","column","seedEntityDraft","descriptors","restoreEntityDraft","seed","persisted","restored","value","readFieldErrors","fieldMeta","errors","meta","Object","entries","message","find","entry","labelOf","label","validateEntityDraft","values","messages","labelFor","assign","validateComposition","required","trim","formatMessage","Number","isNaN","number","getTime","date","enumValues","includes","option","rows","length","forEach","path","issueFieldName","issue","segments","segment","key","readSchemaIssues","schema","translateIssue","result","validate","Promise","issues","fields","form","composeEntityFormErrors","metadata","schemaIssues","DEFAULT_VALIDATION_MESSAGES","field"],"mappings":"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"}
@@ -59,12 +59,12 @@ import { composeEntityFormErrors, DEFAULT_VALIDATION_MESSAGES, readFieldErrors,
59
59
  // Supplied by the caller, and that is the seam.
60
60
  //
61
61
  // ⚠️ This used to reach into react-i18next and `@entifix/i18n` directly,
62
- // because both React packages are `entifix:react` and the boundary rule
63
- // forbids a sideways import of the controls package that owns the `controls`
62
+ // because both React packages are T3 and the tier register declares no
63
+ // sideways import of the controls package that owns the `controls`
64
64
  // catalog. The effect was that anyone taking the integration hooks also took
65
65
  // i18next and a Spanish catalog — the composition defect the tier register
66
66
  // was written to catch. The one real caller, `entity-crud-form.tsx`, sits a
67
- // layer up where `useT('controls')` is reachable, so it resolves the four
67
+ // tier up where `useT('controls')` is reachable, so it resolves the four
68
68
  // sentences and hands them over.
69
69
  const messages = useMemo(()=>validationMessages != null ? validationMessages : DEFAULT_VALIDATION_MESSAGES, [
70
70
  validationMessages
@@ -78,16 +78,27 @@ import { composeEntityFormErrors, DEFAULT_VALIDATION_MESSAGES, readFieldErrors,
78
78
  // and this one is `useTranslateKey`'s job rather than `useT`'s, because a
79
79
  // schema's message is a key only known at runtime. With nothing supplied the
80
80
  // message renders as authored, which is what `defaultValue` always did.
81
+ // How a message names its field: the catalog's word when the member has a
82
+ // `labelKey` and a translator was supplied, so "Cantidad es obligatorio" rather
83
+ // than the accessor's untranslated `label` inside a Spanish sentence. The same
84
+ // resolver reaches a composition's cells, which are described from the child
85
+ // type inside the validator rather than from these descriptors.
86
+ const labelFor = useCallback((descriptor)=>descriptor.labelKey !== undefined && translateKey !== undefined ? translateKey(descriptor.labelKey, {
87
+ defaultValue: descriptor.label
88
+ }) : descriptor.label, [
89
+ translateKey
90
+ ]);
81
91
  const translateIssue = useCallback((message, field)=>{
82
- var _ref;
83
- var _descriptors_find;
84
- return translateKey === undefined ? message : translateKey(message, {
92
+ if (translateKey === undefined) return message;
93
+ const descriptor = descriptors.find((entry)=>entry.name === field);
94
+ return translateKey(message, {
85
95
  defaultValue: message,
86
- field: (_ref = (_descriptors_find = descriptors.find((entry)=>entry.name === field)) == null ? void 0 : _descriptors_find.label) != null ? _ref : field
96
+ field: descriptor === undefined ? field : labelFor(descriptor)
87
97
  });
88
98
  }, [
89
99
  translateKey,
90
- descriptors
100
+ descriptors,
101
+ labelFor
91
102
  ]);
92
103
  /**
93
104
  * Whether the last {@link submit} reached the caller's `onSubmit`.
@@ -107,6 +118,7 @@ import { composeEntityFormErrors, DEFAULT_VALIDATION_MESSAGES, readFieldErrors,
107
118
  descriptors,
108
119
  values: value,
109
120
  messages,
121
+ labelFor,
110
122
  schema,
111
123
  translateIssue,
112
124
  validate
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.ts"],"sourcesContent":["'use client';\n\nimport {\n describeEntityColumns,\n type Entity,\n type EntityDraft,\n type EntityLinkSelection,\n seedEntityLinkSelection,\n} from '@entifix/core';\nimport { revalidateLogic, useForm, useStore } from '@tanstack/react-form';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport {\n composeEntityFormErrors,\n DEFAULT_VALIDATION_MESSAGES,\n readFieldErrors,\n restoreEntityDraft,\n seedEntityDraft,\n} from './use-entity-form.helpers.js';\nimport type {\n UseEntityFormOptions,\n UseEntityFormResult,\n} from './use-entity-form.types.js';\n\n/**\n * The write half of an entity form: holds the string draft and reports\n * metadata-derived validation, the counterpart to {@link useEntityRecord}\n * (which reads the record it seeds from).\n *\n * It stays out of the transport: it neither fetches nor saves. `onSubmit`\n * receives the validated draft, and the per-entity wrapper reconstructs the\n * domain entity and calls the mutation. The result plugs straight into the\n * agnostic `EntityForm` (`values`/`onFieldChange`/`errors`/`onSubmit`).\n *\n * TanStack Form owns the draft and the submission lifecycle; that is an\n * implementation detail, and the returned shape is deliberately the plain\n * `values`/`errors`/`setField`/`submit`/`isDirty` facade so `EntityForm` — which\n * renders native inputs and knows nothing about any form library — stays\n * agnostic. Fields are therefore never registered through `form.Field`; a\n * form-level validator reporting `{ fields }` reaches them anyway, because\n * `FormApi` creates meta for any field name an error arrives for.\n *\n * `revalidateLogic` is what makes a pristine form quiet: nothing validates until\n * the first submit, and every keystroke revalidates after it. Running one\n * validator under a single cause also keeps a fixed field from holding a stale\n * message left behind by another cause.\n *\n * The seed applies once, so a caller that loads its record after mount keys the\n * form by the record id to reseed — the same convention `EntityForm`'s wrappers\n * already follow.\n */\nexport function useEntityForm<TEntity extends Entity>({\n entityConstructor,\n entity,\n draft,\n fields,\n schema,\n validate,\n validationMessages,\n translateKey,\n onSubmit,\n}: UseEntityFormOptions<TEntity>): UseEntityFormResult {\n // Keyed on the **names**, not the array, so a caller may write the list inline\n // — which every wizard step does, since its field set is part of its own\n // declaration rather than something worth hoisting.\n const scope = fields?.join(',');\n const descriptors = useMemo(() => {\n const described = describeEntityColumns(entityConstructor, entity);\n return scope === undefined\n ? described\n : described.filter(descriptor =>\n scope.split(',').includes(descriptor.name),\n );\n }, [entityConstructor, entity, scope]);\n // A persisted draft is layered over the seed, never substituted for it: the\n // entity decides which members exist, the draft only decides their values.\n // See `restoreEntityDraft` for what a straight substitution costs.\n const restored = draft?.draft;\n const seed = useMemo(\n () =>\n restoreEntityDraft(\n descriptors,\n seedEntityDraft(descriptors, entity),\n restored,\n ),\n [descriptors, entity, restored],\n );\n\n // The relations the record already carries, so opening a loaded record needs no\n // label lookup and a save keeps whatever embedded shape it arrived in. Seeded\n // once, like the draft — a caller that loads late keys the form by record id.\n const [links, setLinks] = useState<EntityLinkSelection>(() =>\n seedEntityLinkSelection(descriptors, entity),\n );\n\n // Supplied by the caller, and that is the seam.\n //\n // ⚠️ This used to reach into react-i18next and `@entifix/i18n` directly,\n // because both React packages are `entifix:react` and the boundary rule\n // forbids a sideways import of the controls package that owns the `controls`\n // catalog. The effect was that anyone taking the integration hooks also took\n // i18next and a Spanish catalog — the composition defect the tier register\n // was written to catch. The one real caller, `entity-crud-form.tsx`, sits a\n // layer up where `useT('controls')` is reachable, so it resolves the four\n // sentences and hands them over.\n const messages = useMemo(\n () => validationMessages ?? DEFAULT_VALIDATION_MESSAGES,\n [validationMessages],\n );\n\n // A schema's message is authored as a catalog key (`validation.minLength`),\n // because a rule written in one language would reach the user untranslated —\n // the one thing the i18n gate exists to prevent. `defaultValue` keeps an\n // unkeyed literal readable instead of rendering the key back at the user, and\n // `field` is offered as a parameter so a message can name its own label.\n // Supplied by the caller for the same reason the four sentences above are —\n // and this one is `useTranslateKey`'s job rather than `useT`'s, because a\n // schema's message is a key only known at runtime. With nothing supplied the\n // message renders as authored, which is what `defaultValue` always did.\n const translateIssue = useCallback(\n (message: string, field: string | undefined) =>\n translateKey === undefined\n ? message\n : translateKey(message, {\n defaultValue: message,\n field:\n descriptors.find(entry => entry.name === field)?.label ?? field,\n }),\n [translateKey, descriptors],\n );\n\n /**\n * Whether the last {@link submit} reached the caller's `onSubmit`.\n *\n * A ref rather than state: nothing renders from it, and making it state would\n * re-render the whole form on every submit attempt for a value only the\n * caller awaiting `submit()` ever reads.\n */\n const submitted = useRef(false);\n\n const form = useForm({\n defaultValues: seed,\n // Quiet until the first submit, then revalidate on every change — the\n // behaviour the hand-rolled `submitted` flag used to produce.\n validationLogic: revalidateLogic(),\n validators: {\n onDynamic: ({ value }: { value: EntityDraft }) => {\n const { fields, form } = composeEntityFormErrors({\n descriptors,\n values: value,\n messages,\n schema,\n translateIssue,\n validate,\n });\n\n if (Object.keys(fields).length === 0 && form === undefined)\n return undefined;\n // `{ fields }` is how a form-level validator addresses individual\n // fields; the bare `form` message is what has no field to sit under.\n return { form, fields };\n },\n },\n onSubmit: ({ value }) => {\n // The engine calls this only when validation passed, so recording that it\n // ran *is* the answer {@link UseEntityFormResult.submit} returns. Reading\n // `form.state.isValid` afterwards would ask the engine a second question\n // whose answer depends on when it settled.\n submitted.current = true;\n return onSubmit(value);\n },\n });\n\n const values = useStore(form.store, state => state.values);\n const fieldMeta = useStore(form.store, state => state.fieldMeta);\n const formError = useStore(form.store, state => state.errorMap.onDynamic);\n // `isDefaultValue`, not `isDirty`: the draft is dirty when it *differs* from\n // its seed, so editing a field and typing the seed value back is not a change\n // the workspace should autosave.\n const isDefaultValue = useStore(form.store, state => state.isDefaultValue);\n\n const errors = useMemo(() => readFieldErrors(fieldMeta), [fieldMeta]);\n\n /**\n * Validate, then submit when clean — and **say whether it went**.\n *\n * The promise is what lets a caller gate on the result. A wizard's \"Siguiente\"\n * is the step's own submit, and before this it had nothing to wait for:\n * `handleSubmit()`'s promise was discarded, so the only way to learn the\n * outcome was to await a tick and re-read `errors`, which is the trap this\n * hook's own specs worked around.\n */\n const submit = useCallback(async () => {\n submitted.current = false;\n await form.handleSubmit();\n return submitted.current;\n }, [form]);\n\n // Both halves of a pick, in one call: the id the draft persists and the\n // instance only memory holds.\n const setLink = useCallback(\n (name: string, picked: Entity | undefined) => {\n setLinks(current => ({ ...current, [name]: picked }));\n form.setFieldValue(name, picked?.id == null ? '' : String(picked.id));\n },\n [form],\n );\n\n // The sidecar half alone. A restored draft holds the id and lost the instance,\n // and something resolved it back — that is not a user pick, so it must not\n // touch the draft: writing the field would be an edit the user did not make,\n // and on a form whose seed *is* the restored draft it would also be a no-op\n // that only risks disagreeing with the engine about equality.\n const hydrateLink = useCallback((name: string, resolved: Entity) => {\n setLinks(current =>\n current[name] === undefined ? { ...current, [name]: resolved } : current,\n );\n }, []);\n\n // Autosave. The seam lives here rather than in each page, so a form gets\n // persistence by naming a store and no call site threads a draft through.\n //\n // Two properties keep it from writing more than it means to. It writes only\n // while the draft *differs* from its seed — and on mount over a restored\n // draft the seed **is** that draft, so opening a tab re-persists nothing. And\n // it is keyed on `save`, which the port requires to be referentially stable;\n // a new identity per render would make every render a write.\n //\n // Nothing here clears: a draft is spent when the write commits, and this hook\n // never learns whether it did. See `EntityDraftStore.clear`.\n const save = draft?.save;\n useEffect(() => {\n if (!isDefaultValue) save?.(values);\n }, [values, isDefaultValue, save]);\n\n return {\n values,\n errors,\n formError: typeof formError === 'string' ? formError : undefined,\n setField: form.setFieldValue,\n links,\n setLink,\n hydrateLink,\n submit,\n isDirty: !isDefaultValue,\n };\n}\n"],"names":["describeEntityColumns","seedEntityLinkSelection","revalidateLogic","useForm","useStore","useCallback","useEffect","useMemo","useRef","useState","composeEntityFormErrors","DEFAULT_VALIDATION_MESSAGES","readFieldErrors","restoreEntityDraft","seedEntityDraft","useEntityForm","entityConstructor","entity","draft","fields","schema","validate","validationMessages","translateKey","onSubmit","scope","join","descriptors","described","undefined","filter","descriptor","split","includes","name","restored","seed","links","setLinks","messages","translateIssue","message","field","defaultValue","find","entry","label","submitted","form","defaultValues","validationLogic","validators","onDynamic","value","values","Object","keys","length","current","store","state","fieldMeta","formError","errorMap","isDefaultValue","errors","submit","handleSubmit","setLink","picked","setFieldValue","id","String","hydrateLink","resolved","save","setField","isDirty"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.ts"],"sourcesContent":["'use client';\n\nimport {\n describeEntityColumns,\n type Entity,\n type EntityDraft,\n type EntityFieldDescriptor,\n type EntityLinkSelection,\n seedEntityLinkSelection,\n} from '@entifix/core';\nimport { revalidateLogic, useForm, useStore } from '@tanstack/react-form';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport {\n composeEntityFormErrors,\n DEFAULT_VALIDATION_MESSAGES,\n readFieldErrors,\n restoreEntityDraft,\n seedEntityDraft,\n} from './use-entity-form.helpers.js';\nimport type {\n UseEntityFormOptions,\n UseEntityFormResult,\n} from './use-entity-form.types.js';\n\n/**\n * The write half of an entity form: holds the string draft and reports\n * metadata-derived validation, the counterpart to {@link useEntityRecord}\n * (which reads the record it seeds from).\n *\n * It stays out of the transport: it neither fetches nor saves. `onSubmit`\n * receives the validated draft, and the per-entity wrapper reconstructs the\n * domain entity and calls the mutation. The result plugs straight into the\n * agnostic `EntityForm` (`values`/`onFieldChange`/`errors`/`onSubmit`).\n *\n * TanStack Form owns the draft and the submission lifecycle; that is an\n * implementation detail, and the returned shape is deliberately the plain\n * `values`/`errors`/`setField`/`submit`/`isDirty` facade so `EntityForm` — which\n * renders native inputs and knows nothing about any form library — stays\n * agnostic. Fields are therefore never registered through `form.Field`; a\n * form-level validator reporting `{ fields }` reaches them anyway, because\n * `FormApi` creates meta for any field name an error arrives for.\n *\n * `revalidateLogic` is what makes a pristine form quiet: nothing validates until\n * the first submit, and every keystroke revalidates after it. Running one\n * validator under a single cause also keeps a fixed field from holding a stale\n * message left behind by another cause.\n *\n * The seed applies once, so a caller that loads its record after mount keys the\n * form by the record id to reseed — the same convention `EntityForm`'s wrappers\n * already follow.\n */\nexport function useEntityForm<TEntity extends Entity>({\n entityConstructor,\n entity,\n draft,\n fields,\n schema,\n validate,\n validationMessages,\n translateKey,\n onSubmit,\n}: UseEntityFormOptions<TEntity>): UseEntityFormResult {\n // Keyed on the **names**, not the array, so a caller may write the list inline\n // — which every wizard step does, since its field set is part of its own\n // declaration rather than something worth hoisting.\n const scope = fields?.join(',');\n const descriptors = useMemo(() => {\n const described = describeEntityColumns(entityConstructor, entity);\n return scope === undefined\n ? described\n : described.filter(descriptor =>\n scope.split(',').includes(descriptor.name),\n );\n }, [entityConstructor, entity, scope]);\n // A persisted draft is layered over the seed, never substituted for it: the\n // entity decides which members exist, the draft only decides their values.\n // See `restoreEntityDraft` for what a straight substitution costs.\n const restored = draft?.draft;\n const seed = useMemo(\n () =>\n restoreEntityDraft(\n descriptors,\n seedEntityDraft(descriptors, entity),\n restored,\n ),\n [descriptors, entity, restored],\n );\n\n // The relations the record already carries, so opening a loaded record needs no\n // label lookup and a save keeps whatever embedded shape it arrived in. Seeded\n // once, like the draft — a caller that loads late keys the form by record id.\n const [links, setLinks] = useState<EntityLinkSelection>(() =>\n seedEntityLinkSelection(descriptors, entity),\n );\n\n // Supplied by the caller, and that is the seam.\n //\n // ⚠️ This used to reach into react-i18next and `@entifix/i18n` directly,\n // because both React packages are T3 and the tier register declares no\n // sideways import of the controls package that owns the `controls`\n // catalog. The effect was that anyone taking the integration hooks also took\n // i18next and a Spanish catalog — the composition defect the tier register\n // was written to catch. The one real caller, `entity-crud-form.tsx`, sits a\n // tier up where `useT('controls')` is reachable, so it resolves the four\n // sentences and hands them over.\n const messages = useMemo(\n () => validationMessages ?? DEFAULT_VALIDATION_MESSAGES,\n [validationMessages],\n );\n\n // A schema's message is authored as a catalog key (`validation.minLength`),\n // because a rule written in one language would reach the user untranslated —\n // the one thing the i18n gate exists to prevent. `defaultValue` keeps an\n // unkeyed literal readable instead of rendering the key back at the user, and\n // `field` is offered as a parameter so a message can name its own label.\n // Supplied by the caller for the same reason the four sentences above are —\n // and this one is `useTranslateKey`'s job rather than `useT`'s, because a\n // schema's message is a key only known at runtime. With nothing supplied the\n // message renders as authored, which is what `defaultValue` always did.\n // How a message names its field: the catalog's word when the member has a\n // `labelKey` and a translator was supplied, so \"Cantidad es obligatorio\" rather\n // than the accessor's untranslated `label` inside a Spanish sentence. The same\n // resolver reaches a composition's cells, which are described from the child\n // type inside the validator rather than from these descriptors.\n const labelFor = useCallback(\n (descriptor: EntityFieldDescriptor) =>\n descriptor.labelKey !== undefined && translateKey !== undefined\n ? translateKey(descriptor.labelKey, { defaultValue: descriptor.label })\n : descriptor.label,\n [translateKey],\n );\n\n const translateIssue = useCallback(\n (message: string, field: string | undefined) => {\n if (translateKey === undefined) return message;\n const descriptor = descriptors.find(entry => entry.name === field);\n return translateKey(message, {\n defaultValue: message,\n field: descriptor === undefined ? field : labelFor(descriptor),\n });\n },\n [translateKey, descriptors, labelFor],\n );\n\n /**\n * Whether the last {@link submit} reached the caller's `onSubmit`.\n *\n * A ref rather than state: nothing renders from it, and making it state would\n * re-render the whole form on every submit attempt for a value only the\n * caller awaiting `submit()` ever reads.\n */\n const submitted = useRef(false);\n\n const form = useForm({\n defaultValues: seed,\n // Quiet until the first submit, then revalidate on every change — the\n // behaviour the hand-rolled `submitted` flag used to produce.\n validationLogic: revalidateLogic(),\n validators: {\n onDynamic: ({ value }: { value: EntityDraft }) => {\n const { fields, form } = composeEntityFormErrors({\n descriptors,\n values: value,\n messages,\n labelFor,\n schema,\n translateIssue,\n validate,\n });\n\n if (Object.keys(fields).length === 0 && form === undefined)\n return undefined;\n // `{ fields }` is how a form-level validator addresses individual\n // fields; the bare `form` message is what has no field to sit under.\n return { form, fields };\n },\n },\n onSubmit: ({ value }) => {\n // The engine calls this only when validation passed, so recording that it\n // ran *is* the answer {@link UseEntityFormResult.submit} returns. Reading\n // `form.state.isValid` afterwards would ask the engine a second question\n // whose answer depends on when it settled.\n submitted.current = true;\n return onSubmit(value);\n },\n });\n\n const values = useStore(form.store, state => state.values);\n const fieldMeta = useStore(form.store, state => state.fieldMeta);\n const formError = useStore(form.store, state => state.errorMap.onDynamic);\n // `isDefaultValue`, not `isDirty`: the draft is dirty when it *differs* from\n // its seed, so editing a field and typing the seed value back is not a change\n // the workspace should autosave.\n const isDefaultValue = useStore(form.store, state => state.isDefaultValue);\n\n const errors = useMemo(() => readFieldErrors(fieldMeta), [fieldMeta]);\n\n /**\n * Validate, then submit when clean — and **say whether it went**.\n *\n * The promise is what lets a caller gate on the result. A wizard's \"Siguiente\"\n * is the step's own submit, and before this it had nothing to wait for:\n * `handleSubmit()`'s promise was discarded, so the only way to learn the\n * outcome was to await a tick and re-read `errors`, which is the trap this\n * hook's own specs worked around.\n */\n const submit = useCallback(async () => {\n submitted.current = false;\n await form.handleSubmit();\n return submitted.current;\n }, [form]);\n\n // Both halves of a pick, in one call: the id the draft persists and the\n // instance only memory holds.\n const setLink = useCallback(\n (name: string, picked: Entity | undefined) => {\n setLinks(current => ({ ...current, [name]: picked }));\n form.setFieldValue(name, picked?.id == null ? '' : String(picked.id));\n },\n [form],\n );\n\n // The sidecar half alone. A restored draft holds the id and lost the instance,\n // and something resolved it back — that is not a user pick, so it must not\n // touch the draft: writing the field would be an edit the user did not make,\n // and on a form whose seed *is* the restored draft it would also be a no-op\n // that only risks disagreeing with the engine about equality.\n const hydrateLink = useCallback((name: string, resolved: Entity) => {\n setLinks(current =>\n current[name] === undefined ? { ...current, [name]: resolved } : current,\n );\n }, []);\n\n // Autosave. The seam lives here rather than in each page, so a form gets\n // persistence by naming a store and no call site threads a draft through.\n //\n // Two properties keep it from writing more than it means to. It writes only\n // while the draft *differs* from its seed — and on mount over a restored\n // draft the seed **is** that draft, so opening a tab re-persists nothing. And\n // it is keyed on `save`, which the port requires to be referentially stable;\n // a new identity per render would make every render a write.\n //\n // Nothing here clears: a draft is spent when the write commits, and this hook\n // never learns whether it did. See `EntityDraftStore.clear`.\n const save = draft?.save;\n useEffect(() => {\n if (!isDefaultValue) save?.(values);\n }, [values, isDefaultValue, save]);\n\n return {\n values,\n errors,\n formError: typeof formError === 'string' ? formError : undefined,\n setField: form.setFieldValue,\n links,\n setLink,\n hydrateLink,\n submit,\n isDirty: !isDefaultValue,\n };\n}\n"],"names":["describeEntityColumns","seedEntityLinkSelection","revalidateLogic","useForm","useStore","useCallback","useEffect","useMemo","useRef","useState","composeEntityFormErrors","DEFAULT_VALIDATION_MESSAGES","readFieldErrors","restoreEntityDraft","seedEntityDraft","useEntityForm","entityConstructor","entity","draft","fields","schema","validate","validationMessages","translateKey","onSubmit","scope","join","descriptors","described","undefined","filter","descriptor","split","includes","name","restored","seed","links","setLinks","messages","labelFor","labelKey","defaultValue","label","translateIssue","message","field","find","entry","submitted","form","defaultValues","validationLogic","validators","onDynamic","value","values","Object","keys","length","current","store","state","fieldMeta","formError","errorMap","isDefaultValue","errors","submit","handleSubmit","setLink","picked","setFieldValue","id","String","hydrateLink","resolved","save","setField","isDirty"],"mappings":"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"}
@@ -4,16 +4,16 @@ import type { EntityDraftMessages } from './use-entity-form.helpers.js';
4
4
  * Where an autosaved draft is persisted, as the form sees it.
5
5
  *
6
6
  * A **port**, because the store cannot be imported: `useDraft` and the zustand
7
- * store behind it are `layer:shell`, this package is `layer:entifix`, and
8
- * `@nx/enforce-module-boundaries` fails the build on the upward edge. Retagging
9
- * to make it legal would put an IndexedDB store under the framework layer, so
7
+ * store behind it are in `@entifix/next-shell` (T4), this package is T3, and
8
+ * `@nx/enforce-module-boundaries` fails the build on the upward edge. Moving
9
+ * the store down would put IndexedDB into the agnostic UI tier, so
10
10
  * the shell implements this instead — `useEntityDraft` in `@entifix/next-shell`
11
11
  * is the one adapter today.
12
12
  *
13
13
  * It lives here rather than in `@entifix/core`, and the asymmetry with
14
14
  * `EntityLinkSource` is deliberate: that port sits in core because
15
15
  * `@entifix/react-controls` and `@entifix/react-integration` are both
16
- * `entifix:react` and may not import each other, so they *had* to meet below
16
+ * T3 with no declared edge between them, so they *had* to meet below
17
17
  * both. Nothing forces that here — `@entifix/next-shell` already imports this
18
18
  * hook — so the port stays beside the options it joins.
19
19
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.types.ts"],"sourcesContent":["import type {\n Entity,\n EntityConstructor,\n EntityDraft,\n EntityDraftValue,\n EntityLinkSelection,\n StandardSchemaV1,\n} from '@entifix/core';\n\nimport type { EntityDraftMessages } from './use-entity-form.helpers.js';\n\n/**\n * Where an autosaved draft is persisted, as the form sees it.\n *\n * A **port**, because the store cannot be imported: `useDraft` and the zustand\n * store behind it are `layer:shell`, this package is `layer:entifix`, and\n * `@nx/enforce-module-boundaries` fails the build on the upward edge. Retagging\n * to make it legal would put an IndexedDB store under the framework layer, so\n * the shell implements this instead — `useEntityDraft` in `@entifix/next-shell`\n * is the one adapter today.\n *\n * It lives here rather than in `@entifix/core`, and the asymmetry with\n * `EntityLinkSource` is deliberate: that port sits in core because\n * `@entifix/react-controls` and `@entifix/react-integration` are both\n * `entifix:react` and may not import each other, so they *had* to meet below\n * both. Nothing forces that here — `@entifix/next-shell` already imports this\n * hook — so the port stays beside the options it joins.\n *\n * Everything it carries is JSON round-trippable, because a draft is written\n * through `createJSONStorage`\n * ([ADR 0032](../../../../../../../docs/adr/0032-what-may-live-in-an-autosaved-draft.md)).\n * `EntityDraft` is that guarantee's compile-time half.\n */\nexport interface EntityDraftStore {\n /**\n * The persisted draft, layered over the record's seed — how a workspace\n * restores an autosaved, half-finished edit.\n *\n * It wins on *values*, never on *keys*: a member the entity no longer declares\n * is dropped and one the draft never held keeps its seeded value, so a draft\n * written under an older shape cannot leave a field uncontrolled. See\n * `restoreEntityDraft`.\n */\n readonly draft?: EntityDraft;\n /**\n * Persist the draft. Called on every edit away from the seed.\n *\n * Must be referentially stable across renders: the hook writes from an effect\n * keyed on it, so a new identity each render turns every render into a write.\n */\n save(draft: EntityDraft): void;\n /**\n * Discard the persisted draft.\n *\n * **The hook never calls this.** A draft is spent when the write *commits*,\n * and the hook does not know whether it did — it neither fetches nor saves.\n * The host that owns the mutation calls it on success. Naming it on the port\n * anyway is what stops each host re-deriving the address it would clear by.\n */\n clear(): void;\n}\n\nexport interface UseEntityFormOptions<TEntity extends Entity> {\n /** Metadata source; fields and their validation rules derive from it. */\n entityConstructor: EntityConstructor<TEntity>;\n /**\n * Copy for the four field-level validation failures. Omit and the keys\n * render, which is what an adopter with no i18n library sees.\n */\n validationMessages?: EntityDraftMessages;\n /**\n * Resolves a schema issue's message, which is authored as a catalog key\n * (`validation.minLength`) so a rule written in one language never reaches a\n * user untranslated. `useTranslateKey` from the controls package is what a\n * host passes; omit it and the message renders as authored.\n */\n translateKey?: (key: string, params?: Record<string, unknown>) => string;\n /** The record being edited; seeds the initial draft. Omit to create. */\n entity?: TEntity;\n /**\n * Where to persist this form's draft, so a half-finished edit survives a\n * refresh. Omit and the form is ephemeral — which is what a plain route wants,\n * and why autosave is opt-in rather than a flag a host can set wrong.\n */\n draft?: EntityDraftStore;\n /**\n * Edit only these members, by accessor name. Omit and the form owns them all.\n *\n * It narrows the **descriptors**, so it narrows seeding, coercion *and*\n * validation together — which is the whole point. A wizard splits one entity\n * across several steps, and without this every step would validate the whole\n * record: step one could not advance until members it does not show were\n * filled, and the required-ness of a field would depend on which step you\n * were standing on.\n *\n * The submit therefore receives a **partial** draft. Merging the steps and\n * rebuilding the entity is the host's job, exactly as it already is for one\n * form ([ADR 0045](../../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).\n *\n * A fresh array literal per render is fine: the memo behind it keys on the\n * names rather than the array's identity.\n */\n fields?: readonly string[];\n /**\n * A Standard Schema (Zod, Valibot, ArkType — anything exposing `~standard`)\n * for the rules metadata cannot express: regex, min/max, cross-field.\n *\n * Authored against the **string draft**, so it coerces (`z.coerce.number()`)\n * rather than expecting typed values. Each message is a catalog key\n * (`validation.minLength`), resolved against the `controls` namespace with the\n * literal as its fallback — a rule written in one language would otherwise\n * reach the user untranslated. An issue with no path becomes `formError`.\n *\n * Synchronous rules only: validation runs in a synchronous pass, so an async\n * schema throws rather than silently passing. Put asynchronous checks — a\n * uniqueness lookup, say — behind {@link UseEntityFormOptions.validate}.\n */\n schema?: StandardSchemaV1;\n /**\n * Extra validation beyond the metadata and schema rules, returning a\n * field-name → message map. Applied last, so it wins on conflict.\n */\n validate?: (values: EntityDraft) => Record<string, string>;\n /** Called with the draft once it passes validation. */\n onSubmit: (values: EntityDraft) => void | Promise<void>;\n}\n\nexport interface UseEntityFormResult {\n /** The current draft. Feed straight into `EntityForm`'s `values`. */\n values: EntityDraft;\n /** Field-name → message, surfaced only after a submit attempt. */\n errors: Record<string, string>;\n /**\n * The message that belongs to no single field — a cross-field schema rule.\n * Feed into `EntityForm`'s `formError`, which has nowhere else to show it.\n */\n formError?: string;\n /**\n * Feed into `EntityForm`'s `onFieldChange`.\n *\n * A scalar takes a string; a `composition` takes its list of rows, the one\n * member with no lossless string form.\n */\n setField: (name: string, value: EntityDraftValue) => void;\n /**\n * The instances behind the relation ids the draft holds — seeded from the\n * record's already-materialized links, extended by every pick, and refilled\n * from a resolved id by {@link UseEntityFormResult.hydrateLink}.\n *\n * Pass to `applyEntityLinks` at submit: the draft alone cannot express an\n * embedded relation, because an id is not an entity. It is not persisted — a\n * restored draft arrives with ids and no instances, which is what\n * `hydrateLink` exists to repair.\n */\n links: EntityLinkSelection;\n /**\n * Feed into `EntityForm`'s `onLinkChange`. Records the picked instance and\n * writes its id into the draft, which stays the serializable source of truth.\n */\n setLink: (name: string, entity: Entity | undefined) => void;\n /**\n * Record an instance resolved from an id the draft already held — the restore\n * path, not a pick.\n *\n * Unlike {@link UseEntityFormResult.setLink} it writes only the sidecar, so it\n * cannot dirty a form the user has not touched. It also never overwrites an\n * entry that is already there: a pick in flight is newer than a lookup that\n * started before it.\n */\n hydrateLink: (name: string, resolved: Entity) => void;\n /**\n * Validate, then submit when clean. Feed into `EntityForm`'s `onSubmit`.\n *\n * **Resolves `true` only when the submit actually ran**, so a caller that has\n * to gate on the outcome — a wizard, whose \"Siguiente\" *is* the step's submit\n * — can await it instead of awaiting a tick and re-reading `errors`. A caller\n * with nothing to gate ignores the promise and is unaffected, which is every\n * existing one.\n */\n submit: () => Promise<boolean>;\n /** True once any field has been edited away from its seed. */\n isDirty: boolean;\n}\n"],"names":[],"mappings":"AA+HA,WAuDC"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-form/use-entity-form.types.ts"],"sourcesContent":["import type {\n Entity,\n EntityConstructor,\n EntityDraft,\n EntityDraftValue,\n EntityLinkSelection,\n StandardSchemaV1,\n} from '@entifix/core';\n\nimport type { EntityDraftMessages } from './use-entity-form.helpers.js';\n\n/**\n * Where an autosaved draft is persisted, as the form sees it.\n *\n * A **port**, because the store cannot be imported: `useDraft` and the zustand\n * store behind it are in `@entifix/next-shell` (T4), this package is T3, and\n * `@nx/enforce-module-boundaries` fails the build on the upward edge. Moving\n * the store down would put IndexedDB into the agnostic UI tier, so\n * the shell implements this instead — `useEntityDraft` in `@entifix/next-shell`\n * is the one adapter today.\n *\n * It lives here rather than in `@entifix/core`, and the asymmetry with\n * `EntityLinkSource` is deliberate: that port sits in core because\n * `@entifix/react-controls` and `@entifix/react-integration` are both\n * T3 with no declared edge between them, so they *had* to meet below\n * both. Nothing forces that here — `@entifix/next-shell` already imports this\n * hook — so the port stays beside the options it joins.\n *\n * Everything it carries is JSON round-trippable, because a draft is written\n * through `createJSONStorage`\n * ([ADR 0032](../../../../../../../docs/adr/0032-what-may-live-in-an-autosaved-draft.md)).\n * `EntityDraft` is that guarantee's compile-time half.\n */\nexport interface EntityDraftStore {\n /**\n * The persisted draft, layered over the record's seed — how a workspace\n * restores an autosaved, half-finished edit.\n *\n * It wins on *values*, never on *keys*: a member the entity no longer declares\n * is dropped and one the draft never held keeps its seeded value, so a draft\n * written under an older shape cannot leave a field uncontrolled. See\n * `restoreEntityDraft`.\n */\n readonly draft?: EntityDraft;\n /**\n * Persist the draft. Called on every edit away from the seed.\n *\n * Must be referentially stable across renders: the hook writes from an effect\n * keyed on it, so a new identity each render turns every render into a write.\n */\n save(draft: EntityDraft): void;\n /**\n * Discard the persisted draft.\n *\n * **The hook never calls this.** A draft is spent when the write *commits*,\n * and the hook does not know whether it did — it neither fetches nor saves.\n * The host that owns the mutation calls it on success. Naming it on the port\n * anyway is what stops each host re-deriving the address it would clear by.\n */\n clear(): void;\n}\n\nexport interface UseEntityFormOptions<TEntity extends Entity> {\n /** Metadata source; fields and their validation rules derive from it. */\n entityConstructor: EntityConstructor<TEntity>;\n /**\n * Copy for the four field-level validation failures. Omit and the keys\n * render, which is what an adopter with no i18n library sees.\n */\n validationMessages?: EntityDraftMessages;\n /**\n * Resolves a schema issue's message, which is authored as a catalog key\n * (`validation.minLength`) so a rule written in one language never reaches a\n * user untranslated. `useTranslateKey` from the controls package is what a\n * host passes; omit it and the message renders as authored.\n */\n translateKey?: (key: string, params?: Record<string, unknown>) => string;\n /** The record being edited; seeds the initial draft. Omit to create. */\n entity?: TEntity;\n /**\n * Where to persist this form's draft, so a half-finished edit survives a\n * refresh. Omit and the form is ephemeral — which is what a plain route wants,\n * and why autosave is opt-in rather than a flag a host can set wrong.\n */\n draft?: EntityDraftStore;\n /**\n * Edit only these members, by accessor name. Omit and the form owns them all.\n *\n * It narrows the **descriptors**, so it narrows seeding, coercion *and*\n * validation together — which is the whole point. A wizard splits one entity\n * across several steps, and without this every step would validate the whole\n * record: step one could not advance until members it does not show were\n * filled, and the required-ness of a field would depend on which step you\n * were standing on.\n *\n * The submit therefore receives a **partial** draft. Merging the steps and\n * rebuilding the entity is the host's job, exactly as it already is for one\n * form ([ADR 0045](../../../../../../../docs/adr/0045-the-wizard-a-step-graph-and-a-submit-that-hands-off.md)).\n *\n * A fresh array literal per render is fine: the memo behind it keys on the\n * names rather than the array's identity.\n */\n fields?: readonly string[];\n /**\n * A Standard Schema (Zod, Valibot, ArkType — anything exposing `~standard`)\n * for the rules metadata cannot express: regex, min/max, cross-field.\n *\n * Authored against the **string draft**, so it coerces (`z.coerce.number()`)\n * rather than expecting typed values. Each message is a catalog key\n * (`validation.minLength`), resolved against the `controls` namespace with the\n * literal as its fallback — a rule written in one language would otherwise\n * reach the user untranslated. An issue with no path becomes `formError`.\n *\n * Synchronous rules only: validation runs in a synchronous pass, so an async\n * schema throws rather than silently passing. Put asynchronous checks — a\n * uniqueness lookup, say — behind {@link UseEntityFormOptions.validate}.\n */\n schema?: StandardSchemaV1;\n /**\n * Extra validation beyond the metadata and schema rules, returning a\n * field-name → message map. Applied last, so it wins on conflict.\n */\n validate?: (values: EntityDraft) => Record<string, string>;\n /** Called with the draft once it passes validation. */\n onSubmit: (values: EntityDraft) => void | Promise<void>;\n}\n\nexport interface UseEntityFormResult {\n /** The current draft. Feed straight into `EntityForm`'s `values`. */\n values: EntityDraft;\n /** Field-name → message, surfaced only after a submit attempt. */\n errors: Record<string, string>;\n /**\n * The message that belongs to no single field — a cross-field schema rule.\n * Feed into `EntityForm`'s `formError`, which has nowhere else to show it.\n */\n formError?: string;\n /**\n * Feed into `EntityForm`'s `onFieldChange`.\n *\n * A scalar takes a string; a `composition` takes its list of rows, the one\n * member with no lossless string form.\n */\n setField: (name: string, value: EntityDraftValue) => void;\n /**\n * The instances behind the relation ids the draft holds — seeded from the\n * record's already-materialized links, extended by every pick, and refilled\n * from a resolved id by {@link UseEntityFormResult.hydrateLink}.\n *\n * Pass to `applyEntityLinks` at submit: the draft alone cannot express an\n * embedded relation, because an id is not an entity. It is not persisted — a\n * restored draft arrives with ids and no instances, which is what\n * `hydrateLink` exists to repair.\n */\n links: EntityLinkSelection;\n /**\n * Feed into `EntityForm`'s `onLinkChange`. Records the picked instance and\n * writes its id into the draft, which stays the serializable source of truth.\n */\n setLink: (name: string, entity: Entity | undefined) => void;\n /**\n * Record an instance resolved from an id the draft already held — the restore\n * path, not a pick.\n *\n * Unlike {@link UseEntityFormResult.setLink} it writes only the sidecar, so it\n * cannot dirty a form the user has not touched. It also never overwrites an\n * entry that is already there: a pick in flight is newer than a lookup that\n * started before it.\n */\n hydrateLink: (name: string, resolved: Entity) => void;\n /**\n * Validate, then submit when clean. Feed into `EntityForm`'s `onSubmit`.\n *\n * **Resolves `true` only when the submit actually ran**, so a caller that has\n * to gate on the outcome — a wizard, whose \"Siguiente\" *is* the step's submit\n * — can await it instead of awaiting a tick and re-reading `errors`. A caller\n * with nothing to gate ignores the promise and is unaffected, which is every\n * existing one.\n */\n submit: () => Promise<boolean>;\n /** True once any field has been edited away from its seed. */\n isDirty: boolean;\n}\n"],"names":[],"mappings":"AA+HA,WAuDC"}
@@ -6,7 +6,7 @@ import type { EntityLinkSourceConfig, UseEntityLinkSourceOptions } from './use-e
6
6
  *
7
7
  * This is the data half of the link mechanism. It exists as a hook — rather than
8
8
  * inside the input — because `@entifix/react-controls` may not import this package
9
- * (both are `entifix:react`, and the boundary rule forbids the sideways edge), so
9
+ * (both are T3, and the tier register declares no sideways edge between them), so
10
10
  * the two meet at the framework-free port declared in `@entifix/core`. A form
11
11
  * wrapper calls it once per relation and hands the results to `EntityForm`'s
12
12
  * `linkSources`.
@@ -31,7 +31,7 @@ const DEFAULT_DEBOUNCE_MS = 250;
31
31
  *
32
32
  * This is the data half of the link mechanism. It exists as a hook — rather than
33
33
  * inside the input — because `@entifix/react-controls` may not import this package
34
- * (both are `entifix:react`, and the boundary rule forbids the sideways edge), so
34
+ * (both are T3, and the tier register declares no sideways edge between them), so
35
35
  * the two meet at the framework-free port declared in `@entifix/core`. A form
36
36
  * wrapper calls it once per relation and hands the results to `EntityForm`'s
37
37
  * `linkSources`.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.ts"],"sourcesContent":["'use client';\n\nimport { EntityIdTag, EntityLoadRequestTag } from '@entifix/business';\nimport {\n describeEntityColumns,\n type EntifixError,\n EntifixLogicError,\n type Entity,\n type EntityLinkSource,\n type EntityLoadRequest,\n type EntityPage,\n} from '@entifix/core';\nimport { useQuery } from '@tanstack/react-query';\nimport { Context, Effect } from 'effect';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { combineFilterGroups } from '../../query/combine-filtering.js';\nimport {\n entityQueryKey,\n entityQueryScope,\n} from '../../query/entity-query-key.js';\nimport { useDataLoading } from '../use-data-loading/index.js';\nimport type {\n EntityLinkSourceConfig,\n UseEntityLinkSourceOptions,\n} from './use-entity-link-source.types.js';\n\nconst DEFAULT_QUICK_PAGE_SIZE = 10;\nconst DEFAULT_DEBOUNCE_MS = 250;\n\n/**\n * Fails loudly on a search property the service will reject.\n *\n * A member's `filterable` metadata is also the server-side filter allowlist, so a\n * quick search over a member that lacks it answers `400` — as an empty\n * suggestion list, which reads to the user as \"there are no brands\". Better to\n * break the developer's build than to lie to them.\n */\nfunction assertSearchable<TTarget extends Entity>(\n config: EntityLinkSourceConfig<TTarget, unknown>,\n searchProperty: string,\n): void {\n const target = describeEntityColumns(config.entityConstructor).find(\n column => column.name === searchProperty,\n );\n if (target?.filterable === true) return;\n\n throw new EntifixLogicError(\n `Cannot search ${config.entityConstructor.name} by \"${searchProperty}\": ` +\n 'the member is not filterable, so the service would reject the query. ' +\n 'Declare it `filterable` on the target entity, or point the relation at ' +\n 'another member with `linkSearchProperty`.',\n undefined,\n { entity: config.entityConstructor.name, searchProperty },\n );\n}\n\n/**\n * Produces the {@link EntityLinkSource} a link editor runs on: the quick\n * type-ahead, the browse list, and the label of whatever key is currently held.\n *\n * This is the data half of the link mechanism. It exists as a hook — rather than\n * inside the input — because `@entifix/react-controls` may not import this package\n * (both are `entifix:react`, and the boundary rule forbids the sideways edge), so\n * the two meet at the framework-free port declared in `@entifix/core`. A form\n * wrapper calls it once per relation and hands the results to `EntityForm`'s\n * `linkSources`.\n *\n * What may be picked is never decided here: the caller's `loadUc` (and optional\n * `baseFiltering`) is what restricts the set, so a domain rule about assignable\n * targets lives in a use-case, not in the UI.\n *\n * Three requests, each held back until it means something: the quick search runs\n * a small page and re-runs on a debounced term; the browse list does not fetch at\n * all until the dialog opens; and the label is only looked up when the caller has\n * an id but no instance — a draft restored from storage — because a picked target\n * already knows its own name.\n */\nexport function useEntityLinkSource<TTarget extends Entity, TContext>(\n config: EntityLinkSourceConfig<TTarget, TContext>,\n {\n descriptor,\n selectedId,\n selectedEntity,\n }: UseEntityLinkSourceOptions<TTarget>,\n): EntityLinkSource<TTarget> {\n const {\n entityConstructor,\n loadUc,\n getUc,\n ctx,\n baseFiltering,\n quickPageSize = DEFAULT_QUICK_PAGE_SIZE,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n } = config;\n\n const searchProperty = descriptor.linkSearchProperty;\n assertSearchable(\n config as unknown as EntityLinkSourceConfig<TTarget, unknown>,\n searchProperty,\n );\n\n // Callers rebuild `loadUc`/`getUc`/`ctx` inline on every render, so the latest\n // are held in refs and kept out of the query keys — keying on their identity\n // would refetch on every render. Same reason and same shape as `useDataLoading`.\n const effects = useRef({ loadUc, getUc, ctx });\n useEffect(() => {\n effects.current = { loadUc, getUc, ctx };\n });\n\n const [term, setTerm] = useState('');\n const [debouncedTerm, setDebouncedTerm] = useState('');\n const [isOpen, setOpen] = useState(false);\n\n // Typing is not a request per keystroke: the term settles first, and only the\n // settled value reaches the service.\n useEffect(() => {\n const timer = setTimeout(() => setDebouncedTerm(term), debounceMs);\n return () => clearTimeout(timer);\n }, [term, debounceMs]);\n\n const quickRequest = useMemo<EntityLoadRequest<TTarget>>(() => {\n const filtering = combineFilterGroups<TTarget>(\n baseFiltering,\n debouncedTerm === ''\n ? undefined\n : {\n operator: 'and',\n values: [\n {\n property: searchProperty as keyof TTarget,\n operator: 'like',\n value: debouncedTerm,\n },\n ],\n },\n );\n\n const request: EntityLoadRequest<TTarget> = {\n page: 1,\n pageSize: quickPageSize,\n };\n if (filtering) request.filtering = [filtering];\n return request;\n }, [baseFiltering, debouncedTerm, quickPageSize, searchProperty]);\n\n const quickQuery = useQuery<EntityPage<TTarget>, EntifixError>({\n queryKey: [\n ...entityQueryKey(entityConstructor, quickRequest),\n 'link-quick',\n ],\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n effects.current.loadUc,\n effects.current.ctx.pipe(\n Context.add(\n EntityLoadRequestTag,\n // The tag carries no generic, so the entity-typed request crosses\n // it — the same cast `loadUCFactory` makes reading it back.\n quickRequest as unknown as EntityLoadRequest,\n ),\n ),\n ),\n ),\n });\n\n const browse = useDataLoading<TTarget, TContext>({\n uc: loadUc,\n ctx,\n queryKey: entityQueryScope(entityConstructor),\n baseFiltering,\n enabled: isOpen,\n });\n\n const needsLabel =\n selectedEntity === undefined && selectedId != null && getUc !== undefined;\n\n const labelQuery = useQuery<TTarget, EntifixError>({\n queryKey: [\n ...entityQueryScope(entityConstructor),\n 'link-label',\n String(selectedId),\n ],\n enabled: needsLabel,\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n // Guarded by `enabled`: the query only runs when both are present.\n effects.current.getUc as NonNullable<typeof getUc>,\n effects.current.ctx.pipe(Context.add(EntityIdTag, selectedId)),\n ),\n ),\n });\n\n // A blank label is as useless as a missing one, so both fall back to the key —\n // an unnamed row must still be distinguishable from the row above it.\n const labelOf = useCallback(\n (target: TTarget) => {\n const label = (target as Record<string, unknown>)[\n descriptor.linkLabelProperty\n ];\n return label == null || label === ''\n ? String(target.id ?? '')\n : String(label);\n },\n [descriptor.linkLabelProperty],\n );\n\n const resolved = selectedEntity ?? labelQuery.data;\n\n return {\n entityConstructor,\n labelOf,\n selected: {\n label: resolved === undefined ? undefined : labelOf(resolved),\n // The whole target, not only its name. The lookup above already fetched\n // it, and a form restoring a draft has nothing else to rebuild an\n // `embedded` relation from — the draft kept the id and lost the instance.\n entity: resolved,\n isLoading: needsLabel && labelQuery.isFetching,\n },\n quick: {\n term,\n setTerm,\n options: quickQuery.data?.items ?? [],\n isLoading: quickQuery.isFetching,\n error: quickQuery.error ?? undefined,\n },\n browse: {\n ...browse,\n isOpen,\n open: () => setOpen(true),\n close: () => setOpen(false),\n },\n };\n}\n"],"names":["EntityIdTag","EntityLoadRequestTag","describeEntityColumns","EntifixLogicError","useQuery","Context","Effect","useCallback","useEffect","useMemo","useRef","useState","combineFilterGroups","entityQueryKey","entityQueryScope","useDataLoading","DEFAULT_QUICK_PAGE_SIZE","DEFAULT_DEBOUNCE_MS","assertSearchable","config","searchProperty","target","entityConstructor","find","column","name","filterable","undefined","entity","useEntityLinkSource","descriptor","selectedId","selectedEntity","quickQuery","loadUc","getUc","ctx","baseFiltering","quickPageSize","debounceMs","linkSearchProperty","effects","current","term","setTerm","debouncedTerm","setDebouncedTerm","isOpen","setOpen","timer","setTimeout","clearTimeout","quickRequest","filtering","operator","values","property","value","request","page","pageSize","queryKey","queryFn","runPromise","provide","pipe","add","browse","uc","enabled","needsLabel","labelQuery","String","labelOf","label","linkLabelProperty","id","resolved","data","selected","isLoading","isFetching","quick","options","items","error","open","close"],"mappings":"AAAA;;AAEA,SAASA,WAAW,EAAEC,oBAAoB,QAAQ,oBAAoB;AACtE,SACEC,qBAAqB,EAErBC,iBAAiB,QAKZ,gBAAgB;AACvB,SAASC,QAAQ,QAAQ,wBAAwB;AACjD,SAASC,OAAO,EAAEC,MAAM,QAAQ,SAAS;AACzC,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAQ;AAE1E,SAASC,mBAAmB,QAAQ,mCAAmC;AACvE,SACEC,cAAc,EACdC,gBAAgB,QACX,kCAAkC;AACzC,SAASC,cAAc,QAAQ,+BAA+B;AAM9D,MAAMC,0BAA0B;AAChC,MAAMC,sBAAsB;AAE5B;;;;;;;CAOC,GACD,SAASC,iBACPC,MAAgD,EAChDC,cAAsB;IAEtB,MAAMC,SAASnB,sBAAsBiB,OAAOG,iBAAiB,EAAEC,IAAI,CACjEC,CAAAA,SAAUA,OAAOC,IAAI,KAAKL;IAE5B,IAAIC,CAAAA,0BAAAA,OAAQK,UAAU,MAAK,MAAM;IAEjC,MAAM,IAAIvB,kBACR,CAAC,cAAc,EAAEgB,OAAOG,iBAAiB,CAACG,IAAI,CAAC,KAAK,EAAEL,eAAe,GAAG,CAAC,GACvE,0EACA,4EACA,6CACFO,WACA;QAAEC,QAAQT,OAAOG,iBAAiB,CAACG,IAAI;QAAEL;IAAe;AAE5D;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASS,oBACdV,MAAiD,EACjD,EACEW,UAAU,EACVC,UAAU,EACVC,cAAc,EACsB;cA+I3BC;QAFEA;IA3Ib,MAAM,EACJX,iBAAiB,EACjBY,MAAM,EACNC,KAAK,EACLC,GAAG,EACHC,aAAa,EACbC,gBAAgBtB,uBAAuB,EACvCuB,aAAatB,mBAAmB,EACjC,GAAGE;IAEJ,MAAMC,iBAAiBU,WAAWU,kBAAkB;IACpDtB,iBACEC,QACAC;IAGF,+EAA+E;IAC/E,6EAA6E;IAC7E,iFAAiF;IACjF,MAAMqB,UAAU/B,OAAO;QAAEwB;QAAQC;QAAOC;IAAI;IAC5C5B,UAAU;QACRiC,QAAQC,OAAO,GAAG;YAAER;YAAQC;YAAOC;QAAI;IACzC;IAEA,MAAM,CAACO,MAAMC,QAAQ,GAAGjC,SAAS;IACjC,MAAM,CAACkC,eAAeC,iBAAiB,GAAGnC,SAAS;IACnD,MAAM,CAACoC,QAAQC,QAAQ,GAAGrC,SAAS;IAEnC,8EAA8E;IAC9E,qCAAqC;IACrCH,UAAU;QACR,MAAMyC,QAAQC,WAAW,IAAMJ,iBAAiBH,OAAOJ;QACvD,OAAO,IAAMY,aAAaF;IAC5B,GAAG;QAACN;QAAMJ;KAAW;IAErB,MAAMa,eAAe3C,QAAoC;QACvD,MAAM4C,YAAYzC,oBAChByB,eACAQ,kBAAkB,KACdlB,YACA;YACE2B,UAAU;YACVC,QAAQ;gBACN;oBACEC,UAAUpC;oBACVkC,UAAU;oBACVG,OAAOZ;gBACT;aACD;QACH;QAGN,MAAMa,UAAsC;YAC1CC,MAAM;YACNC,UAAUtB;QACZ;QACA,IAAIe,WAAWK,QAAQL,SAAS,GAAG;YAACA;SAAU;QAC9C,OAAOK;IACT,GAAG;QAACrB;QAAeQ;QAAeP;QAAelB;KAAe;IAEhE,MAAMa,aAAa7B,SAA4C;QAC7DyD,UAAU;eACLhD,eAAeS,mBAAmB8B;YACrC;SACD;QACDU,SAAS,IACPxD,OAAOyD,UAAU,CACfzD,OAAO0D,OAAO,CACZvB,QAAQC,OAAO,CAACR,MAAM,EACtBO,QAAQC,OAAO,CAACN,GAAG,CAAC6B,IAAI,CACtB5D,QAAQ6D,GAAG,CACTjE,sBACA,kEAAkE;YAClE,4DAA4D;YAC5DmD;IAKZ;IAEA,MAAMe,SAASpD,eAAkC;QAC/CqD,IAAIlC;QACJE;QACAyB,UAAU/C,iBAAiBQ;QAC3Be;QACAgC,SAAStB;IACX;IAEA,MAAMuB,aACJtC,mBAAmBL,aAAaI,cAAc,QAAQI,UAAUR;IAElE,MAAM4C,aAAanE,SAAgC;QACjDyD,UAAU;eACL/C,iBAAiBQ;YACpB;YACAkD,OAAOzC;SACR;QACDsC,SAASC;QACTR,SAAS,IACPxD,OAAOyD,UAAU,CACfzD,OAAO0D,OAAO,CACZ,mEAAmE;YACnEvB,QAAQC,OAAO,CAACP,KAAK,EACrBM,QAAQC,OAAO,CAACN,GAAG,CAAC6B,IAAI,CAAC5D,QAAQ6D,GAAG,CAAClE,aAAa+B;IAG1D;IAEA,+EAA+E;IAC/E,sEAAsE;IACtE,MAAM0C,UAAUlE,YACd,CAACc;YAKYA;QAJX,MAAMqD,QAAQ,AAACrD,MAAkC,CAC/CS,WAAW6C,iBAAiB,CAC7B;QACD,OAAOD,SAAS,QAAQA,UAAU,KAC9BF,QAAOnD,aAAAA,OAAOuD,EAAE,YAATvD,aAAa,MACpBmD,OAAOE;IACb,GACA;QAAC5C,WAAW6C,iBAAiB;KAAC;IAGhC,MAAME,WAAW7C,yBAAAA,iBAAkBuC,WAAWO,IAAI;IAElD,OAAO;QACLxD;QACAmD;QACAM,UAAU;YACRL,OAAOG,aAAalD,YAAYA,YAAY8C,QAAQI;YACpD,wEAAwE;YACxE,kEAAkE;YAClE,0EAA0E;YAC1EjD,QAAQiD;YACRG,WAAWV,cAAcC,WAAWU,UAAU;QAChD;QACAC,OAAO;YACLvC;YACAC;YACAuC,OAAO,WAAElD,mBAAAA,WAAW6C,IAAI,qBAAf7C,iBAAiBmD,KAAK,mBAAI,EAAE;YACrCJ,WAAW/C,WAAWgD,UAAU;YAChCI,KAAK,GAAEpD,oBAAAA,WAAWoD,KAAK,YAAhBpD,oBAAoBN;QAC7B;QACAwC,QAAQ,aACHA;YACHpB;YACAuC,MAAM,IAAMtC,QAAQ;YACpBuC,OAAO,IAAMvC,QAAQ;;IAEzB;AACF"}
1
+ {"version":3,"sources":["../../../src/hooks/use-entity-link-source/use-entity-link-source.ts"],"sourcesContent":["'use client';\n\nimport { EntityIdTag, EntityLoadRequestTag } from '@entifix/business';\nimport {\n describeEntityColumns,\n type EntifixError,\n EntifixLogicError,\n type Entity,\n type EntityLinkSource,\n type EntityLoadRequest,\n type EntityPage,\n} from '@entifix/core';\nimport { useQuery } from '@tanstack/react-query';\nimport { Context, Effect } from 'effect';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { combineFilterGroups } from '../../query/combine-filtering.js';\nimport {\n entityQueryKey,\n entityQueryScope,\n} from '../../query/entity-query-key.js';\nimport { useDataLoading } from '../use-data-loading/index.js';\nimport type {\n EntityLinkSourceConfig,\n UseEntityLinkSourceOptions,\n} from './use-entity-link-source.types.js';\n\nconst DEFAULT_QUICK_PAGE_SIZE = 10;\nconst DEFAULT_DEBOUNCE_MS = 250;\n\n/**\n * Fails loudly on a search property the service will reject.\n *\n * A member's `filterable` metadata is also the server-side filter allowlist, so a\n * quick search over a member that lacks it answers `400` — as an empty\n * suggestion list, which reads to the user as \"there are no brands\". Better to\n * break the developer's build than to lie to them.\n */\nfunction assertSearchable<TTarget extends Entity>(\n config: EntityLinkSourceConfig<TTarget, unknown>,\n searchProperty: string,\n): void {\n const target = describeEntityColumns(config.entityConstructor).find(\n column => column.name === searchProperty,\n );\n if (target?.filterable === true) return;\n\n throw new EntifixLogicError(\n `Cannot search ${config.entityConstructor.name} by \"${searchProperty}\": ` +\n 'the member is not filterable, so the service would reject the query. ' +\n 'Declare it `filterable` on the target entity, or point the relation at ' +\n 'another member with `linkSearchProperty`.',\n undefined,\n { entity: config.entityConstructor.name, searchProperty },\n );\n}\n\n/**\n * Produces the {@link EntityLinkSource} a link editor runs on: the quick\n * type-ahead, the browse list, and the label of whatever key is currently held.\n *\n * This is the data half of the link mechanism. It exists as a hook — rather than\n * inside the input — because `@entifix/react-controls` may not import this package\n * (both are T3, and the tier register declares no sideways edge between them), so\n * the two meet at the framework-free port declared in `@entifix/core`. A form\n * wrapper calls it once per relation and hands the results to `EntityForm`'s\n * `linkSources`.\n *\n * What may be picked is never decided here: the caller's `loadUc` (and optional\n * `baseFiltering`) is what restricts the set, so a domain rule about assignable\n * targets lives in a use-case, not in the UI.\n *\n * Three requests, each held back until it means something: the quick search runs\n * a small page and re-runs on a debounced term; the browse list does not fetch at\n * all until the dialog opens; and the label is only looked up when the caller has\n * an id but no instance — a draft restored from storage — because a picked target\n * already knows its own name.\n */\nexport function useEntityLinkSource<TTarget extends Entity, TContext>(\n config: EntityLinkSourceConfig<TTarget, TContext>,\n {\n descriptor,\n selectedId,\n selectedEntity,\n }: UseEntityLinkSourceOptions<TTarget>,\n): EntityLinkSource<TTarget> {\n const {\n entityConstructor,\n loadUc,\n getUc,\n ctx,\n baseFiltering,\n quickPageSize = DEFAULT_QUICK_PAGE_SIZE,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n } = config;\n\n const searchProperty = descriptor.linkSearchProperty;\n assertSearchable(\n config as unknown as EntityLinkSourceConfig<TTarget, unknown>,\n searchProperty,\n );\n\n // Callers rebuild `loadUc`/`getUc`/`ctx` inline on every render, so the latest\n // are held in refs and kept out of the query keys — keying on their identity\n // would refetch on every render. Same reason and same shape as `useDataLoading`.\n const effects = useRef({ loadUc, getUc, ctx });\n useEffect(() => {\n effects.current = { loadUc, getUc, ctx };\n });\n\n const [term, setTerm] = useState('');\n const [debouncedTerm, setDebouncedTerm] = useState('');\n const [isOpen, setOpen] = useState(false);\n\n // Typing is not a request per keystroke: the term settles first, and only the\n // settled value reaches the service.\n useEffect(() => {\n const timer = setTimeout(() => setDebouncedTerm(term), debounceMs);\n return () => clearTimeout(timer);\n }, [term, debounceMs]);\n\n const quickRequest = useMemo<EntityLoadRequest<TTarget>>(() => {\n const filtering = combineFilterGroups<TTarget>(\n baseFiltering,\n debouncedTerm === ''\n ? undefined\n : {\n operator: 'and',\n values: [\n {\n property: searchProperty as keyof TTarget,\n operator: 'like',\n value: debouncedTerm,\n },\n ],\n },\n );\n\n const request: EntityLoadRequest<TTarget> = {\n page: 1,\n pageSize: quickPageSize,\n };\n if (filtering) request.filtering = [filtering];\n return request;\n }, [baseFiltering, debouncedTerm, quickPageSize, searchProperty]);\n\n const quickQuery = useQuery<EntityPage<TTarget>, EntifixError>({\n queryKey: [\n ...entityQueryKey(entityConstructor, quickRequest),\n 'link-quick',\n ],\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n effects.current.loadUc,\n effects.current.ctx.pipe(\n Context.add(\n EntityLoadRequestTag,\n // The tag carries no generic, so the entity-typed request crosses\n // it — the same cast `loadUCFactory` makes reading it back.\n quickRequest as unknown as EntityLoadRequest,\n ),\n ),\n ),\n ),\n });\n\n const browse = useDataLoading<TTarget, TContext>({\n uc: loadUc,\n ctx,\n queryKey: entityQueryScope(entityConstructor),\n baseFiltering,\n enabled: isOpen,\n });\n\n const needsLabel =\n selectedEntity === undefined && selectedId != null && getUc !== undefined;\n\n const labelQuery = useQuery<TTarget, EntifixError>({\n queryKey: [\n ...entityQueryScope(entityConstructor),\n 'link-label',\n String(selectedId),\n ],\n enabled: needsLabel,\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n // Guarded by `enabled`: the query only runs when both are present.\n effects.current.getUc as NonNullable<typeof getUc>,\n effects.current.ctx.pipe(Context.add(EntityIdTag, selectedId)),\n ),\n ),\n });\n\n // A blank label is as useless as a missing one, so both fall back to the key —\n // an unnamed row must still be distinguishable from the row above it.\n const labelOf = useCallback(\n (target: TTarget) => {\n const label = (target as Record<string, unknown>)[\n descriptor.linkLabelProperty\n ];\n return label == null || label === ''\n ? String(target.id ?? '')\n : String(label);\n },\n [descriptor.linkLabelProperty],\n );\n\n const resolved = selectedEntity ?? labelQuery.data;\n\n return {\n entityConstructor,\n labelOf,\n selected: {\n label: resolved === undefined ? undefined : labelOf(resolved),\n // The whole target, not only its name. The lookup above already fetched\n // it, and a form restoring a draft has nothing else to rebuild an\n // `embedded` relation from — the draft kept the id and lost the instance.\n entity: resolved,\n isLoading: needsLabel && labelQuery.isFetching,\n },\n quick: {\n term,\n setTerm,\n options: quickQuery.data?.items ?? [],\n isLoading: quickQuery.isFetching,\n error: quickQuery.error ?? undefined,\n },\n browse: {\n ...browse,\n isOpen,\n open: () => setOpen(true),\n close: () => setOpen(false),\n },\n };\n}\n"],"names":["EntityIdTag","EntityLoadRequestTag","describeEntityColumns","EntifixLogicError","useQuery","Context","Effect","useCallback","useEffect","useMemo","useRef","useState","combineFilterGroups","entityQueryKey","entityQueryScope","useDataLoading","DEFAULT_QUICK_PAGE_SIZE","DEFAULT_DEBOUNCE_MS","assertSearchable","config","searchProperty","target","entityConstructor","find","column","name","filterable","undefined","entity","useEntityLinkSource","descriptor","selectedId","selectedEntity","quickQuery","loadUc","getUc","ctx","baseFiltering","quickPageSize","debounceMs","linkSearchProperty","effects","current","term","setTerm","debouncedTerm","setDebouncedTerm","isOpen","setOpen","timer","setTimeout","clearTimeout","quickRequest","filtering","operator","values","property","value","request","page","pageSize","queryKey","queryFn","runPromise","provide","pipe","add","browse","uc","enabled","needsLabel","labelQuery","String","labelOf","label","linkLabelProperty","id","resolved","data","selected","isLoading","isFetching","quick","options","items","error","open","close"],"mappings":"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"}