@cosmicdrift/kumiko-headless 0.270.0 → 0.273.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.270.0",
3
+ "version": "0.273.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.270.0",
39
+ "@cosmicdrift/kumiko-framework": "0.273.0",
40
40
  "temporal-polyfill": "^0.3.2",
41
41
  "zod": "^4.4.3"
42
42
  },
@@ -847,6 +847,46 @@ describe("computeEditViewModel — relatedList searchable/facets passthrough (fw
847
847
  });
848
848
  });
849
849
 
850
+ describe("computeEditViewModel — relatedList parentFilter passthrough", () => {
851
+ test("parentFilter passes through onto the relatedList section view-model verbatim", () => {
852
+ const vm = computeEditViewModel({
853
+ screen: editScreen({
854
+ sections: [
855
+ {
856
+ kind: "relatedList",
857
+ title: "History",
858
+ query: "app:query:history",
859
+ columns: ["name"],
860
+ parentFilter: { field: "orderId" },
861
+ },
862
+ ],
863
+ }),
864
+ entity: orderEntity,
865
+ values: {},
866
+ translate,
867
+ featureName: "orders",
868
+ });
869
+
870
+ expect(asRelatedList(vm.sections[0]).parentFilter).toEqual({ field: "orderId" });
871
+ });
872
+
873
+ test("parentFilter is absent from the view-model when the section spec has none", () => {
874
+ const vm = computeEditViewModel({
875
+ screen: editScreen({
876
+ sections: [
877
+ { kind: "relatedList", title: "History", query: "app:query:history", columns: ["name"] },
878
+ ],
879
+ }),
880
+ entity: orderEntity,
881
+ values: {},
882
+ translate,
883
+ featureName: "orders",
884
+ });
885
+
886
+ expect(asRelatedList(vm.sections[0]).parentFilter).toBeUndefined();
887
+ });
888
+ });
889
+
850
890
  describe("computeEditViewModel — writeForm sections (fw editable-detail-screens)", () => {
851
891
  test("resolves the section's own fieldDefs/fields through the same per-field pipeline as a fields section", () => {
852
892
  const vm = computeEditViewModel({
@@ -123,6 +123,7 @@ function computeRelatedListSectionViewModel(
123
123
  title: translate(sectionSpec.title),
124
124
  query: sectionSpec.query,
125
125
  ...(sectionSpec.parentParam !== undefined && { parentParam: sectionSpec.parentParam }),
126
+ ...(sectionSpec.parentFilter !== undefined && { parentFilter: sectionSpec.parentFilter }),
126
127
  columns: sectionSpec.columns,
127
128
  ...(sectionSpec.pageSize !== undefined && { pageSize: sectionSpec.pageSize }),
128
129
  ...(sectionSpec.defaultSort !== undefined && { defaultSort: sectionSpec.defaultSort }),
@@ -130,6 +131,7 @@ function computeRelatedListSectionViewModel(
130
131
  ...(sectionSpec.facets !== undefined && { facets: sectionSpec.facets }),
131
132
  ...(sectionSpec.rowClick !== undefined && { rowClick: sectionSpec.rowClick }),
132
133
  ...(sectionSpec.rowActions !== undefined && { rowActions: sectionSpec.rowActions }),
134
+ ...(sectionSpec.toolbarActions !== undefined && { toolbarActions: sectionSpec.toolbarActions }),
133
135
  };
134
136
  }
135
137
 
@@ -163,7 +165,13 @@ export function computeEditViewModel<
163
165
  if (!isFieldsEditSection(sectionSpec)) {
164
166
  return computeRelatedListSectionViewModel(sectionSpec, translate);
165
167
  }
166
- const fields: EditFieldViewModel[] = sectionSpec.fields.map((fieldSpec) => {
168
+ // `groups` flattens into the same per-field pipeline as plain `fields`;
169
+ // 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) => {
167
175
  const normalized = normalizeEditField(fieldSpec);
168
176
  const fieldDef = entity.fields[normalized.field];
169
177
  if (!fieldDef) {
@@ -424,9 +432,27 @@ export function computeEditViewModel<
424
432
  };
425
433
  return view;
426
434
  });
427
- // Boot-validator rejects fields.length === 0 (screens.ts), so an empty
428
- // section never reaches this code.
435
+ // Boot-validator rejects fields.length === 0 with no groups (screens.ts),
436
+ // so an empty section never reaches this code.
429
437
  const visible = fields.some((field) => field.visible);
438
+ // Tabs mode renders this section as its own card, where two columns reads
439
+ // better than the single-column default a stacked form keeps.
440
+ const defaultColumns = screen.layout.mode === "tabs" ? 2 : 1;
441
+ const groups = sectionSpec.groups?.map((group) => ({
442
+ title: translate(group.title),
443
+ columns: group.columns ?? 2,
444
+ fields: group.fields.map((fieldSpec) => {
445
+ const fieldName = normalizeEditField(fieldSpec).field;
446
+ const fieldView = fields.find((f) => f.field === fieldName);
447
+ if (fieldView === undefined) {
448
+ throw new Error(
449
+ `computeEditViewModel: screen "${screen.id}" group "${group.title}" references field ` +
450
+ `"${fieldName}" that failed to resolve.`,
451
+ );
452
+ }
453
+ return fieldView;
454
+ }),
455
+ }));
430
456
  return {
431
457
  kind: "fields" as const,
432
458
  visible,
@@ -435,8 +461,9 @@ export function computeEditViewModel<
435
461
  ...(sectionSpec.description !== undefined && {
436
462
  description: translate(sectionSpec.description),
437
463
  }),
438
- columns: sectionSpec.columns ?? 1,
464
+ columns: sectionSpec.columns ?? defaultColumns,
439
465
  fields,
466
+ ...(groups !== undefined && { groups }),
440
467
  ...(sectionSpec.icon !== undefined && { icon: sectionSpec.icon }),
441
468
  };
442
469
  });
@@ -8,6 +8,7 @@ import type {
8
8
  ListFacetSpec,
9
9
  ListSortSpec,
10
10
  PlatformComponent,
11
+ RelatedListToolbarAction,
11
12
  RowAction,
12
13
  ScreenSlots,
13
14
  } from "@cosmicdrift/kumiko-framework/ui-types";
@@ -270,6 +271,13 @@ export type EditFieldsSectionViewModel = {
270
271
  readonly description?: string;
271
272
  readonly columns: number;
272
273
  readonly fields: readonly EditFieldViewModel[];
274
+ /** From `EditFieldsSection.groups` — each entry's `fields` are the same
275
+ * view-model objects as `fields` above, re-grouped (not recomputed). */
276
+ readonly groups?: readonly {
277
+ readonly title: string;
278
+ readonly columns: number;
279
+ readonly fields: readonly EditFieldViewModel[];
280
+ }[];
273
281
  /** From `EditFieldsSection.icon` — closed IconKey vocabulary, renders
274
282
  * left of the title. No effect without `title`. */
275
283
  readonly icon?: IconKey;
@@ -292,6 +300,7 @@ export type EditRelatedListSectionViewModel = {
292
300
  readonly title: string;
293
301
  readonly query: string;
294
302
  readonly parentParam?: string;
303
+ readonly parentFilter?: { readonly field: string };
295
304
  readonly columns: readonly ListColumnSpec[];
296
305
  readonly pageSize?: number;
297
306
  readonly defaultSort?: ListSortSpec;
@@ -299,6 +308,7 @@ export type EditRelatedListSectionViewModel = {
299
308
  readonly facets?: readonly ListFacetSpec[];
300
309
  readonly rowClick?: { readonly entity: string; readonly idColumn?: string };
301
310
  readonly rowActions?: readonly RowAction[];
311
+ readonly toolbarActions?: readonly RelatedListToolbarAction[];
302
312
  };
303
313
 
304
314
  // Mirrors EditWriteFormSection, except `fields` is already resolved through