@cosmicdrift/kumiko-headless 0.289.0 → 0.291.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-headless",
3
- "version": "0.289.0",
3
+ "version": "0.291.0",
4
4
  "description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -36,7 +36,7 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@cosmicdrift/kumiko-framework": "0.289.0",
39
+ "@cosmicdrift/kumiko-framework": "0.291.0",
40
40
  "temporal-polyfill": "^0.3.2",
41
41
  "zod": "^4.4.3"
42
42
  },
@@ -424,6 +424,31 @@ describe("computeEditViewModel — embedded-list cells (#1835)", () => {
424
424
  refFeature: "orders",
425
425
  refLabelField: "name",
426
426
  });
427
+ expect(byField["product"]?.refOptionsQuery).toBeUndefined();
428
+ });
429
+
430
+ test("a reference sub-field's optionsQuery reaches the cell view-model (fw#2780)", () => {
431
+ const vm = computeEditViewModel({
432
+ screen: editScreen({ sections: [{ title: "x", fields: ["lines"] }] }),
433
+ entity: embeddedListEntity({
434
+ schema: {
435
+ ...lineFieldSchema,
436
+ product: {
437
+ type: "reference",
438
+ entity: "product",
439
+ labelField: "name",
440
+ optionsQuery: "orders:query:product:options",
441
+ },
442
+ },
443
+ }),
444
+ values: { lines: [] },
445
+ translate,
446
+ featureName: "orders",
447
+ });
448
+
449
+ const cells = asFields(vm.sections[0]).fields[0]?.embeddedListCells;
450
+ const product = (cells ?? []).find((cell) => cell.field === "product");
451
+ expect(product?.refOptionsQuery).toBe("orders:query:product:options");
427
452
  });
428
453
 
