@cosmicdrift/kumiko-renderer 0.358.0 → 0.359.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.
@@ -4,6 +4,9 @@ import type { ActionFormScreenDefinition, EntityDefinition, EntityEditScreenDefi
4
4
  * iterieren über entity.fields zur Render-Zeit; alle weiteren
5
5
  * EntityDefinition-Felder bleiben undefined. */
6
6
  export declare function synthesizeActionFormEntity(fields: Readonly<Record<string, FieldDefinition>>): EntityDefinition;
7
+ /** Fields the form may seed and submit. An excluded field the layout does not show
8
+ * would otherwise reach the payload through its `default`, and the handler rejects it. */
9
+ export declare function actionFormSubmittableFields(screen: ActionFormScreenDefinition): Readonly<Record<string, FieldDefinition>>;
7
10
  /** Wandelt ein ActionFormScreenDefinition oder SecretMintScreenDefinition in
8
11
  * die EntityEditScreen-Shape die RenderEdit erwartet. type wird auf
9
12
  * "entityEdit" gesetzt damit der Type-Constraint hält; entity wird auf den
@@ -14,6 +14,7 @@
14
14
  // RenderActionForm-Komponente, die `fields` direkt als Input nimmt.
15
15
  // Der Boot-Validator kennt actionForm bereits eigenständig (kein
16
16
  // entity-Lookup), also ist der Server-Pfad davon nicht betroffen.
17
+ import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
17
18
  const ACTION_FORM_PSEUDO_ENTITY = "__action-form__";
18
19
  /** Baut eine minimale EntityDefinition aus den Inline-Fields eines
19
20
  * ActionForm- oder SecretMint-Screens. RenderEdit + computeEditViewModel
@@ -22,6 +23,51 @@ const ACTION_FORM_PSEUDO_ENTITY = "__action-form__";
22
23
  export function synthesizeActionFormEntity(fields) {
23
24
  return { fields };
24
25
  }
26
+ // An actionForm has no create/update mode, so the entityEdit `writeExcludedFields`
27
+ // (which hides the field in create mode) does not fit. The values arrive via prefill
28
+ // (row drawer, handoff, URL), so show them read-only: `submit: false` because the
29
+ // handler does not accept the field, `required: false` so an empty one cannot block submit.
30
+ function lockExcludedFieldSpec(spec, excluded) {
31
+ const normalized = normalizeEditField(spec);
32
+ if (!excluded.has(normalized.field))
33
+ return spec;
34
+ return { ...normalized, readOnly: true, required: false, submit: false };
35
+ }
36
+ function lockExcludedFieldsInSection(section, excluded) {
37
+ if (!isFieldsEditSection(section))
38
+ return section;
39
+ return {
40
+ ...section,
41
+ // kumiko-lint-ignore section-fields-raw rewrites both sources in place — the groups branch below does the same for groups[].fields
42
+ fields: section.fields.map((spec) => lockExcludedFieldSpec(spec, excluded)),
43
+ ...(section.groups !== undefined && {
44
+ groups: section.groups.map((group) => ({
45
+ ...group,
46
+ fields: group.fields.map((spec) => lockExcludedFieldSpec(spec, excluded)),
47
+ })),
48
+ }),
49
+ };
50
+ }
51
+ function layoutWithLockedExcludedFields(screen) {
52
+ if (screen.type !== "actionForm" || !screen.writeExcludedFields?.length)
53
+ return screen.layout;
54
+ const excluded = new Set(screen.writeExcludedFields);
55
+ return {
56
+ ...screen.layout,
57
+ sections: screen.layout.sections.map((section) => lockExcludedFieldsInSection(section, excluded)),
58
+ };
59
+ }
60
+ /** Fields the form may seed and submit. An excluded field the layout does not show
61
+ * would otherwise reach the payload through its `default`, and the handler rejects it. */
62
+ export function actionFormSubmittableFields(screen) {
63
+ const excluded = screen.writeExcludedFields;
64
+ if (!excluded?.length)
65
+ return screen.fields;
66
+ const inLayout = new Set(screen.layout.sections.flatMap((section) => isFieldsEditSection(section)
67
+ ? sectionFieldSpecs(section).map((spec) => normalizeEditField(spec).field)
68
+ : []));
69
+ return Object.fromEntries(Object.entries(screen.fields).filter(([name]) => inLayout.has(name) || !excluded.includes(name)));
70
+ }
25
71
  /** Wandelt ein ActionFormScreenDefinition oder SecretMintScreenDefinition in
26
72
  * die EntityEditScreen-Shape die RenderEdit erwartet. type wird auf
27
73
  * "entityEdit" gesetzt damit der Type-Constraint hält; entity wird auf den
@@ -31,7 +77,7 @@ export function synthesizeActionFormScreen(screen) {
31
77
  id: screen.id,
32
78
  type: "entityEdit",
33
79
  entity: ACTION_FORM_PSEUDO_ENTITY,
34
- layout: screen.layout,
80
+ layout: layoutWithLockedExcludedFields(screen),
35
81
  ...(screen.description !== undefined && { description: screen.description }),
36
82
  ...(screen.access !== undefined && { access: screen.access }),
37
83
  // Only ActionFormScreenDefinition carries slots/fieldLabels; `in` narrows
@@ -23,7 +23,7 @@ import { InsideDrawerProvider, useInsideDrawer } from "../inside-drawer.js";
23
23
  import { PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "../page-header-slot.js";
24
24
  import { statusToneForOptionTone, statusToneForValue, usePrimitives, } from "../primitives.js";
25
25
  import { screenFillsHeight } from "../screen-fills-height.js";
26
- import { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./action-form-shim.js";
26
+ import { actionFormSubmittableFields, synthesizeActionFormEntity, synthesizeActionFormScreen, } from "./action-form-shim.js";
27
27
  import { useAppFeatures } from "./app-features-context.js";
28
28
  import { buildConfigEditSchema, synthesizeConfigEditEntity, synthesizeConfigEditScreen, } from "./config-edit-shim.js";
29
29
  import { useCustomScreenComponent } from "./custom-screens.js";
@@ -2356,7 +2356,8 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
2356
2356
  const { Banner } = usePrimitives();
2357
2357
  // Unused when drawer-hosted — onSuccess/onCancelOverride win below first.
2358
2358
  const returnTarget = useReturnTarget(screen.id);
2359
- const synthEntity = useMemo(() => synthesizeActionFormEntity(screen.fields), [screen.fields]);
2359
+ const submittableFields = useMemo(() => actionFormSubmittableFields(screen), [screen]);
2360
+ const synthEntity = useMemo(() => synthesizeActionFormEntity(submittableFields), [submittableFields]);
2360
2361
  const synthScreen = useMemo(() => synthesizeActionFormScreen(screen), [screen]);
2361
2362
  const pendingHandoff = useInitialValuesHandoff(screen.id);
2362
2363
  // A drawer-hosted form is not a navigation target, so it never takes a handoff.
@@ -2368,7 +2369,7 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
2368
2369
  // submitting a guessed currency would mis-book the amount — block instead.
2369
2370
  const tenantCurrencyFieldNames = useMemo(() => tenantCurrencyMoneyFieldNames(screen.fields), [screen.fields]);
2370
2371
  const { overrides: moneyCurrencyOverrides, loading: currencyLoading, unresolved: currencyUnresolved, } = useMoneyCurrencyOverrides(screen.fields, tenantCurrencyFieldNames, undefined);
2371
- const initial = useMemo(() => mergeSearchParamsIntoInitial(screen.fields, {
2372
+ const initial = useMemo(() => mergeSearchParamsIntoInitial(submittableFields, {
2372
2373
  searchParams: nav.searchParams,
2373
2374
  urlPrefillFields: screen.urlPrefillFields,
2374
2375
  renderableFields: layoutFieldNames(synthScreen),
@@ -2376,7 +2377,7 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
2376
2377
  ...(initialOverrides !== undefined && { drawerOverrides: initialOverrides }),
2377
2378
  ...(handoffValues !== undefined && { handoffValues }),
2378
2379
  }), [
2379
- screen.fields,
2380
+ submittableFields,
2380
2381
  screen.urlPrefillFields,
2381
2382
  nav.searchParams,
2382
2383
  synthScreen,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.358.0",
3
+ "version": "0.359.0",
4
4
  "description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -15,9 +15,9 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.358.0",
19
- "@cosmicdrift/kumiko-headless": "0.358.0",
20
- "@cosmicdrift/kumiko-types": "0.358.0",
18
+ "@cosmicdrift/kumiko-framework": "0.359.0",
19
+ "@cosmicdrift/kumiko-headless": "0.359.0",
20
+ "@cosmicdrift/kumiko-types": "0.359.0",
21
21
  "react": "^19.2.6",
22
22
  "temporal-polyfill": "^0.3.2",
23
23
  "zod": "^4.4.3"
@@ -28,7 +28,7 @@
28
28
  "@types/react-dom": "^19.2.3",
29
29
  "jsdom": "^29.1.1",
30
30
  "react-dom": "^19.2.6",
31
- "@cosmicdrift/kumiko-locale-de": "0.358.0"
31
+ "@cosmicdrift/kumiko-locale-de": "0.359.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",