@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,90 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ErrorReference } from "../app/error-reference.js";
|
|
3
|
+
import { formatWhen } from "../format-when.js";
|
|
4
|
+
import { tabIdAt } from "./render-edit-helpers.js";
|
|
5
|
+
import { isDispatcherRejection } from "./render-edit-logic.js";
|
|
6
|
+
export function renderAllRequiredHint({ engine: { state }, }) {
|
|
7
|
+
const { Text } = state.primitives;
|
|
8
|
+
return (_jsx(Text, { variant: "muted", testId: "render-edit-all-required-hint", children: state.translate("kumiko.form.all-required") }));
|
|
9
|
+
}
|
|
10
|
+
export function renderDraftPicker({ engine: { state, draftActions }, }) {
|
|
11
|
+
const { Banner, Button, Text } = state.primitives;
|
|
12
|
+
const { translate } = state;
|
|
13
|
+
const { draftCandidates, setDraftCandidates } = state.draftState;
|
|
14
|
+
return (draftCandidates !== null && (_jsx(Banner, { variant: "info", testId: "render-edit-draft-picker", actions: [
|
|
15
|
+
...draftCandidates.map((candidate) => (_jsx(Button, { type: "button", variant: "link", onClick: () => draftActions.adoptDraft(candidate), testId: `render-edit-draft-pick-${candidate.id}`, children: formatWhen(candidate.savedAt) }, candidate.id))),
|
|
16
|
+
_jsx(Button, { type: "button", variant: "link", onClick: () => setDraftCandidates(null), testId: "render-edit-draft-start-new", children: translate("kumiko.form.draft.start-new") }, "start-new"),
|
|
17
|
+
], children: _jsx(Text, { children: translate(draftCandidates.length === 1
|
|
18
|
+
? "kumiko.form.draft.resume-single"
|
|
19
|
+
: "kumiko.form.draft.resume-multiple") }) })));
|
|
20
|
+
}
|
|
21
|
+
export function renderWizardProgress({ engine: { state, steps, sections, nav }, titles, }) {
|
|
22
|
+
const { Progress, StepBar, Text } = state.primitives;
|
|
23
|
+
const { currentStep, lastStepIndex, updateModeStepBarProps } = steps;
|
|
24
|
+
return (state.isWizard &&
|
|
25
|
+
!titles.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: titles.compactStepLabel })) : (_jsx(StepBar, { steps: sections.filteredSections.map((section) => section.title ?? ""), currentIndex: currentStep, compactLabel: titles.compactStepLabel, onStepSelect: nav.handleWizardStepSelect, ...updateModeStepBarProps, testId: "render-edit-wizard-steps", compactTestId: "render-edit-wizard-step-label" }))] })));
|
|
26
|
+
}
|
|
27
|
+
export function renderTabStrip({ engine: { state, steps, sections, nav }, }) {
|
|
28
|
+
const { Tabs } = state.primitives;
|
|
29
|
+
const { filteredSections } = sections;
|
|
30
|
+
// No Tabs primitive registered → every section renders stacked
|
|
31
|
+
// (stepHidden below is gated on the same condition), which is the
|
|
32
|
+
// single-layout behaviour. Degrading to one long form beats
|
|
33
|
+
// hiding sections behind a strip that never drew.
|
|
34
|
+
if (Tabs === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
return (_jsx(Tabs, { testId: "render-edit-tabs", items: filteredSections.map((section, index) => ({
|
|
37
|
+
id: tabIdAt(section, index),
|
|
38
|
+
label: section.title ?? "",
|
|
39
|
+
})), activeId: tabIdAt(filteredSections[steps.currentStep], steps.currentStep), onSelect: (id) => {
|
|
40
|
+
const index = filteredSections.findIndex((s, i) => tabIdAt(s, i) === id);
|
|
41
|
+
// skip: the strip reported an id no section owns.
|
|
42
|
+
if (index === -1)
|
|
43
|
+
return;
|
|
44
|
+
nav.jumpToStep(index);
|
|
45
|
+
} }));
|
|
46
|
+
}
|
|
47
|
+
export function renderFormErrorBanner({ engine: { props, state }, layout, }) {
|
|
48
|
+
const { Banner, Button, Text } = state.primitives;
|
|
49
|
+
const { translate, formError } = state;
|
|
50
|
+
const { onReload } = props;
|
|
51
|
+
return (formError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
|
|
52
|
+
onReload();
|
|
53
|
+
state.setFormError(null);
|
|
54
|
+
state.setExtraDeleteIssues([]);
|
|
55
|
+
}, 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) }), layout.rootIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-root", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`))), state.extraDeleteIssues.map((issue) => (_jsx(Text, { testId: "render-edit-form-error-extra", children: translate(issue.i18nKey, issue.params) }, `${issue.path}:${issue.i18nKey}`)))] })));
|
|
56
|
+
}
|
|
57
|
+
export function renderRootIssueBanners({ engine: { state }, layout, }) {
|
|
58
|
+
const { Banner, Text } = state.primitives;
|
|
59
|
+
return (state.formError === null &&
|
|
60
|
+
layout.rootIssues.map((issue) => (_jsx(Banner, { variant: "error", testId: "render-edit-root-issue", children: _jsx(Text, { children: state.translate(issue.i18nKey, issue.params) }) }, issue.i18nKey))));
|
|
61
|
+
}
|
|
62
|
+
export function renderExtensionErrorBanner({ engine: { state }, }) {
|
|
63
|
+
const { Banner, Text } = state.primitives;
|
|
64
|
+
const { extensionErrorKey } = state;
|
|
65
|
+
return (extensionErrorKey !== null && (_jsx(Banner, { variant: "error", testId: "render-edit-extension-error", children: _jsx(Text, { testId: "render-edit-extension-error-key", children: state.translate(extensionErrorKey) }) })));
|
|
66
|
+
}
|
|
67
|
+
export function renderActionErrorBanner({ engine: { state }, }) {
|
|
68
|
+
const { Banner } = state.primitives;
|
|
69
|
+
const { actionError } = state;
|
|
70
|
+
return (actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })));
|
|
71
|
+
}
|
|
72
|
+
// A rejected delete (e.g. a preDelete hook) carries its reason in
|
|
73
|
+
// the field issues; the top-level key is only "validation failed".
|
|
74
|
+
function applyDeleteRejection(state, rejection) {
|
|
75
|
+
const [issue, ...otherIssues] = rejection.details?.fields ?? [];
|
|
76
|
+
state.setExtraDeleteIssues(otherIssues);
|
|
77
|
+
state.setFormErrorSource("delete");
|
|
78
|
+
state.setFormError(issue === undefined
|
|
79
|
+
? rejection
|
|
80
|
+
: { ...rejection, i18nKey: issue.i18nKey, i18nParams: issue.params });
|
|
81
|
+
}
|
|
82
|
+
export function renderDeleteDialog({ engine: { props, state }, }) {
|
|
83
|
+
const { Dialog } = state.primitives;
|
|
84
|
+
const { onDelete } = props;
|
|
85
|
+
return (onDelete !== undefined && (_jsx(Dialog, { open: state.confirmDeleteOpen, onOpenChange: state.setConfirmDeleteOpen, title: state.translate("kumiko.actions.delete-confirm"), confirmLabel: state.translate("kumiko.actions.delete"), variant: "danger", onConfirm: async () => {
|
|
86
|
+
const rejection = await onDelete();
|
|
87
|
+
if (isDispatcherRejection(rejection))
|
|
88
|
+
applyDeleteRejection(state, rejection);
|
|
89
|
+
}, testId: "render-edit-delete-dialog" })));
|
|
90
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MutableRefObject } from "react";
|
|
2
|
+
import type { FormValues } from "../headless/index.ts";
|
|
3
|
+
import type { ResolvedRenderEditProps } from "./render-edit-render-model.ts";
|
|
4
|
+
import type { RenderEditForm } from "./use-render-edit-form.ts";
|
|
5
|
+
type ControlsOf<TValues extends FormValues> = Parameters<NonNullable<ResolvedRenderEditProps<TValues>["onControlsReady"]>>[0];
|
|
6
|
+
type ChangeStateOf<TValues extends FormValues> = Parameters<NonNullable<ResolvedRenderEditProps<TValues>["onChange"]>>[0];
|
|
7
|
+
export declare function flushPendingDraftSave(draftSaveTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>, saveDraftRef: MutableRefObject<(stepIndex: number) => void>, currentStepRef: MutableRefObject<number>): void;
|
|
8
|
+
export declare function buildControlsFor<TValues extends FormValues>({ controller, patch, validate, handleSubmitRef, wizardNextRef, wizardBackRef, }: {
|
|
9
|
+
readonly controller: RenderEditForm<TValues>["controller"];
|
|
10
|
+
readonly patch: ControlsOf<TValues>["patch"];
|
|
11
|
+
readonly validate: ControlsOf<TValues>["validate"];
|
|
12
|
+
readonly handleSubmitRef: MutableRefObject<(options?: {
|
|
13
|
+
readonly saveNow?: boolean;
|
|
14
|
+
}) => Promise<void>>;
|
|
15
|
+
readonly wizardNextRef: MutableRefObject<() => void>;
|
|
16
|
+
readonly wizardBackRef: MutableRefObject<() => void>;
|
|
17
|
+
}): ControlsOf<TValues>;
|
|
18
|
+
export declare function deriveChangeState<TValues extends FormValues>(snapshot: RenderEditForm<TValues>["snapshot"], schema: ResolvedRenderEditProps<TValues>["schema"], scopeFieldNames: readonly string[] | undefined, submitting: boolean): ChangeStateOf<TValues>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { dryRunFieldIssues } from "./render-edit-helpers.js";
|
|
2
|
+
export function flushPendingDraftSave(draftSaveTimerRef, saveDraftRef, currentStepRef) {
|
|
3
|
+
if (draftSaveTimerRef.current !== null) {
|
|
4
|
+
clearTimeout(draftSaveTimerRef.current);
|
|
5
|
+
// Flush the pending debounced save instead of dropping it — a
|
|
6
|
+
// navigation/unmount inside the debounce window would otherwise
|
|
7
|
+
// silently discard the last patch() (discardDraft nulls this ref
|
|
8
|
+
// first on the post-submit path, so this can't resurrect a
|
|
9
|
+
// just-submitted draft).
|
|
10
|
+
saveDraftRef.current(currentStepRef.current);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function buildControlsFor({ controller, patch, validate, handleSubmitRef, wizardNextRef, wizardBackRef, }) {
|
|
14
|
+
return {
|
|
15
|
+
patch,
|
|
16
|
+
validate,
|
|
17
|
+
getValues: () => controller.getSnapshot().values,
|
|
18
|
+
// saveNow: a host-driven submit must save on a wizard's intermediate
|
|
19
|
+
// steps too instead of silently advancing; stepping is next()/back().
|
|
20
|
+
submit: () => handleSubmitRef.current({ saveNow: true }),
|
|
21
|
+
next: () => wizardNextRef.current(),
|
|
22
|
+
back: () => wizardBackRef.current(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function deriveChangeState(snapshot, schema, scopeFieldNames, submitting) {
|
|
26
|
+
// Dry-run parse against `schema` — NOT controller.validate(). Calling
|
|
27
|
+
// validate() here would write field-level errors into snapshot.errors
|
|
28
|
+
// on every keystroke, painting error messages while the user is still
|
|
29
|
+
// typing. `valid` can therefore legitimately diverge from what's
|
|
30
|
+
// currently rendered under the fields (the last *mutating* validate()
|
|
31
|
+
// call, e.g. from controls.validate() or submit()). The issues are
|
|
32
|
+
// filtered exactly like submit()'s validate: hidden fields and fields
|
|
33
|
+
// outside the `fields` scope never make the form invalid.
|
|
34
|
+
const valid = schema === undefined || dryRunFieldIssues(schema, snapshot, scopeFieldNames, true).length === 0;
|
|
35
|
+
return {
|
|
36
|
+
values: snapshot.values,
|
|
37
|
+
changes: snapshot.changes,
|
|
38
|
+
dirty: snapshot.isDirty,
|
|
39
|
+
valid,
|
|
40
|
+
submitting,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { EditFieldViewModel, FormValues } from "../headless/index.ts";
|
|
3
|
+
import type { FieldsSection, RenderEditModel } from "./render-edit-render-model.ts";
|
|
4
|
+
export declare function renderFieldGrid<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>, section: FieldsSection, fields: readonly EditFieldViewModel[], columns: number, gridKey: string, columnsDeclared: boolean | undefined): ReactNode;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { GridCellForField } from "./grid-cell-for-field.js";
|
|
3
|
+
function appendixCellProps(props, fieldName) {
|
|
4
|
+
const { labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription } = props;
|
|
5
|
+
return {
|
|
6
|
+
...(labelAppendix !== undefined && { labelAppendix: labelAppendix(fieldName) }),
|
|
7
|
+
...(fieldAppendix !== undefined && { fieldAppendix: fieldAppendix(fieldName) }),
|
|
8
|
+
...(fieldAppendixPlacement !== undefined && { appendixPlacement: fieldAppendixPlacement }),
|
|
9
|
+
...(fieldDescription !== undefined && { description: fieldDescription(fieldName) }),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function createFieldCellRenderer(model, section, columns) {
|
|
13
|
+
const { props, state, form } = model.engine;
|
|
14
|
+
const { snapshot } = form;
|
|
15
|
+
const { GridCell } = state.primitives;
|
|
16
|
+
const { isSettingsList, isCreate } = model.footer;
|
|
17
|
+
const { usesFieldFlow } = model.titles;
|
|
18
|
+
const { disabled, fieldAccent } = props;
|
|
19
|
+
const showsChangedMarker = !isCreate || props.markChangedFields;
|
|
20
|
+
const clearErrorsAndValidate = (fieldName) => {
|
|
21
|
+
form.clearErrorsOnInput(fieldName);
|
|
22
|
+
if (props.validateOnChange === true)
|
|
23
|
+
form.controller.validate([fieldName]);
|
|
24
|
+
};
|
|
25
|
+
return (field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(!isSettingsList && usesFieldFlow && { flow: true }), ...(isSettingsList && {
|
|
26
|
+
layout: "row",
|
|
27
|
+
accent: fieldAccent?.(field.field) === true,
|
|
28
|
+
}), issues: snapshot.errors[field.field], onChange: (v) => {
|
|
29
|
+
model.setFieldUntyped(field.field, v);
|
|
30
|
+
clearErrorsAndValidate(field.field);
|
|
31
|
+
}, GridCell: GridCell, featureName: props.featureName, ...appendixCellProps(props, field.field), allIssues: snapshot.errors, valueDisplay: props.valueDisplay, row: snapshot.values, ...(disabled && { fillDisabled: true }), ...(props.hideSectionTitles !== true &&
|
|
32
|
+
section.title !== undefined && { sectionTitle: section.title }), ...(showsChangedMarker && field.field in snapshot.changes && { changed: true }) }, field.field));
|
|
33
|
+
}
|
|
34
|
+
export function renderFieldGrid(model, section, fields, columns, gridKey, columnsDeclared) {
|
|
35
|
+
const { Grid } = model.engine.state.primitives;
|
|
36
|
+
const { isSettingsList } = model.footer;
|
|
37
|
+
const { usesFieldFlow } = model.titles;
|
|
38
|
+
const renderCell = createFieldCellRenderer(model, section, columns);
|
|
39
|
+
return (_jsx(Grid, { columns: usesFieldFlow && columnsDeclared !== true ? 1 : columns, ...(isSettingsList ? { list: true } : usesFieldFlow && { flow: true }), children: fields.map((field) => renderCell(field)) }, gridKey));
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { FormValues } from "../headless/index.ts";
|
|
3
|
+
import type { FieldsSection, SectionRenderContext } from "./render-edit-render-model.ts";
|
|
4
|
+
export declare function renderFieldsSection<TValues extends FormValues, TCtx>(rc: SectionRenderContext<TValues, TCtx>, section: FieldsSection): ReactNode;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { renderAllRequiredHint } from "./render-edit-banners.js";
|
|
3
|
+
import { renderFieldGrid } from "./render-edit-field-grid.js";
|
|
4
|
+
import { formSectionDomId } from "./render-edit-helpers.js";
|
|
5
|
+
function renderTabbedGroupsSection(rc, section, sectionKey) {
|
|
6
|
+
const { Grid, Card } = rc.model.engine.state.primitives;
|
|
7
|
+
const groupsGrid = (_jsx(Grid, { columns: 1, children: section.groups?.map((group, groupIndex) => (_jsx(Card, { slots: { title: group.title }, testId: `section-${sectionKey}-group-${groupIndex}`, children: renderFieldGrid(rc.model, section, group.fields, group.columns, `${sectionKey}-group-${groupIndex}-grid`, group.columnsDeclared) }, group.title))) }, `${sectionKey}-groups`));
|
|
8
|
+
// `actions` on a `groups` section has no group of its own to
|
|
9
|
+
// sit in and would need an extra title-less Card around the
|
|
10
|
+
// group cards (Card-in-Card) — the boot-validator rejects that
|
|
11
|
+
// combination in tabs mode, so `sectionActionsEl` is always
|
|
12
|
+
// undefined here at runtime.
|
|
13
|
+
return rc.wrapWizardStep(sectionKey, _jsx(Card, { options: { framed: false }, children: groupsGrid }, sectionKey));
|
|
14
|
+
}
|
|
15
|
+
function renderTabbedFieldsSection(rc, section, sectionKey) {
|
|
16
|
+
if (section.groups !== undefined)
|
|
17
|
+
return renderTabbedGroupsSection(rc, section, sectionKey);
|
|
18
|
+
const { Card } = rc.model.engine.state.primitives;
|
|
19
|
+
const { sectionActionsEl } = rc;
|
|
20
|
+
// No section title here — the Tab strip right above
|
|
21
|
+
// already names this panel, so a repeated Card title would just
|
|
22
|
+
// duplicate it. The header row only exists to carry the section
|
|
23
|
+
// description and actions, never the title text.
|
|
24
|
+
const cardEl = (_jsx(Card, { options: { framed: false }, ...((sectionActionsEl !== undefined || section.description !== undefined) && {
|
|
25
|
+
slots: {
|
|
26
|
+
...(section.description !== undefined && { subtitle: section.description }),
|
|
27
|
+
...(sectionActionsEl !== undefined && { headerActions: sectionActionsEl }),
|
|
28
|
+
},
|
|
29
|
+
}), testId: `section-${sectionKey}`, children: renderFieldGrid(rc.model, section, section.fields, section.columns, `${sectionKey}-grid`, section.columnsDeclared) }, sectionKey));
|
|
30
|
+
return rc.wrapWizardStep(sectionKey, cardEl);
|
|
31
|
+
}
|
|
32
|
+
// Suppress the section header when it would just repeat the form
|
|
33
|
+
// title verbatim (typical for single-section actionForms), or the
|
|
34
|
+
// tab label right above it. Without a Tabs primitive the sections
|
|
35
|
+
// render stacked, and then the titles are what tells them apart.
|
|
36
|
+
function resolveSectionTitle({ model, sectionIndex }, section) {
|
|
37
|
+
const { titles } = model;
|
|
38
|
+
const { state } = model.engine;
|
|
39
|
+
const repeatsTabLabel = state.isTabs && state.isStepped;
|
|
40
|
+
if (titles.titleInSettingsHeader && sectionIndex === 0)
|
|
41
|
+
return section.title ?? titles.formTitle;
|
|
42
|
+
return section.title === titles.formTitle || repeatsTabLabel ? undefined : section.title;
|
|
43
|
+
}
|
|
44
|
+
function resolveSectionSubtitle({ model, sectionIndex }, section) {
|
|
45
|
+
const headerFallback = model.titles.titleInSettingsHeader && sectionIndex === 0;
|
|
46
|
+
return section.description ?? (headerFallback ? model.titles.formSubtitle : undefined);
|
|
47
|
+
}
|
|
48
|
+
function renderDefaultFieldsSection(rc, section, sectionKey) {
|
|
49
|
+
const { model, sectionIndex, sectionActionsEl } = rc;
|
|
50
|
+
const { Section } = model.engine.state.primitives;
|
|
51
|
+
const sectionTitle = resolveSectionTitle(rc, section);
|
|
52
|
+
const sectionSubtitle = resolveSectionSubtitle(rc, section);
|
|
53
|
+
const sectionEl = (_jsxs(Section, { ...(model.titles.sectionNavItems.some((item) => item.id === formSectionDomId(sectionIndex)) && {
|
|
54
|
+
id: formSectionDomId(sectionIndex),
|
|
55
|
+
}), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(sectionSubtitle !== undefined && { subtitle: sectionSubtitle }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(model.footer.isSettingsList && { layout: "settings-list" }), testId: `section-${sectionKey}`, children: [model.engine.sections.showsAllRequiredHint &&
|
|
56
|
+
model.engine.state.insideDrawer &&
|
|
57
|
+
sectionIndex === 0 &&
|
|
58
|
+
renderAllRequiredHint(model), renderFieldGrid(model, section, section.fields, section.columns, `${sectionKey}-grid`, section.columnsDeclared)] }, sectionKey));
|
|
59
|
+
return rc.wrapWizardStep(sectionKey, sectionEl);
|
|
60
|
+
}
|
|
61
|
+
export function renderFieldsSection(rc, section) {
|
|
62
|
+
if (!section.visible)
|
|
63
|
+
return null;
|
|
64
|
+
// Title-less sections would otherwise collide on key/testId, so use the index
|
|
65
|
+
// as a fallback.
|
|
66
|
+
const sectionKey = section.title ?? `section-${rc.sectionIndex}`;
|
|
67
|
+
// Tabs mode: the tab panel is the surface, so field sections render
|
|
68
|
+
// as unframed padded panels; group cards inside stay framed.
|
|
69
|
+
// Stays `Card`, not `Section` — Section always flattens to a
|
|
70
|
+
// borderless divider when rendered inside this component's own
|
|
71
|
+
// `<Form>` (InsideFormContext), even with `chromeless` set, so it
|
|
72
|
+
// cannot stand in for the tabs-mode card here.
|
|
73
|
+
return rc.model.engine.props.hideSectionTitles === true
|
|
74
|
+
? renderTabbedFieldsSection(rc, section, sectionKey)
|
|
75
|
+
: renderDefaultFieldsSection(rc, section, sectionKey);
|
|
76
|
+
}
|
|
@@ -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 renderFormActions<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>): ReactNode;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, STICKY_PRIMARY_ACTION_PROP, } from "../primitives.js";
|
|
3
|
+
import { EditSlotMount } from "./edit-mounts.js";
|
|
4
|
+
import { resolveExtensionEntityId } from "./render-edit-logic.js";
|
|
5
|
+
function renderWizardBackButton({ engine: { state, nav }, footer, }) {
|
|
6
|
+
const { Button } = state.primitives;
|
|
7
|
+
return (footer.showWizardBack && (_jsx(Button, { type: "button", variant: "secondary", icon: "chevron-left", onClick: nav.handleWizardBack, testId: "render-edit-wizard-back", [FOOTER_ACTION_ROLE_PROP]: "back", children: state.translate("kumiko.actions.back") })));
|
|
8
|
+
}
|
|
9
|
+
function renderFooterSlot({ engine: { props, state, form, draft, sections, steps }, footer, }) {
|
|
10
|
+
const footerSlot = props.screen.slots?.footer;
|
|
11
|
+
return (footerSlot !== undefined && (_jsx(EditSlotMount, { slot: footerSlot, slotName: "footer", screenId: props.screen.id, entityName: sections.vm.entityName, entityId: resolveExtensionEntityId(props.entityId, sections.vm.id), values: form.snapshot.values, markSaved: draft.markSaved, hasUnsavedChanges: footer.hasUnsavedInput, wizardStep: state.isWizard ? { index: steps.currentStep, isLast: steps.isLastWizardStep } : undefined, ...(props.screen.slots?.footerPrimary === true && { [STICKY_PRIMARY_ACTION_PROP]: true }) })));
|
|
12
|
+
}
|
|
13
|
+
function renderSaveAndCloseButton({ engine: { props, state, steps, handleSubmit }, footer, }) {
|
|
14
|
+
const { Button } = state.primitives;
|
|
15
|
+
return (state.isWizard &&
|
|
16
|
+
!steps.isLastWizardStep &&
|
|
17
|
+
footer.showsSubmit &&
|
|
18
|
+
!state.isCreateMode && (_jsx(Button, { type: "button", variant: "ghost", disabled: state.isSubmitting || props.disabled, onClick: () => void handleSubmit({ saveNow: true }), testId: "render-edit-wizard-save-close", [STICKY_PRIMARY_ACTION_PROP]: true, children: state.translate("kumiko.wizard.save-and-close") })));
|
|
19
|
+
}
|
|
20
|
+
function renderWizardNextButton({ engine: { state }, footer, }) {
|
|
21
|
+
const { Button } = state.primitives;
|
|
22
|
+
const { translate } = state;
|
|
23
|
+
return (footer.showWizardNext && (_jsx(Button, { type: "submit", variant: "primary", iconEnd: "chevron-right", testId: "render-edit-wizard-next", [FOOTER_ACTION_ROLE_PROP]: "primary",
|
|
24
|
+
[NARROW_LABEL_PROP]: translate("kumiko.actions.next"), children: footer.nextStepTitle !== undefined
|
|
25
|
+
? translate("kumiko.wizard.next-with-title", { title: footer.nextStepTitle })
|
|
26
|
+
: translate("kumiko.actions.next") })));
|
|
27
|
+
}
|
|
28
|
+
function renderFooterActionButtons({ engine: { props, state, form, handleSubmit }, footer, }) {
|
|
29
|
+
const { Button } = state.primitives;
|
|
30
|
+
return (footer.showSubmit &&
|
|
31
|
+
props.footerActions?.map((action) => (_jsx(Button, { type: "button", variant: action.variant ?? "secondary", ...(action.icon !== undefined && { icon: action.icon }), disabled: state.isSubmitting || props.disabled, onClick: () => {
|
|
32
|
+
form.controller.setValues(action.patch); // @cast-boundary form-values
|
|
33
|
+
void handleSubmit();
|
|
34
|
+
}, testId: `render-edit-footer-action-${action.id}`, children: state.translate(action.label) }, action.id))));
|
|
35
|
+
}
|
|
36
|
+
function renderDiscardButton({ engine: { props, state, form }, footer, }) {
|
|
37
|
+
const { Button } = state.primitives;
|
|
38
|
+
const { unsavedCount } = footer;
|
|
39
|
+
return (footer.hasDirtyFooter &&
|
|
40
|
+
footer.showsSubmit &&
|
|
41
|
+
unsavedCount !== undefined &&
|
|
42
|
+
unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: state.isSubmitting || props.disabled, onClick: () => form.controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: state.translate("kumiko.form.discard") })));
|
|
43
|
+
}
|
|
44
|
+
function isSubmitDisabled({ engine: { props, state, form }, footer, }) {
|
|
45
|
+
const isPristine = form.snapshot.isUnchanged &&
|
|
46
|
+
!state.extensionDirty &&
|
|
47
|
+
!footer.isFieldlessSubmitForm &&
|
|
48
|
+
!state.insideDrawer &&
|
|
49
|
+
props.submitWithoutChanges !== true;
|
|
50
|
+
return isPristine || state.isSubmitting || props.disabled || props.submitBlocked;
|
|
51
|
+
}
|
|
52
|
+
function resolveSubmitLabelKey({ engine: { props, state }, footer, }) {
|
|
53
|
+
if (props.submitLabel !== undefined)
|
|
54
|
+
return props.submitLabel;
|
|
55
|
+
if (state.isWizard)
|
|
56
|
+
return "kumiko.actions.finish";
|
|
57
|
+
return footer.hasDirtyFooter ? "kumiko.form.saveChanges" : "kumiko.actions.save";
|
|
58
|
+
}
|
|
59
|
+
function renderSubmitButton(model) {
|
|
60
|
+
const { props, state } = model.engine;
|
|
61
|
+
const { Button } = state.primitives;
|
|
62
|
+
return (model.footer.showSubmit && (_jsx(Button, { type: "submit", disabled: isSubmitDisabled(model), loading: state.isSubmitting, variant: props.submitVariant ?? "primary", testId: "render-edit-submit", children: state.translate(resolveSubmitLabelKey(model)) })));
|
|
63
|
+
}
|
|
64
|
+
export function renderFormActions(model) {
|
|
65
|
+
return (_jsxs(_Fragment, { children: [renderWizardBackButton(model), renderFooterSlot(model), renderSaveAndCloseButton(model), renderWizardNextButton(model), renderFooterActionButtons(model), renderDiscardButton(model), renderSubmitButton(model), model.engine.sections.writeFormFooterAction] }));
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { FormValues } from "../headless/index.ts";
|
|
3
|
+
import type { ActionMenuItemSpec } from "../primitives.tsx";
|
|
4
|
+
import type { RenderEditModel } from "./render-edit-render-model.ts";
|
|
5
|
+
export type HeaderActionsModel = {
|
|
6
|
+
readonly headerMenuItems: readonly ActionMenuItemSpec[];
|
|
7
|
+
readonly secondaryFormActions: ReactNode;
|
|
8
|
+
readonly formHeaderRegion: ReactNode;
|
|
9
|
+
readonly titleActionMount: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
export declare function buildHeaderActions<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>): HeaderActionsModel;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { EditSlotMount } from "./edit-mounts.js";
|
|
3
|
+
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
4
|
+
import { resolveExtensionEntityId } from "./render-edit-logic.js";
|
|
5
|
+
function copyLinkMenuItems({ engine: { props, state }, }) {
|
|
6
|
+
const { onCopyLink } = props;
|
|
7
|
+
if (onCopyLink === undefined)
|
|
8
|
+
return [];
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
id: "copy-link",
|
|
12
|
+
label: state.translate(state.linkCopied ? "kumiko.actions.copyLinkCopied" : "kumiko.actions.copyLink"),
|
|
13
|
+
icon: "link",
|
|
14
|
+
onSelect: () => {
|
|
15
|
+
void Promise.resolve(onCopyLink()).then(() => state.setLinkCopied(true));
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
function cancelMenuItems({ engine: { props, state }, layout, }) {
|
|
21
|
+
const { onCancel } = props;
|
|
22
|
+
if (!layout.cancelInHeaderMenu || onCancel === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
id: "cancel",
|
|
27
|
+
label: state.translate("kumiko.actions.cancel"),
|
|
28
|
+
icon: "x",
|
|
29
|
+
onSelect: () => onCancel(),
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
function deleteMenuItems({ engine: { props, state }, }) {
|
|
34
|
+
if (props.onDelete === undefined)
|
|
35
|
+
return [];
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
id: "delete",
|
|
39
|
+
label: state.translate("kumiko.actions.delete"),
|
|
40
|
+
icon: "trash",
|
|
41
|
+
variant: "danger",
|
|
42
|
+
onSelect: () => state.setConfirmDeleteOpen(true),
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function buildHeaderMenuItems(model) {
|
|
47
|
+
if (!model.layout.menuPageHeaderActions)
|
|
48
|
+
return [];
|
|
49
|
+
const { menuActions } = model.engine.props;
|
|
50
|
+
return [
|
|
51
|
+
...(menuActions ?? []).map(model.engine.state.menuActionToItem),
|
|
52
|
+
...copyLinkMenuItems(model),
|
|
53
|
+
...cancelMenuItems(model),
|
|
54
|
+
...deleteMenuItems(model),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function renderDeleteButton({ engine: { props, state }, layout, }) {
|
|
58
|
+
const { Button } = state.primitives;
|
|
59
|
+
return (props.onDelete !== undefined &&
|
|
60
|
+
!layout.menuPageHeaderActions && (_jsx(Button, { type: "button", variant: "danger-ghost", icon: "trash", testId: "render-edit-delete", disabled: props.disabled, onClick: () => state.setConfirmDeleteOpen(true), children: state.translate("kumiko.actions.delete") })));
|
|
61
|
+
}
|
|
62
|
+
function renderCopyLinkButton({ engine: { props, state }, layout, }) {
|
|
63
|
+
const { Button } = state.primitives;
|
|
64
|
+
const { onCopyLink } = props;
|
|
65
|
+
const { iconOnlyMidActions } = layout;
|
|
66
|
+
const { translate, linkCopied } = state;
|
|
67
|
+
return (onCopyLink !== undefined &&
|
|
68
|
+
!layout.menuPageHeaderActions && (_jsx(Button, { type: "button", variant: iconOnlyMidActions ? "secondary" : "link", icon: linkCopied ? "check" : "link", testId: "render-edit-copy-link", ...(iconOnlyMidActions && {
|
|
69
|
+
size: "icon",
|
|
70
|
+
ariaLabel: translate(linkCopied ? "kumiko.actions.copyLinkCopied" : "kumiko.actions.copyLink"),
|
|
71
|
+
}), onClick: async () => {
|
|
72
|
+
await onCopyLink();
|
|
73
|
+
state.setLinkCopied(true);
|
|
74
|
+
}, children: iconOnlyMidActions
|
|
75
|
+
? null
|
|
76
|
+
: translate(linkCopied ? "kumiko.actions.copyLinkCopied" : "kumiko.actions.copyLink") })));
|
|
77
|
+
}
|
|
78
|
+
function renderCustomActions({ engine: { props, state }, layout, }) {
|
|
79
|
+
const { Button, Dialog } = state.primitives;
|
|
80
|
+
return props.actions?.map((action) => (_jsx(RenderEditActionButton, { action: action, iconOnly: layout.iconOnlyMidActions, Button: Button, Dialog: Dialog, onError: state.setActionError }, action.id)));
|
|
81
|
+
}
|
|
82
|
+
function renderMenuActionButtons({ engine: { props, state }, layout, }) {
|
|
83
|
+
const { Button, Dialog } = state.primitives;
|
|
84
|
+
return (!layout.menuPageHeaderActions &&
|
|
85
|
+
props.menuActions?.map((action) => (_jsx(RenderEditActionButton, { action: action, Button: Button, Dialog: Dialog, onError: state.setActionError }, action.id))));
|
|
86
|
+
}
|
|
87
|
+
function renderCancelButton({ engine: { props, state }, layout, }) {
|
|
88
|
+
const { Button } = state.primitives;
|
|
89
|
+
const { onCancel } = props;
|
|
90
|
+
return (onCancel !== undefined &&
|
|
91
|
+
!layout.cancelInHeaderMenu && (_jsx(Button, { type: "button", variant: "secondary", onClick: () => onCancel(), testId: "render-edit-cancel", children: state.translate("kumiko.actions.cancel") })));
|
|
92
|
+
}
|
|
93
|
+
function renderSecondaryFormActions(model) {
|
|
94
|
+
return (_jsxs(_Fragment, { children: [renderDeleteButton(model), renderCopyLinkButton(model), renderCustomActions(model), renderMenuActionButtons(model), renderCancelButton(model)] }));
|
|
95
|
+
}
|
|
96
|
+
function renderEditSlot({ engine: { props, form, sections } }, slot, slotName) {
|
|
97
|
+
if (slot === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
return (_jsx(EditSlotMount, { slot: slot, slotName: slotName, screenId: props.screen.id, entityName: sections.vm.entityName, entityId: resolveExtensionEntityId(props.entityId, sections.vm.id), values: form.snapshot.values }));
|
|
100
|
+
}
|
|
101
|
+
// A function headerRegion places the slot itself (projectionDetail puts it
|
|
102
|
+
// into its header card next to the actions); otherwise the slot renders
|
|
103
|
+
// above the caller's own headerRegion.
|
|
104
|
+
function placeHeaderSlot(headerRegion, headerSlotMount) {
|
|
105
|
+
if (typeof headerRegion === "function")
|
|
106
|
+
return headerRegion(headerSlotMount);
|
|
107
|
+
if (headerSlotMount === undefined)
|
|
108
|
+
return headerRegion;
|
|
109
|
+
return (_jsxs(_Fragment, { children: [headerSlotMount, headerRegion] }));
|
|
110
|
+
}
|
|
111
|
+
export function buildHeaderActions(model) {
|
|
112
|
+
const { props } = model.engine;
|
|
113
|
+
const headerSlotMount = renderEditSlot(model, props.screen.slots?.header, "header");
|
|
114
|
+
return {
|
|
115
|
+
headerMenuItems: buildHeaderMenuItems(model),
|
|
116
|
+
secondaryFormActions: renderSecondaryFormActions(model),
|
|
117
|
+
formHeaderRegion: placeHeaderSlot(props.headerRegion, headerSlotMount),
|
|
118
|
+
titleActionMount: renderEditSlot(model, props.screen.slots?.titleAction, "titleAction"),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { useExtensionSectionComponent } from "../app/extension-sections.tsx";
|
|
2
|
+
import type { FieldIssue, FormValues } from "../headless/index.ts";
|
|
3
|
+
import type { RenderEditEngine } from "./render-edit-render-model.ts";
|
|
4
|
+
export type LayoutFlags = {
|
|
5
|
+
readonly rootIssues: readonly FieldIssue[];
|
|
6
|
+
readonly isScreenForm: boolean;
|
|
7
|
+
readonly titleInShell: boolean;
|
|
8
|
+
readonly menuPageHeaderActions: boolean;
|
|
9
|
+
readonly cancelInHeaderMenu: boolean;
|
|
10
|
+
readonly hasSecondaryFormActions: boolean;
|
|
11
|
+
readonly iconOnlyMidActions: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type FooterFlags = {
|
|
14
|
+
readonly isFieldlessSubmitForm: boolean;
|
|
15
|
+
readonly showsSubmit: boolean;
|
|
16
|
+
readonly showWizardBack: boolean;
|
|
17
|
+
readonly showWizardNext: boolean;
|
|
18
|
+
readonly showSubmit: boolean;
|
|
19
|
+
readonly hasFormActions: boolean;
|
|
20
|
+
readonly nextStepTitle: string | undefined;
|
|
21
|
+
readonly isCreate: boolean;
|
|
22
|
+
readonly hasDirtyFooter: boolean;
|
|
23
|
+
readonly unsavedCount: number | undefined;
|
|
24
|
+
readonly isSettingsList: boolean;
|
|
25
|
+
readonly hasUnsavedInput: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type FooterSlotComponent = ReturnType<typeof useExtensionSectionComponent>;
|
|
28
|
+
export declare function deriveLayoutFlags<TValues extends FormValues, TCtx>({ props, state, form, }: RenderEditEngine<TValues, TCtx>): LayoutFlags;
|
|
29
|
+
export declare function deriveFooterFlags<TValues extends FormValues, TCtx>(engine: RenderEditEngine<TValues, TCtx>, layout: LayoutFlags, footerSlotComponent: FooterSlotComponent): FooterFlags;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { PROJECTION_DETAIL_ENTITY } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { shouldRenderActionsIconOnly } from "../primitives.js";
|
|
3
|
+
import { ROOT_ISSUE_PATH, resolveExtensionEntityId } from "./render-edit-logic.js";
|
|
4
|
+
function hasSecondaryFormActions(props, menuPageHeaderActions, cancelInHeaderMenu) {
|
|
5
|
+
const { onDelete, onCopyLink, actions, menuActions, onCancel } = props;
|
|
6
|
+
const hasRecordActions = !menuPageHeaderActions && (onDelete !== undefined || onCopyLink !== undefined);
|
|
7
|
+
const hasCustomActions = actions !== undefined && actions.length > 0;
|
|
8
|
+
const hasMenuActions = !menuPageHeaderActions && menuActions !== undefined && menuActions.length > 0;
|
|
9
|
+
const hasCancel = onCancel !== undefined && !cancelInHeaderMenu;
|
|
10
|
+
return hasRecordActions || hasCustomActions || hasMenuActions || hasCancel;
|
|
11
|
+
}
|
|
12
|
+
export function deriveLayoutFlags({ props, state, form, }) {
|
|
13
|
+
const { fillScreenHeight, hideSectionTitles, headerRegion, actions, onCancel } = props;
|
|
14
|
+
const { PageHeader, ActionOverflowMenu } = state.primitives;
|
|
15
|
+
// Screen forms (entityEdit/actionForm filling the shell height) drop the card;
|
|
16
|
+
// the title then lives in the shell header when the shell offers a slot.
|
|
17
|
+
const rootIssues = form.snapshot.errors[ROOT_ISSUE_PATH] ?? [];
|
|
18
|
+
const isScreenForm = fillScreenHeight === true && hideSectionTitles !== true && !state.insideDrawer;
|
|
19
|
+
const titleInShell = PageHeader !== undefined &&
|
|
20
|
+
state.pageHeaderSlotAvailable &&
|
|
21
|
+
hideSectionTitles !== true &&
|
|
22
|
+
headerRegion === undefined;
|
|
23
|
+
const menuPageHeaderActions = titleInShell && isScreenForm && ActionOverflowMenu !== undefined;
|
|
24
|
+
const cancelInHeaderMenu = menuPageHeaderActions && state.isWizard && onCancel !== undefined;
|
|
25
|
+
return {
|
|
26
|
+
rootIssues,
|
|
27
|
+
isScreenForm,
|
|
28
|
+
titleInShell,
|
|
29
|
+
menuPageHeaderActions,
|
|
30
|
+
cancelInHeaderMenu,
|
|
31
|
+
// Two groups — wizard navigation on the right/top, record actions on the
|
|
32
|
+
// left/below; destructive action sits outermost, farthest from the target
|
|
33
|
+
// action.
|
|
34
|
+
hasSecondaryFormActions: hasSecondaryFormActions(props, menuPageHeaderActions, cancelInHeaderMenu),
|
|
35
|
+
// Teil-C collapse rule: driven by the custom `actions` group only — a
|
|
36
|
+
// resolved true also folds the Copy-Link button (same visual group in the
|
|
37
|
+
// footer), but never Delete/Cancel, which keep their text regardless.
|
|
38
|
+
iconOnlyMidActions: actions !== undefined && shouldRenderActionsIconOnly(actions),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function isCreateRecord(entityIdProp, vmId) {
|
|
42
|
+
const id = resolveExtensionEntityId(entityIdProp, vmId);
|
|
43
|
+
return id == null || id === "";
|
|
44
|
+
}
|
|
45
|
+
function resolveUnsavedCount(hasDirtyFooter, changedFieldCount, extensionDirty) {
|
|
46
|
+
if (!hasDirtyFooter)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (changedFieldCount > 0)
|
|
49
|
+
return changedFieldCount;
|
|
50
|
+
return extensionDirty ? 1 : 0;
|
|
51
|
+
}
|
|
52
|
+
function showsSubmitButton({ props, state, sections }, isFieldlessSubmitForm) {
|
|
53
|
+
return (!props.viewMode &&
|
|
54
|
+
(sections.isFormEditable ||
|
|
55
|
+
state.hasExtensionRegistrations ||
|
|
56
|
+
isFieldlessSubmitForm ||
|
|
57
|
+
props.submitWithoutChanges !== undefined));
|
|
58
|
+
}
|
|
59
|
+
function deriveFormActionFlags({ state, sections, steps }, showsSubmit, footerSlotComponent) {
|
|
60
|
+
const { isWizard } = state;
|
|
61
|
+
// Mirrors every branch inside formActions below — without this guard
|
|
62
|
+
// DefaultForm renders an empty footer strip (border + padding, no content)
|
|
63
|
+
// on read-only detail screens, since `actions` would otherwise always be
|
|
64
|
+
// a defined (if empty) fragment.
|
|
65
|
+
const showWizardBack = isWizard && steps.currentStep > 0;
|
|
66
|
+
const showWizardNext = isWizard && !steps.isLastWizardStep;
|
|
67
|
+
const showSubmit = showsSubmit && (!isWizard || steps.isLastWizardStep);
|
|
68
|
+
const hasFormActions = showWizardBack ||
|
|
69
|
+
showWizardNext ||
|
|
70
|
+
showSubmit ||
|
|
71
|
+
footerSlotComponent !== undefined ||
|
|
72
|
+
sections.writeFormFooterAction !== undefined;
|
|
73
|
+
return { showWizardBack, showWizardNext, showSubmit, hasFormActions };
|
|
74
|
+
}
|
|
75
|
+
function deriveHasDirtyFooter({ props, state }, layout, isCreate) {
|
|
76
|
+
// Edit-mode screen forms always carry the dirty status (0 = quiet "No unsaved
|
|
77
|
+
// changes"); wizards keep the status-only-when-dirty behaviour.
|
|
78
|
+
return (!state.isWizard &&
|
|
79
|
+
!props.viewMode &&
|
|
80
|
+
(props.dirtyFooter === true ||
|
|
81
|
+
(layout.isScreenForm && !isCreate && props.headerRegion === undefined)));
|
|
82
|
+
}
|
|
83
|
+
export function deriveFooterFlags(engine, layout, footerSlotComponent) {
|
|
84
|
+
const { props, state, form, sections, steps } = engine;
|
|
85
|
+
// A projectionDetail's synthesized entity is empty when every section is an
|
|
86
|
+
// extension or relatedList; that is not a fieldless submit form, so it must not
|
|
87
|
+
// draw a no-op Save. Extension registrations still get their Save, since
|
|
88
|
+
// persistExtensions() runs on the customSubmit path too.
|
|
89
|
+
const isFieldlessSubmitForm = sections.isFieldless && props.screen.entity !== PROJECTION_DETAIL_ENTITY;
|
|
90
|
+
const showsSubmit = showsSubmitButton(engine, isFieldlessSubmitForm);
|
|
91
|
+
const isCreate = isCreateRecord(props.entityId, sections.vm.id);
|
|
92
|
+
const hasDirtyFooter = deriveHasDirtyFooter(engine, layout, isCreate);
|
|
93
|
+
return {
|
|
94
|
+
isFieldlessSubmitForm,
|
|
95
|
+
showsSubmit,
|
|
96
|
+
...deriveFormActionFlags(engine, showsSubmit, footerSlotComponent),
|
|
97
|
+
nextStepTitle: state.isWizard
|
|
98
|
+
? sections.filteredSections[steps.currentStep + 1]?.title
|
|
99
|
+
: undefined,
|
|
100
|
+
isCreate,
|
|
101
|
+
hasDirtyFooter,
|
|
102
|
+
unsavedCount: resolveUnsavedCount(hasDirtyFooter, Object.keys(form.snapshot.changes).length, state.extensionDirty),
|
|
103
|
+
isSettingsList: props.screen.layout.variant === "settings-list" && props.hideSectionTitles !== true,
|
|
104
|
+
hasUnsavedInput: form.snapshot.isDirty || state.extensionDirty,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -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 renderEditLayout<TValues extends FormValues, TCtx>(model: RenderEditModel<TValues, TCtx>): ReactNode;
|