@cosmicdrift/kumiko-renderer 0.384.0 → 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.
- package/dist/app/action-form-body.d.ts +50 -0
- package/dist/app/action-form-body.js +275 -0
- package/dist/app/client-actions.js +2 -2
- package/dist/app/config-edit-body.d.ts +9 -0
- package/dist/app/config-edit-body.js +340 -0
- package/dist/app/entity-edit-prefill.d.ts +27 -0
- package/dist/app/entity-edit-prefill.js +377 -0
- package/dist/app/entity-edit-screen.d.ts +11 -0
- package/dist/app/entity-edit-screen.js +395 -0
- package/dist/app/entity-list-screen.d.ts +10 -0
- package/dist/app/entity-list-screen.js +547 -0
- package/dist/app/kumiko-screen.d.ts +1 -36
- package/dist/app/kumiko-screen.js +16 -2855
- package/dist/app/projection-detail-body.d.ts +12 -0
- package/dist/app/projection-detail-body.js +464 -0
- package/dist/app/projection-detail-shim.d.ts +1 -1
- package/dist/app/projection-detail-shim.js +5 -1
- package/dist/app/projection-list-body.d.ts +10 -0
- package/dist/app/projection-list-body.js +245 -0
- package/dist/app/projection-list-shim.d.ts +2 -1
- package/dist/app/projection-list-shim.js +4 -0
- package/dist/app/reference-facet-bridge.js +2 -2
- package/dist/app/row-actions.js +3 -4
- package/dist/app/screen-shared.d.ts +72 -0
- package/dist/app/screen-shared.js +325 -0
- package/dist/app/secret-mint-body.js +3 -3
- package/dist/components/edit-mounts.d.ts +33 -0
- package/dist/components/edit-mounts.js +53 -0
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/reference-create-dialog.js +1 -1
- package/dist/components/related-list-section.js +2 -2
- package/dist/components/render-edit-helpers.d.ts +29 -0
- package/dist/components/render-edit-helpers.js +121 -0
- package/dist/components/render-edit.d.ts +1 -4
- package/dist/components/render-edit.js +41 -443
- package/dist/components/render-field.js +3 -4
- package/dist/components/render-list.js +2 -2
- package/dist/components/use-wizard-form-draft.d.ts +51 -0
- package/dist/components/use-wizard-form-draft.js +291 -0
- package/dist/dev-console.d.ts +3 -0
- package/dist/dev-console.js +15 -0
- package/dist/headless/contracts/primitives.js +14 -17
- package/dist/headless/format/index.js +3 -3
- package/dist/headless/view-model/edit.d.ts +1 -1
- package/dist/headless/view-model/edit.js +9 -16
- package/dist/headless/view-model/list.d.ts +1 -1
- package/dist/headless/view-model/list.js +3 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -5
- package/dist/live/error-mapping.js +3 -4
- package/dist/web/app/create-app.js +5 -8
- package/dist/web/app/dashboard-body.js +4 -599
- package/dist/web/app/dashboard-panels.d.ts +53 -0
- package/dist/web/app/dashboard-panels.js +373 -0
- package/dist/web/app/dashboard-shared.d.ts +90 -0
- package/dist/web/app/dashboard-shared.js +171 -0
- package/dist/web/app/dashboard-text.d.ts +6 -0
- package/dist/web/app/dashboard-text.js +65 -0
- package/dist/web/layout/nav-tree.js +3 -4
- package/dist/web/layout/target-resolver-stub.js +2 -2
- package/dist/web/primitives/banner.d.ts +4 -0
- package/dist/web/primitives/banner.js +28 -0
- package/dist/web/primitives/button.d.ts +7 -0
- package/dist/web/primitives/button.js +48 -0
- package/dist/web/primitives/card-styles.d.ts +8 -0
- package/dist/web/primitives/card-styles.js +18 -0
- package/dist/web/primitives/card.d.ts +4 -0
- package/dist/web/primitives/card.js +41 -0
- package/dist/web/primitives/contexts.d.ts +22 -0
- package/dist/web/primitives/contexts.js +50 -0
- package/dist/web/primitives/data-table-cell.d.ts +76 -0
- package/dist/web/primitives/data-table-cell.js +403 -0
- package/dist/web/primitives/data-table.d.ts +4 -0
- package/dist/web/primitives/data-table.js +279 -0
- package/dist/web/primitives/default-form.d.ts +4 -0
- package/dist/web/primitives/default-form.js +79 -0
- package/dist/web/primitives/facet-filter.d.ts +14 -0
- package/dist/web/primitives/facet-filter.js +39 -0
- package/dist/web/primitives/field.d.ts +63 -0
- package/dist/web/primitives/field.js +181 -0
- package/dist/web/primitives/form.d.ts +53 -0
- package/dist/web/primitives/form.js +252 -0
- package/dist/web/primitives/index.d.ts +7 -30
- package/dist/web/primitives/index.js +27 -2006
- package/dist/web/primitives/input.d.ts +4 -0
- package/dist/web/primitives/input.js +166 -0
- package/dist/web/primitives/layout-elements.d.ts +14 -0
- package/dist/web/primitives/layout-elements.js +194 -0
- package/dist/web/primitives/modal.js +1 -1
- package/dist/web/primitives/pager.d.ts +22 -0
- package/dist/web/primitives/pager.js +89 -0
- package/dist/web/primitives/row-actions.d.ts +13 -0
- package/dist/web/primitives/row-actions.js +178 -0
- package/dist/web/primitives/secret-reveal.d.ts +4 -0
- package/dist/web/primitives/secret-reveal.js +67 -0
- package/package.json +5 -5
- package/src/changes.json +6 -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
|
-
|
|
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;
|