@cosmicdrift/kumiko-renderer 0.383.1 → 0.385.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.
Files changed (103) hide show
  1. package/dist/app/action-form-body.d.ts +50 -0
  2. package/dist/app/action-form-body.js +275 -0
  3. package/dist/app/client-actions.js +2 -2
  4. package/dist/app/config-edit-body.d.ts +9 -0
  5. package/dist/app/config-edit-body.js +340 -0
  6. package/dist/app/entity-edit-prefill.d.ts +27 -0
  7. package/dist/app/entity-edit-prefill.js +377 -0
  8. package/dist/app/entity-edit-screen.d.ts +11 -0
  9. package/dist/app/entity-edit-screen.js +395 -0
  10. package/dist/app/entity-list-screen.d.ts +10 -0
  11. package/dist/app/entity-list-screen.js +547 -0
  12. package/dist/app/kumiko-screen.d.ts +1 -36
  13. package/dist/app/kumiko-screen.js +16 -2855
  14. package/dist/app/projection-detail-body.d.ts +12 -0
  15. package/dist/app/projection-detail-body.js +464 -0
  16. package/dist/app/projection-detail-shim.d.ts +1 -1
  17. package/dist/app/projection-detail-shim.js +5 -1
  18. package/dist/app/projection-list-body.d.ts +10 -0
  19. package/dist/app/projection-list-body.js +245 -0
  20. package/dist/app/projection-list-shim.d.ts +2 -1
  21. package/dist/app/projection-list-shim.js +4 -0
  22. package/dist/app/reference-facet-bridge.js +2 -2
  23. package/dist/app/row-actions.js +3 -4
  24. package/dist/app/screen-shared.d.ts +72 -0
  25. package/dist/app/screen-shared.js +325 -0
  26. package/dist/app/secret-mint-body.js +3 -3
  27. package/dist/components/edit-mounts.d.ts +33 -0
  28. package/dist/components/edit-mounts.js +53 -0
  29. package/dist/components/error-boundary.js +2 -2
  30. package/dist/components/reference-create-dialog.js +1 -1
  31. package/dist/components/related-list-section.js +2 -2
  32. package/dist/components/render-edit-helpers.d.ts +29 -0
  33. package/dist/components/render-edit-helpers.js +121 -0
  34. package/dist/components/render-edit.d.ts +1 -4
  35. package/dist/components/render-edit.js +41 -443
  36. package/dist/components/render-field.js +3 -4
  37. package/dist/components/render-list.js +2 -2
  38. package/dist/components/use-wizard-form-draft.d.ts +51 -0
  39. package/dist/components/use-wizard-form-draft.js +291 -0
  40. package/dist/dev-console.d.ts +3 -0
  41. package/dist/dev-console.js +15 -0
  42. package/dist/headless/contracts/primitives.js +14 -17
  43. package/dist/headless/format/index.js +25 -6
  44. package/dist/headless/view-model/edit.d.ts +1 -1
  45. package/dist/headless/view-model/edit.js +16 -17
  46. package/dist/headless/view-model/list.d.ts +1 -1
  47. package/dist/headless/view-model/list.js +3 -8
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +5 -5
  50. package/dist/live/error-mapping.js +3 -4
  51. package/dist/web/app/create-app.js +5 -8
  52. package/dist/web/app/dashboard-body.js +4 -599
  53. package/dist/web/app/dashboard-panels.d.ts +53 -0
  54. package/dist/web/app/dashboard-panels.js +373 -0
  55. package/dist/web/app/dashboard-shared.d.ts +90 -0
  56. package/dist/web/app/dashboard-shared.js +171 -0
  57. package/dist/web/app/dashboard-text.d.ts +6 -0
  58. package/dist/web/app/dashboard-text.js +65 -0
  59. package/dist/web/layout/nav-tree.js +3 -4
  60. package/dist/web/layout/target-resolver-stub.js +2 -2
  61. package/dist/web/primitives/banner.d.ts +4 -0
  62. package/dist/web/primitives/banner.js +28 -0
  63. package/dist/web/primitives/button.d.ts +7 -0
  64. package/dist/web/primitives/button.js +48 -0
  65. package/dist/web/primitives/calendar-day-picker.d.ts +13 -0
  66. package/dist/web/primitives/calendar-day-picker.js +38 -0
  67. package/dist/web/primitives/calendar-popover.d.ts +1 -1
  68. package/dist/web/primitives/calendar-popover.js +5 -39
  69. package/dist/web/primitives/card-styles.d.ts +8 -0
  70. package/dist/web/primitives/card-styles.js +18 -0
  71. package/dist/web/primitives/card.d.ts +4 -0
  72. package/dist/web/primitives/card.js +41 -0
  73. package/dist/web/primitives/contexts.d.ts +22 -0
  74. package/dist/web/primitives/contexts.js +50 -0
  75. package/dist/web/primitives/data-table-cell.d.ts +76 -0
  76. package/dist/web/primitives/data-table-cell.js +403 -0
  77. package/dist/web/primitives/data-table.d.ts +4 -0
  78. package/dist/web/primitives/data-table.js +279 -0
  79. package/dist/web/primitives/default-form.d.ts +4 -0
  80. package/dist/web/primitives/default-form.js +79 -0
  81. package/dist/web/primitives/facet-filter.d.ts +14 -0
  82. package/dist/web/primitives/facet-filter.js +39 -0
  83. package/dist/web/primitives/field.d.ts +63 -0
  84. package/dist/web/primitives/field.js +181 -0
  85. package/dist/web/primitives/file-thumbnail.js +69 -7
  86. package/dist/web/primitives/form.d.ts +53 -0
  87. package/dist/web/primitives/form.js +252 -0
  88. package/dist/web/primitives/index.d.ts +7 -30
  89. package/dist/web/primitives/index.js +27 -2005
  90. package/dist/web/primitives/input.d.ts +4 -0
  91. package/dist/web/primitives/input.js +166 -0
  92. package/dist/web/primitives/layout-elements.d.ts +14 -0
  93. package/dist/web/primitives/layout-elements.js +194 -0
  94. package/dist/web/primitives/modal.js +1 -1
  95. package/dist/web/primitives/pager.d.ts +22 -0
  96. package/dist/web/primitives/pager.js +89 -0
  97. package/dist/web/primitives/row-actions.d.ts +13 -0
  98. package/dist/web/primitives/row-actions.js +178 -0
  99. package/dist/web/primitives/secret-reveal.d.ts +4 -0
  100. package/dist/web/primitives/secret-reveal.js +67 -0
  101. package/dist/web/tokens.js +2 -1
  102. package/package.json +5 -5
  103. package/src/changes.json +24 -0
