@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.1
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/projection-detail-body.js +59 -394
- package/dist/app/projection-detail-header-model.d.ts +11 -0
- package/dist/app/projection-detail-header-model.js +33 -0
- package/dist/app/projection-detail-header-region.d.ts +28 -0
- package/dist/app/projection-detail-header-region.js +81 -0
- package/dist/app/projection-detail-header.d.ts +29 -0
- package/dist/app/projection-detail-header.js +84 -0
- package/dist/app/projection-detail-navigation.d.ts +13 -0
- package/dist/app/projection-detail-navigation.js +91 -0
- package/dist/app/projection-detail-states.d.ts +22 -0
- package/dist/app/projection-detail-states.js +26 -0
- package/dist/app/projection-drawer-host.d.ts +12 -0
- package/dist/app/projection-drawer-host.js +10 -0
- package/dist/app/projection-list-body.d.ts +1 -1
- package/dist/app/projection-list-body.js +69 -189
- package/dist/app/projection-list-render-props.d.ts +56 -0
- package/dist/app/projection-list-render-props.js +47 -0
- package/dist/app/projection-list-render.d.ts +26 -0
- package/dist/app/projection-list-render.js +20 -0
- package/dist/app/projection-list-row-click.d.ts +12 -0
- package/dist/app/projection-list-row-click.js +14 -0
- package/dist/app/use-projection-detail-actions.d.ts +35 -0
- package/dist/app/use-projection-detail-actions.js +156 -0
- package/dist/app/use-projection-detail-data.d.ts +19 -0
- package/dist/app/use-projection-detail-data.js +47 -0
- package/dist/app/use-projection-detail-layout.d.ts +17 -0
- package/dist/app/use-projection-detail-layout.js +23 -0
- package/dist/app/use-projection-list-actions.d.ts +31 -0
- package/dist/app/use-projection-list-actions.js +73 -0
- package/dist/app/use-projection-list-data.d.ts +20 -0
- package/dist/app/use-projection-list-data.js +26 -0
- package/dist/app/use-projection-list-definitions.d.ts +11 -0
- package/dist/app/use-projection-list-definitions.js +20 -0
- package/dist/app/use-projection-list-query.d.ts +28 -0
- package/dist/app/use-projection-list-query.js +83 -0
- package/dist/components/render-edit-banners.d.ts +12 -0
- package/dist/components/render-edit-banners.js +90 -0
- package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
- package/dist/components/render-edit-draft-save-helpers.js +42 -0
- package/dist/components/render-edit-field-grid.d.ts +4 -0
- package/dist/components/render-edit-field-grid.js +40 -0
- package/dist/components/render-edit-fields-section.d.ts +4 -0
- package/dist/components/render-edit-fields-section.js +76 -0
- package/dist/components/render-edit-form-actions.d.ts +4 -0
- package/dist/components/render-edit-form-actions.js +66 -0
- package/dist/components/render-edit-header-actions.d.ts +11 -0
- package/dist/components/render-edit-header-actions.js +120 -0
- package/dist/components/render-edit-layout-flags.d.ts +29 -0
- package/dist/components/render-edit-layout-flags.js +106 -0
- package/dist/components/render-edit-layout.d.ts +4 -0
- package/dist/components/render-edit-layout.js +79 -0
- package/dist/components/render-edit-render-model.d.ts +63 -0
- package/dist/components/render-edit-render-model.js +14 -0
- package/dist/components/render-edit-section-kinds.d.ts +4 -0
- package/dist/components/render-edit-section-kinds.js +115 -0
- package/dist/components/render-edit-submit-result.d.ts +8 -0
- package/dist/components/render-edit-submit-result.js +77 -0
- package/dist/components/render-edit-submit.d.ts +7 -0
- package/dist/components/render-edit-submit.js +113 -0
- package/dist/components/render-edit-titles.d.ts +17 -0
- package/dist/components/render-edit-titles.js +99 -0
- package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
- package/dist/components/render-edit-wizard-navigation.js +115 -0
- package/dist/components/render-edit.d.ts +2 -2
- package/dist/components/render-edit.js +63 -1128
- package/dist/components/render-field-choice-inputs.d.ts +7 -0
- package/dist/components/render-field-choice-inputs.js +52 -0
- package/dist/components/render-field-control.d.ts +12 -0
- package/dist/components/render-field-control.js +93 -0
- package/dist/components/render-field-display-text.d.ts +4 -0
- package/dist/components/render-field-display-text.js +48 -0
- package/dist/components/render-field-fill-action.d.ts +13 -0
- package/dist/components/render-field-fill-action.js +63 -0
- package/dist/components/render-field-input-shared.d.ts +39 -0
- package/dist/components/render-field-input-shared.js +20 -0
- package/dist/components/render-field-input.d.ts +3 -0
- package/dist/components/render-field-input.js +34 -0
- package/dist/components/render-field-meta.d.ts +6 -0
- package/dist/components/render-field-meta.js +51 -0
- package/dist/components/render-field-read-only.d.ts +11 -0
- package/dist/components/render-field-read-only.js +75 -0
- package/dist/components/render-field-reference.d.ts +15 -0
- package/dist/components/render-field-reference.js +103 -0
- package/dist/components/render-field-scalar-inputs.d.ts +10 -0
- package/dist/components/render-field-scalar-inputs.js +35 -0
- package/dist/components/render-field-text-inputs.d.ts +3 -0
- package/dist/components/render-field-text-inputs.js +34 -0
- package/dist/components/render-field-values.d.ts +18 -0
- package/dist/components/render-field-values.js +111 -0
- package/dist/components/render-field-write-only.d.ts +9 -0
- package/dist/components/render-field-write-only.js +29 -0
- package/dist/components/render-field.d.ts +2 -2
- package/dist/components/render-field.js +39 -670
- package/dist/components/render-list-header-slot.d.ts +5 -0
- package/dist/components/render-list-header-slot.js +30 -0
- package/dist/components/render-list-reference-bridge.d.ts +3 -0
- package/dist/components/render-list-reference-bridge.js +25 -0
- package/dist/components/render-list-reference-columns.d.ts +25 -0
- package/dist/components/render-list-reference-columns.js +137 -0
- package/dist/components/render-list-table-props.d.ts +52 -0
- package/dist/components/render-list-table-props.js +53 -0
- package/dist/components/render-list-toolbar-model.d.ts +42 -0
- package/dist/components/render-list-toolbar-model.js +82 -0
- package/dist/components/render-list-toolbar.d.ts +38 -0
- package/dist/components/render-list-toolbar.js +40 -0
- package/dist/components/render-list-types.d.ts +113 -0
- package/dist/components/render-list-types.js +1 -0
- package/dist/components/render-list.d.ts +4 -113
- package/dist/components/render-list.js +91 -307
- package/dist/components/use-reference-input-state.d.ts +20 -0
- package/dist/components/use-reference-input-state.js +85 -0
- package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
- package/dist/components/use-render-edit-controlled-sync.js +26 -0
- package/dist/components/use-render-edit-draft-save.d.ts +29 -0
- package/dist/components/use-render-edit-draft-save.js +141 -0
- package/dist/components/use-render-edit-form.d.ts +10 -0
- package/dist/components/use-render-edit-form.js +101 -0
- package/dist/components/use-render-edit-sections.d.ts +15 -0
- package/dist/components/use-render-edit-sections.js +100 -0
- package/dist/components/use-render-edit-state.d.ts +65 -0
- package/dist/components/use-render-edit-state.js +158 -0
- package/dist/components/use-render-edit-steps.d.ts +13 -0
- package/dist/components/use-render-edit-steps.js +52 -0
- package/dist/components/use-render-list-columns.d.ts +18 -0
- package/dist/components/use-render-list-columns.js +41 -0
- package/dist/components/use-render-list-search.d.ts +4 -0
- package/dist/components/use-render-list-search.js +50 -0
- package/dist/primitives.d.ts +1 -1
- package/dist/web/app/dashboard-body.d.ts +1 -1
- package/dist/web/app/dashboard-body.js +42 -246
- package/dist/web/app/dashboard-header.d.ts +34 -0
- package/dist/web/app/dashboard-header.js +58 -0
- package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
- package/dist/web/app/dashboard-screen-controls.js +42 -0
- package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
- package/dist/web/app/dashboard-screen-tiles.js +42 -0
- package/dist/web/app/dashboard-visibility.d.ts +42 -0
- package/dist/web/app/dashboard-visibility.js +102 -0
- package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
- package/dist/web/app/use-dashboard-screen-state.js +69 -0
- package/dist/web/primitives/data-table/actions-column.js +6 -5
- package/dist/web/primitives/data-table/card-body.d.ts +6 -0
- package/dist/web/primitives/data-table/card-body.js +32 -0
- package/dist/web/primitives/data-table/card-row.d.ts +5 -0
- package/dist/web/primitives/data-table/card-row.js +31 -0
- package/dist/web/primitives/data-table/card-view.d.ts +3 -0
- package/dist/web/primitives/data-table/card-view.js +13 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
- package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
- package/dist/web/primitives/data-table/data-cell-props.js +8 -0
- package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
- package/dist/web/primitives/data-table/row-render-parts.js +35 -0
- package/dist/web/primitives/data-table/table-body.d.ts +3 -0
- package/dist/web/primitives/data-table/table-body.js +32 -0
- package/dist/web/primitives/data-table/table-content.d.ts +3 -0
- package/dist/web/primitives/data-table/table-content.js +41 -0
- package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
- package/dist/web/primitives/data-table/table-data-cell.js +25 -0
- package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
- package/dist/web/primitives/data-table/table-header-row.js +21 -0
- package/dist/web/primitives/data-table/table-view.d.ts +3 -0
- package/dist/web/primitives/data-table/table-view.js +25 -0
- package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
- package/dist/web/primitives/data-table/toolbar.js +53 -0
- package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
- package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
- package/dist/web/primitives/data-table.d.ts +3 -4
- package/dist/web/primitives/data-table.js +32 -252
- package/dist/web/primitives/file-value-list.js +1 -1
- package/dist/web/primitives/form-footer-parts.d.ts +25 -0
- package/dist/web/primitives/form-footer-parts.js +55 -0
- package/dist/web/primitives/form.js +33 -43
- package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
- package/dist/web/primitives/input-choice-kinds.js +67 -0
- package/dist/web/primitives/input-file-kinds.d.ts +3 -0
- package/dist/web/primitives/input-file-kinds.js +6 -0
- package/dist/web/primitives/input-number-kinds.d.ts +4 -0
- package/dist/web/primitives/input-number-kinds.js +16 -0
- package/dist/web/primitives/input-optional-props.d.ts +20 -0
- package/dist/web/primitives/input-optional-props.js +24 -0
- package/dist/web/primitives/input-select-presentation.d.ts +13 -0
- package/dist/web/primitives/input-select-presentation.js +19 -0
- package/dist/web/primitives/input-shared.d.ts +22 -0
- package/dist/web/primitives/input-shared.js +10 -0
- package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
- package/dist/web/primitives/input-temporal-kinds.js +10 -0
- package/dist/web/primitives/input-text-kinds.d.ts +7 -0
- package/dist/web/primitives/input-text-kinds.js +33 -0
- package/dist/web/primitives/input.js +35 -161
- package/dist/web/widgets/chart-data.d.ts +35 -0
- package/dist/web/widgets/chart-data.js +37 -0
- package/dist/web/widgets/chart-markers.d.ts +26 -0
- package/dist/web/widgets/chart-markers.js +64 -0
- package/dist/web/widgets/chart-parts.d.ts +20 -0
- package/dist/web/widgets/chart-parts.js +15 -0
- package/dist/web/widgets/charts.d.ts +5 -78
- package/dist/web/widgets/charts.js +7 -485
- package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
- package/dist/web/widgets/drawer-panel-class.js +40 -0
- package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
- package/dist/web/widgets/drawer-resize-controls.js +12 -0
- package/dist/web/widgets/drawer-sizing.d.ts +19 -0
- package/dist/web/widgets/drawer-sizing.js +60 -0
- package/dist/web/widgets/drawer.js +17 -116
- package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
- package/dist/web/widgets/stacked-area-brush.js +91 -0
- package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
- package/dist/web/widgets/stacked-area-chart.js +45 -0
- package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
- package/dist/web/widgets/stacked-area-chrome.js +77 -0
- package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
- package/dist/web/widgets/stacked-area-geometry.js +83 -0
- package/dist/web/widgets/stacked-area-model.d.ts +11 -0
- package/dist/web/widgets/stacked-area-model.js +1 -0
- package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
- package/dist/web/widgets/stacked-area-plot.js +51 -0
- package/dist/web/widgets/stacked-area-props.d.ts +83 -0
- package/dist/web/widgets/stacked-area-props.js +16 -0
- package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
- package/dist/web/widgets/stacked-area-svg.js +38 -0
- package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
- package/dist/web/widgets/stacked-area-tooltip.js +86 -0
- package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
- package/dist/web/widgets/use-drawer-resize.js +52 -0
- package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
- package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
- package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
- package/dist/web/widgets/use-stacked-area-window.js +76 -0
- package/package.json +10 -10
- package/src/changes.json +54 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExtensionFormRegistryProvider } from "../app/extension-form-submit.js";
|
|
3
|
+
import { renderActionErrorBanner, renderAllRequiredHint, renderDeleteDialog, renderDraftPicker, renderExtensionErrorBanner, renderFormErrorBanner, renderRootIssueBanners, renderTabStrip, renderWizardProgress, } from "./render-edit-banners.js";
|
|
4
|
+
import { renderFormActions } from "./render-edit-form-actions.js";
|
|
5
|
+
import { buildHeaderActions } from "./render-edit-header-actions.js";
|
|
6
|
+
import { renderEditSections } from "./render-edit-section-kinds.js";
|
|
7
|
+
import { AllFieldsRequiredProvider } from "./render-field.js";
|
|
8
|
+
function renderPageHeader({ engine: { state }, layout, titles }, { headerMenuItems }) {
|
|
9
|
+
const { PageHeader, ActionOverflowMenu } = state.primitives;
|
|
10
|
+
const { pageHeaderCompact } = state;
|
|
11
|
+
return (layout.titleInShell &&
|
|
12
|
+
PageHeader !== undefined && (_jsx(PageHeader, { title: titles.formTitle, ...(titles.recordTitle !== undefined && { recordTitle: titles.recordTitle }), ...(headerMenuItems.length > 0 && pageHeaderCompact && { overflowItems: headerMenuItems }), ...(headerMenuItems.length > 0 &&
|
|
13
|
+
!pageHeaderCompact &&
|
|
14
|
+
ActionOverflowMenu !== undefined && {
|
|
15
|
+
actions: (_jsx(ActionOverflowMenu, { label: state.translate("kumiko.list.row-actions.more"), items: headerMenuItems, testId: "render-edit-header-menu" })),
|
|
16
|
+
}) })));
|
|
17
|
+
}
|
|
18
|
+
function renderWizardSideRail({ engine: { state, steps, sections, nav }, titles, }) {
|
|
19
|
+
const { StepBar } = state.primitives;
|
|
20
|
+
const { translate } = state;
|
|
21
|
+
const { upNextSection, formSubtitle } = titles;
|
|
22
|
+
if (StepBar === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
return (_jsx(StepBar, { steps: sections.filteredSections.map((section) => section.title ?? ""), currentIndex: steps.currentStep, compactLabel: titles.compactStepLabel, onStepSelect: nav.handleWizardStepSelect, ...steps.updateModeStepBarProps, orientation: "vertical", heading: translate("kumiko.wizard.step", {
|
|
25
|
+
current: steps.currentStep + 1,
|
|
26
|
+
total: steps.lastStepIndex + 1,
|
|
27
|
+
}), ...(formSubtitle !== undefined && { description: formSubtitle }), subtitles: sections.filteredSections.map((section) => section.kind === "fields" ? section.subtitle : undefined), ...(upNextSection !== undefined && {
|
|
28
|
+
upNext: {
|
|
29
|
+
heading: translate("kumiko.wizard.up-next"),
|
|
30
|
+
title: upNextSection.title ?? "",
|
|
31
|
+
...(upNextSection.kind === "fields" &&
|
|
32
|
+
upNextSection.subtitle !== undefined && { subtitle: upNextSection.subtitle }),
|
|
33
|
+
},
|
|
34
|
+
}), testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" }));
|
|
35
|
+
}
|
|
36
|
+
function buildFormHeadingProps(model) {
|
|
37
|
+
const { props } = model.engine;
|
|
38
|
+
const { layout, footer, titles } = model;
|
|
39
|
+
const { unsavedCount, hasDirtyFooter } = footer;
|
|
40
|
+
return {
|
|
41
|
+
...(props.hideSectionTitles !== true &&
|
|
42
|
+
!layout.titleInShell &&
|
|
43
|
+
!titles.titleInSettingsHeader &&
|
|
44
|
+
!titles.recordTitleInHeaderRegion && { title: titles.formTitle }),
|
|
45
|
+
...(titles.formSubtitle !== undefined &&
|
|
46
|
+
!titles.stepRail &&
|
|
47
|
+
!titles.titleInSettingsHeader && { subtitle: titles.formSubtitle }),
|
|
48
|
+
...(layout.isScreenForm && { screenForm: true }),
|
|
49
|
+
...(unsavedCount !== undefined && (unsavedCount > 0 || hasDirtyFooter) && { unsavedCount }),
|
|
50
|
+
...(titles.sectionNavItems.length > 0 && { sectionNav: titles.sectionNavItems }),
|
|
51
|
+
...(titles.stepRail && { sideRail: renderWizardSideRail(model) }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function buildFormActionProps(model, header) {
|
|
55
|
+
const { hideActions } = model.engine.props;
|
|
56
|
+
const { footer, layout } = model;
|
|
57
|
+
return {
|
|
58
|
+
...(hideActions !== true && footer.hasFormActions && { actions: renderFormActions(model) }),
|
|
59
|
+
...(hideActions !== true &&
|
|
60
|
+
layout.hasSecondaryFormActions && { secondaryActions: header.secondaryFormActions }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function buildFormChromeProps({ engine: { props, sections } }, { formHeaderRegion, titleActionMount }) {
|
|
64
|
+
const { summary } = props;
|
|
65
|
+
return {
|
|
66
|
+
...(props.screen.layout.width !== undefined && { width: props.screen.layout.width }),
|
|
67
|
+
...(formHeaderRegion !== undefined && { headerRegion: formHeaderRegion }),
|
|
68
|
+
...(titleActionMount !== undefined && { titleAction: titleActionMount }),
|
|
69
|
+
...(sections.fillHeight && { fillHeight: sections.fillHeight }),
|
|
70
|
+
...(props.hideSectionTitles === true && { chromeless: true }),
|
|
71
|
+
...(summary !== undefined && { summary }),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function renderEditLayout(model) {
|
|
75
|
+
const { props, state, sections, handleSubmit } = model.engine;
|
|
76
|
+
const header = buildHeaderActions(model);
|
|
77
|
+
const { Form } = state.primitives;
|
|
78
|
+
return (_jsxs(ExtensionFormRegistryProvider, { value: state.extensionFormRegistry, children: [state.menuActionConfirmDialog, renderPageHeader(model, header), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(state.focusFirstErrorKey > 0 && { focusFirstErrorKey: state.focusFirstErrorKey }), ...buildFormHeadingProps(model), ...buildFormActionProps(model, header), testId: "render-edit-form", stickyActions: state.isWizard || props.fillScreenHeight === true, ...buildFormChromeProps(model, header), children: _jsxs(AllFieldsRequiredProvider, { value: sections.showsAllRequiredHint, children: [props.leadContent, sections.showsAllRequiredHint && !state.insideDrawer && renderAllRequiredHint(model), renderDraftPicker(model), renderWizardProgress(model), state.isTabs && state.isStepped && renderTabStrip(model), renderEditSections(model), renderFormErrorBanner(model), renderRootIssueBanners(model), renderExtensionErrorBanner(model), renderActionErrorBanner(model), renderDeleteDialog(model)] }) })] }));
|
|
79
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { EditSectionViewModel, EditViewModel, FormValues } from "../headless/index.ts";
|
|
3
|
+
import type { FooterFlags, LayoutFlags } from "./render-edit-layout-flags.ts";
|
|
4
|
+
import type { TitleModel } from "./render-edit-titles.ts";
|
|
5
|
+
import type { RenderEditProps } from "./render-edit-types.ts";
|
|
6
|
+
import type { WizardNavigation } from "./render-edit-wizard-navigation.ts";
|
|
7
|
+
import type { RenderEditDraft } from "./use-render-edit-draft-save.ts";
|
|
8
|
+
import type { RenderEditForm } from "./use-render-edit-form.ts";
|
|
9
|
+
import type { RenderEditSections } from "./use-render-edit-sections.ts";
|
|
10
|
+
import type { RenderEditState } from "./use-render-edit-state.ts";
|
|
11
|
+
import type { RenderEditSteps } from "./use-render-edit-steps.ts";
|
|
12
|
+
import type { buildWizardDraftActions } from "./use-wizard-form-draft.ts";
|
|
13
|
+
type ResolvedKeys = "payloadMode" | "markChangedFields" | "disabled" | "viewMode" | "valueDisplay" | "submitBlocked" | "i18nScreenId";
|
|
14
|
+
export type ResolvedRenderEditProps<TValues extends FormValues, TCtx = unknown> = Omit<RenderEditProps<TValues, TCtx>, ResolvedKeys> & {
|
|
15
|
+
readonly payloadMode: "values" | "changes";
|
|
16
|
+
readonly markChangedFields: boolean;
|
|
17
|
+
readonly disabled: boolean;
|
|
18
|
+
readonly viewMode: boolean;
|
|
19
|
+
readonly valueDisplay: "form" | "text";
|
|
20
|
+
readonly submitBlocked: boolean;
|
|
21
|
+
readonly i18nScreenId: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function resolveRenderEditProps<TValues extends FormValues, TCtx>(props: RenderEditProps<TValues, TCtx>): ResolvedRenderEditProps<TValues, TCtx>;
|
|
24
|
+
export type RenderEditCore<TValues extends FormValues, TCtx = unknown> = {
|
|
25
|
+
readonly props: ResolvedRenderEditProps<TValues, TCtx>;
|
|
26
|
+
readonly state: RenderEditState;
|
|
27
|
+
readonly form: RenderEditForm<TValues>;
|
|
28
|
+
readonly draft: RenderEditDraft<TValues>;
|
|
29
|
+
};
|
|
30
|
+
export type RenderEditSectioned<TValues extends FormValues, TCtx = unknown> = RenderEditCore<TValues, TCtx> & {
|
|
31
|
+
readonly sections: RenderEditSections;
|
|
32
|
+
};
|
|
33
|
+
export type RenderEditSteppedCore<TValues extends FormValues, TCtx = unknown> = RenderEditSectioned<TValues, TCtx> & {
|
|
34
|
+
readonly steps: RenderEditSteps;
|
|
35
|
+
};
|
|
36
|
+
export type WizardDraftActions<TValues extends FormValues> = ReturnType<typeof buildWizardDraftActions<TValues>>;
|
|
37
|
+
export type RenderEditEngineBase<TValues extends FormValues, TCtx = unknown> = RenderEditSteppedCore<TValues, TCtx> & {
|
|
38
|
+
readonly draftActions: WizardDraftActions<TValues>;
|
|
39
|
+
readonly nav: WizardNavigation;
|
|
40
|
+
};
|
|
41
|
+
export type RenderEditEngine<TValues extends FormValues, TCtx = unknown> = RenderEditEngineBase<TValues, TCtx> & {
|
|
42
|
+
readonly handleSubmit: (options?: {
|
|
43
|
+
readonly saveNow?: boolean;
|
|
44
|
+
}) => Promise<void>;
|
|
45
|
+
};
|
|
46
|
+
export type { EditSectionViewModel, EditViewModel };
|
|
47
|
+
export type RenderEditModel<TValues extends FormValues, TCtx = unknown> = {
|
|
48
|
+
readonly engine: RenderEditEngine<TValues, TCtx>;
|
|
49
|
+
readonly layout: LayoutFlags;
|
|
50
|
+
readonly footer: FooterFlags;
|
|
51
|
+
readonly titles: TitleModel;
|
|
52
|
+
readonly setFieldUntyped: (k: string, v: unknown) => void;
|
|
53
|
+
readonly patchForExtension: (partial: Readonly<Record<string, unknown>>) => void;
|
|
54
|
+
};
|
|
55
|
+
export type SectionRenderContext<TValues extends FormValues, TCtx = unknown> = {
|
|
56
|
+
readonly model: RenderEditModel<TValues, TCtx>;
|
|
57
|
+
readonly sectionIndex: number;
|
|
58
|
+
readonly sectionActionsEl: ReactNode;
|
|
59
|
+
readonly wrapWizardStep: (key: string, el: ReactNode) => ReactNode;
|
|
60
|
+
};
|
|
61
|
+
export type FieldsSection = Extract<EditSectionViewModel, {
|
|
62
|
+
readonly kind: "fields";
|
|
63
|
+
}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// View mode is a locked form, so everything keyed on `disabled` follows.
|
|
2
|
+
export function resolveRenderEditProps(props) {
|
|
3
|
+
const viewMode = props.viewMode ?? false;
|
|
4
|
+
return {
|
|
5
|
+
...props,
|
|
6
|
+
payloadMode: props.payloadMode ?? "values",
|
|
7
|
+
markChangedFields: props.markChangedFields ?? false,
|
|
8
|
+
viewMode,
|
|
9
|
+
disabled: (props.disabled ?? false) || viewMode,
|
|
10
|
+
valueDisplay: props.valueDisplay ?? "form",
|
|
11
|
+
submitBlocked: props.submitBlocked ?? false,
|
|
12
|
+
i18nScreenId: props.i18nScreenId ?? props.screen.id,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { FormValues } from "../headless/index.ts";
|
|
3
|
+
import type { RenderEditModel } from "./render-edit-render-model.ts";
|
|
4
|
+
export declare function renderEditSections<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>): ReactNode;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ExtensionSectionMount } from "./edit-mounts.js";
|
|
3
|
+
import { RelatedListSection } from "./related-list-section.js";
|
|
4
|
+
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
5
|
+
import { renderFieldsSection } from "./render-edit-fields-section.js";
|
|
6
|
+
import { wizardStepKey } from "./render-edit-helpers.js";
|
|
7
|
+
import { resolveExtensionEntityId } from "./render-edit-logic.js";
|
|
8
|
+
import { WriteFormSection } from "./write-form-section.js";
|
|
9
|
+
function renderExtensionSection({ model, sectionIndex, sectionActionsEl, wrapWizardStep }, section) {
|
|
10
|
+
const { props, state, form, draft, sections } = model.engine;
|
|
11
|
+
const { hideSectionTitles } = props;
|
|
12
|
+
const { Card } = state.primitives;
|
|
13
|
+
const mount = (_jsx(ExtensionSectionMount, { section: section, entityName: sections.vm.entityName, entityId: resolveExtensionEntityId(props.entityId, sections.vm.id), initialValues: props.extensionInitialValues, values: form.snapshot.values, patch: model.patchForExtension, validate: draft.scopedValidate, markSaved: draft.markSaved, hideTitle: hideSectionTitles, ...(state.isWizard &&
|
|
14
|
+
!state.isCreateMode && {
|
|
15
|
+
stepKey: wizardStepKey(section, sectionIndex),
|
|
16
|
+
onReportStepComplete: state.reportExtensionStepComplete,
|
|
17
|
+
}), ...(hideSectionTitles !== true &&
|
|
18
|
+
sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title));
|
|
19
|
+
// Tabs mode: an unframed padded panel (the tab panel is the
|
|
20
|
+
// surface), so extension content neither sits flush at the shell
|
|
21
|
+
// edge nor touches the tab strip. testId stays only on the inner
|
|
22
|
+
// Section — giving the wrapper the same one would register two
|
|
23
|
+
// elements under one id.
|
|
24
|
+
const wrapped = hideSectionTitles !== true ? (mount) : (_jsx(Card, { options: { framed: false }, ...(sectionActionsEl !== undefined && {
|
|
25
|
+
slots: { headerActions: sectionActionsEl },
|
|
26
|
+
}), children: mount }, section.title));
|
|
27
|
+
return wrapWizardStep(section.title, wrapped);
|
|
28
|
+
}
|
|
29
|
+
function renderRelatedListSection({ model, sectionActionsEl }, section) {
|
|
30
|
+
const { props, form, sections } = model.engine;
|
|
31
|
+
const { hideSectionTitles, fillScreenHeight, refreshToken, onRelatedListDrawerAction } = props;
|
|
32
|
+
// parentId is the displayed record's id — without it there's no
|
|
33
|
+
// parent row whose related rows could be queried.
|
|
34
|
+
// Rejected at boot in wizard layouts, so no WizardStepGroup here.
|
|
35
|
+
const parentId = resolveExtensionEntityId(props.entityId, sections.vm.id);
|
|
36
|
+
if (parentId === null)
|
|
37
|
+
return null;
|
|
38
|
+
const mount = (_jsx(RelatedListSection, { section: section, parentId: parentId,
|
|
39
|
+
// @cast-boundary form-values: TValues is structurally a Record.
|
|
40
|
+
record: form.snapshot.values, featureName: props.featureName, translate: model.engine.state.translate, hideTitle: hideSectionTitles, ...(refreshToken !== undefined && { refreshToken }), ...(fillScreenHeight === true && { grow: true }), ...(hideSectionTitles !== true &&
|
|
41
|
+
sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(onRelatedListDrawerAction !== undefined && {
|
|
42
|
+
onOpenDrawer: onRelatedListDrawerAction,
|
|
43
|
+
}) }, section.title));
|
|
44
|
+
// Tabs mode: the table runs full-bleed under the tab strip (board
|
|
45
|
+
// layout); the toolbar buttons live in the list's own toolbar.
|
|
46
|
+
return hideSectionTitles === true ? (_jsx("div", { className: fillScreenHeight === true ? "flex min-h-0 flex-1 flex-col" : undefined, children: mount }, section.title)) : (mount);
|
|
47
|
+
}
|
|
48
|
+
function renderWriteFormSection({ model, sectionIndex, sectionActionsEl }, section) {
|
|
49
|
+
const { props, state, sections } = model.engine;
|
|
50
|
+
// Own submit button + dispatcher call, entirely independent of
|
|
51
|
+
// this screen's (nonexistent, on projectionDetail) form submit —
|
|
52
|
+
// rejected at boot in wizard layouts, so no WizardStepGroup here.
|
|
53
|
+
return (_jsx(WriteFormSection, { section: section, featureName: props.featureName, translate: state.translate, hideTitle: props.hideSectionTitles, onSubmitted: () => props.onReload?.(), ...(sections.isLoneWriteFormTab && { onFooterAction: sections.setWriteFormFooterAction }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }) }, section.title ?? `write-form-${sectionIndex}`));
|
|
54
|
+
}
|
|
55
|
+
function renderSectionByKind(rc, section) {
|
|
56
|
+
switch (section.kind) {
|
|
57
|
+
case "extension":
|
|
58
|
+
return renderExtensionSection(rc, section);
|
|
59
|
+
case "relatedList":
|
|
60
|
+
return renderRelatedListSection(rc, section);
|
|
61
|
+
case "writeForm":
|
|
62
|
+
return renderWriteFormSection(rc, section);
|
|
63
|
+
case "fields":
|
|
64
|
+
return renderFieldsSection(rc, section);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// turns this section's own `actions` (RowAction[]) into
|
|
68
|
+
// already-bound buttons via the caller-supplied builder, rendered
|
|
69
|
+
// through the same RenderEditActionButton the top-level `actions`
|
|
70
|
+
// prop uses (same style/confirm/icon/error handling).
|
|
71
|
+
function renderSectionActions({ engine: { props, state } }, section) {
|
|
72
|
+
const sectionActions = section.actions;
|
|
73
|
+
if (sectionActions === undefined || sectionActions.length === 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
const { Button, Dialog } = state.primitives;
|
|
76
|
+
return props
|
|
77
|
+
.buildSectionActions?.(sectionActions)
|
|
78
|
+
?.map((action) => (_jsx(RenderEditActionButton, { action: action, Button: Button, Dialog: Dialog, onError: state.setActionError }, action.id)));
|
|
79
|
+
}
|
|
80
|
+
// Wizard steps and tabs stay mounted while off-screen (native
|
|
81
|
+
// `hidden`, not unmounted) so an extension section's submit-registry
|
|
82
|
+
// entry (useExtensionFormSubmit → registry.remove on unmount)
|
|
83
|
+
// survives navigating past its step — otherwise Finish only ran the
|
|
84
|
+
// last mounted step's handler and silently dropped earlier steps'
|
|
85
|
+
// writes. For tabs this is also what makes ONE submit cover every
|
|
86
|
+
// tab, including tabs the user never opened.
|
|
87
|
+
// Off-screen wizard steps stay mounted (see comment above) but must
|
|
88
|
+
// not participate in native constraint validation, or the Next
|
|
89
|
+
// button's `type="submit"` triggers the browser's full-form check
|
|
90
|
+
// — including required fields on unvisited steps, which are not
|
|
91
|
+
// focusable while hidden, so the wizard silently stops navigating
|
|
92
|
+
// (only a console warning, no visible error). A plain `hidden`
|
|
93
|
+
// attribute does NOT bar descendants from constraint validation on
|
|
94
|
+
// web — see WizardStepGroupProps for what implementations must
|
|
95
|
+
// guarantee.
|
|
96
|
+
function createWizardStepWrapper({ engine: { props, state, steps } }, sectionIndex) {
|
|
97
|
+
const { WizardStepGroup } = state.primitives;
|
|
98
|
+
const stepHidden = state.isStepped && sectionIndex !== steps.currentStep;
|
|
99
|
+
return (key, el) => {
|
|
100
|
+
if (!state.isStepped)
|
|
101
|
+
return el;
|
|
102
|
+
if (WizardStepGroup === undefined) {
|
|
103
|
+
throw new Error(`RenderEdit: ${props.screen.layout.mode} layout requires primitives.WizardStepGroup, but none is registered.`);
|
|
104
|
+
}
|
|
105
|
+
return (_jsx(WizardStepGroup, { hidden: stepHidden, ...(props.hideSectionTitles === true && { inset: true }), children: el }, key));
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function renderEditSections(model) {
|
|
109
|
+
return model.engine.sections.filteredSections.map((section, sectionIndex) => renderSectionByKind({
|
|
110
|
+
model,
|
|
111
|
+
sectionIndex,
|
|
112
|
+
sectionActionsEl: renderSectionActions(model, section),
|
|
113
|
+
wrapWizardStep: createWizardStepWrapper(model, sectionIndex),
|
|
114
|
+
}, section));
|
|
115
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FormValues, SubmitResult } from "../headless/index.ts";
|
|
2
|
+
import type { RenderEditEngineBase } from "./render-edit-render-model.ts";
|
|
3
|
+
export declare function createSubmitResultHandlers<TValues extends FormValues, TCtx>({ props, state, form, draft, sections, draftActions, nav, }: RenderEditEngineBase<TValues, TCtx>): {
|
|
4
|
+
handleSubmitSuccess: (isNoOp: boolean) => Promise<boolean>;
|
|
5
|
+
applySubmitFailure: (result: Exclude<SubmitResult<unknown>, {
|
|
6
|
+
isSuccess: true;
|
|
7
|
+
}>) => void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { groupIssuesByPath } from "../headless/index.js";
|
|
2
|
+
import { hasIssueWithoutRenderedField } from "./render-edit-logic.js";
|
|
3
|
+
export function createSubmitResultHandlers({ props, state, form, draft, sections, draftActions, nav, }) {
|
|
4
|
+
const { controller } = form;
|
|
5
|
+
const { customSubmit } = props;
|
|
6
|
+
const { submittedRef } = draft.refs;
|
|
7
|
+
// `isNoOp: true` (payloadMode "changes", pre-filled form submitted
|
|
8
|
+
// untouched) means controller.submit() never called dispatcher.write
|
|
9
|
+
// — nothing to discard, and discarding here would delete a draft the
|
|
10
|
+
// form never persisted. Awaited, not fire-and-forget:
|
|
11
|
+
// `onSubmit` typically navigates away and unmounts this form, which
|
|
12
|
+
// would abort an in-flight discard and leave the draft behind after
|
|
13
|
+
// a successful submit.
|
|
14
|
+
// Resolves to whether the extension sections persisted (true when none ran).
|
|
15
|
+
async function handleSubmitSuccess(isNoOp) {
|
|
16
|
+
state.setFormError(null);
|
|
17
|
+
state.setExtraDeleteIssues([]);
|
|
18
|
+
if (!isNoOp) {
|
|
19
|
+
submittedRef.current = true;
|
|
20
|
+
await draftActions.discardDraft();
|
|
21
|
+
}
|
|
22
|
+
const extensionsPersisted = await nav.persistExtensions();
|
|
23
|
+
if (extensionsPersisted)
|
|
24
|
+
state.extensionDirtyRef.current = false;
|
|
25
|
+
return extensionsPersisted;
|
|
26
|
+
}
|
|
27
|
+
function jumpToFirstErroringStep(fieldPaths) {
|
|
28
|
+
// skip: unstepped layouts have no step to jump to
|
|
29
|
+
if (!state.isStepped)
|
|
30
|
+
return;
|
|
31
|
+
const step = nav.findFirstErroringStep(fieldPaths);
|
|
32
|
+
if (step !== undefined)
|
|
33
|
+
nav.jumpToStep(step);
|
|
34
|
+
}
|
|
35
|
+
// Root-level `.refine()`/cross-field issues from controller.validate()
|
|
36
|
+
// — same "jump to the erroring step" treatment as a server field
|
|
37
|
+
// error below, sourced from the freshly-validated snapshot.
|
|
38
|
+
function handleValidationBlocked() {
|
|
39
|
+
state.setFocusFirstErrorKey((key) => key + 1);
|
|
40
|
+
jumpToFirstErroringStep(Object.keys(controller.getSnapshot().errors));
|
|
41
|
+
}
|
|
42
|
+
function handleSubmitError(error) {
|
|
43
|
+
const fieldIssues = error.details?.fields ?? [];
|
|
44
|
+
const issuePaths = fieldIssues.map((i) => i.path);
|
|
45
|
+
if (fieldIssues.length > 0)
|
|
46
|
+
state.setFocusFirstErrorKey((key) => key + 1);
|
|
47
|
+
// controller.submit() writes server issues into the controller itself;
|
|
48
|
+
// customSubmit bypasses it, so push them here to reach the fields.
|
|
49
|
+
if (customSubmit !== undefined && fieldIssues.length > 0) {
|
|
50
|
+
controller.setErrors(groupIssuesByPath(fieldIssues));
|
|
51
|
+
}
|
|
52
|
+
// A server field error on a step the form isn't currently showing is
|
|
53
|
+
// otherwise invisible — jump to the first step that contains one of
|
|
54
|
+
// the errored fields. For tabs this is what keeps the submit from
|
|
55
|
+
// blocking in silence, the failure mode the boot-validator used to
|
|
56
|
+
// prevent by rejecting the layout outright. An issue no
|
|
57
|
+
// rendered field can show (`version`, `id`, a root-level refine, a
|
|
58
|
+
// hidden field) keeps the banner, jump or not.
|
|
59
|
+
jumpToFirstErroringStep(issuePaths);
|
|
60
|
+
state.setExtraDeleteIssues([]);
|
|
61
|
+
state.setFormErrorSource("submit");
|
|
62
|
+
state.setFormError(hasIssueWithoutRenderedField(issuePaths, sections.filteredSections, new Set(Object.keys(props.formDerivation?.derive ?? {})))
|
|
63
|
+
? error
|
|
64
|
+
: null);
|
|
65
|
+
}
|
|
66
|
+
// Form-level errors (without field-level details) go to the banner.
|
|
67
|
+
// Field errors flow via snapshot.errors into the individual fields.
|
|
68
|
+
// Synchronous on purpose: awaiting here would defer the state updates by a
|
|
69
|
+
// microtask and let React render intermediate states.
|
|
70
|
+
function applySubmitFailure(result) {
|
|
71
|
+
if (result.validationBlocked)
|
|
72
|
+
handleValidationBlocked();
|
|
73
|
+
else
|
|
74
|
+
handleSubmitError(result.error);
|
|
75
|
+
}
|
|
76
|
+
return { handleSubmitSuccess, applySubmitFailure };
|
|
77
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FormValues } from "../headless/index.ts";
|
|
2
|
+
import type { RenderEditEngineBase } from "./render-edit-render-model.ts";
|
|
3
|
+
type SubmitOptions = {
|
|
4
|
+
readonly saveNow?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function createHandleSubmit<TValues extends FormValues, TCtx>(engine: RenderEditEngineBase<TValues, TCtx>): (options?: SubmitOptions) => Promise<void>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { shouldNotifyCaller } from "./render-edit-logic.js";
|
|
2
|
+
import { createSubmitResultHandlers } from "./render-edit-submit-result.js";
|
|
3
|
+
// Rebuilt every render on purpose: the closures read this render's snapshot
|
|
4
|
+
// and extension state, while handleSubmitRef publishes the current one.
|
|
5
|
+
export function createHandleSubmit(engine) {
|
|
6
|
+
const { props, state, form, draft, steps, draftActions, nav } = engine;
|
|
7
|
+
const { handleSubmitSuccess, applySubmitFailure } = createSubmitResultHandlers(engine);
|
|
8
|
+
const { controller, snapshot } = form;
|
|
9
|
+
const { customSubmit } = props;
|
|
10
|
+
const { submittedRef, isSubmittingRef } = draft.refs;
|
|
11
|
+
// Same discard as the main path: an extension-only save is still a
|
|
12
|
+
// successful submit, so the draft must not survive it.
|
|
13
|
+
async function submitExtensionsOnly() {
|
|
14
|
+
// skip: an extension section failed, its error banner is showing
|
|
15
|
+
if (!(await nav.persistExtensions()))
|
|
16
|
+
return;
|
|
17
|
+
state.extensionDirtyRef.current = false;
|
|
18
|
+
submittedRef.current = true;
|
|
19
|
+
await draftActions.discardDraft();
|
|
20
|
+
}
|
|
21
|
+
// customSubmit path (e.g. configEdit, which performs a separate write per
|
|
22
|
+
// field). Validate on the client first, then call the caller; on success,
|
|
23
|
+
// explicitly rebase the form state because controller.submit() normally
|
|
24
|
+
// does this itself, and the controller otherwise knows nothing about a
|
|
25
|
+
// successful custom submission (isUnchanged would remain false).
|
|
26
|
+
//
|
|
27
|
+
// Read the snapshot directly from the controller rather than React state.
|
|
28
|
+
// With rapid fill-then-click input, React batching may not have committed
|
|
29
|
+
// state updates when the submit click fires. handleSubmit's closure could otherwise
|
|
30
|
+
// see stale snapshot.changes={} and perform no writes, then return success
|
|
31
|
+
// and rebase the form, making it appear that the data was saved.
|
|
32
|
+
// controller.getSnapshot() is always current: the controller is the
|
|
33
|
+
// source of truth, while React state is only a rendering mirror.
|
|
34
|
+
function blockedCustomSubmitResult() {
|
|
35
|
+
if (controller.validate(form.scopeFieldNames))
|
|
36
|
+
return undefined;
|
|
37
|
+
// Field order matters: validationBlocked:true is its own
|
|
38
|
+
// variant in the SubmitResult union (NOT mixed with data/error);
|
|
39
|
+
// TS only narrows cleanly without a discriminator fight.
|
|
40
|
+
const blocked = {
|
|
41
|
+
validationBlocked: true,
|
|
42
|
+
isSuccess: false,
|
|
43
|
+
};
|
|
44
|
+
return blocked;
|
|
45
|
+
}
|
|
46
|
+
// Enter in the active step triggers the native form submit (Next is
|
|
47
|
+
// type="submit" for Enter support) — on intermediate steps that means
|
|
48
|
+
// "Next", not "Save". Checked BEFORE the `disabled` guard below:
|
|
49
|
+
// `disabled` means "no input/no write", not "no navigation" — a
|
|
50
|
+
// disabled wizard must still be steppable (handleWizardNext itself
|
|
51
|
+
// blocks validate()/saveDraft() while disabled). `saveNow` is the explicit
|
|
52
|
+
// "Save and close" button and skips the step advance.
|
|
53
|
+
function advancesWizardInsteadOfSaving(options) {
|
|
54
|
+
return state.isWizard && !steps.isLastWizardStep && options?.saveNow !== true;
|
|
55
|
+
}
|
|
56
|
+
// Locked state: the submit button is visibly disabled, but a
|
|
57
|
+
// native form submit (Enter key) reaches this handler regardless of the
|
|
58
|
+
// button's disabled attribute — block it here too, not just in the UI.
|
|
59
|
+
// Sync guard for the in-flight part — React state `isSubmitting` is too late
|
|
60
|
+
// for double-clicks in the same tick (customSubmit has no submitInFlight of its own).
|
|
61
|
+
function isSubmitBlocked() {
|
|
62
|
+
return props.disabled === true || props.submitBlocked === true || isSubmittingRef.current;
|
|
63
|
+
}
|
|
64
|
+
// Awaits only where the original did: a validation-blocked customSubmit
|
|
65
|
+
// must settle in the same tick so setIsSubmitting(true/false) batch.
|
|
66
|
+
return async function handleSubmit(options) {
|
|
67
|
+
if (advancesWizardInsteadOfSaving(options)) {
|
|
68
|
+
nav.handleWizardNext();
|
|
69
|
+
// skip: handled as a wizard step advance
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// skip: locked, blocked or a submit is already in flight
|
|
73
|
+
if (isSubmitBlocked())
|
|
74
|
+
return;
|
|
75
|
+
isSubmittingRef.current = true;
|
|
76
|
+
state.setIsSubmitting(true);
|
|
77
|
+
state.setExtensionErrorKey(null);
|
|
78
|
+
try {
|
|
79
|
+
// Extension-only: only a section is dirty and the main form is unchanged.
|
|
80
|
+
// Skip the entity write (it would send an empty changes payload) and only
|
|
81
|
+
// run the section handlers.
|
|
82
|
+
if (snapshot.isUnchanged && state.extensionDirty) {
|
|
83
|
+
await submitExtensionsOnly();
|
|
84
|
+
// skip: extension-only save needs no entity write
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let result = customSubmit !== undefined ? blockedCustomSubmitResult() : undefined;
|
|
88
|
+
if (result === undefined && customSubmit !== undefined) {
|
|
89
|
+
result = await customSubmit(controller.getSnapshot());
|
|
90
|
+
if (result.isSuccess)
|
|
91
|
+
controller.rebase();
|
|
92
|
+
}
|
|
93
|
+
else if (result === undefined) {
|
|
94
|
+
result = await controller.submit();
|
|
95
|
+
}
|
|
96
|
+
let extensionsPersisted = true;
|
|
97
|
+
if (result.isSuccess)
|
|
98
|
+
extensionsPersisted = await handleSubmitSuccess(result.isNoOp === true);
|
|
99
|
+
else
|
|
100
|
+
applySubmitFailure(result);
|
|
101
|
+
// On entity-success-but-extension-failure the extension-error
|
|
102
|
+
// banner is showing — don't notify (the caller navigates away on success
|
|
103
|
+
// and would unmount it before the user sees the failure).
|
|
104
|
+
if (shouldNotifyCaller(result, extensionsPersisted)) {
|
|
105
|
+
props.onSubmit?.(result, controller.getSnapshot().values);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
isSubmittingRef.current = false;
|
|
110
|
+
state.setIsSubmitting(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EditSectionViewModel, FormValues } from "../headless/index.ts";
|
|
2
|
+
import type { FormSectionNavItem } from "../primitives.tsx";
|
|
3
|
+
import type { FooterFlags, LayoutFlags } from "./render-edit-layout-flags.ts";
|
|
4
|
+
import type { RenderEditEngine } from "./render-edit-render-model.ts";
|
|
5
|
+
export type TitleModel = {
|
|
6
|
+
readonly recordTitle: string | undefined;
|
|
7
|
+
readonly formTitle: string;
|
|
8
|
+
readonly formSubtitle: string | undefined;
|
|
9
|
+
readonly titleInSettingsHeader: boolean;
|
|
10
|
+
readonly usesFieldFlow: boolean;
|
|
11
|
+
readonly recordTitleInHeaderRegion: boolean;
|
|
12
|
+
readonly stepRail: boolean;
|
|
13
|
+
readonly upNextSection: EditSectionViewModel | undefined;
|
|
14
|
+
readonly sectionNavItems: readonly FormSectionNavItem[];
|
|
15
|
+
readonly compactStepLabel: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function deriveTitles<TValues extends FormValues, TCtx>(engine: RenderEditEngine<TValues, TCtx>, layout: LayoutFlags, footer: FooterFlags): TitleModel;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { formSectionDomId, resolveRecordTitle, resolveTitleTemplate, } from "./render-edit-helpers.js";
|
|
2
|
+
// Title + Subtitle, create/edit-aware. i18n-Keys (mode = "create"|"edit"):
|
|
3
|
+
// screen:<id>.<mode>.title / .<mode>.subtitle (mode = "create"|"edit"|"view")
|
|
4
|
+
// Fallback chain: mode-specific → generic (screen:<id>.title/.subtitle).
|
|
5
|
+
// title falls back to screenId; subtitle falls back to screen.description,
|
|
6
|
+
// itself run through translate() so an i18n key (the established
|
|
7
|
+
// convention for description, e.g. bundled-features) resolves instead of
|
|
8
|
+
// showing the raw key — translate() returns the input unchanged for plain
|
|
9
|
+
// prose, same as it does for an unknown key. Same slot
|
|
10
|
+
// section.description already fills for a section. Falls back to
|
|
11
|
+
// undefined (no subtitle) when neither is set.
|
|
12
|
+
function createScreenTextResolver(translate, i18nScreenId, formMode) {
|
|
13
|
+
return (suffix) => {
|
|
14
|
+
for (const key of [
|
|
15
|
+
`screen:${i18nScreenId}.${formMode}.${suffix}`,
|
|
16
|
+
`screen:${i18nScreenId}.${suffix}`,
|
|
17
|
+
]) {
|
|
18
|
+
const value = translate(key);
|
|
19
|
+
if (value !== key)
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// In tabs mode (hideSectionTitles) screen.description is dropped — the head
|
|
26
|
+
// card carries it — but an explicit subtitle i18n override has no other
|
|
27
|
+
// source there, so it stays unless it merely resolves to the description.
|
|
28
|
+
function resolveFormSubtitle(hideSectionTitles, explicitSubtitle, descriptionSubtitle) {
|
|
29
|
+
if (hideSectionTitles !== true)
|
|
30
|
+
return explicitSubtitle ?? descriptionSubtitle;
|
|
31
|
+
return explicitSubtitle !== descriptionSubtitle ? explicitSubtitle : undefined;
|
|
32
|
+
}
|
|
33
|
+
function sectionNavItemsFor(section, index, formTitle) {
|
|
34
|
+
const isNavigable = section.kind === "fields" &&
|
|
35
|
+
section.visible &&
|
|
36
|
+
section.title !== undefined &&
|
|
37
|
+
section.title !== formTitle;
|
|
38
|
+
return isNavigable && section.title !== undefined
|
|
39
|
+
? [{ id: formSectionDomId(index), title: section.title }]
|
|
40
|
+
: [];
|
|
41
|
+
}
|
|
42
|
+
function deriveSectionNavItems(filteredSections, formTitle) {
|
|
43
|
+
return filteredSections.flatMap((section, index) => sectionNavItemsFor(section, index, formTitle));
|
|
44
|
+
}
|
|
45
|
+
function deriveCompactStepLabel(translate, currentTitle, currentStep, lastStepIndex) {
|
|
46
|
+
return currentTitle !== undefined
|
|
47
|
+
? translate("kumiko.wizard.step-with-title", {
|
|
48
|
+
current: currentStep + 1,
|
|
49
|
+
total: lastStepIndex + 1,
|
|
50
|
+
title: currentTitle,
|
|
51
|
+
})
|
|
52
|
+
: translate("kumiko.wizard.step", { current: currentStep + 1, total: lastStepIndex + 1 });
|
|
53
|
+
}
|
|
54
|
+
function deriveFormTitleTexts({ props, state, form }, isCreate) {
|
|
55
|
+
const { screen, hideSectionTitles } = props;
|
|
56
|
+
const { translate } = state;
|
|
57
|
+
const formMode = isCreate ? "create" : props.viewMode ? "view" : "edit";
|
|
58
|
+
const recordTitle = !isCreate && screen.recordTitleField !== undefined
|
|
59
|
+
? resolveRecordTitle(props.initial[screen.recordTitleField])
|
|
60
|
+
: undefined;
|
|
61
|
+
const resolveScreenText = createScreenTextResolver(translate, props.i18nScreenId, formMode);
|
|
62
|
+
const plainFormTitle = resolveScreenText("title") ?? props.i18nScreenId;
|
|
63
|
+
const formTitle = resolveTitleTemplate(screen.titleTemplate, form.snapshot.values, translate, plainFormTitle);
|
|
64
|
+
const explicitSubtitle = resolveScreenText("subtitle");
|
|
65
|
+
const descriptionSubtitle = screen.description !== undefined ? translate(screen.description) : undefined;
|
|
66
|
+
const formSubtitle = resolveFormSubtitle(hideSectionTitles, explicitSubtitle, descriptionSubtitle);
|
|
67
|
+
return { recordTitle, formTitle, formSubtitle };
|
|
68
|
+
}
|
|
69
|
+
export function deriveTitles(engine, layout, footer) {
|
|
70
|
+
const { props, state, sections, steps } = engine;
|
|
71
|
+
const { screen, headerRegion } = props;
|
|
72
|
+
const { translate } = state;
|
|
73
|
+
const { filteredSections } = sections;
|
|
74
|
+
const { currentStep, lastStepIndex } = steps;
|
|
75
|
+
const { PageHeader, StepBar } = state.primitives;
|
|
76
|
+
const { recordTitle, formTitle, formSubtitle } = deriveFormTitleTexts(engine, footer.isCreate);
|
|
77
|
+
return {
|
|
78
|
+
recordTitle,
|
|
79
|
+
formTitle,
|
|
80
|
+
formSubtitle,
|
|
81
|
+
// Embedded settings-list forms (dashboard panels) have no shell header: the
|
|
82
|
+
// form title/subtitle move into the first section's header column instead of
|
|
83
|
+
// floating as a heading above the band.
|
|
84
|
+
titleInSettingsHeader: footer.isSettingsList && !layout.titleInShell,
|
|
85
|
+
usesFieldFlow: layout.isScreenForm || state.insideDrawer,
|
|
86
|
+
// The caller's headerRegion already shows the record title in the shell page
|
|
87
|
+
// header, so the type title ("Release") above the form would repeat it.
|
|
88
|
+
recordTitleInHeaderRegion: headerRegion !== undefined &&
|
|
89
|
+
recordTitle !== undefined &&
|
|
90
|
+
PageHeader !== undefined &&
|
|
91
|
+
state.pageHeaderSlotAvailable,
|
|
92
|
+
stepRail: state.isWizard && layout.isScreenForm && StepBar !== undefined,
|
|
93
|
+
upNextSection: screen.layout.wizard?.aside?.upNext === true ? filteredSections[currentStep + 1] : undefined,
|
|
94
|
+
sectionNavItems: layout.isScreenForm && !state.isStepped
|
|
95
|
+
? deriveSectionNavItems(filteredSections, formTitle)
|
|
96
|
+
: [],
|
|
97
|
+
compactStepLabel: deriveCompactStepLabel(translate, filteredSections[currentStep]?.title, currentStep, lastStepIndex),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FormValues } from "../headless/index.ts";
|
|
2
|
+
import type { RenderEditSteppedCore } from "./render-edit-render-model.ts";
|
|
3
|
+
export type WizardNavigation = {
|
|
4
|
+
readonly persistExtensions: () => Promise<boolean>;
|
|
5
|
+
readonly jumpToStep: (index: number) => void;
|
|
6
|
+
readonly handleWizardNext: () => void;
|
|
7
|
+
readonly handleWizardBack: () => void;
|
|
8
|
+
readonly handleWizardStepSelect: (targetStep: number) => void;
|
|
9
|
+
readonly findFirstErroringStep: (fieldPaths: readonly string[]) => number | undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare function createWizardNavigation<TValues extends FormValues, TCtx>({ props, state, form, draft, sections, steps }: RenderEditSteppedCore<TValues, TCtx>, saveDraft: (stepIndex: number) => void): WizardNavigation;
|