429
454
  test("minItems/maxItems/derived/totals pass through onto the field view-model when set", () => {
@@ -720,6 +745,34 @@ describe("computeEditViewModel — declared reference metadata (fw#2662)", () =>
720
745
  expect(field?.refMultiple).toBeUndefined();
721
746
  });
722
747
 
748
+ test("a reference field's optionsQuery reaches the field view-model, labelField untouched (fw#2780)", () => {
749
+ const entity = {
750
+ fields: {
751
+ leaseId: {
752
+ type: "reference",
753
+ entity: "lease",
754
+ labelField: "startDate",
755
+ optionsQuery: "deposits:query:lease:options",
756
+ },
757
+ },
758
+ } as unknown as EntityDefinition;
759
+ const vm = computeEditViewModel({
760
+ screen: editScreen({ sections: [{ fields: ["leaseId"] }] }),
761
+ entity,
762
+ values: {},
763
+ translate,
764
+ featureName: "deposits",
765
+ });
766
+
767
+ const field = asFields(vm.sections[0]).fields[0];
768
+ expect(field).toMatchObject({
769
+ type: "reference",
770
+ refEntity: "lease",
771
+ refLabelField: "startDate",
772
+ refOptionsQuery: "deposits:query:lease:options",
773
+ });
774
+ });
775
+
723
776
  test("non-regression: a projectionDetail field without refEntity metadata stays 'text', unchanged (fw#2662)", () => {
724
777
  const vm = computeEditViewModel({
725
778
  screen: editScreen({ sections: [{ fields: ["ip"] }] }),
@@ -12,6 +12,7 @@ import {
12
12
  isWriteFormEditSection,
13
13
  normalizeEditField,
14
14
  parseRefTarget,
15
+ sectionFieldSpecs,
15
16
  WRITE_FORM_SECTION_ENTITY,
16
17
  } from "@cosmicdrift/kumiko-framework/ui-types";
17
18
  import {
@@ -51,6 +52,7 @@ type EmbeddedSubFieldShape = {
51
52
  readonly options?: readonly string[];
52
53
  readonly entity?: string;
53
54
  readonly labelField?: string;
55
+ readonly optionsQuery?: string;
54
56
  readonly scale?: number;
55
57
  };
56
58
 
@@ -167,11 +169,7 @@ export function computeEditViewModel<
167
169
  }
168
170
  // `groups` flattens into the same per-field pipeline as plain `fields`;
169
171
  // the group structure below just re-groups the computed views by name.
170
- const flatFieldSpecs =
171
- sectionSpec.groups !== undefined
172
- ? sectionSpec.groups.flatMap((group) => group.fields)
173
- : sectionSpec.fields;
174
- const fields: EditFieldViewModel[] = flatFieldSpecs.map((fieldSpec) => {
172
+ const fields: EditFieldViewModel[] = sectionFieldSpecs(sectionSpec).map((fieldSpec) => {
175
173
  const normalized = normalizeEditField(fieldSpec);
176
174
  const fieldDef = entity.fields[normalized.field];
177
175
  if (!fieldDef) {
@@ -286,13 +284,17 @@ export function computeEditViewModel<
286
284
  : fieldDef.type === "reference"
287
285
  ? ((fieldDef as unknown as { labelField?: string }).labelField ?? "id")
288
286
  : undefined;
289
- // Declared reference metadata has no `multiple` concept (it targets
290
- // row-meta/derived fields, always single-valued) — only a real
291
- // ReferenceFieldDef carries it.
292
- const refMultiple =
287
+ // Declared reference metadata has no `multiple` or `optionsQuery`
288
+ // concept (it targets row-meta/derived fields, always single-valued and
289
+ // resolved through the target entity) — only a real ReferenceFieldDef
290
+ // carries them.
291
+ const ownReferenceDef =
293
292
  declaredRefTarget === undefined && fieldDef.type === "reference"
294
- ? ((fieldDef as unknown as { multiple?: boolean }).multiple ?? false)
293
+ ? (fieldDef as unknown as { multiple?: boolean; optionsQuery?: string })
295
294
  : undefined;
295
+ const refOptionsQuery = ownReferenceDef?.optionsQuery;
296
+ const refMultiple =
297
+ ownReferenceDef === undefined ? undefined : (ownReferenceDef.multiple ?? false);
296
298
  // file/image: accept/maxSize ins ViewModel + entityType/fieldName für
297
299
  // den Upload-POST (Endpoint validiert gegen die richtige Field-Def).
298
300
  const isFileType =
@@ -359,9 +361,10 @@ export function computeEditViewModel<
359
361
  cellOptions,
360
362
  )
361
363
  : undefined;
364
+ const cellRef = subField.type === "reference" ? subField : undefined;
362
365
  const cellRefTarget =
363
- subField.type === "reference" && subField.entity !== undefined
364
- ? parseRefTarget(subField.entity, featureName)
366
+ cellRef?.entity !== undefined
367
+ ? parseRefTarget(cellRef.entity, featureName)
365
368
  : undefined;
366
369
  const cell: EmbeddedListCellViewModel = {
367
370
  field: subFieldName,
@@ -372,8 +375,10 @@ export function computeEditViewModel<
372
375
  ...(cellOptionLabels !== undefined && { optionLabels: cellOptionLabels }),
373
376
  ...(cellRefTarget !== undefined && { refEntity: cellRefTarget.entityName }),
374
377
  ...(cellRefTarget !== undefined && { refFeature: cellRefTarget.featureName }),
375
- ...(subField.type === "reference" &&
376
- subField.labelField !== undefined && { refLabelField: subField.labelField }),
378
+ ...(cellRef?.labelField !== undefined && { refLabelField: cellRef.labelField }),
379
+ ...(cellRef?.optionsQuery !== undefined && {
380
+ refOptionsQuery: cellRef.optionsQuery,
381
+ }),
377
382
  ...(subField.type === "decimal" &&
378
383
  subField.scale !== undefined && { scale: subField.scale }),
379
384
  };
@@ -404,6 +409,7 @@ export function computeEditViewModel<
404
409
  ...(refEntity !== undefined && { refEntity }),
405
410
  ...(refFeature !== undefined && { refFeature }),
406
411
  ...(refLabelField !== undefined && { refLabelField }),
412
+ ...(refOptionsQuery !== undefined && { refOptionsQuery }),
407
413
  ...(refMultiple !== undefined && { refMultiple }),
408
414
  ...(fileDef?.accept !== undefined && { accept: fileDef.accept }),
409
415
  ...(fileDef?.maxSize !== undefined && { maxSize: fileDef.maxSize }),
@@ -112,6 +112,7 @@ export type EmbeddedListCellViewModel = {
112
112
  readonly refEntity?: string;
113
113
  readonly refFeature?: string;
114
114
  readonly refLabelField?: string;
115
+ readonly refOptionsQuery?: string;
115
116
  /** Only for `type: "decimal"`. */
116
117
  readonly scale?: number;
117
118
  };
@@ -190,6 +191,11 @@ export type EditFieldViewModel = {
190
191
  /** Nur bei `type: "reference"` gesetzt — Welches Feld der referenced
191
192
  * Entity als Display-Label im Dropdown erscheint. Default: "id". */
192
193
  readonly refLabelField?: string;
194
+ /** Only for `type: "reference"` — query handler QN the picker sources its
195
+ * options from (fw#2780), replacing the referenced entity's list handler.
196
+ * Rows carry `{ id, label }`; `refLabelField` is then unused for the
197
+ * picker but still drives list cells. */
198
+ readonly refOptionsQuery?: string;
193
199
  /** Nur bei `type: "reference"` — Multi-Mode (Tier 2.7e-Multi):
194
200
  * Wert ist UUID-Array, Renderer mountet Multi-Combobox mit Tags. */
195
201
  readonly refMultiple?: boolean;