@@ -0,0 +1,50 @@
1
+ import type { ActionFormScreenDefinition, RowActionDrawer, ToolbarAction } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { type ReactNode } from "react";
3
+ import type { Translate } from "../headless/index.ts";
4
+ import type { FeatureSchema } from "./feature-schema.ts";
5
+ type DrawerLikeAction = Extract<ToolbarAction, {
6
+ kind: "drawer";
7
+ }> | RowActionDrawer;
8
+ export declare function useDrawerAction(schema: FeatureSchema): {
9
+ readonly drawerAction: DrawerLikeAction | null;
10
+ readonly drawerScreen: ActionFormScreenDefinition | undefined;
11
+ readonly drawerInitialValues: Readonly<Record<string, unknown>> | undefined;
12
+ readonly openDrawer: (action: DrawerLikeAction, initialValues?: Readonly<Record<string, unknown>>) => void;
13
+ readonly closeDrawer: () => void;
14
+ };
15
+ export declare function DrawerHost({ schema, drawerAction, drawerScreen, drawerInitialValues, userRoles, translate, onClose, onSuccess, }: {
16
+ readonly schema: FeatureSchema;
17
+ readonly drawerAction: DrawerLikeAction | null;
18
+ readonly drawerScreen: ActionFormScreenDefinition | undefined;
19
+ readonly drawerInitialValues?: Readonly<Record<string, unknown>>;
20
+ readonly userRoles: readonly string[] | undefined;
21
+ readonly translate?: Translate;
22
+ readonly onClose: () => void;
23
+ readonly onSuccess: () => void;
24
+ }): ReactNode;
25
+ export declare function ActionFormBody({ schema, screen, translate, entityId, initialOverrides, onSuccess, onCancelOverride, onDirtyChange, submitLabelFallback, }: {
26
+ readonly schema: FeatureSchema;
27
+ readonly screen: ActionFormScreenDefinition;
28
+ readonly translate?: Translate;
29
+ /** Record id from the route path; the source of the `load` query's id. */
30
+ readonly entityId?: string;
31
+ /** Row-drawer usage (RowAction kind:"drawer", fw#2710): prefill extracted
32
+ * from the clicked row, applied on top of searchParams through the same
33
+ * renderableFields/sensitive gates (see mergeSearchParamsIntoInitial). */
34
+ readonly initialOverrides?: Readonly<Record<string, unknown>>;
35
+ /** Drawer-hosted usage (toolbarAction kind:"drawer", fw#2225): called
36
+ * instead of the redirect-based navigation on successful submit, so the
37
+ * host closes the drawer + refetches its list regardless of whether
38
+ * `screen.redirect` is set — a full-page redirect would navigate away
39
+ * from the list the drawer sits on top of. */
40
+ readonly onSuccess?: () => void;
41
+ /** Drawer-hosted usage: replaces the cancelTarget/redirect-based Cancel
42
+ * handler so Cancel closes the drawer instead of navigating. */
43
+ readonly onCancelOverride?: () => void;
44
+ /** Drawer-hosted usage: reports unsaved input so the host can confirm
45
+ * before discarding it. */
46
+ readonly onDirtyChange?: (dirty: boolean) => void;
47
+ /** Drawer-hosted usage: submit label when the screen declares none (the opening action's label). */
48
+ readonly submitLabelFallback?: string;
49
+ }): ReactNode;
50
+ export {};
@@ -0,0 +1,275 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { withDerivedValues } from "../components/form-derivation-patch.js";
4
+ import { RenderEdit } from "../components/render-edit.js";
5
+ import { useDispatcher } from "../context/dispatcher-context.js";
6
+ import { rendererWarn } from "../dev-console.js";
7
+ import { useLocale, useTranslation } from "../i18n.js";
8
+ import { InsideDrawerProvider, useInsideDrawer } from "../inside-drawer.js";
9
+ import { PageHeaderSlotAvailableProvider } from "../page-header-slot.js";
10
+ import { usePrimitives } from "../primitives.js";
11
+ import { screenFillsHeight } from "../screen-fills-height.js";
12
+ import { actionFormSubmittableFields, synthesizeActionFormEntity, synthesizeActionFormScreen, } from "./action-form-shim.js";
13
+ import { ActionFormLoadStatus, ActionFormSuccessBanner, isSecretFormField, resolveActionFormRecordId, resolveSuccessFacts, useActionFormLoad, } from "./action-form-support.js";
14
+ import { useAppFeatures } from "./app-features-context.js";
15
+ import { mergeSearchParamsIntoInitial, tenantCurrencyMoneyFieldNames, useMoneyCurrencyOverrides, } from "./entity-edit-prefill.js";
16
+ import { hiddenRequiredFieldsFilled, layoutFieldNames } from "./layout-fields.js";
17
+ import { useInitialValuesHandoff, useNav } from "./nav.js";
18
+ import { lastSegment } from "./qn.js";
19
+ import { qualifyScreenId } from "./qualify-screen-id.js";
20
+ import { QueryLoadingBanner } from "./query-state-banners.js";
21
+ import { navigateToReturn, navigateToReturnOr, useReturnTarget } from "./return-to.js";
22
+ import { screenAccessAllows } from "./screen-access.js";
23
+ import { formatSummaryParams, navigateToRedirectTarget, redirectScreenTarget, resolveActionFormSummary, resolveRedirectTarget, resolveSuccessLinks, resolveSuccessMessageParams, returnToWinsOverRedirect, ScreenAccessDeniedBanner, ScreenMissingState, } from "./screen-shared.js";
24
+ export function useDrawerAction(schema) {
25
+ const [drawerState, setDrawerState] = useState(null);
26
+ const drawerAction = drawerState?.action ?? null;
27
+ const drawerScreen = useMemo(() => {
28
+ if (drawerAction === null)
29
+ return undefined;
30
+ // Same same-feature, short-id resolution as runNavigate — the drawer's
31
+ // `screen` reference is scoped to schema.screens, not cross-feature.
32
+ return schema.screens.find((s) => s.type === "actionForm" && lastSegment(s.id) === drawerAction.screen);
33
+ }, [drawerAction, schema.screens]);
34
+ const openDrawer = useCallback((action, initialValues) => setDrawerState({ action, initialValues }), []);
35
+ const closeDrawer = useCallback(() => setDrawerState(null), []);
36
+ return {
37
+ drawerAction,
38
+ drawerScreen,
39
+ drawerInitialValues: drawerState?.initialValues,
40
+ openDrawer,
41
+ closeDrawer,
42
+ };
43
+ }
44
+ export function DrawerHost({ schema, drawerAction, drawerScreen, drawerInitialValues, userRoles, translate, onClose, onSuccess, }) {
45
+ const { Drawer, Dialog } = usePrimitives();
46
+ const t = useTranslation();
47
+ const effectiveTranslate = translate ?? t;
48
+ const locale = useLocale().locale();
49
+ const [hasUnsavedInput, setHasUnsavedInput] = useState(false);
50
+ const [confirmDiscardOpen, setConfirmDiscardOpen] = useState(false);
51
+ const closeAndReset = useCallback(() => {
52
+ setHasUnsavedInput(false);
53
+ setConfirmDiscardOpen(false);
54
+ onClose();
55
+ }, [onClose]);
56
+ const requestClose = useCallback(() => {
57
+ if (hasUnsavedInput)
58
+ setConfirmDiscardOpen(true);
59
+ else
60
+ closeAndReset();
61
+ }, [hasUnsavedInput, closeAndReset]);
62
+ // Drawer is an optional Core-Primitive (additive rollout) — same "skip +
63
+ // warn" precedent as rowActions without a mounted DispatcherProvider above,
64
+ // instead of crashing when a web app hasn't upgraded its createKumikoApp
65
+ // wiring yet. The warning repeats per mount and whenever the action changes.
66
+ // Dev-time setup error, so no dedupe.
67
+ useEffect(() => {
68
+ if (drawerAction !== null && Drawer === undefined) {
69
+ rendererWarn(`[kumiko] toolbarAction "${drawerAction.id}" is kind:"drawer", but no <Drawer> primitive is registered — it will not open. createKumikoApp() from kumiko-renderer-web wires it automatically.`);
70
+ }
71
+ }, [drawerAction, Drawer]);
72
+ if (drawerAction === null || Drawer === undefined)
73
+ return null;
74
+ // Access mirrors kind:"navigate" exactly: the toolbar button itself stays
75
+ // visible either way (same as navigate — access is enforced at the
76
+ // target, not by hiding the trigger), but the drawer shows the same
77
+ // "Access denied" state KumikoScreen's top-level gate would show for a
78
+ // direct hit on that screen, never the form.
79
+ const allowed = screenAccessAllows(drawerScreen?.access, userRoles);
80
+ const titleParams = formatSummaryParams(drawerInitialValues, locale);
81
+ return (_jsxs(_Fragment, { children: [_jsx(Drawer, { open: true, onOpenChange: (open) => {
82
+ if (!open)
83
+ requestClose();
84
+ }, title: effectiveTranslate(drawerAction.title ?? drawerAction.label, titleParams), ...(drawerAction.subtitle !== undefined && {
85
+ subtitle: effectiveTranslate(drawerAction.subtitle, titleParams),
86
+ }), testId: `toolbar-drawer-${drawerAction.id}`, children: _jsx(PageHeaderSlotAvailableProvider, { value: false, children: _jsx(InsideDrawerProvider, { value: true, children: drawerScreen === undefined ? (_jsx(ScreenMissingState, { screenQn: drawerAction.screen, testId: "kumiko-toolbar-drawer-not-found" })) : !allowed ? (_jsx(ScreenAccessDeniedBanner, { screenQn: drawerAction.screen, testId: "kumiko-toolbar-drawer-access-denied" })) : (_jsx(ActionFormBody, { schema: schema, screen: drawerScreen, ...(translate !== undefined && { translate }), ...(drawerInitialValues !== undefined && {
87
+ initialOverrides: drawerInitialValues,
88
+ }), onSuccess: () => {
89
+ setHasUnsavedInput(false);
90
+ onSuccess();
91
+ }, onCancelOverride: requestClose, onDirtyChange: setHasUnsavedInput, submitLabelFallback: drawerAction.label })) }) }) }), _jsx(Dialog, { open: confirmDiscardOpen, onOpenChange: setConfirmDiscardOpen, title: t("kumiko.drawer.discard.title"), description: t("kumiko.drawer.discard.body"), confirmLabel: t("kumiko.drawer.discard.confirm"), cancelLabel: t("kumiko.drawer.discard.cancel"), variant: "danger", initialFocus: "cancel", onConfirm: closeAndReset, testId: "drawer-discard-dialog" })] }));
92
+ }
93
+ // Action form body: non-CRUD form driven by a write handler. Reuses
94
+ // RenderEdit via a synthesized EntityDefinition + EntityEditScreen
95
+ // definition (see action-form-shim.ts for the debt doc). The form mechanics
96
+ // (useForm, RenderEdit, DefaultInput, Banner, submit button) are identical
97
+ // to entityEdit, only the submit path switches from the CRUD verb to the
98
+ // author-declared handler QN + payloadMode="values" (send all form values
99
+ // instead of only the changes).
100
+ export function ActionFormBody({ schema, screen, translate, entityId, initialOverrides, onSuccess, onCancelOverride, onDirtyChange, submitLabelFallback, }) {
101
+ const nav = useNav();
102
+ const appFeatures = useAppFeatures();
103
+ const insideDrawer = useInsideDrawer();
104
+ const locale = useLocale().locale();
105
+ const { Banner } = usePrimitives();
106
+ // Unused when drawer-hosted — onSuccess/onCancelOverride win below first.
107
+ const returnTarget = useReturnTarget(screen.id);
108
+ const submittableFields = useMemo(() => actionFormSubmittableFields(screen), [screen]);
109
+ const synthEntity = useMemo(() => synthesizeActionFormEntity(submittableFields), [submittableFields]);
110
+ const synthScreen = useMemo(() => synthesizeActionFormScreen(screen), [screen]);
111
+ const pendingHandoff = useInitialValuesHandoff(screen.id);
112
+ // A drawer-hosted form is not a navigation target, so it never takes a handoff.
113
+ const handoffValues = onSuccess === undefined ? pendingHandoff : undefined;
114
+ // This screen has no entity, so a money field here declares its own currency
115
+ // source (fw#2839, boot-enforced) — without one the form would seed a bare
116
+ // `0` the handler's schema rejects. When a tenant-declared field's config
117
+ // query errors or holds no value there is no entity default to use, and
118
+ // submitting a guessed currency would mis-book the amount — block instead.
119
+ const tenantCurrencyFieldNames = useMemo(() => tenantCurrencyMoneyFieldNames(screen.fields), [screen.fields]);
120
+ const { overrides: moneyCurrencyOverrides, loading: currencyLoading, unresolved: currencyUnresolved, } = useMoneyCurrencyOverrides(screen.fields, tenantCurrencyFieldNames, undefined);
121
+ const loadableFieldNames = useMemo(() => Object.entries(submittableFields)
122
+ .filter(([, field]) => !isSecretFormField(field))
123
+ .map(([name]) => name), [submittableFields]);
124
+ const loadState = useActionFormLoad(screen.load, screen.load !== undefined
125
+ ? resolveActionFormRecordId({
126
+ routeEntityId: entityId,
127
+ drawerOverrides: initialOverrides,
128
+ searchParams: nav.searchParams,
129
+ })
130
+ : undefined, loadableFieldNames);
131
+ const loadedValues = loadState?.status === "ready" ? loadState.values : undefined;
132
+ const initial = useMemo(() => {
133
+ // With `load` the loaded record is the only source of values: a crafted link
134
+ // or a row's params must not seed fields the record would otherwise own.
135
+ const prefillSources = screen.load !== undefined
136
+ ? { searchParams: {}, urlPrefillFields: undefined }
137
+ : {
138
+ searchParams: nav.searchParams,
139
+ urlPrefillFields: screen.urlPrefillFields,
140
+ ...(initialOverrides !== undefined && { drawerOverrides: initialOverrides }),
141
+ ...(handoffValues !== undefined && { handoffValues }),
142
+ };
143
+ return {
144
+ ...mergeSearchParamsIntoInitial(submittableFields, {
145
+ ...prefillSources,
146
+ renderableFields: layoutFieldNames(synthScreen),
147
+ ...(moneyCurrencyOverrides !== undefined && { moneyCurrencyOverrides }),
148
+ }),
149
+ ...loadedValues,
150
+ };
151
+ }, [
152
+ submittableFields,
153
+ screen.load,
154
+ screen.urlPrefillFields,
155
+ nav.searchParams,
156
+ synthScreen,
157
+ moneyCurrencyOverrides,
158
+ initialOverrides,
159
+ handoffValues,
160
+ loadedValues,
161
+ ]);
162
+ const derivedInitial = useMemo(() => withDerivedValues(initial, screen.derive !== undefined ? screen : undefined), [initial, screen]);
163
+ const t = useTranslation();
164
+ const effectiveTranslate = translate ?? t;
165
+ const summary = useMemo(() => resolveActionFormSummary(screen.summary, initialOverrides, effectiveTranslate, locale), [screen.summary, initialOverrides, effectiveTranslate, locale]);
166
+ const dispatcher = useDispatcher();
167
+ const [success, setSuccess] = useState(undefined);
168
+ // Bumped after a successful submit with resetOnSuccess: remounts RenderEdit, which
169
+ // re-seeds the form from `initial` (prefill kept, typed values cleared).
170
+ const [formEpoch, setFormEpoch] = useState(0);
171
+ // A slow label lookup of an earlier submit must not overwrite the banner of a later one.
172
+ const successSequence = useRef(0);
173
+ useEffect(() => () => {
174
+ successSequence.current += 1;
175
+ }, []);
176
+ const handleSubmitted = useCallback((result, values) => {
177
+ successSequence.current += 1;
178
+ const sequence = successSequence.current;
179
+ setSuccess(undefined);
180
+ if (!result.isSuccess)
181
+ return;
182
+ if (onSuccess !== undefined) {
183
+ onSuccess();
184
+ return;
185
+ }
186
+ if (screen.redirect === undefined) {
187
+ const hasSuccessLinks = (screen.successLinks?.length ?? 0) > 0 || (screen.successFacts?.length ?? 0) > 0;
188
+ const publish = (text) => {
189
+ if (successSequence.current !== sequence)
190
+ return;
191
+ setSuccess({
192
+ text,
193
+ links: resolveSuccessLinks(screen.successLinks, result.data, schema, appFeatures, nav, effectiveTranslate),
194
+ facts: resolveSuccessFacts(screen.successFacts, result.data, effectiveTranslate, locale),
195
+ });
196
+ };
197
+ const messageKey = screen.successMessage;
198
+ if (messageKey !== undefined) {
199
+ const show = (params) => publish(effectiveTranslate(messageKey, params));
200
+ void resolveSuccessMessageParams(screen, schema.featureName, values, dispatcher, locale)
201
+ .then(show)
202
+ .catch(() => show(formatSummaryParams(values, locale)));
203
+ }
204
+ else if (hasSuccessLinks || screen.resetOnSuccess === true) {
205
+ publish(effectiveTranslate("kumiko.actionForm.success"));
206
+ }
207
+ if (screen.resetOnSuccess === true)
208
+ setFormEpoch((epoch) => epoch + 1);
209
+ }
210
+ // Without a redirect the form stays put; returnTo only replaces an existing
211
+ // navigation, and never one to a record screen (see redirectTargetsRecord).
212
+ if (screen.redirect !== undefined) {
213
+ if (returnToWinsOverRedirect(returnTarget, screen.redirect, schema, appFeatures)) {
214
+ navigateToReturn(nav, returnTarget);
215
+ }
216
+ else {
217
+ navigateToRedirectTarget(nav, resolveRedirectTarget(screen.redirect, result.data, schema, appFeatures));
218
+ }
219
+ }
220
+ }, [
221
+ nav,
222
+ screen,
223
+ onSuccess,
224
+ schema,
225
+ appFeatures,
226
+ returnTarget,
227
+ dispatcher,
228
+ locale,
229
+ effectiveTranslate,
230
+ ]);
231
+ // The success box disappears with the next edit; a submit clears it itself.
232
+ const handleDirtyChange = useCallback((dirty) => {
233
+ if (dirty) {
234
+ successSequence.current += 1;
235
+ setSuccess(undefined);
236
+ }
237
+ onDirtyChange?.(dirty);
238
+ }, [onDirtyChange]);
239
+ // Cancel only makes sense when a navigation target exists, otherwise the
240
+ // button would have nowhere to navigate. cancelTarget wins over redirect;
241
+ // `false` switches the button off explicitly (single-action screens where
242
+ // cancel would just be submit-without-sending).
243
+ // returnTo wins over the declared target when both are present.
244
+ const handleCancel = useMemo(() => {
245
+ if (onCancelOverride !== undefined)
246
+ return onCancelOverride;
247
+ const target = screen.cancelTarget ??
248
+ (screen.redirect !== undefined ? redirectScreenTarget(screen.redirect) : undefined);
249
+ if (target === undefined || target === false)
250
+ return undefined;
251
+ return () => navigateToReturnOr(nav, returnTarget, () => nav.navigate({ screenId: lastSegment(target) }));
252
+ }, [nav, screen.redirect, screen.cancelTarget, onCancelOverride, returnTarget]);
253
+ // Rendering the form before a tenant-declared currency lands would show the
254
+ // fallback and, on a fast click, submit it (fw#2933) — hold instead.
255
+ if (currencyLoading) {
256
+ return _jsx(QueryLoadingBanner, {});
257
+ }
258
+ if (currencyUnresolved) {
259
+ return (_jsx(Banner, { padded: true, variant: "error", testId: "kumiko-screen-currency-unresolved", children: "The tenant currency could not be loaded, so this form cannot be submitted." }));
260
+ }
261
+ if (loadState !== undefined && loadState.status !== "ready") {
262
+ return (_jsx(ActionFormLoadStatus, { state: loadState, screenId: screen.id, translate: effectiveTranslate }));
263
+ }
264
+ return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: derivedInitial, ...(screen.derive !== undefined && { formDerivation: screen }), actionFormScreenQn: qualifyScreenId(schema.featureName, screen.id),
265
+ // Extension sections in the actionForm see the initial form values
266
+ // (incl. searchParams prefill, e.g. ?incidentId=...) as initialValues;
267
+ // unlike entityEdit there is no record they could draw context from.
268
+ extensionInitialValues: initial, ...(screen.submitPrefilled === true && {
269
+ submitWithoutChanges: hiddenRequiredFieldsFilled(synthScreen, screen.fields, initial),
270
+ }), ...(success !== undefined && {
271
+ leadContent: _jsx(ActionFormSuccessBanner, { success: success }),
272
+ }), writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, ...(handleCancel !== undefined && { onCancel: handleCancel }), onDirtyChange: handleDirtyChange, ...((insideDrawer || screenFillsHeight(screen)) && { fillScreenHeight: true }), ...((screen.submitLabel ?? submitLabelFallback) !== undefined && {
273
+ submitLabel: screen.submitLabel ?? submitLabelFallback,
274
+ }), ...(screen.submitStyle !== undefined && { submitVariant: screen.submitStyle }), ...(screen.footerActions !== undefined && { footerActions: screen.footerActions }), ...(summary !== undefined && { summary }), ...(translate !== undefined && { translate }) }, formEpoch));
275
+ }
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo } from "react";
3
+ import { rendererError } from "../dev-console.js";
3
4
  const ClientActionsContext = createContext(undefined);
