@cosmicdrift/kumiko-renderer 0.357.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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "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.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.359.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|