@cosmicdrift/kumiko-renderer 0.333.0 → 0.335.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/config-edit-shim.d.ts +7 -0
- package/dist/app/config-edit-shim.js +39 -0
- package/dist/app/content-preview.js +7 -9
- package/dist/app/extension-sections.d.ts +6 -0
- package/dist/app/form-schema.d.ts +2 -1
- package/dist/app/form-schema.js +11 -2
- package/dist/app/kumiko-screen.d.ts +5 -0
- package/dist/app/kumiko-screen.js +259 -101
- package/dist/app/nav.d.ts +5 -0
- package/dist/app/nav.js +17 -4
- package/dist/app/query-state-banners.d.ts +8 -0
- package/dist/app/query-state-banners.js +14 -0
- package/dist/app/reference-facet-bridge.js +8 -1
- package/dist/app/row-actions.d.ts +12 -0
- package/dist/app/row-actions.js +24 -7
- package/dist/app/screen-access.js +13 -9
- package/dist/app/secret-mint-body.js +41 -7
- package/dist/app/secrets-edit-body.js +89 -33
- package/dist/components/embedded-list-field.js +2 -4
- package/dist/components/grid-cell-for-field.d.ts +7 -2
- package/dist/components/grid-cell-for-field.js +2 -2
- package/dist/components/multi-select-checkboxes.js +4 -1
- package/dist/components/query-options-select.d.ts +12 -3
- package/dist/components/query-options-select.js +45 -2
- package/dist/components/reference-create-dialog.js +9 -4
- package/dist/components/related-list-section.js +8 -10
- package/dist/components/render-edit-action-button.d.ts +1 -1
- package/dist/components/render-edit-action-button.js +5 -1
- package/dist/components/render-edit-logic.d.ts +2 -1
- package/dist/components/render-edit-logic.js +8 -5
- package/dist/components/render-edit-types.d.ts +30 -1
- package/dist/components/render-edit.js +248 -76
- package/dist/components/render-field.d.ts +7 -1
- package/dist/components/render-field.js +40 -15
- package/dist/components/render-list.js +6 -6
- package/dist/components/write-form-section.d.ts +5 -1
- package/dist/components/write-form-section.js +29 -25
- package/dist/context/embedded-screen-context.d.ts +5 -0
- package/dist/context/embedded-screen-context.js +12 -0
- package/dist/hooks/use-reference-lookup.d.ts +7 -0
- package/dist/hooks/use-reference-lookup.js +10 -0
- package/dist/i18n-defaults.js +30 -5
- package/dist/i18n.js +8 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +70 -19
- package/dist/session/session-ended.d.ts +16 -0
- package/dist/session/session-ended.js +73 -0
- package/dist/sort-by-accessor.d.ts +6 -2
- package/dist/sort-by-accessor.js +13 -3
- package/package.json +5 -5
- package/src/changes.json +40 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import { computeEditViewModel } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
+
import { computeEditViewModel, relevantFieldIssues, zodErrorToFieldIssues, } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
5
5
|
import { AllFieldsRequiredProvider } from "./render-field.js";
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -77,7 +77,7 @@ function deriveFormFields(screen) {
|
|
|
77
77
|
// relatedList carries no `fields` for the form-condition map either.
|
|
78
78
|
if (!isFieldsEditSection(section))
|
|
79
79
|
continue;
|
|
80
|
-
for (const spec of section
|
|
80
|
+
for (const spec of sectionFieldSpecs(section)) {
|
|
81
81
|
const normalized = normalizeEditField(spec);
|
|
82
82
|
out[normalized.field] = {
|
|
83
83
|
...(normalized.visible !== undefined && {
|
|
@@ -94,16 +94,67 @@ function deriveFormFields(screen) {
|
|
|
94
94
|
}
|
|
95
95
|
return out;
|
|
96
96
|
}
|
|
97
|
+
// Dry-run parse against `schema` — NOT controller.validate(), which writes
|
|
98
|
+
// field errors into the snapshot. Issues are filtered exactly like the
|
|
99
|
+
// controller's validate(): hidden fields never count, `scope` narrows.
|
|
100
|
+
function dryRunFieldIssues(schema, snapshot, scope, includeRoot) {
|
|
101
|
+
const parsed = schema.safeParse(snapshot.values);
|
|
102
|
+
if (parsed.success)
|
|
103
|
+
return [];
|
|
104
|
+
const hiddenFields = new Set(Object.entries(snapshot.fields)
|
|
105
|
+
.filter(([, state]) => !state.visible)
|
|
106
|
+
.map(([fieldKey]) => fieldKey));
|
|
107
|
+
return relevantFieldIssues(zodErrorToFieldIssues(parsed.error), {
|
|
108
|
+
hiddenFields,
|
|
109
|
+
...(scope !== undefined && { scope }),
|
|
110
|
+
includeRoot,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Index-independent: indices shift when a fields section hides.
|
|
114
|
+
function wizardStepKey(section, index) {
|
|
115
|
+
const declaredId = section.kind === "fields" || section.kind === "extension" ? section.id : undefined;
|
|
116
|
+
return declaredId ?? section.title ?? `step-${index}`;
|
|
117
|
+
}
|
|
118
|
+
function isMoneyValue(value) {
|
|
119
|
+
return typeof value === "object" && value !== null && "amount" in value;
|
|
120
|
+
}
|
|
121
|
+
// The seeds buildInitialValues writes for NULL columns of an existing record.
|
|
122
|
+
function isEmptyFormValue(value) {
|
|
123
|
+
if (value === null || value === undefined || value === "" || value === 0 || value === false) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(value))
|
|
127
|
+
return value.length === 0;
|
|
128
|
+
return isMoneyValue(value) && value.amount === 0;
|
|
129
|
+
}
|
|
130
|
+
// select/boolean are excluded from the "holds user data" check: they are
|
|
131
|
+
// often pre-filled (defaults, server-set values), so they say nothing about
|
|
132
|
+
// whether the step was filled in. They still count through the validation.
|
|
133
|
+
function isFieldsStepFilled(section, schema, snapshot) {
|
|
134
|
+
const fieldNames = section.fields.map((f) => f.field);
|
|
135
|
+
if (schema !== undefined && dryRunFieldIssues(schema, snapshot, fieldNames, false).length > 0) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return section.fields.some((f) => f.visible &&
|
|
139
|
+
!f.readOnly &&
|
|
140
|
+
f.type !== "boolean" &&
|
|
141
|
+
f.type !== "select" &&
|
|
142
|
+
!isEmptyFormValue(f.value));
|
|
143
|
+
}
|
|
97
144
|
// Resolves an extension-section's `{ react: { __component: "X" } }` marker
|
|
98
145
|
// to a registered React component via ExtensionSectionsProvider (filled in
|
|
99
146
|
// createKumikoApp from clientFeatures.extensionSectionComponents) and
|
|
100
147
|
// mounts it with the host entity name + id. Hook lives in its own
|
|
101
148
|
// component so we don't call `use*` inside vm.sections.map (rules-of-
|
|
102
149
|
// hooks would punish reordering sections between renders).
|
|
103
|
-
function ExtensionSectionMount({ section, entityName: hostEntityName, entityId, initialValues, values, patch, validate, hideTitle, actions, }) {
|
|
150
|
+
function ExtensionSectionMount({ section, entityName: hostEntityName, entityId, initialValues, values, patch, validate, hideTitle, actions, stepKey, onReportStepComplete, }) {
|
|
104
151
|
const { Banner, FillContainer, Section, Text } = usePrimitives();
|
|
105
152
|
const name = extensionSectionName(section.component);
|
|
106
153
|
const Component = useExtensionSectionComponent(name);
|
|
154
|
+
const reportStepComplete = useCallback((complete) => {
|
|
155
|
+
if (stepKey !== undefined)
|
|
156
|
+
onReportStepComplete?.(stepKey, complete);
|
|
157
|
+
}, [stepKey, onReportStepComplete]);
|
|
107
158
|
const testId = `section-extension-${section.title}`;
|
|
108
159
|
if (Component === undefined) {
|
|
109
160
|
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."] }) }));
|
|
@@ -115,7 +166,7 @@ function ExtensionSectionMount({ section, entityName: hostEntityName, entityId,
|
|
|
115
166
|
// uses for its own hideTitle branch).
|
|
116
167
|
return hideTitle === true ? (FillContainer !== undefined ? (_jsx(FillContainer, { testId: testId, children: placeholder })) : (placeholder)) : (_jsx(Section, { title: section.title, testId: testId, children: placeholder }, section.title));
|
|
117
168
|
}
|
|
118
|
-
const mounted = (_jsx(Component, { entityName: section.entityName ?? hostEntityName, entityId: entityId, initialValues: initialValues, values: values, patch: patch, validate: validate }));
|
|
169
|
+
const mounted = (_jsx(Component, { entityName: section.entityName ?? hostEntityName, entityId: entityId, initialValues: initialValues, values: values, patch: patch, validate: validate, ...(onReportStepComplete !== undefined && { reportStepComplete }) }));
|
|
119
170
|
// Same reasoning as the placeholder branch above: tabs mode's outer Card
|
|
120
171
|
// is the only frame this content gets, no nested Section divider.
|
|
121
172
|
return hideTitle === true ? (FillContainer !== undefined ? (_jsx(FillContainer, { testId: testId, children: mounted })) : (mounted)) : (_jsx(Section, { ...(actions !== undefined && { actions }), title: section.title, testId: testId, children: mounted }));
|
|
@@ -162,7 +213,8 @@ function resolveTitleTemplate(template, values, translate, fallback) {
|
|
|
162
213
|
return translate(template, values);
|
|
163
214
|
}
|
|
164
215
|
export function RenderEdit(props) {
|
|
165
|
-
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, footerActions, summary, labelAppendix, fieldAppendix, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, headerRegion, buildSectionActions, } = props;
|
|
216
|
+
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, footerActions, summary, labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription, fieldAccent, markChangedFields = false, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, dirtyFooter, validateOnChange, headerRegion, buildSectionActions, leadContent, } = props;
|
|
217
|
+
const i18nScreenId = props.i18nScreenId ?? screen.id;
|
|
166
218
|
const { customSubmit } = props;
|
|
167
219
|
// Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
|
|
168
220
|
// konsumieren wir den i18next-Context direkt. Sonst wären Field-
|
|
@@ -185,6 +237,9 @@ export function RenderEdit(props) {
|
|
|
185
237
|
const [linkCopied, setLinkCopied] = useState(false);
|
|
186
238
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
187
239
|
const [formError, setFormError] = useState(null);
|
|
240
|
+
// A rejected delete can carry several blocking reasons; the first one is the
|
|
241
|
+
// banner's headline (formError), the rest are listed beneath it.
|
|
242
|
+
const [extraDeleteIssues, setExtraDeleteIssues] = useState([]);
|
|
188
243
|
// One state for all header actions (actions?), not per-button — only one
|
|
189
244
|
// action can be in flight at a time, and this keeps the error surfaced in
|
|
190
245
|
// the shared formError-adjacent banner region instead of inside the
|
|
@@ -192,6 +247,14 @@ export function RenderEdit(props) {
|
|
|
192
247
|
// flex justify-end action bar).
|
|
193
248
|
const [actionError, setActionError] = useState(null);
|
|
194
249
|
const [rawStep, setRawStep] = useState(0);
|
|
250
|
+
const [passedStepKeys, setPassedStepKeys] = useState(() => new Set());
|
|
251
|
+
const [extensionStepComplete, setExtensionStepComplete] = useState({});
|
|
252
|
+
const reportExtensionStepComplete = useCallback((stepKey, complete) => {
|
|
253
|
+
setExtensionStepComplete((prev) => (prev[stepKey] === true) === complete ? prev : { ...prev, [stepKey]: complete });
|
|
254
|
+
}, []);
|
|
255
|
+
// Tabs have stable ids, so the active tab follows its id: a field change
|
|
256
|
+
// that hides an earlier tab must not shift the index onto another tab.
|
|
257
|
+
const [activeTabId, setActiveTabId] = useState(undefined);
|
|
195
258
|
// Create-mode draftId (issue #1913) — resumed from `sessionStorage` (web)
|
|
196
259
|
// on mount so a same-tab reload finds the right one of several parallel
|
|
197
260
|
// create-sessions on this screen; `null` until the first step change
|
|
@@ -400,9 +463,12 @@ export function RenderEdit(props) {
|
|
|
400
463
|
// on every keystroke, painting error messages while the user is still
|
|
401
464
|
// typing. `valid` can therefore legitimately diverge from what's
|
|
402
465
|
// currently rendered under the fields (the last *mutating* validate()
|
|
403
|
-
// call, e.g. from controls.validate() or submit()).
|
|
466
|
+
// call, e.g. from controls.validate() or submit()). The issues are
|
|
467
|
+
// filtered exactly like submit()'s validate: hidden fields and fields
|
|
468
|
+
// outside the `fields` scope never make the form invalid.
|
|
404
469
|
const currentSchema = schemaRef.current;
|
|
405
|
-
const valid = currentSchema === undefined
|
|
470
|
+
const valid = currentSchema === undefined ||
|
|
471
|
+
dryRunFieldIssues(currentSchema, snapshot, scopeFieldNamesRef.current, true).length === 0;
|
|
406
472
|
cb({
|
|
407
473
|
values: snapshot.values,
|
|
408
474
|
changes: snapshot.changes,
|
|
@@ -569,6 +635,11 @@ export function RenderEdit(props) {
|
|
|
569
635
|
// keeps normal document-flow height untouched.
|
|
570
636
|
const fillHeight = fillScreenHeight === true ||
|
|
571
637
|
(hideSectionTitles === true && filteredSections[0]?.kind === "relatedList");
|
|
638
|
+
// A lone writeForm tab is the whole page, so its submit joins this form's
|
|
639
|
+
// footer (pinned to the window edge like every other screen's save)
|
|
640
|
+
// instead of sitting in the section's title row.
|
|
641
|
+
const isLoneWriteFormTab = hideSectionTitles === true && filteredSections[0]?.kind === "writeForm";
|
|
642
|
+
const [writeFormFooterAction, setWriteFormFooterAction] = useState(undefined);
|
|
572
643
|
// Persistiert alle composed Extension-Sections mit der aufgelösten entityId.
|
|
573
644
|
// false = eine Section schlug fehl (ihr i18n-Key landet im Banner). Ohne
|
|
574
645
|
// Entity-Kontext (create-mode ohne route-id) gibt es nichts zu schreiben.
|
|
@@ -588,12 +659,45 @@ export function RenderEdit(props) {
|
|
|
588
659
|
// Clamped on read, not on write: section visibility is value-dependent, so a
|
|
589
660
|
// stored stepIndex can point past what is currently rendered — that lands on
|
|
590
661
|
// the last step instead of an empty one.
|
|
591
|
-
const
|
|
662
|
+
const activeTabIndex = isTabs && activeTabId !== undefined
|
|
663
|
+
? filteredSections.findIndex((s, i) => tabIdAt(s, i) === activeTabId)
|
|
664
|
+
: -1;
|
|
665
|
+
const currentStep = activeTabIndex !== -1 ? activeTabIndex : Math.min(rawStep, lastStepIndex);
|
|
592
666
|
// Kept fresh for patchAndScheduleDraftSave's debounce timer above, which
|
|
593
667
|
// is defined before `currentStep` exists (it depends on `filteredSections`,
|
|
594
668
|
// computed further up from `vm`) and so cannot close over it directly.
|
|
595
669
|
currentStepRef.current = currentStep;
|
|
596
670
|
const isLastWizardStep = currentStep >= lastStepIndex;
|
|
671
|
+
const wizardDoneSteps = useMemo(() => {
|
|
672
|
+
if (!isWizard || isCreateMode)
|
|
673
|
+
return undefined;
|
|
674
|
+
const snapshotForDryRun = { values: snapshot.values, fields: snapshot.fields };
|
|
675
|
+
return filteredSections.map((section, index) => {
|
|
676
|
+
if (passedStepKeys.has(wizardStepKey(section, index)))
|
|
677
|
+
return true;
|
|
678
|
+
if (section.kind === "fields")
|
|
679
|
+
return isFieldsStepFilled(section, schema, snapshotForDryRun);
|
|
680
|
+
return (section.kind === "extension" &&
|
|
681
|
+
extensionStepComplete[wizardStepKey(section, index)] === true);
|
|
682
|
+
});
|
|
683
|
+
}, [
|
|
684
|
+
isWizard,
|
|
685
|
+
isCreateMode,
|
|
686
|
+
filteredSections,
|
|
687
|
+
passedStepKeys,
|
|
688
|
+
extensionStepComplete,
|
|
689
|
+
schema,
|
|
690
|
+
snapshot.values,
|
|
691
|
+
snapshot.fields,
|
|
692
|
+
]);
|
|
693
|
+
const updateModeStepBarProps = wizardDoneSteps === undefined
|
|
694
|
+
? {}
|
|
695
|
+
: { doneSteps: wizardDoneSteps, selectableSteps: "all" };
|
|
696
|
+
function jumpToStep(index) {
|
|
697
|
+
setRawStep(index);
|
|
698
|
+
if (isTabs)
|
|
699
|
+
setActiveTabId(tabIdAt(filteredSections[index], index));
|
|
700
|
+
}
|
|
597
701
|
// Step transitions only — never per keystroke. Deliberately not awaited: a
|
|
598
702
|
// failed draft save must not block the step change.
|
|
599
703
|
//
|
|
@@ -633,11 +737,13 @@ export function RenderEdit(props) {
|
|
|
633
737
|
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.`);
|
|
634
738
|
return;
|
|
635
739
|
}
|
|
636
|
-
const
|
|
637
|
-
? section.fields
|
|
740
|
+
const secretFieldNames = new Set(vm.sections.flatMap((section) => section.kind === "fields"
|
|
741
|
+
? section.fields
|
|
742
|
+
.filter((f) => f.format === "password" || f.writeOnly === true)
|
|
743
|
+
.map((f) => f.field)
|
|
638
744
|
: []));
|
|
639
|
-
// A password field never enters the persisted draft blob — it would be stored in the clear and restored on resume.
|
|
640
|
-
const draftValues = Object.fromEntries(Object.entries(controller.getSnapshot().values).filter(([field]) => !
|
|
745
|
+
// A password or writeOnly field never enters the persisted draft blob — it would be stored in the clear and restored on resume.
|
|
746
|
+
const draftValues = Object.fromEntries(Object.entries(controller.getSnapshot().values).filter(([field]) => !secretFieldNames.has(field)));
|
|
641
747
|
void dispatcher
|
|
642
748
|
.write(FORM_DRAFT_SAVE, {
|
|
643
749
|
draftKey: key,
|
|
@@ -665,29 +771,45 @@ export function RenderEdit(props) {
|
|
|
665
771
|
setRawStep(next);
|
|
666
772
|
return;
|
|
667
773
|
}
|
|
668
|
-
const section = filteredSections[currentStep];
|
|
669
|
-
const fieldNames = section?.kind === "fields" ? section.fields.map((f) => f.field) : [];
|
|
670
774
|
// skip: the current step has field errors — no transition, no draft save.
|
|
671
|
-
if (
|
|
775
|
+
if (currentStepHasFieldErrors())
|
|
672
776
|
return;
|
|
777
|
+
if (!isCreateMode) {
|
|
778
|
+
const leftSection = filteredSections[currentStep];
|
|
779
|
+
if (leftSection !== undefined) {
|
|
780
|
+
const leftKey = wizardStepKey(leftSection, currentStep);
|
|
781
|
+
setPassedStepKeys((prev) => (prev.has(leftKey) ? prev : new Set(prev).add(leftKey)));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
673
784
|
setRawStep(next);
|
|
674
785
|
saveDraft(next);
|
|
675
786
|
}
|
|
787
|
+
function currentStepHasFieldErrors() {
|
|
788
|
+
const section = filteredSections[currentStep];
|
|
789
|
+
const fieldNames = section?.kind === "fields" ? section.fields.map((f) => f.field) : [];
|
|
790
|
+
return fieldNames.length > 0 && !controller.validate(fieldNames);
|
|
791
|
+
}
|
|
676
792
|
function handleWizardBack() {
|
|
677
|
-
|
|
793
|
+
handleWizardStepSelect(currentStep - 1);
|
|
678
794
|
}
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
795
|
+
// Update mode: the record already exists and the final Save validates the
|
|
796
|
+
// whole form and jumps to the first erroring step (findFirstErroringStep), so
|
|
797
|
+
// skipping steps forward is safe; leaving the current step still goes through
|
|
798
|
+
// its validate gate.
|
|
799
|
+
function handleWizardStepSelect(targetStep) {
|
|
800
|
+
if (targetStep === currentStep)
|
|
682
801
|
return;
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
if (
|
|
686
|
-
setRawStep(previous);
|
|
802
|
+
const isForward = targetStep > currentStep;
|
|
803
|
+
// Create mode: a forward jump would skip the per-step validate() gate.
|
|
804
|
+
if (isForward && isCreateMode)
|
|
687
805
|
return;
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
806
|
+
if (isForward && !disabled && currentStepHasFieldErrors())
|
|
807
|
+
return;
|
|
808
|
+
const clampedTarget = Math.min(Math.max(targetStep, 0), lastStepIndex);
|
|
809
|
+
hasNavigatedRef.current = true;
|
|
810
|
+
setRawStep(clampedTarget);
|
|
811
|
+
if (!disabled)
|
|
812
|
+
saveDraft(clampedTarget);
|
|
691
813
|
}
|
|
692
814
|
async function discardDraft() {
|
|
693
815
|
// skip: this screen does not persist a draft.
|
|
@@ -818,6 +940,7 @@ export function RenderEdit(props) {
|
|
|
818
940
|
let extensionsPersisted = true;
|
|
819
941
|
if (result.isSuccess) {
|
|
820
942
|
setFormError(null);
|
|
943
|
+
setExtraDeleteIssues([]);
|
|
821
944
|
// `isNoOp: true` (payloadMode "changes", pre-filled form submitted
|
|
822
945
|
// untouched) means controller.submit() never called dispatcher.write
|
|
823
946
|
// — nothing to discard, and discarding here would delete a draft the
|
|
@@ -838,7 +961,7 @@ export function RenderEdit(props) {
|
|
|
838
961
|
if (isStepped) {
|
|
839
962
|
const step = findFirstErroringStep(Object.keys(controller.getSnapshot().errors));
|
|
840
963
|
if (step !== undefined)
|
|
841
|
-
|
|
964
|
+
jumpToStep(step);
|
|
842
965
|
}
|
|
843
966
|
}
|
|
844
967
|
else {
|
|
@@ -854,8 +977,9 @@ export function RenderEdit(props) {
|
|
|
854
977
|
if (isStepped) {
|
|
855
978
|
const step = findFirstErroringStep(issuePaths);
|
|
856
979
|
if (step !== undefined)
|
|
857
|
-
|
|
980
|
+
jumpToStep(step);
|
|
858
981
|
}
|
|
982
|
+
setExtraDeleteIssues([]);
|
|
859
983
|
setFormError(hasIssueWithoutRenderedField(issuePaths, filteredSections) ? result.error : null);
|
|
860
984
|
}
|
|
861
985
|
// skip: on entity-success-but-extension-failure the extension-error
|
|
@@ -872,10 +996,7 @@ export function RenderEdit(props) {
|
|
|
872
996
|
handleSubmitRef.current = handleSubmit;
|
|
873
997
|
// Screen forms (entityEdit/actionForm filling the shell height) drop the card;
|
|
874
998
|
// the title then lives in the shell header when the shell offers a slot.
|
|
875
|
-
const isScreenForm = fillScreenHeight === true &&
|
|
876
|
-
hideSectionTitles !== true &&
|
|
877
|
-
headerRegion === undefined &&
|
|
878
|
-
!insideDrawer;
|
|
999
|
+
const isScreenForm = fillScreenHeight === true && hideSectionTitles !== true && !insideDrawer;
|
|
879
1000
|
const titleInShell = PageHeader !== undefined &&
|
|
880
1001
|
pageHeaderSlotAvailable &&
|
|
881
1002
|
hideSectionTitles !== true &&
|
|
@@ -953,25 +1074,53 @@ export function RenderEdit(props) {
|
|
|
953
1074
|
const isFieldlessSubmitForm = isFieldless && screen.entity !== PROJECTION_DETAIL_ENTITY;
|
|
954
1075
|
const showsSubmit = isFormEditable || hasExtensionRegistrations || isFieldlessSubmitForm;
|
|
955
1076
|
const footerSlot = screen.slots?.footer;
|
|
1077
|
+
// An unregistered footer component renders null, so only a resolvable one may
|
|
1078
|
+
// count as an action (else read-only screens get an empty footer strip).
|
|
1079
|
+
const FooterSlotComponent = useExtensionSectionComponent(footerSlot !== undefined ? extensionSectionName(footerSlot) : undefined);
|
|
956
1080
|
// Mirrors every branch inside formActions below — without this guard
|
|
957
1081
|
// DefaultForm renders an empty footer strip (border + padding, no content)
|
|
958
1082
|
// on read-only detail screens, since `actions` would otherwise always be
|
|
959
1083
|
// a defined (if empty) fragment.
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1084
|
+
const showWizardBack = isWizard && currentStep > 0;
|
|
1085
|
+
const showWizardNext = isWizard && !isLastWizardStep;
|
|
1086
|
+
const showSubmit = showsSubmit && (!isWizard || isLastWizardStep);
|
|
1087
|
+
const hasFormActions = showWizardBack ||
|
|
1088
|
+
showWizardNext ||
|
|
1089
|
+
showSubmit ||
|
|
1090
|
+
FooterSlotComponent !== undefined ||
|
|
1091
|
+
writeFormFooterAction !== undefined;
|
|
964
1092
|
const nextStepTitle = isWizard ? filteredSections[currentStep + 1]?.title : undefined;
|
|
965
|
-
const
|
|
1093
|
+
const isCreate = (() => {
|
|
1094
|
+
const id = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
1095
|
+
return id == null || id === "";
|
|
1096
|
+
})();
|
|
1097
|
+
const changedFieldCount = Object.keys(snapshot.changes).length;
|
|
1098
|
+
// Edit-mode screen forms always carry the dirty status (0 = quiet "No unsaved
|
|
1099
|
+
// changes"); wizards keep the status-only-when-dirty behaviour.
|
|
1100
|
+
const hasDirtyFooter = !isWizard &&
|
|
1101
|
+
(dirtyFooter === true || (isScreenForm && !isCreate && headerRegion === undefined));
|
|
1102
|
+
const unsavedCount = !hasDirtyFooter
|
|
1103
|
+
? undefined
|
|
1104
|
+
: changedFieldCount > 0
|
|
1105
|
+
? changedFieldCount
|
|
1106
|
+
: extensionDirty
|
|
1107
|
+
? 1
|
|
1108
|
+
: 0;
|
|
1109
|
+
const isSettingsList = screen.layout.variant === "settings-list" && hideSectionTitles !== true;
|
|
1110
|
+
const formActions = (_jsxs(_Fragment, { children: [showWizardBack && (_jsx(Button, { type: "button", variant: "secondary", icon: "chevron-left", onClick: handleWizardBack, testId: "render-edit-wizard-back", children: translate("kumiko.actions.back") })), footerSlot !== undefined && (_jsx(EditSlotMount, { slot: footerSlot, slotName: "footer", screenId: screen.id, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), values: snapshot.values, hasUnsavedChanges: hasUnsavedInput, wizardStep: isWizard ? { index: currentStep, isLast: isLastWizardStep } : undefined, ...(screen.slots?.footerPrimary === true && { [STICKY_PRIMARY_ACTION_PROP]: true }) })), isWizard && !isLastWizardStep && showsSubmit && !isCreateMode && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => void handleSubmit({ saveNow: true }), testId: "render-edit-wizard-save-close", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.wizard.save-and-close") })), showWizardNext && (_jsx(Button, { type: "submit", variant: "primary", iconEnd: "chevron-right", testId: "render-edit-wizard-next", children: nextStepTitle !== undefined
|
|
966
1111
|
? translate("kumiko.wizard.next-with-title", { title: nextStepTitle })
|
|
967
|
-
: translate("kumiko.actions.next") })),
|
|
968
|
-
(!isWizard || isLastWizardStep) &&
|
|
1112
|
+
: translate("kumiko.actions.next") })), showSubmit &&
|
|
969
1113
|
footerActions?.map((action) => (_jsx(Button, { type: "button", variant: action.variant ?? "secondary", ...(action.icon !== undefined && { icon: action.icon }), disabled: isSubmitting || disabled, onClick: () => {
|
|
970
1114
|
controller.setValues(action.patch); // @cast-boundary form-values
|
|
971
1115
|
void handleSubmit();
|
|
972
|
-
}, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), showsSubmit && (
|
|
1116
|
+
}, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), hasDirtyFooter && showsSubmit && unsavedCount !== undefined && unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.form.discard") })), showSubmit && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged && !extensionDirty && !isFieldless && !insideDrawer) ||
|
|
973
1117
|
isSubmitting ||
|
|
974
|
-
disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
|
|
1118
|
+
disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
|
|
1119
|
+
(isWizard
|
|
1120
|
+
? "kumiko.actions.finish"
|
|
1121
|
+
: hasDirtyFooter
|
|
1122
|
+
? "kumiko.form.saveChanges"
|
|
1123
|
+
: "kumiko.actions.save")) })), writeFormFooterAction] }));
|
|
975
1124
|
// Title + Subtitle, create/edit-bewusst. i18n-Keys (mode = "create"|"edit"):
|
|
976
1125
|
// screen:<id>.<mode>.title / .<mode>.subtitle
|
|
977
1126
|
// Fallback-Kette: mode-spezifisch → generisch (screen:<id>.title/.subtitle).
|
|
@@ -982,18 +1131,14 @@ export function RenderEdit(props) {
|
|
|
982
1131
|
// prose, same as it does for an unknown key (fw#2723 review). Same slot
|
|
983
1132
|
// section.description already fills for a section. Falls back to
|
|
984
1133
|
// undefined (no subtitle) when neither is set.
|
|
985
|
-
const isCreate = (() => {
|
|
986
|
-
const id = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
987
|
-
return id == null || id === "";
|
|
988
|
-
})();
|
|
989
1134
|
const formMode = isCreate ? "create" : "edit";
|
|
990
1135
|
const recordTitle = !isCreate && screen.recordTitleField !== undefined
|
|
991
1136
|
? resolveRecordTitle(initial[screen.recordTitleField])
|
|
992
1137
|
: undefined;
|
|
993
1138
|
const resolveScreenText = (suffix) => {
|
|
994
1139
|
for (const key of [
|
|
995
|
-
`screen:${
|
|
996
|
-
`screen:${
|
|
1140
|
+
`screen:${i18nScreenId}.${formMode}.${suffix}`,
|
|
1141
|
+
`screen:${i18nScreenId}.${suffix}`,
|
|
997
1142
|
]) {
|
|
998
1143
|
const value = translate(key);
|
|
999
1144
|
if (value !== key)
|
|
@@ -1001,26 +1146,26 @@ export function RenderEdit(props) {
|
|
|
1001
1146
|
}
|
|
1002
1147
|
return undefined;
|
|
1003
1148
|
};
|
|
1004
|
-
const plainFormTitle = resolveScreenText("title") ??
|
|
1149
|
+
const plainFormTitle = resolveScreenText("title") ?? i18nScreenId;
|
|
1005
1150
|
const formTitle = resolveTitleTemplate(screen.titleTemplate, snapshot.values, translate, plainFormTitle);
|
|
1006
|
-
// screen.description is
|
|
1007
|
-
//
|
|
1151
|
+
// In tabs mode (hideSectionTitles) screen.description is dropped — the head
|
|
1152
|
+
// card carries it — but an explicit subtitle i18n override has no other
|
|
1153
|
+
// source there, so it stays unless it merely resolves to the description.
|
|
1154
|
+
const explicitSubtitle = resolveScreenText("subtitle");
|
|
1155
|
+
const descriptionSubtitle = screen.description !== undefined ? translate(screen.description) : undefined;
|
|
1008
1156
|
const formSubtitle = hideSectionTitles !== true
|
|
1009
|
-
? (
|
|
1010
|
-
|
|
1011
|
-
|
|
1157
|
+
? (explicitSubtitle ?? descriptionSubtitle)
|
|
1158
|
+
: explicitSubtitle !== descriptionSubtitle
|
|
1159
|
+
? explicitSubtitle
|
|
1160
|
+
: undefined;
|
|
1161
|
+
// Embedded settings-list forms (dashboard panels) have no shell header: the
|
|
1162
|
+
// form title/subtitle move into the first section's header column instead of
|
|
1163
|
+
// floating as a heading above the band.
|
|
1164
|
+
const titleInSettingsHeader = isSettingsList && !titleInShell;
|
|
1012
1165
|
const usesFieldFlow = isScreenForm || insideDrawer;
|
|
1013
1166
|
const allRequiredHint = (_jsx(Text, { variant: "muted", testId: "render-edit-all-required-hint", children: translate("kumiko.form.all-required") }));
|
|
1014
1167
|
const stepRail = isWizard && isScreenForm && StepBar !== undefined;
|
|
1015
1168
|
const upNextSection = screen.layout.wizard?.aside?.upNext === true ? filteredSections[currentStep + 1] : undefined;
|
|
1016
|
-
const changedFieldCount = isCreate ? 0 : Object.keys(snapshot.changes).length;
|
|
1017
|
-
const unsavedCount = isCreate || !isScreenForm
|
|
1018
|
-
? 0
|
|
1019
|
-
: changedFieldCount > 0
|
|
1020
|
-
? changedFieldCount
|
|
1021
|
-
: extensionDirty
|
|
1022
|
-
? 1
|
|
1023
|
-
: 0;
|
|
1024
1169
|
const sectionNavItems = isScreenForm && !isStepped
|
|
1025
1170
|
? filteredSections.flatMap((section, index) => section.kind !== "extension" &&
|
|
1026
1171
|
section.kind !== "relatedList" &&
|
|
@@ -1046,8 +1191,13 @@ export function RenderEdit(props) {
|
|
|
1046
1191
|
!pageHeaderCompact &&
|
|
1047
1192
|
ActionOverflowMenu !== undefined && {
|
|
1048
1193
|
actions: (_jsx(ActionOverflowMenu, { label: translate("kumiko.list.row-actions.more"), items: headerMenuItems, testId: "render-edit-header-menu" })),
|
|
1049
|
-
}) })), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(hideSectionTitles !== true &&
|
|
1050
|
-
|
|
1194
|
+
}) })), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(hideSectionTitles !== true &&
|
|
1195
|
+
!titleInShell &&
|
|
1196
|
+
!titleInSettingsHeader && { title: formTitle }), ...(formSubtitle !== undefined &&
|
|
1197
|
+
!stepRail &&
|
|
1198
|
+
!titleInSettingsHeader && { subtitle: formSubtitle }), ...(isScreenForm && { screenForm: true }), ...(unsavedCount !== undefined &&
|
|
1199
|
+
(unsavedCount > 0 || hasDirtyFooter) && { unsavedCount }), ...(sectionNavItems.length > 0 && { sectionNav: sectionNavItems }), ...(stepRail && {
|
|
1200
|
+
sideRail: (_jsx(StepBar, { steps: filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: compactStepLabel, onStepSelect: handleWizardStepSelect, ...updateModeStepBarProps, orientation: "vertical", heading: translate("kumiko.wizard.step", {
|
|
1051
1201
|
current: currentStep + 1,
|
|
1052
1202
|
total: lastStepIndex + 1,
|
|
1053
1203
|
}), ...(formSubtitle !== undefined && { description: formSubtitle }), subtitles: filteredSections.map((section) => section.kind === "fields" ? section.subtitle : undefined), ...(upNextSection !== undefined && {
|
|
@@ -1059,12 +1209,12 @@ export function RenderEdit(props) {
|
|
|
1059
1209
|
},
|
|
1060
1210
|
}), testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" })),
|
|
1061
1211
|
}), ...(hideActions !== true && hasFormActions && { actions: formActions }), ...(hideActions !== true &&
|
|
1062
|
-
hasSecondaryFormActions && { secondaryActions: secondaryFormActions }), testId: "render-edit-form", stickyActions: isWizard || fillScreenHeight === true, ...(screen.layout.width !== undefined && { width: screen.layout.width }), ...(formHeaderRegion !== undefined && { headerRegion: formHeaderRegion }), ...(titleActionMount !== undefined && { titleAction: titleActionMount }), ...(fillHeight && { fillHeight }), ...(hideSectionTitles === true && { chromeless: true }), ...(summary !== undefined && { summary }), children: _jsxs(AllFieldsRequiredProvider, { value: showsAllRequiredHint, children: [showsAllRequiredHint && !insideDrawer && allRequiredHint, draftCandidates !== null && (_jsx(Banner, { variant: "info", testId: "render-edit-draft-picker", actions: [
|
|
1212
|
+
hasSecondaryFormActions && { secondaryActions: secondaryFormActions }), testId: "render-edit-form", stickyActions: isWizard || fillScreenHeight === true, ...(screen.layout.width !== undefined && { width: screen.layout.width }), ...(formHeaderRegion !== undefined && { headerRegion: formHeaderRegion }), ...(titleActionMount !== undefined && { titleAction: titleActionMount }), ...(fillHeight && { fillHeight }), ...(hideSectionTitles === true && { chromeless: true }), ...(summary !== undefined && { summary }), children: _jsxs(AllFieldsRequiredProvider, { value: showsAllRequiredHint, children: [leadContent, showsAllRequiredHint && !insideDrawer && allRequiredHint, draftCandidates !== null && (_jsx(Banner, { variant: "info", testId: "render-edit-draft-picker", actions: [
|
|
1063
1213
|
...draftCandidates.map((candidate) => (_jsx(Button, { type: "button", variant: "link", onClick: () => adoptDraft(candidate), testId: `render-edit-draft-pick-${candidate.id}`, children: formatWhen(candidate.savedAt) }, candidate.id))),
|
|
1064
1214
|
_jsx(Button, { type: "button", variant: "link", onClick: () => setDraftCandidates(null), testId: "render-edit-draft-start-new", children: translate("kumiko.form.draft.start-new") }, "start-new"),
|
|
1065
1215
|
], children: _jsx(Text, { children: translate(draftCandidates.length === 1
|
|
1066
1216
|
? "kumiko.form.draft.resume-single"
|
|
1067
|
-
: "kumiko.form.draft.resume-multiple") }) })), isWizard && !stepRail && (_jsxs(_Fragment, { children: [Progress !== undefined && (_jsx(Progress, { value: (currentStep + 1) / (lastStepIndex + 1), testId: "render-edit-wizard-progress" })), StepBar === undefined ? (_jsx(Text, { variant: "small", testId: "render-edit-wizard-step-label", children: compactStepLabel })) : (_jsx(StepBar, { steps: filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: compactStepLabel, onStepSelect:
|
|
1217
|
+
: "kumiko.form.draft.resume-multiple") }) })), isWizard && !stepRail && (_jsxs(_Fragment, { children: [Progress !== undefined && (_jsx(Progress, { value: (currentStep + 1) / (lastStepIndex + 1), testId: "render-edit-wizard-progress" })), StepBar === undefined ? (_jsx(Text, { variant: "small", testId: "render-edit-wizard-step-label", children: compactStepLabel })) : (_jsx(StepBar, { steps: filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: compactStepLabel, onStepSelect: handleWizardStepSelect, ...updateModeStepBarProps, testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" }))] })), isTabs &&
|
|
1068
1218
|
(() => {
|
|
1069
1219
|
// No Tabs primitive registered → every section renders stacked
|
|
1070
1220
|
// (stepHidden below is gated on the same condition), which is the
|
|
@@ -1080,7 +1230,7 @@ export function RenderEdit(props) {
|
|
|
1080
1230
|
// skip: the strip reported an id no section owns.
|
|
1081
1231
|
if (index === -1)
|
|
1082
1232
|
return;
|
|
1083
|
-
|
|
1233
|
+
jumpToStep(index);
|
|
1084
1234
|
} }));
|
|
1085
1235
|
})(), filteredSections.map((section, sectionIndex) => {
|
|
1086
1236
|
// turns this section's own `actions` (RowAction[]) into
|
|
@@ -1120,7 +1270,11 @@ export function RenderEdit(props) {
|
|
|
1120
1270
|
const mount = (_jsx(ExtensionSectionMount, { section: section, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), initialValues: extensionInitialValues, values: snapshot.values,
|
|
1121
1271
|
// @cast-boundary form-values: ExtensionSectionProps is not generic
|
|
1122
1272
|
// over TValues; controller is mount-lifetime-stable, see onControlsReady above.
|
|
1123
|
-
patch: patchAndScheduleDraftSave, validate: scopedValidate, hideTitle: hideSectionTitles, ...(
|
|
1273
|
+
patch: patchAndScheduleDraftSave, validate: scopedValidate, hideTitle: hideSectionTitles, ...(isWizard &&
|
|
1274
|
+
!isCreateMode && {
|
|
1275
|
+
stepKey: wizardStepKey(section, sectionIndex),
|
|
1276
|
+
onReportStepComplete: reportExtensionStepComplete,
|
|
1277
|
+
}), ...(hideSectionTitles !== true &&
|
|
1124
1278
|
sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title));
|
|
1125
1279
|
// Tabs mode: an unframed padded panel (the tab panel is the
|
|
1126
1280
|
// surface), so extension content neither sits flush at the shell
|
|
@@ -1153,19 +1307,29 @@ export function RenderEdit(props) {
|
|
|
1153
1307
|
// Own submit button + dispatcher call, entirely independent of
|
|
1154
1308
|
// this screen's (nonexistent, on projectionDetail) form submit —
|
|
1155
1309
|
// rejected at boot in wizard layouts, so no WizardStepGroup here.
|
|
1156
|
-
return (_jsx(WriteFormSection, { section: section, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, onSubmitted: () => onReload?.(), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title ?? `write-form-${sectionIndex}`));
|
|
1310
|
+
return (_jsx(WriteFormSection, { section: section, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, onSubmitted: () => onReload?.(), ...(isLoneWriteFormTab && { onFooterAction: setWriteFormFooterAction }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title ?? `write-form-${sectionIndex}`));
|
|
1157
1311
|
}
|
|
1158
1312
|
if (!section.visible)
|
|
1159
1313
|
return null;
|
|
1160
1314
|
// Titellose Sections kollidieren sonst auf key/testId — Index-Fallback.
|
|
1161
1315
|
const sectionKey = section.title ?? `section-${sectionIndex}`;
|
|
1162
|
-
const renderFieldGrid = (fields, columns, gridKey) => (_jsx(Grid, { columns: columns, ...(usesFieldFlow && { flow: true }), children: fields.map((field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(usesFieldFlow && { flow: true }),
|
|
1316
|
+
const renderFieldGrid = (fields, columns, gridKey) => (_jsx(Grid, { columns: columns, ...(isSettingsList ? { list: true } : usesFieldFlow && { flow: true }), children: fields.map((field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(!isSettingsList && usesFieldFlow && { flow: true }), ...(isSettingsList && {
|
|
1317
|
+
layout: "row",
|
|
1318
|
+
accent: fieldAccent?.(field.field) === true,
|
|
1319
|
+
}), issues: snapshot.errors[field.field], onChange: (v) => {
|
|
1163
1320
|
controller.setField(field.field, v);
|
|
1321
|
+
if (validateOnChange === true)
|
|
1322
|
+
controller.validate([field.field]);
|
|
1164
1323
|
}, GridCell: GridCell, featureName: featureName, ...(labelAppendix !== undefined && {
|
|
1165
1324
|
labelAppendix: labelAppendix(field.field),
|
|
1166
1325
|
}), ...(fieldAppendix !== undefined && {
|
|
1167
1326
|
fieldAppendix: fieldAppendix(field.field),
|
|
1168
|
-
}),
|
|
1327
|
+
}), ...(fieldAppendixPlacement !== undefined && {
|
|
1328
|
+
appendixPlacement: fieldAppendixPlacement,
|
|
1329
|
+
}), ...(fieldDescription !== undefined && {
|
|
1330
|
+
description: fieldDescription(field.field),
|
|
1331
|
+
}), allIssues: snapshot.errors, valueDisplay: valueDisplay, row: snapshot.values, ...((!isCreate || markChangedFields) &&
|
|
1332
|
+
field.field in snapshot.changes && { changed: true }) }, field.field))) }, gridKey));
|
|
1169
1333
|
// Tabs mode: the tab panel is the surface, so field sections render
|
|
1170
1334
|
// as unframed padded panels; group cards inside stay framed.
|
|
1171
1335
|
// Stays `Card`, not `Section` — Section always flattens to a
|
|
@@ -1196,20 +1360,28 @@ export function RenderEdit(props) {
|
|
|
1196
1360
|
// tab label right above it. Without a Tabs primitive the sections
|
|
1197
1361
|
// render stacked, and then the titles are what tells them apart.
|
|
1198
1362
|
const repeatsTabLabel = isTabs && isStepped;
|
|
1199
|
-
const
|
|
1363
|
+
const headerFallback = titleInSettingsHeader && sectionIndex === 0;
|
|
1364
|
+
const sectionTitle = headerFallback
|
|
1365
|
+
? (section.title ?? formTitle)
|
|
1366
|
+
: section.title === formTitle || repeatsTabLabel
|
|
1367
|
+
? undefined
|
|
1368
|
+
: section.title;
|
|
1369
|
+
const sectionSubtitle = section.description ?? (headerFallback ? formSubtitle : undefined);
|
|
1200
1370
|
const sectionEl = (_jsxs(Section, { ...(sectionNavItems.some((item) => item.id === formSectionDomId(sectionIndex)) && {
|
|
1201
1371
|
id: formSectionDomId(sectionIndex),
|
|
1202
|
-
}), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(
|
|
1372
|
+
}), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(sectionSubtitle !== undefined && { subtitle: sectionSubtitle }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(isSettingsList && { layout: "settings-list" }), testId: `section-${sectionKey}`, children: [showsAllRequiredHint && insideDrawer && sectionIndex === 0 && allRequiredHint, renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`)] }, sectionKey));
|
|
1203
1373
|
return wrapWizardStep(sectionKey, sectionEl);
|
|
1204
|
-
}), formError !== null && (
|
|
1374
|
+
}), formError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
|
|
1205
1375
|
onReload();
|
|
1206
1376
|
setFormError(null);
|
|
1207
|
-
|
|
1377
|
+
setExtraDeleteIssues([]);
|
|
1378
|
+
}, testId: "render-edit-form-error-reload", children: translate("kumiko.actions.reload") })) : undefined, children: [_jsx(Text, { testId: "render-edit-form-error-key", children: translate(formError.i18nKey, formError.i18nParams) }), extraDeleteIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-extra", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`)))] })), (snapshot.errors["(root)"] ?? []).map((issue) => (_jsx(Banner, { variant: "error", testId: "render-edit-root-issue", children: _jsx(Text, { children: translate(issue.i18nKey, issue.params) }) }, issue.i18nKey))), extensionErrorKey !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-extension-error", children: _jsx(Text, { testId: "render-edit-extension-error-key", children: translate(extensionErrorKey) }) })), actionError !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError })), onDelete !== undefined && (_jsx(Dialog, { open: confirmDeleteOpen, onOpenChange: setConfirmDeleteOpen, title: translate("kumiko.actions.delete-confirm"), confirmLabel: translate("kumiko.actions.delete"), variant: "danger", onConfirm: async () => {
|
|
1208
1379
|
const rejection = await onDelete();
|
|
1209
1380
|
// A rejected delete (e.g. a preDelete hook) carries its reason in
|
|
1210
1381
|
// the field issues; the top-level key is only "validation failed".
|
|
1211
1382
|
if (rejection !== undefined) {
|
|
1212
|
-
const issue = rejection.details?.fields
|
|
1383
|
+
const [issue, ...otherIssues] = rejection.details?.fields ?? [];
|
|
1384
|
+
setExtraDeleteIssues(otherIssues);
|
|
1213
1385
|
setFormError(issue === undefined
|
|
1214
1386
|
? rejection
|
|
1215
1387
|
: { ...rejection, i18nKey: issue.i18nKey, i18nParams: issue.params });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type EditFieldViewModel, type FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
|
+
import { type FieldProps } from "../primitives.js";
|
|
3
4
|
export type RenderFieldProps = {
|
|
4
5
|
readonly field: EditFieldViewModel;
|
|
5
6
|
readonly issues?: readonly FieldIssue[];
|
|
@@ -14,6 +15,9 @@ export type RenderFieldProps = {
|
|
|
14
15
|
/** Optionaler Zusatz-Inhalt der nach dem Input gerendert wird (z.B.
|
|
15
16
|
* ConfigCascade). */
|
|
16
17
|
readonly fieldAppendix?: ReactNode;
|
|
18
|
+
readonly appendixPlacement?: FieldProps["appendixPlacement"];
|
|
19
|
+
readonly description?: ReactNode;
|
|
20
|
+
readonly status?: FieldProps["status"];
|
|
17
21
|
/** Flat issues-by-path map (FormSnapshot.errors) — only relevant for
|
|
18
22
|
* type:"embedded" with embeddedListCells, to bucket row-/cell-issues
|
|
19
23
|
* (`${field}.${rowIndex}` / `${field}.${rowIndex}.${cellField}`).
|
|
@@ -33,6 +37,8 @@ export type RenderFieldProps = {
|
|
|
33
37
|
readonly row?: Readonly<Record<string, unknown>>;
|
|
34
38
|
/** Marks the field as modified against the loaded record (edit mode only). */
|
|
35
39
|
readonly changed?: boolean;
|
|
40
|
+
readonly layout?: FieldProps["layout"];
|
|
41
|
+
readonly accent?: boolean;
|
|
36
42
|
};
|
|
37
43
|
export declare const AllFieldsRequiredProvider: import("react").Provider<boolean>;
|
|
38
|
-
export declare function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, }: RenderFieldProps): ReactNode;
|
|
44
|
+
export declare function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay, row, changed, layout, accent, }: RenderFieldProps): ReactNode;
|