4
5
  const ActionNoticeContext = createContext(undefined);
5
6
  export function ClientActionsProvider({ children, value, }) {
@@ -21,8 +22,7 @@ function findClientActionHandler(handlers, name) {
21
22
  return handlers !== undefined && Object.hasOwn(handlers, name) ? handlers[name] : undefined;
22
23
  }
23
24
  function logClientActionProblem(name, problem, cause) {
24
- // biome-ignore lint/suspicious/noConsole: the client has no logger; the toast text is generic, so the action name and cause must reach the DevTools console
25
- console.error(`[kumiko] clientAction "${name}" ${problem}`, ...(cause === undefined ? [] : [cause]));
25
+ rendererError(`[kumiko] clientAction "${name}" ${problem}`, ...(cause === undefined ? [] : [cause]));
26
26
  }
27
27
  /** Runs the handler registered for `action.action` and reports the outcome.
28
28
  * Resolves true when the handler returned `{ ok: true }` (the caller then
@@ -0,0 +1,9 @@
1
+ import type { ConfigEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { type ReactNode } from "react";
3
+ import type { Translate } from "../headless/index.ts";
4
+ import type { FeatureSchema } from "./feature-schema.ts";
5
+ export declare function ConfigEditBody({ schema, screen, translate, }: {
6
+ readonly schema: FeatureSchema;
7
+ readonly screen: ConfigEditScreenDefinition;
8
+ readonly translate?: Translate;
9
+ }): ReactNode;