@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
|
@@ -1,236 +1,31 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
|
-
import { ErrorReference } from "../app/error-reference.js";
|
|
5
|
-
import { computeEditViewModel, groupIssuesByPath, relevantFieldIssues, zodErrorToFieldIssues, } from "../headless/index.js";
|
|
6
|
-
import { changedDerivedValues } from "./form-derivation-patch.js";
|
|
7
|
-
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
8
|
-
import { AllFieldsRequiredProvider } from "./render-field.js";
|
|
9
|
-
import { useActionMenuItems } from "./use-action-menu-items.js";
|
|
2
|
+
import { PROJECTION_DETAIL_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
10
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { ErrorReference } from "../app/error-reference.js";
|
|
11
5
|
import { ExtensionFormRegistryProvider, useExtensionFormHost, } from "../app/extension-form-submit.js";
|
|
12
6
|
import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections.js";
|
|
13
7
|
import { layoutEditFields } from "../app/layout-fields.js";
|
|
14
8
|
import { useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
15
9
|
import { useDraftStorage } from "../context/draft-storage-context.js";
|
|
16
10
|
import { formatWhen } from "../format-when.js";
|
|
11
|
+
import { computeEditViewModel, groupIssuesByPath } from "../headless/index.js";
|
|
17
12
|
import { useForm } from "../hooks/use-form.js";
|
|
18
13
|
import { useTranslation } from "../i18n.js";
|
|
19
14
|
import { useInsideDrawer } from "../inside-drawer.js";
|
|
20
15
|
import { useUnsavedInputSource } from "../leave-guard.js";
|
|
21
16
|
import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
|
|
22
17
|
import { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, usePrimitives, } from "../primitives.js";
|
|
18
|
+
import { EditSlotMount, ExtensionSectionMount } from "./edit-mounts.js";
|
|
19
|
+
import { changedDerivedValues } from "./form-derivation-patch.js";
|
|
23
20
|
import { GridCellForField } from "./grid-cell-for-field.js";
|
|
24
21
|
import { RelatedListSection } from "./related-list-section.js";
|
|
22
|
+
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
23
|
+
import { deriveFormFields, dryRunFieldIssues, dryRunParse, formSectionDomId, isFieldsStepFilled, resolveRecordTitle, resolveTitleTemplate, tabIdAt, wizardStepKey, } from "./render-edit-helpers.js";
|
|
25
24
|
import { filterEditSections, findFirstErroringSectionIndex, hasEditableSection, hasIssueWithoutRenderedField, isDispatcherRejection, issuesAfterEdit, ROOT_ISSUE_PATH, resolveExtensionEntityId, shouldNotifyCaller, } from "./render-edit-logic.js";
|
|
25
|
+
import { AllFieldsRequiredProvider } from "./render-field.js";
|
|
26
|
+
import { useActionMenuItems } from "./use-action-menu-items.js";
|
|
27
|
+
import { buildWizardDraftActions, PATCH_DRAFT_SAVE_DEBOUNCE_MS, useWizardFormDraftRestore, useWizardFormDraftState, } from "./use-wizard-form-draft.js";
|
|
26
28
|
import { WriteFormSection } from "./write-form-section.js";
|
|
27
|
-
// Qualified names of the bundled `form-draft` feature. Hardcoded because the
|
|
28
|
-
// renderer must not depend on @cosmicdrift/kumiko-bundled-features; a screen
|
|
29
|
-
// with `layout.draft: true` and that feature unmounted is a boot error, so
|
|
30
|
-
// these can never dangle silently.
|
|
31
|
-
const FORM_DRAFT_GET = "form-draft:query:get";
|
|
32
|
-
const FORM_DRAFT_SAVE = "form-draft:write:save";
|
|
33
|
-
const FORM_DRAFT_DISCARD = "form-draft:write:discard";
|
|
34
|
-
const FORM_DRAFT_LIST = "form-draft:query:list";
|
|
35
|
-
// Mirrors bundled-features/src/form-draft/web/constants.ts's FORM_DRAFT_KEY_MAX_LENGTH
|
|
36
|
-
// — same hardcoding rationale as the QNs above (no dependency on that package).
|
|
37
|
-
const FORM_DRAFT_KEY_MAX_LENGTH = 256;
|
|
38
|
-
// Trailing-edge debounce for patch()-triggered draft saves (#1914). A single
|
|
39
|
-
// patch() call (VIN-decode, an extension section) should not save immediately
|
|
40
|
-
// per call, but patch() can also fire from inside onChange on every keystroke
|
|
41
|
-
// (the #1888 derived-field shape) — 500ms collapses a typing burst into one
|
|
42
|
-
// save instead of one per keystroke, while still saving well before a user
|
|
43
|
-
// abandons the tab.
|
|
44
|
-
export const PATCH_DRAFT_SAVE_DEBOUNCE_MS = 500;
|
|
45
|
-
// draftKey convention for a create-mode draft (framework-wizard-mode.md,
|
|
46
|
-
// lookup.ts): `${screenId}:new:${draftId}`. Shared by the mount-time list
|
|
47
|
-
// fallback (strip the prefix to recover a candidate's draftId) and the
|
|
48
|
-
// list query itself (filter out edit-mode drafts, which share the same
|
|
49
|
-
// `${screenId}:%` LIKE-prefix scan server-side).
|
|
50
|
-
function newDraftPrefix(screenId) {
|
|
51
|
-
return `${screenId}:new:`;
|
|
52
|
-
}
|
|
53
|
-
// `crypto.randomUUID` is missing in non-secure contexts and React Native/Hermes
|
|
54
|
-
// without a polyfill — guard like generateRequestId in headless/dispatcher/request-id.ts.
|
|
55
|
-
function mintDraftId() {
|
|
56
|
-
const webCrypto = propertyOf(globalThis, "crypto");
|
|
57
|
-
const randomUUID = propertyOf(webCrypto, "randomUUID");
|
|
58
|
-
if (typeof randomUUID === "function") {
|
|
59
|
-
const id = Reflect.apply(randomUUID, webCrypto, []);
|
|
60
|
-
if (typeof id === "string")
|
|
61
|
-
return id;
|
|
62
|
-
}
|
|
63
|
-
return `draft-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
64
|
-
}
|
|
65
|
-
// End-to-end renderer für einen entityEdit screen. Rendert aus-
|
|
66
|
-
// schließlich über Primitives — kein raw HTML. Ein Native-Renderer
|
|
67
|
-
// der dieselbe Primitives-Registry füllt kriegt das Form ohne weitere
|
|
68
|
-
// Änderungen.
|
|
69
|
-
function toConditionValue(cond) {
|
|
70
|
-
if (typeof cond === "boolean")
|
|
71
|
-
return cond;
|
|
72
|
-
// @cast-boundary form-values: TValues ist strukturell ein Record.
|
|
73
|
-
return (values) => evalFieldCondition(cond, values);
|
|
74
|
-
}
|
|
75
|
-
// Tab-strip key. The boot-validator makes `id` mandatory on a tabs layout and
|
|
76
|
-
// entityEdit only ever has "fields"/"extension" sections (relatedList and
|
|
77
|
-
// writeForm are rejected outright), so the positional fallback is unreachable
|
|
78
|
-
// for a tabs screen — it only keeps this total for other layouts.
|
|
79
|
-
function tabIdAt(section, index) {
|
|
80
|
-
const declared = section?.kind === "fields" || section?.kind === "extension" ? section.id : undefined;
|
|
81
|
-
return declared ?? `section-${index}`;
|
|
82
|
-
}
|
|
83
|
-
function deriveFormFields(screen) {
|
|
84
|
-
const out = {};
|
|
85
|
-
for (const section of screen.layout.sections) {
|
|
86
|
-
// relatedList carries no `fields` for the form-condition map either.
|
|
87
|
-
if (!isFieldsEditSection(section))
|
|
88
|
-
continue;
|
|
89
|
-
for (const spec of sectionFieldSpecs(section)) {
|
|
90
|
-
const normalized = normalizeEditField(spec);
|
|
91
|
-
out[normalized.field] = {
|
|
92
|
-
...(normalized.visible !== undefined && {
|
|
93
|
-
visible: toConditionValue(normalized.visible),
|
|
94
|
-
}),
|
|
95
|
-
...(normalized.readOnly !== undefined && {
|
|
96
|
-
readonly: toConditionValue(normalized.readOnly),
|
|
97
|
-
}),
|
|
98
|
-
...(normalized.required !== undefined && {
|
|
99
|
-
required: toConditionValue(normalized.required),
|
|
100
|
-
}),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return out;
|
|
105
|
-
}
|
|
106
|
-
// Dry-run parse against `schema` — NOT controller.validate(), which writes
|
|
107
|
-
// field errors into the snapshot. Undefined when the values parse cleanly.
|
|
108
|
-
// Parse once, then narrow per scope with scopedDryRunIssues: the wizard step
|
|
109
|
-
// check would otherwise re-parse the whole form per section on every keystroke.
|
|
110
|
-
function dryRunParse(schema, snapshot) {
|
|
111
|
-
const parsed = schema.safeParse(snapshot.values);
|
|
112
|
-
if (parsed.success)
|
|
113
|
-
return undefined;
|
|
114
|
-
const hiddenFields = new Set(Object.entries(snapshot.fields)
|
|
115
|
-
.filter(([, state]) => !state.visible)
|
|
116
|
-
.map(([fieldKey]) => fieldKey));
|
|
117
|
-
return { issues: zodErrorToFieldIssues(parsed.error), hiddenFields };
|
|
118
|
-
}
|
|
119
|
-
// Issues are filtered exactly like the controller's validate(): hidden
|
|
120
|
-
// fields never count, `scope` narrows.
|
|
121
|
-
function scopedDryRunIssues(parse, scope, includeRoot) {
|
|
122
|
-
if (parse === undefined)
|
|
123
|
-
return [];
|
|
124
|
-
return relevantFieldIssues(parse.issues, {
|
|
125
|
-
hiddenFields: parse.hiddenFields,
|
|
126
|
-
...(scope !== undefined && { scope }),
|
|
127
|
-
includeRoot,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
function dryRunFieldIssues(schema, snapshot, scope, includeRoot) {
|
|
131
|
-
return scopedDryRunIssues(dryRunParse(schema, snapshot), scope, includeRoot);
|
|
132
|
-
}
|
|
133
|
-
// Index-independent: indices shift when a fields section hides.
|
|
134
|
-
function wizardStepKey(section, index) {
|
|
135
|
-
const declaredId = section.kind === "fields" || section.kind === "extension" ? section.id : undefined;
|
|
136
|
-
return declaredId ?? section.title ?? `step-${index}`;
|
|
137
|
-
}
|
|
138
|
-
function isMoneyValue(value) {
|
|
139
|
-
return typeof value === "object" && value !== null && "amount" in value;
|
|
140
|
-
}
|
|
141
|
-
// The seeds buildInitialValues writes for NULL columns of an existing record.
|
|
142
|
-
function isEmptyFormValue(value) {
|
|
143
|
-
if (value === null || value === undefined || value === "" || value === 0 || value === false) {
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
if (Array.isArray(value))
|
|
147
|
-
return value.length === 0;
|
|
148
|
-
return isMoneyValue(value) && value.amount === 0;
|
|
149
|
-
}
|
|
150
|
-
// select/boolean are excluded from the "holds user data" check: they are
|
|
151
|
-
// often pre-filled (defaults, server-set values), so they say nothing about
|
|
152
|
-
// whether the step was filled in. They still count through the validation.
|
|
153
|
-
function isFieldsStepFilled(section, parse) {
|
|
154
|
-
const fieldNames = section.fields.map((f) => f.field);
|
|
155
|
-
if (scopedDryRunIssues(parse, fieldNames, false).length > 0)
|
|
156
|
-
return false;
|
|
157
|
-
return section.fields.some((f) => f.visible &&
|
|
158
|
-
!f.readOnly &&
|
|
159
|
-
f.type !== "boolean" &&
|
|
160
|
-
f.type !== "select" &&
|
|
161
|
-
!isEmptyFormValue(f.value));
|
|
162
|
-
}
|
|
163
|
-
// Resolves an extension-section's `{ react: { __component: "X" } }` marker
|
|
164
|
-
// to a registered React component via ExtensionSectionsProvider (filled in
|
|
165
|
-
// createKumikoApp from clientFeatures.extensionSectionComponents) and
|
|
166
|
-
// mounts it with the host entity name + id. Hook lives in its own
|
|
167
|
-
// component so we don't call `use*` inside vm.sections.map (rules-of-
|
|
168
|
-
// hooks would punish reordering sections between renders).
|
|
169
|
-
function ExtensionSectionMount({ section, entityName: hostEntityName, entityId, initialValues, values, patch, validate, markSaved, hideTitle, actions, stepKey, onReportStepComplete, }) {
|
|
170
|
-
const { Banner, FillContainer, Section, Text } = usePrimitives();
|
|
171
|
-
const name = extensionSectionName(section.component);
|
|
172
|
-
const Component = useExtensionSectionComponent(name);
|
|
173
|
-
const reportStepComplete = useCallback((complete) => {
|
|
174
|
-
if (stepKey !== undefined)
|
|
175
|
-
onReportStepComplete?.(stepKey, complete);
|
|
176
|
-
}, [stepKey, onReportStepComplete]);
|
|
177
|
-
const testId = `section-extension-${section.title}`;
|
|
178
|
-
if (Component === undefined) {
|
|
179
|
-
const placeholder = (_jsx(Banner, { variant: "info", testId: `section-extension-placeholder-${section.title}`, children: _jsxs(Text, { children: ["Extension section component ", _jsx(Text, { variant: "code", children: name ?? "(no __component name)" }), " ", "not registered in clientFeatures.extensionSectionComponents."] }) }));
|
|
180
|
-
// Tabs mode already frames this in the caller's own Card — a Section
|
|
181
|
-
// here would add a second, unwanted divider inset (px-6 py-4) on top of
|
|
182
|
-
// the Card's own padding, misaligning this tab's content against the
|
|
183
|
-
// fields tab (Grid sits directly in the Card there). FillContainer keeps
|
|
184
|
-
// the testId without adding chrome (same primitive RelatedListSection
|
|
185
|
-
// uses for its own hideTitle branch).
|
|
186
|
-
return hideTitle === true ? (FillContainer !== undefined ? (_jsx(FillContainer, { testId: testId, children: placeholder })) : (placeholder)) : (_jsx(Section, { title: section.title, testId: testId, children: placeholder }, section.title));
|
|
187
|
-
}
|
|
188
|
-
const mounted = (_jsx(Component, { entityName: section.entityName ?? hostEntityName, entityId: entityId, initialValues: initialValues, values: values, patch: patch, validate: validate, markSaved: markSaved, ...(onReportStepComplete !== undefined && { reportStepComplete }) }));
|
|
189
|
-
// Same reasoning as the placeholder branch above: tabs mode's outer Card
|
|
190
|
-
// is the only frame this content gets, no nested Section divider.
|
|
191
|
-
return hideTitle === true ? (FillContainer !== undefined ? (_jsx(FillContainer, { testId: testId, children: mounted })) : (mounted)) : (_jsx(Section, { ...(actions !== undefined && { actions }), title: section.title, testId: testId, children: mounted }));
|
|
192
|
-
}
|
|
193
|
-
// Resolves an entityEdit slot's (`screen.slots.header` / `.footer`)
|
|
194
|
-
// `{ react: { __component: "X" } }` marker via the same ExtensionSectionsProvider
|
|
195
|
-
// registry as ExtensionSectionMount / ListHeaderSlotMount and mounts it. One
|
|
196
|
-
// component for both slots — header passes no hasUnsavedChanges/wizardStep,
|
|
197
|
-
// footer does. Own component for rules-of-hooks (useExtensionSectionComponent
|
|
198
|
-
// must not run conditionally inside RenderEdit's render body).
|
|
199
|
-
function EditSlotMount({ slot, slotName, screenId, entityName, entityId, values, markSaved, hasUnsavedChanges, wizardStep, }) {
|
|
200
|
-
const name = extensionSectionName(slot);
|
|
201
|
-
const Component = useExtensionSectionComponent(name);
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
if (name !== undefined && Component === undefined) {
|
|
204
|
-
// biome-ignore lint/suspicious/noConsole: dev warning for a setup error
|
|
205
|
-
console.warn(`[kumiko] Edit ${slotName} slot component "${name}" on screen "${screenId}" is not registered in clientFeatures.extensionSectionComponents — the ${slotName} slot renders nothing.`);
|
|
206
|
-
}
|
|
207
|
-
}, [name, Component, screenId, slotName]);
|
|
208
|
-
if (Component === undefined)
|
|
209
|
-
return null;
|
|
210
|
-
return (_jsx(Component, { entityName: entityName, entityId: entityId, values: values, ...(markSaved !== undefined && { markSaved }), ...(hasUnsavedChanges !== undefined && { hasUnsavedChanges }), ...(wizardStep !== undefined && { wizardStep }) }));
|
|
211
|
-
}
|
|
212
|
-
function formSectionDomId(sectionIndex) {
|
|
213
|
-
return `form-section-${sectionIndex}`;
|
|
214
|
-
}
|
|
215
|
-
/** A record title is a non-blank string; anything else leaves the screen title alone. */
|
|
216
|
-
export function resolveRecordTitle(value) {
|
|
217
|
-
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
218
|
-
}
|
|
219
|
-
const TITLE_PLACEHOLDER_PATTERN = /\{(\w+)\}/g;
|
|
220
|
-
// Falls back to the plain title while any placeholder value is still empty,
|
|
221
|
-
// so the header never shows "Add " mid-typing.
|
|
222
|
-
function resolveTitleTemplate(template, values, translate, fallback) {
|
|
223
|
-
if (template === undefined)
|
|
224
|
-
return fallback;
|
|
225
|
-
const placeholders = [...translate(template).matchAll(TITLE_PLACEHOLDER_PATTERN)].map((match) => match[1] ?? "");
|
|
226
|
-
const isFilled = (name) => {
|
|
227
|
-
const value = values[name];
|
|
228
|
-
return value !== undefined && value !== null && String(value).trim() !== "";
|
|
229
|
-
};
|
|
230
|
-
if (!placeholders.every(isFilled))
|
|
231
|
-
return fallback;
|
|
232
|
-
return translate(template, values);
|
|
233
|
-
}
|
|
234
29
|
export function RenderEdit(props) {
|
|
235
30
|
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, refreshToken, onDirtyChange, onCopyLink, actions, menuActions, onRelatedListDrawerAction, submitLabel, submitVariant, submitWithoutChanges, footerActions, summary, labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription, fieldAccent, markChangedFields = false, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled: disabledProp = false, viewMode = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, dirtyFooter, validateOnChange, headerRegion, buildSectionActions, leadContent, submitBlocked = false, formDerivation, actionFormScreenQn, } = props;
|
|
236
31
|
// View mode is a locked form, so everything keyed on `disabled` follows.
|
|
@@ -280,24 +75,13 @@ export function RenderEdit(props) {
|
|
|
280
75
|
// Tabs have stable ids, so the active tab follows its id: a field change
|
|
281
76
|
// that hides an earlier tab must not shift the index onto another tab.
|
|
282
77
|
const [activeTabId, setActiveTabId] = useState(undefined);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
// Marks a draftId this instance minted itself (saveDraft, first step
|
|
291
|
-
// change) so the restore effect below doesn't immediately re-fetch what
|
|
292
|
-
// it just wrote — there is nothing to restore, the row is brand new.
|
|
293
|
-
const mintedDraftIdRef = useRef(null);
|
|
294
|
-
// Marks that the mount-time `form-draft:query:list` fallback already ran
|
|
295
|
-
// once for this mount. Without this, a create-mode discard resets `draftId`
|
|
296
|
-
// to `null` (see discardDraft below) and would re-arm the list effect —
|
|
297
|
-
// silently adopting an unrelated parallel draft on the same screen right
|
|
298
|
-
// after the user just submitted (#1908).
|
|
299
|
-
const didListRef = useRef(false);
|
|
300
|
-
const [draftCandidates, setDraftCandidates] = useState(null);
|
|
78
|
+
const draftState = useWizardFormDraftState({
|
|
79
|
+
screenId: screen.id,
|
|
80
|
+
entityId: entityIdProp,
|
|
81
|
+
isCreateMode,
|
|
82
|
+
draftStorage,
|
|
83
|
+
});
|
|
84
|
+
const { draftCandidates, setDraftCandidates } = draftState;
|
|
301
85
|
// Composed-Save: Extension-Sections melden hier ihren dirty-State (damit der
|
|
302
86
|
// Save-Button aktiv wird wenn NUR eine Section geändert wurde) + ihren
|
|
303
87
|
// Submit-Handler (läuft nach dem Entity-Write). extensionErrorKey hält den
|
|
@@ -407,18 +191,6 @@ export function RenderEdit(props) {
|
|
|
407
191
|
controller.setValues(initial);
|
|
408
192
|
controller.rebase();
|
|
409
193
|
}, [refreshToken, controller, initial]);
|
|
410
|
-
// Derived from the screen id + the host entity id only — never from
|
|
411
|
-
// `vm.id`, which lives in the form values a restore mutates (load under one
|
|
412
|
-
// key, save under another). Edit-mode: `${screen.id}:${entityIdProp}`,
|
|
413
|
-
// unchanged. Create-mode: `${screen.id}:new:${draftId}` once a draftId
|
|
414
|
-
// exists (resumed, adopted, or minted), `undefined` before that — two
|
|
415
|
-
// parallel create sessions on the same screen must never collapse onto
|
|
416
|
-
// the same key (kumiko-framework#1908).
|
|
417
|
-
const draftKey = useMemo(() => isCreateMode
|
|
418
|
-
? draftId !== null
|
|
419
|
-
? `${screen.id}:new:${draftId}`
|
|
420
|
-
: undefined
|
|
421
|
-
: `${screen.id}:${entityIdProp}`, [screen.id, entityIdProp, isCreateMode, draftId]);
|
|
422
194
|
// Controlled mode (Issue #1887). Both callbacks live in refs so a
|
|
423
195
|
// re-rendering caller (identity-unstable `onChange`/`onControlsReady`
|
|
424
196
|
// closures, e.g. inline arrows) doesn't retrigger these effects — only a
|
|
@@ -455,8 +227,7 @@ export function RenderEdit(props) {
|
|
|
455
227
|
// wizard step without depending on `currentStep` (computed further down,
|
|
456
228
|
// from `filteredSections`) at the point patchAndScheduleDraftSave itself
|
|
457
229
|
// is defined — kept fresh where `currentStep` is computed below.
|
|
458
|
-
const saveDraftRef = useRef(
|
|
459
|
-
saveDraftRef.current = saveDraft;
|
|
230
|
+
const saveDraftRef = useRef(() => { });
|
|
460
231
|
// `handleSubmit` (defined below) is a fresh closure over this render's
|
|
461
232
|
// snapshot/extensionDirty, while the onControlsReady effect fires once per
|
|
462
233
|
// mount — the ref lets controls.submit() always reach the current one.
|
|
@@ -566,111 +337,16 @@ export function RenderEdit(props) {
|
|
|
566
337
|
// Draft wizards keep their input server-side, so leaving loses nothing.
|
|
567
338
|
const persistsDraft = isWizard && screen.layout.draft === true;
|
|
568
339
|
useUnsavedInputSource(() => !persistsDraft && (controller.getSnapshot().isDirty || extensionDirtyRef.current));
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
// is exactly the current form state, re-fetching it would be a no-op
|
|
580
|
-
// round-trip at best and a stale-echo clobber at worst.
|
|
581
|
-
if (draftId !== null && draftId === mintedDraftIdRef.current)
|
|
582
|
-
return;
|
|
583
|
-
let cancelled = false;
|
|
584
|
-
void (async () => {
|
|
585
|
-
try {
|
|
586
|
-
const result = await dispatcher.query(FORM_DRAFT_GET, { draftKey });
|
|
587
|
-
// skip: superseded by a newer mount, or the draft lookup failed — an
|
|
588
|
-
// unreachable draft store must not break the form itself.
|
|
589
|
-
if (cancelled || !result.isSuccess)
|
|
590
|
-
return;
|
|
591
|
-
const draft = result.data?.draft ?? null;
|
|
592
|
-
// skip: nothing saved yet for this key.
|
|
593
|
-
if (draft === null)
|
|
594
|
-
return;
|
|
595
|
-
// skip: the user already typed or wizard-navigated while the lookup
|
|
596
|
-
// was in flight — their input wins over the stored draft.
|
|
597
|
-
if (controller.getSnapshot().isDirty || hasNavigatedRef.current)
|
|
598
|
-
return;
|
|
599
|
-
// @cast-boundary form-values draft blob: `values` round-trips through an opaque
|
|
600
|
-
// jsonb column and comes back untyped.
|
|
601
|
-
controller.setValues(draft.values);
|
|
602
|
-
setRawStep(Math.max(draft.stepIndex, 0));
|
|
603
|
-
}
|
|
604
|
-
catch (err) {
|
|
605
|
-
// biome-ignore lint/suspicious/noConsole: draft restore must not red-screen the form
|
|
606
|
-
console.warn("render-edit: draft restore query failed", err);
|
|
607
|
-
}
|
|
608
|
-
})();
|
|
609
|
-
return () => {
|
|
610
|
-
cancelled = true;
|
|
611
|
-
};
|
|
612
|
-
}, [draftEnabled, draftKey, draftId, dispatcher, controller]);
|
|
613
|
-
// Mount-time fallback (issue #1913) for create-mode when no draftId
|
|
614
|
-
// survived in storage (new tab, cleared storage): ask the server for this
|
|
615
|
-
// screen's open drafts and let the user choose via a picker (below) —
|
|
616
|
-
// even for exactly one candidate. Auto-adopting a lone candidate would
|
|
617
|
-
// silently hand this tab someone else's in-progress draft from a parallel
|
|
618
|
-
// session on the same screen (cross-tab hijack); the picker's "start new"
|
|
619
|
-
// action covers the common case (it really was this tab's own draft) with
|
|
620
|
-
// one extra click. Zero candidates → stay null, saveDraft mints a fresh
|
|
621
|
-
// draftId on the first step change same as any other fresh create.
|
|
622
|
-
useEffect(() => {
|
|
623
|
-
// skip: this screen does not persist a draft, this is edit-mode, a
|
|
624
|
-
// draftId is already known (from storage or an earlier adoption), or
|
|
625
|
-
// this mount already ran the list lookup once (didListRef).
|
|
626
|
-
if (!draftEnabled ||
|
|
627
|
-
!isCreateMode ||
|
|
628
|
-
draftId !== null ||
|
|
629
|
-
didListRef.current ||
|
|
630
|
-
dispatcher === undefined)
|
|
631
|
-
return;
|
|
632
|
-
let cancelled = false;
|
|
633
|
-
void (async () => {
|
|
634
|
-
try {
|
|
635
|
-
const result = await dispatcher.query(FORM_DRAFT_LIST, { screenId: screen.id });
|
|
636
|
-
// skip: superseded by a newer mount, or the lookup failed — an
|
|
637
|
-
// unreachable draft store must not break a fresh create.
|
|
638
|
-
if (cancelled || !result.isSuccess)
|
|
639
|
-
return;
|
|
640
|
-
didListRef.current = true;
|
|
641
|
-
const prefix = newDraftPrefix(screen.id);
|
|
642
|
-
// list's LIKE-prefix scan also matches edit-mode drafts
|
|
643
|
-
// (`${screenId}:${entityId}`) — keep only create-mode ones.
|
|
644
|
-
const candidates = (result.data?.drafts ?? []).filter((d) => d.draftKey.startsWith(prefix));
|
|
645
|
-
// skip: no open drafts for this screen — stay null, saveDraft mints a
|
|
646
|
-
// fresh draftId on the first step change same as any other create.
|
|
647
|
-
if (candidates.length === 0)
|
|
648
|
-
return;
|
|
649
|
-
setDraftCandidates(candidates);
|
|
650
|
-
}
|
|
651
|
-
catch (err) {
|
|
652
|
-
// biome-ignore lint/suspicious/noConsole: draft lookup must not red-screen the form
|
|
653
|
-
console.warn("render-edit: draft list query failed", err);
|
|
654
|
-
}
|
|
655
|
-
})();
|
|
656
|
-
return () => {
|
|
657
|
-
cancelled = true;
|
|
658
|
-
};
|
|
659
|
-
}, [draftEnabled, isCreateMode, draftId, dispatcher, screen.id]);
|
|
660
|
-
// User picked one of several open drafts from the mount-time picker
|
|
661
|
-
// (see draftCandidates below) — adopt it the same way a single
|
|
662
|
-
// auto-adopted candidate would be.
|
|
663
|
-
function adoptDraft(candidate) {
|
|
664
|
-
// Locked state (#1896/fw#1909): `disabled` means "no write possible" —
|
|
665
|
-
// repointing draftKey and patching in the candidate's values is exactly
|
|
666
|
-
// that, so a locked form must not adopt a draft.
|
|
667
|
-
if (disabled)
|
|
668
|
-
return;
|
|
669
|
-
const adoptedId = candidate.draftKey.slice(newDraftPrefix(screen.id).length);
|
|
670
|
-
draftStorage.setDraftId(screen.id, adoptedId);
|
|
671
|
-
setDraftId(adoptedId);
|
|
672
|
-
setDraftCandidates(null);
|
|
673
|
-
}
|
|
340
|
+
useWizardFormDraftRestore({
|
|
341
|
+
screenId: screen.id,
|
|
342
|
+
isCreateMode,
|
|
343
|
+
draftEnabled,
|
|
344
|
+
dispatcher,
|
|
345
|
+
controller,
|
|
346
|
+
hasNavigatedRef,
|
|
347
|
+
setRawStep,
|
|
348
|
+
draftState,
|
|
349
|
+
});
|
|
674
350
|
const vm = useMemo(() => computeEditViewModel({
|
|
675
351
|
screen,
|
|
676
352
|
entity,
|
|
@@ -799,63 +475,19 @@ export function RenderEdit(props) {
|
|
|
799
475
|
if (isTabs)
|
|
800
476
|
setActiveTabId(tabIdAt(filteredSections[index], index));
|
|
801
477
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
return;
|
|
816
|
-
let key = draftKey;
|
|
817
|
-
if (isCreateMode && draftId === null) {
|
|
818
|
-
const mintedId = mintDraftId();
|
|
819
|
-
mintedDraftIdRef.current = mintedId;
|
|
820
|
-
draftStorage.setDraftId(screen.id, mintedId);
|
|
821
|
-
setDraftId(mintedId);
|
|
822
|
-
key = `${screen.id}:new:${mintedId}`;
|
|
823
|
-
// A stale picker from the mount-time list fallback must not survive a
|
|
824
|
-
// mint — picking a candidate afterwards would repoint draftKey at an
|
|
825
|
-
// unrelated draft mid-edit and overwrite it (#1908).
|
|
826
|
-
setDraftCandidates(null);
|
|
827
|
-
}
|
|
828
|
-
// skip: create-mode with a draftId that failed to mint — unreachable
|
|
829
|
-
// in practice (mint above always produces one), kept as a type-level
|
|
830
|
-
// guard against a stale `undefined` key ever reaching the write.
|
|
831
|
-
if (key === undefined)
|
|
832
|
-
return;
|
|
833
|
-
// draftKeySchema.max(FORM_DRAFT_KEY_MAX_LENGTH) rejects this server-side —
|
|
834
|
-
// catch it here instead of losing the save silently to the fire-and-forget
|
|
835
|
-
// write below (the user would only notice on resume, with nothing to resume).
|
|
836
|
-
if (key.length > FORM_DRAFT_KEY_MAX_LENGTH) {
|
|
837
|
-
// biome-ignore lint/suspicious/noConsole: no error-surfacing path exists for a fire-and-forget draft save.
|
|
838
|
-
console.warn(`RenderEdit: draftKey "${key}" is ${key.length} chars, over the server's ${FORM_DRAFT_KEY_MAX_LENGTH}-char limit — skipping this draft save. Shorten screen.id or entity id.`);
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
const secretFieldNames = new Set(vm.sections.flatMap((section) => section.kind === "fields"
|
|
842
|
-
? section.fields
|
|
843
|
-
.filter((f) => f.format === "password" || f.writeOnly === true || f.sensitive === true)
|
|
844
|
-
.map((f) => f.field)
|
|
845
|
-
: []));
|
|
846
|
-
// A password, writeOnly or sensitive field never enters the persisted draft blob — it would be stored in the clear and restored on resume.
|
|
847
|
-
const draftValues = Object.fromEntries(Object.entries(controller.getSnapshot().values).filter(([field]) => !secretFieldNames.has(field)));
|
|
848
|
-
void dispatcher
|
|
849
|
-
.write(FORM_DRAFT_SAVE, {
|
|
850
|
-
draftKey: key,
|
|
851
|
-
values: draftValues,
|
|
852
|
-
stepIndex,
|
|
853
|
-
})
|
|
854
|
-
.catch((err) => {
|
|
855
|
-
// biome-ignore lint/suspicious/noConsole: fire-and-forget draft save must not red-screen
|
|
856
|
-
console.warn("render-edit: draft save failed", err);
|
|
857
|
-
});
|
|
858
|
-
}
|
|
478
|
+
const { adoptDraft, saveDraft, discardDraft } = buildWizardDraftActions({
|
|
479
|
+
screenId: screen.id,
|
|
480
|
+
isCreateMode,
|
|
481
|
+
draftEnabled,
|
|
482
|
+
disabled,
|
|
483
|
+
dispatcher,
|
|
484
|
+
draftStorage,
|
|
485
|
+
controller,
|
|
486
|
+
sections: vm.sections,
|
|
487
|
+
draftSaveTimerRef,
|
|
488
|
+
draftState,
|
|
489
|
+
});
|
|
490
|
+
saveDraftRef.current = saveDraft;
|
|
859
491
|
// Next: scoped validate() on the current step's fields — errors stay
|
|
860
492
|
// attached to the field and block the step transition. Extension steps
|
|
861
493
|
// have no field scope here (they validate via the controlled-mode
|
|
@@ -912,40 +544,6 @@ export function RenderEdit(props) {
|
|
|
912
544
|
if (!disabled)
|
|
913
545
|
saveDraft(clampedTarget);
|
|
914
546
|
}
|
|
915
|
-
async function discardDraft() {
|
|
916
|
-
// skip: this screen does not persist a draft.
|
|
917
|
-
if (!draftEnabled || dispatcher === undefined)
|
|
918
|
-
return;
|
|
919
|
-
// A pending debounced patch-save must not fire after discard — it would
|
|
920
|
-
// resurrect the draft it just deleted.
|
|
921
|
-
if (draftSaveTimerRef.current !== null) {
|
|
922
|
-
clearTimeout(draftSaveTimerRef.current);
|
|
923
|
-
draftSaveTimerRef.current = null;
|
|
924
|
-
}
|
|
925
|
-
// skip: create-mode, no step change happened yet — no draftId was ever
|
|
926
|
-
// minted, so no row exists to discard.
|
|
927
|
-
if (draftKey === undefined)
|
|
928
|
-
return;
|
|
929
|
-
// Best-effort: a rejected write (e.g. network error) must not propagate
|
|
930
|
-
// out of handleSubmit AFTER the entity write already succeeded — that
|
|
931
|
-
// would break onSubmit/navigation/toast and risk a duplicate submit on
|
|
932
|
-
// retry. An orphaned draft row is swept later by cleanup.job.ts. Local
|
|
933
|
-
// cleanup below still runs on failure so this instance doesn't try to
|
|
934
|
-
// resume a draft it already considers submitted.
|
|
935
|
-
try {
|
|
936
|
-
await dispatcher.write(FORM_DRAFT_DISCARD, { draftKey });
|
|
937
|
-
}
|
|
938
|
-
catch {
|
|
939
|
-
// best-effort, see comment above.
|
|
940
|
-
}
|
|
941
|
-
// A successful submit ends this draftId's life — a subsequent create on
|
|
942
|
-
// the same screen (new mount) must mint its own, not resume this one.
|
|
943
|
-
if (isCreateMode) {
|
|
944
|
-
draftStorage.clearDraftId(screen.id);
|
|
945
|
-
mintedDraftIdRef.current = null;
|
|
946
|
-
setDraftId(null);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
547
|
// Stepped modes (wizard, tabs): locates the first step whose "fields"
|
|
950
548
|
// section contains one of the given field paths — matched on the path's
|
|
951
549
|
// first segment, since `FieldIssue.path`/`snapshot.errors` keys can be
|
|
@@ -11,6 +11,7 @@ import { evalRowExtractor } from "../app/row-actions.js";
|
|
|
11
11
|
import { screenAccessAllows } from "../app/screen-access.js";
|
|
12
12
|
import { useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
13
13
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
14
|
+
import { rendererError, rendererWarn } from "../dev-console.js";
|
|
14
15
|
import { fileIdsOf } from "../file-ids-of.js";
|
|
15
16
|
import { applyFormatSpec, currencyDecimals, EMPTY_VALUE_PLACEHOLDER, } from "../headless/index.js";
|
|
16
17
|
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
|
|
@@ -228,8 +229,7 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
|
|
|
228
229
|
// be missing from `options` (shown as a raw id instead of a label).
|
|
229
230
|
setSearchTerm("");
|
|
230
231
|
queryResult.refetch().catch((err) => {
|
|
231
|
-
|
|
232
|
-
console.error("render-field: refetch after create failed", err);
|
|
232
|
+
rendererError("render-field: refetch after create failed", err);
|
|
233
233
|
});
|
|
234
234
|
if (newId === undefined) {
|
|
235
235
|
// Record was created server-side but the payload carried no id —
|
|
@@ -319,8 +319,7 @@ function FieldRendererOutput({ field, renderer, row, appLocale, }) {
|
|
|
319
319
|
if (Component !== undefined) {
|
|
320
320
|
return (_jsx(Component, { value: field.value, row: row ?? { [field.field]: field.value }, column: { field: field.field } }));
|
|
321
321
|
}
|
|
322
|
-
|
|
323
|
-
console.warn(`[kumiko] fieldRenderer "${componentName}" not registered`);
|
|
322
|
+
rendererWarn(`[kumiko] fieldRenderer "${componentName}" not registered`);
|
|
324
323
|
}
|
|
325
324
|
return _jsx(Text, { testId: `field-value-${field.field}`, children: stringValue(field.value) });
|
|
326
325
|
}
|
|
@@ -3,6 +3,7 @@ import { UNRESOLVED_REFERENCE_LABEL_KEYS, } from "@cosmicdrift/kumiko-framework/
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections.js";
|
|
5
5
|
import { useReferenceLinkResolver } from "../app/reference-link.js";
|
|
6
|
+
import { rendererWarn } from "../dev-console.js";
|
|
6
7
|
import { computeListViewModel, EMPTY_VALUE_PLACEHOLDER } from "../headless/index.js";
|
|
7
8
|
import { useReferenceLookup } from "../hooks/use-reference-lookup.js";
|
|
8
9
|
import { useTranslation } from "../i18n.js";
|
|
@@ -251,8 +252,7 @@ function ListHeaderSlotMount({ screen, }) {
|
|
|
251
252
|
// (the toolbar has no room for a banner, so a dev-warn it is).
|
|
252
253
|
useEffect(() => {
|
|
253
254
|
if (name !== undefined && Component === undefined) {
|
|
254
|
-
|
|
255
|
-
console.warn(`[kumiko] List header slot component "${name}" on screen "${screen.id}" is not registered in clientFeatures.extensionSectionComponents — the header slot renders nothing.`);
|
|
255
|
+
rendererWarn(`[kumiko] List header slot component "${name}" on screen "${screen.id}" is not registered in clientFeatures.extensionSectionComponents — the header slot renders nothing.`);
|
|
256
256
|
}
|
|
257
257
|
}, [name, Component, screen.id]);
|
|
258
258
|
if (Component === undefined)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type MutableRefObject } from "react";
|
|
2
|
+
import type { DraftStorage } from "../context/draft-storage-context.tsx";
|
|
3
|
+
import type { Dispatcher, EditSectionViewModel, FormController, FormValues } from "../headless/index.ts";
|
|
4
|
+
export declare const PATCH_DRAFT_SAVE_DEBOUNCE_MS = 500;
|
|
5
|
+
export type DraftCandidate = {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly draftKey: string;
|
|
8
|
+
readonly stepIndex: number;
|
|
9
|
+
readonly savedAt: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function useWizardFormDraftState({ screenId, entityId, isCreateMode, draftStorage, }: {
|
|
12
|
+
readonly screenId: string;
|
|
13
|
+
readonly entityId: string | null | undefined;
|
|
14
|
+
readonly isCreateMode: boolean;
|
|
15
|
+
readonly draftStorage: DraftStorage;
|
|
16
|
+
}): {
|
|
17
|
+
draftId: string | null;
|
|
18
|
+
setDraftId: import("react").Dispatch<import("react").SetStateAction<string | null>>;
|
|
19
|
+
draftCandidates: readonly DraftCandidate[] | null;
|
|
20
|
+
setDraftCandidates: import("react").Dispatch<import("react").SetStateAction<readonly DraftCandidate[] | null>>;
|
|
21
|
+
mintedDraftIdRef: import("react").RefObject<string | null>;
|
|
22
|
+
didListRef: import("react").RefObject<boolean>;
|
|
23
|
+
draftKey: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
export type WizardFormDraftState = ReturnType<typeof useWizardFormDraftState>;
|
|
26
|
+
export declare function useWizardFormDraftRestore<TValues extends FormValues>({ screenId, isCreateMode, draftEnabled, dispatcher, controller, hasNavigatedRef, setRawStep, draftState: { draftId, draftKey, mintedDraftIdRef, didListRef, setDraftCandidates }, }: {
|
|
27
|
+
readonly screenId: string;
|
|
28
|
+
readonly isCreateMode: boolean;
|
|
29
|
+
readonly draftEnabled: boolean;
|
|
30
|
+
readonly dispatcher: Dispatcher | undefined;
|
|
31
|
+
readonly controller: FormController<TValues>;
|
|
32
|
+
readonly hasNavigatedRef: MutableRefObject<boolean>;
|
|
33
|
+
readonly setRawStep: (step: number) => void;
|
|
34
|
+
readonly draftState: WizardFormDraftState;
|
|
35
|
+
}): void;
|
|
36
|
+
export declare function buildWizardDraftActions<TValues extends FormValues>({ screenId, isCreateMode, draftEnabled, disabled, dispatcher, draftStorage, controller, sections, draftSaveTimerRef, draftState: { draftId, draftKey, mintedDraftIdRef, setDraftId, setDraftCandidates }, }: {
|
|
37
|
+
readonly screenId: string;
|
|
38
|
+
readonly isCreateMode: boolean;
|
|
39
|
+
readonly draftEnabled: boolean;
|
|
40
|
+
readonly disabled: boolean;
|
|
41
|
+
readonly dispatcher: Dispatcher | undefined;
|
|
42
|
+
readonly draftStorage: DraftStorage;
|
|
43
|
+
readonly controller: FormController<TValues>;
|
|
44
|
+
readonly sections: readonly EditSectionViewModel[];
|
|
45
|
+
readonly draftSaveTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>;
|
|
46
|
+
readonly draftState: WizardFormDraftState;
|
|
47
|
+
}): {
|
|
48
|
+
adoptDraft: (candidate: DraftCandidate) => void;
|
|
49
|
+
saveDraft: (stepIndex: number) => void;
|
|
50
|
+
discardDraft: () => Promise<void>;
|
|
51
|
+
};
|