@cosmicdrift/kumiko-headless 0.290.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.290.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.290.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"] }] }),
@@ -52,6 +52,7 @@ type EmbeddedSubFieldShape = {
52
52
  readonly options?: readonly string[];
53
53
  readonly entity?: string;
54
54
  readonly labelField?: string;
55
+ readonly optionsQuery?: string;
55
56
  readonly scale?: number;
56
57
  };
57
58
 
@@ -283,13 +284,17 @@ export function computeEditViewModel<
283
284
  : fieldDef.type === "reference"
284
285
  ? ((fieldDef as unknown as { labelField?: string }).labelField ?? "id")
285
286
  : undefined;
286
- // Declared reference metadata has no `multiple` concept (it targets
287
- // row-meta/derived fields, always single-valued) — only a real
288
- // ReferenceFieldDef carries it.
289
- 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 =
290
292
  declaredRefTarget === undefined && fieldDef.type === "reference"
291
- ? ((fieldDef as unknown as { multiple?: boolean }).multiple ?? false)
293
+ ? (fieldDef as unknown as { multiple?: boolean; optionsQuery?: string })
292
294
  : undefined;
295
+ const refOptionsQuery = ownReferenceDef?.optionsQuery;
296
+ const refMultiple =
297
+ ownReferenceDef === undefined ? undefined : (ownReferenceDef.multiple ?? false);
293
298
  // file/image: accept/maxSize ins ViewModel + entityType/fieldName für
294
299
  // den Upload-POST (Endpoint validiert gegen die richtige Field-Def).
295
300
  const isFileType =
@@ -356,9 +361,10 @@ export function computeEditViewModel<
356
361
  cellOptions,
357
362
  )
358
363
  : undefined;
364
+ const cellRef = subField.type === "reference" ? subField : undefined;
359
365
  const cellRefTarget =
360
- subField.type === "reference" && subField.entity !== undefined
361
- ? parseRefTarget(subField.entity, featureName)
366
+ cellRef?.entity !== undefined
367
+ ? parseRefTarget(cellRef.entity, featureName)
362
368
  : undefined;
363
369
  const cell: EmbeddedListCellViewModel = {
364
370
  field: subFieldName,
@@ -369,8 +375,10 @@ export function computeEditViewModel<
369
375
  ...(cellOptionLabels !== undefined && { optionLabels: cellOptionLabels }),
370
376
  ...(cellRefTarget !== undefined && { refEntity: cellRefTarget.entityName }),
371
377
  ...(cellRefTarget !== undefined && { refFeature: cellRefTarget.featureName }),
372
- ...(subField.type === "reference" &&
373
- subField.labelField !== undefined && { refLabelField: subField.labelField }),
378
+ ...(cellRef?.labelField !== undefined && { refLabelField: cellRef.labelField }),
379
+ ...(cellRef?.optionsQuery !== undefined && {
380
+ refOptionsQuery: cellRef.optionsQuery,
381
+ }),
374
382
  ...(subField.type === "decimal" &&
375
383
  subField.scale !== undefined && { scale: subField.scale }),
376
384
  };
@@ -401,6 +409,7 @@ export function computeEditViewModel<
401
409
  ...(refEntity !== undefined && { refEntity }),
402
410
  ...(refFeature !== undefined && { refFeature }),
403
411
  ...(refLabelField !== undefined && { refLabelField }),
412
+ ...(refOptionsQuery !== undefined && { refOptionsQuery }),
404
413
  ...(refMultiple !== undefined && { refMultiple }),
405
414
  ...(fileDef?.accept !== undefined && { accept: fileDef.accept }),
406
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;