@cosmicdrift/kumiko-renderer 0.386.0 → 0.388.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -57
- package/dist/app/action-form-body.d.ts +2 -2
- package/dist/app/action-form-body.js +12 -4
- package/dist/app/action-form-shim.d.ts +8 -8
- package/dist/app/action-form-shim.js +20 -22
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- package/dist/app/app-features-context.js +4 -4
- package/dist/app/column-renderers.d.ts +7 -9
- package/dist/app/column-renderers.js +15 -18
- package/dist/app/config-edit-body.js +7 -3
- package/dist/app/config-edit-shim.d.ts +7 -8
- package/dist/app/config-edit-shim.js +18 -20
- package/dist/app/custom-screens.d.ts +3 -3
- package/dist/app/custom-screens.js +8 -8
- package/dist/app/dashboard-body.d.ts +2 -2
- package/dist/app/dashboard-body.js +2 -2
- package/dist/app/entity-edit-prefill.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +18 -13
- package/dist/app/entity-edit-screen.js +4 -4
- package/dist/app/entity-list-screen.js +3 -3
- package/dist/app/extension-form-submit.d.ts +1 -1
- package/dist/app/extension-form-submit.js +14 -15
- package/dist/app/extension-sections.d.ts +29 -28
- package/dist/app/extension-sections.js +10 -10
- package/dist/app/feature-schema.js +11 -11
- package/dist/app/form-schema.js +2 -2
- package/dist/app/header-actions-bar.js +2 -2
- package/dist/app/kumiko-screen.js +8 -10
- package/dist/app/layout-fields.js +1 -1
- package/dist/app/list-facets.js +3 -3
- package/dist/app/nav.d.ts +10 -10
- package/dist/app/nav.js +13 -13
- package/dist/app/projection-detail-body.js +4 -4
- package/dist/app/projection-detail-shim.d.ts +6 -5
- package/dist/app/projection-detail-shim.js +23 -22
- package/dist/app/projection-list-body.js +10 -10
- package/dist/app/projection-list-shim.d.ts +5 -5
- package/dist/app/projection-list-shim.js +17 -18
- package/dist/app/qn.js +10 -12
- package/dist/app/qualify-screen-id.js +1 -1
- package/dist/app/screen-access.js +2 -2
- package/dist/app/screen-shared.js +1 -1
- package/dist/app/secret-mint-body.js +4 -4
- package/dist/app/write-failed-error.js +8 -7
- package/dist/components/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +5 -5
- package/dist/components/grid-cell-for-field.d.ts +1 -1
- package/dist/components/reference-create-dialog.js +3 -3
- package/dist/components/related-list-section.d.ts +1 -1
- package/dist/components/related-list-section.js +15 -2
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- package/dist/components/render-edit-logic.js +1 -1
- package/dist/components/render-edit-types.d.ts +6 -6
- package/dist/components/render-edit.js +74 -73
- package/dist/components/render-field.d.ts +7 -7
- package/dist/components/render-field.js +62 -61
- package/dist/components/render-list.d.ts +42 -43
- package/dist/components/render-list.js +93 -84
- package/dist/components/use-wizard-form-draft.js +10 -10
- package/dist/context/dispatcher-context.js +5 -5
- package/dist/context/user-roles-context.js +2 -2
- package/dist/flow-diagram/render.js +9 -1
- package/dist/headless/apex/index.d.ts +1 -2
- package/dist/headless/apex/index.js +5 -6
- package/dist/headless/form/form-controller.js +14 -6
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/form/zod-bridge.js +2 -2
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/format/money.js +1 -1
- package/dist/headless/index.js +2 -2
- package/dist/headless/view-model/edit.js +4 -2
- package/dist/headless/view-model/list.js +184 -150
- package/dist/headless/view-model/types.d.ts +6 -3
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- package/dist/hooks/reference-limits.js +11 -12
- package/dist/hooks/use-list-url-state.d.ts +13 -13
- package/dist/hooks/use-list-url-state.js +21 -22
- package/dist/hooks/use-query.js +3 -3
- package/dist/hooks/use-reference-lookup.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +32 -19
- package/dist/hooks/use-store.js +18 -22
- package/dist/hooks/use-stream-handler.js +4 -2
- package/dist/i18n-defaults.js +6 -8
- package/dist/i18n.d.ts +10 -10
- package/dist/i18n.js +25 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -4
- package/dist/live/dispatcher-live.js +2 -2
- package/dist/primitives.d.ts +244 -221
- package/dist/primitives.js +1 -1
- package/dist/sse/live-events.d.ts +5 -5
- package/dist/sse/live-events.js +17 -17
- package/dist/tokens.d.ts +8 -8
- package/dist/tokens.js +16 -17
- package/dist/web/app/browser-locale.d.ts +9 -9
- package/dist/web/app/browser-locale.js +19 -19
- package/dist/web/app/client-plugin.d.ts +44 -52
- package/dist/web/app/client-plugin.js +4 -5
- package/dist/web/app/create-app.d.ts +28 -31
- package/dist/web/app/create-app.js +29 -25
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- package/dist/web/app/document-lang-sync.js +1 -1
- package/dist/web/app/draft-storage.js +1 -1
- package/dist/web/app/nav-providers-context.d.ts +3 -3
- package/dist/web/app/nav-providers-context.js +3 -3
- package/dist/web/app/nav.d.ts +18 -17
- package/dist/web/app/nav.js +66 -64
- package/dist/web/app/rich-content-editor.js +1 -1
- package/dist/web/components/config-cascade.js +17 -18
- package/dist/web/components/config-source-badge.js +4 -4
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +1 -1
- package/dist/web/layout/app-layout.d.ts +9 -9
- package/dist/web/layout/app-layout.js +2 -2
- package/dist/web/layout/avatar.d.ts +4 -3
- package/dist/web/layout/avatar.js +15 -17
- package/dist/web/layout/default-app-shell.d.ts +22 -23
- package/dist/web/layout/editor-panel.js +3 -3
- package/dist/web/layout/nav-tree.js +86 -84
- package/dist/web/layout/profile-menu.d.ts +1 -1
- package/dist/web/layout/shell-header.js +2 -1
- package/dist/web/layout/sidebar-brand.d.ts +7 -7
- package/dist/web/layout/sidebar-user.d.ts +1 -1
- package/dist/web/layout/sidebar-user.js +4 -4
- package/dist/web/layout/sidebar.d.ts +8 -8
- package/dist/web/layout/sidebar.js +7 -7
- package/dist/web/layout/target-resolver-stub.d.ts +5 -5
- package/dist/web/layout/target-resolver-stub.js +20 -20
- package/dist/web/layout/target-url.d.ts +7 -7
- package/dist/web/layout/target-url.js +22 -22
- package/dist/web/layout/theme-toggle.js +4 -5
- package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- package/dist/web/layout/workspace-shell.d.ts +21 -21
- package/dist/web/layout/workspace-shell.js +29 -30
- package/dist/web/lib/cn.d.ts +4 -4
- package/dist/web/lib/cn.js +4 -4
- package/dist/web/lib/file-ref-download.js +4 -1
- package/dist/web/primitives/action-menu.d.ts +8 -8
- package/dist/web/primitives/action-menu.js +14 -13
- package/dist/web/primitives/calendar-popover.d.ts +1 -1
- package/dist/web/primitives/calendar-popover.js +10 -10
- package/dist/web/primitives/card-styles.js +1 -1
- package/dist/web/primitives/card.js +2 -2
- package/dist/web/primitives/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- package/dist/web/primitives/combobox.d.ts +12 -12
- package/dist/web/primitives/combobox.js +34 -35
- package/dist/web/primitives/data-table-cell.js +2 -2
- package/dist/web/primitives/data-table.js +6 -6
- package/dist/web/primitives/date-field.js +2 -2
- package/dist/web/primitives/date-input.js +4 -4
- package/dist/web/primitives/default-form.js +2 -2
- package/dist/web/primitives/dropdown-menu.js +14 -15
- package/dist/web/primitives/embedded-list-input.js +3 -3
- package/dist/web/primitives/facet-filter.js +2 -2
- package/dist/web/primitives/field.js +9 -8
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/form.d.ts +1 -1
- package/dist/web/primitives/form.js +6 -6
- package/dist/web/primitives/input.js +8 -3
- package/dist/web/primitives/layout-elements.js +7 -5
- package/dist/web/primitives/layout.js +2 -2
- package/dist/web/primitives/located-timestamp-input.js +4 -4
- package/dist/web/primitives/money-input.js +13 -17
- package/dist/web/primitives/page-header.js +3 -1
- package/dist/web/primitives/pager.js +1 -1
- package/dist/web/primitives/row-actions.js +6 -7
- package/dist/web/primitives/secret-reveal.js +1 -1
- package/dist/web/primitives/timestamp-input.d.ts +9 -9
- package/dist/web/primitives/timestamp-input.js +21 -21
- package/dist/web/primitives/toast.js +12 -13
- package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- package/dist/web/sse/live-events.d.ts +6 -6
- package/dist/web/sse/live-events.js +3 -3
- package/dist/web/tokens.d.ts +11 -12
- package/dist/web/tokens.js +35 -37
- package/dist/web/version/update-checker.js +14 -13
- package/dist/web/widgets/ai-text-field.js +1 -1
- package/dist/web/widgets/bounded-load-cache.d.ts +1 -1
- package/dist/web/widgets/bounded-load-cache.js +10 -4
- package/dist/web/widgets/charts.d.ts +10 -10
- package/dist/web/widgets/charts.js +8 -8
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.d.ts +3 -3
- package/dist/web/widgets/collapsible-section.js +7 -7
- package/dist/web/widgets/detail-list.d.ts +5 -3
- package/dist/web/widgets/detail-list.js +5 -3
- package/dist/web/widgets/feed-list.d.ts +2 -2
- package/dist/web/widgets/feed-list.js +2 -2
- package/dist/web/widgets/form-fields.d.ts +14 -14
- package/dist/web/widgets/form-fields.js +14 -13
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +5 -4
- package/dist/web/widgets/infinity-list.js +3 -3
- package/dist/web/widgets/mode-switch.d.ts +2 -2
- package/dist/web/widgets/mode-switch.js +2 -2
- package/dist/web/widgets/pdf-preview.js +1 -1
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- package/dist/web/widgets/progress-list.d.ts +1 -2
- package/dist/web/widgets/progress-list.js +1 -2
- package/dist/web/widgets/query-table.d.ts +6 -6
- package/dist/web/widgets/result-panel.d.ts +16 -16
- package/dist/web/widgets/result-panel.js +10 -10
- package/dist/web/widgets/section-card.d.ts +3 -3
- package/dist/web/widgets/section-card.js +3 -3
- package/dist/web/widgets/stat.d.ts +4 -4
- package/dist/web/widgets/stat.js +6 -6
- package/dist/web/widgets/states.d.ts +4 -4
- package/dist/web/widgets/states.js +4 -4
- package/dist/web/widgets/upload-zone.js +13 -5
- package/dist/web/widgets/use-draft.d.ts +7 -7
- package/dist/web/widgets/use-draft.js +7 -7
- package/package.json +6 -6
- package/src/changes.json +49 -0
|
@@ -32,11 +32,11 @@ export function RenderEdit(props) {
|
|
|
32
32
|
const disabled = disabledProp || viewMode;
|
|
33
33
|
const i18nScreenId = props.i18nScreenId ?? screen.id;
|
|
34
34
|
const { customSubmit } = props;
|
|
35
|
-
// Translate
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// useTranslation
|
|
39
|
-
//
|
|
35
|
+
// Translate fallback: when the caller does not provide a translate function,
|
|
36
|
+
// consume the i18next context directly. Otherwise field labels would remain raw
|
|
37
|
+
// keys (`feature:entity:foo:field:title`) without caller wiring.
|
|
38
|
+
// useTranslation throws without a LocaleProvider, which is acceptable because
|
|
39
|
+
// RenderEdit only runs inside a mounted Kumiko app tree.
|
|
40
40
|
const t = useTranslation();
|
|
41
41
|
const translate = translateProp ?? t;
|
|
42
42
|
const dispatcher = useOptionalDispatcher();
|
|
@@ -44,7 +44,7 @@ export function RenderEdit(props) {
|
|
|
44
44
|
// Tabs reuse the wizard's step machinery: one section visible at a time,
|
|
45
45
|
// all of them mounted, `currentStep` as the active index. What they do NOT
|
|
46
46
|
// share is navigation — a tab form submits from any tab, so every
|
|
47
|
-
// step-transition path below stays wizard-only
|
|
47
|
+
// step-transition path below stays wizard-only.
|
|
48
48
|
const isTabs = screen.layout.mode === "tabs";
|
|
49
49
|
const draftEnabled = isWizard && screen.layout.draft === true && dispatcher !== undefined;
|
|
50
50
|
const isCreateMode = entityIdProp === undefined || entityIdProp === null || entityIdProp === "";
|
|
@@ -63,8 +63,8 @@ export function RenderEdit(props) {
|
|
|
63
63
|
// One state for all header actions (actions?), not per-button — only one
|
|
64
64
|
// action can be in flight at a time, and this keeps the error surfaced in
|
|
65
65
|
// the shared formError-adjacent banner region instead of inside the
|
|
66
|
-
// button row
|
|
67
|
-
// flex justify-end action bar
|
|
66
|
+
// button row: a full-width Banner there breaks the
|
|
67
|
+
// flex justify-end action bar.
|
|
68
68
|
const [actionError, setActionError] = useState(null);
|
|
69
69
|
const [rawStep, setRawStep] = useState(0);
|
|
70
70
|
const [passedStepKeys, setPassedStepKeys] = useState(() => new Set());
|
|
@@ -82,15 +82,15 @@ export function RenderEdit(props) {
|
|
|
82
82
|
draftStorage,
|
|
83
83
|
});
|
|
84
84
|
const { draftCandidates, setDraftCandidates } = draftState;
|
|
85
|
-
// Composed
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
85
|
+
// Composed save: extension sections report their dirty state so the Save button
|
|
86
|
+
// activates when only a section changed, and provide a submit handler that runs
|
|
87
|
+
// after the entity write. extensionErrorKey stores the i18n key for a failed
|
|
88
|
+
// section persistence.
|
|
89
89
|
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
90
|
+
// This is not a second write path for saveDraft()'s data: deriveFormFields
|
|
91
|
+
// skips extension sections entirely, so extension-owned field state never enters
|
|
92
|
+
// `fields` or `controller.getSnapshot().values`; there is no draft-covered state
|
|
93
|
+
// left for persistExtensions() to compete over.
|
|
94
94
|
const [extensionDirty, setExtensionDirty] = useState(false);
|
|
95
95
|
// Mirror for the leave guard: it must see "saved" before React re-renders.
|
|
96
96
|
const extensionDirtyRef = useRef(false);
|
|
@@ -109,7 +109,7 @@ export function RenderEdit(props) {
|
|
|
109
109
|
// Both stepped layouts show one section at a time and keep the rest mounted
|
|
110
110
|
// but inert. A tabs layout on a host without the Tabs primitive has no strip
|
|
111
111
|
// to switch with, so it falls back to rendering every section stacked rather
|
|
112
|
-
// than stranding the user on section 0
|
|
112
|
+
// than stranding the user on section 0. Same fallback when the
|
|
113
113
|
// WizardStepGroup wrapper is missing.
|
|
114
114
|
const isStepped = isWizard || (isTabs && Tabs !== undefined && WizardStepGroup !== undefined);
|
|
115
115
|
const fields = useMemo(() => deriveFormFields(screen), [screen]);
|
|
@@ -121,7 +121,7 @@ export function RenderEdit(props) {
|
|
|
121
121
|
// scoping would silently drop root-level .refine() issues.
|
|
122
122
|
// A `fieldsFilter` that matches nothing in `fields` (typo, renamed field)
|
|
123
123
|
// must fall back to unscoped too — an empty scope array would filter out
|
|
124
|
-
// ALL validation issues
|
|
124
|
+
// ALL validation issues, letting submit() through
|
|
125
125
|
// unvalidated on a form that also renders no sections.
|
|
126
126
|
const scopeFieldNames = useMemo(() => {
|
|
127
127
|
if (fieldsFilter === undefined)
|
|
@@ -129,9 +129,9 @@ export function RenderEdit(props) {
|
|
|
129
129
|
const matched = fieldsFilter.filter((f) => Object.hasOwn(fields, f));
|
|
130
130
|
return matched.length === 0 ? undefined : matched;
|
|
131
131
|
}, [fieldsFilter, fields]);
|
|
132
|
-
//
|
|
133
|
-
// customSubmit
|
|
134
|
-
//
|
|
132
|
+
// Configure submission only when the caller provides a writeCommand; with
|
|
133
|
+
// customSubmit, the form controller has no submit wiring because
|
|
134
|
+
// controller.submit() is not called.
|
|
135
135
|
const writeVerb = isCreateMode ? "create" : "update";
|
|
136
136
|
// A write-excluded field must never reach the payload: the handler schema is
|
|
137
137
|
// z.never() for it, so even an unchanged read-only value would be rejected.
|
|
@@ -191,12 +191,12 @@ export function RenderEdit(props) {
|
|
|
191
191
|
controller.setValues(initial);
|
|
192
192
|
controller.rebase();
|
|
193
193
|
}, [refreshToken, controller, initial]);
|
|
194
|
-
// Controlled mode
|
|
194
|
+
// Controlled mode. Both callbacks live in refs so a
|
|
195
195
|
// re-rendering caller (identity-unstable `onChange`/`onControlsReady`
|
|
196
196
|
// closures, e.g. inline arrows) doesn't retrigger these effects — only a
|
|
197
197
|
// real snapshot mutation (typing, patch()) does. Without this, a caller
|
|
198
198
|
// whose onChange calls patch() to fill other fields (the VIN-decode use
|
|
199
|
-
// case
|
|
199
|
+
// case) would loop: patch → new snapshot → effect
|
|
200
200
|
// re-subscribes with a "new" onChange → refires.
|
|
201
201
|
const onChangeRef = useRef(onChange);
|
|
202
202
|
onChangeRef.current = onChange;
|
|
@@ -205,7 +205,7 @@ export function RenderEdit(props) {
|
|
|
205
205
|
// effect below on every render since the callback itself is now a dep).
|
|
206
206
|
// Tracks the actual prop, not a ref snapshot, so a caller that swaps in a
|
|
207
207
|
// real handler after mount (e.g. `onControlsReady={ready ? cb : undefined}`)
|
|
208
|
-
// gets delivered to for THIS mount instead of never
|
|
208
|
+
// gets delivered to for THIS mount instead of never.
|
|
209
209
|
const deliveredControlsToRef = useRef(undefined);
|
|
210
210
|
const onControlsReadyRef = useRef(onControlsReady);
|
|
211
211
|
onControlsReadyRef.current = onControlsReady;
|
|
@@ -215,10 +215,13 @@ export function RenderEdit(props) {
|
|
|
215
215
|
const scopeFieldNamesRef = useRef(scopeFieldNames);
|
|
216
216
|
scopeFieldNamesRef.current = scopeFieldNames;
|
|
217
217
|
const scopedValidate = useCallback(() => controller.validate(scopeFieldNamesRef.current), [controller]);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
// The extension dirty flag aggregates every section; each section clears its
|
|
219
|
+
// own part, so a host-side markSaved must not reset it.
|
|
220
|
+
const markSaved = useCallback((fields) => {
|
|
221
|
+
if (fields === undefined)
|
|
222
|
+
controller.rebase();
|
|
223
|
+
else
|
|
224
|
+
controller.rebaseFields(fields);
|
|
222
225
|
}, [controller]);
|
|
223
226
|
// `saveDraft` (defined below) is a fresh closure every render, over that
|
|
224
227
|
// render's `draftKey`/`draftId` — the ref lets the debounce timer below
|
|
@@ -250,9 +253,9 @@ export function RenderEdit(props) {
|
|
|
250
253
|
};
|
|
251
254
|
}, []);
|
|
252
255
|
// patch() (controlled mode, extension sections) applies immediately —
|
|
253
|
-
// only the resulting draft save is debounced
|
|
256
|
+
// only the resulting draft save is debounced, so a patch() burst
|
|
254
257
|
// (VIN-decode filling several fields, or onChange fanning a keystroke out
|
|
255
|
-
// through patch(), see the
|
|
258
|
+
// through patch(), see the VIN-decode test above) collapses into one save.
|
|
256
259
|
const patchAndScheduleDraftSave = useCallback((partial) => {
|
|
257
260
|
controller.setValues(partial);
|
|
258
261
|
if (!draftEnabled || dispatcher === undefined || disabled || submittedRef.current)
|
|
@@ -270,7 +273,7 @@ export function RenderEdit(props) {
|
|
|
270
273
|
// execution order) so that an onChange fired on mount always sees
|
|
271
274
|
// `controls !== undefined` — both effects depend only on mount-stable
|
|
272
275
|
// values (controller/scopedValidate/patchAndScheduleDraftSave), so the
|
|
273
|
-
// swap is otherwise a no-op. See the
|
|
276
|
+
// swap is otherwise a no-op. See the VIN-decode test: an initial
|
|
274
277
|
// value that should derive dependent fields on mount needs controls.patch
|
|
275
278
|
// available on the very first onChange call, not just from the second
|
|
276
279
|
// keystroke onward.
|
|
@@ -377,7 +380,7 @@ export function RenderEdit(props) {
|
|
|
377
380
|
// run their own query / own submit), so they always pass through. A
|
|
378
381
|
// section with no fields at all (e.g. a review-only step) has
|
|
379
382
|
// `visible: fields.some(...)` = false vacuously; that's "no fields to
|
|
380
|
-
// hide", not "hidden", so it stays too
|
|
383
|
+
// hide", not "hidden", so it stays too. Unless the create
|
|
381
384
|
// exclusion emptied it: that would leave an empty wizard step / tab.
|
|
382
385
|
() => filterEditSections(vm.sections, fieldsFilter).filter((section) => section.kind === "extension" ||
|
|
383
386
|
section.kind === "relatedList" ||
|
|
@@ -391,9 +394,9 @@ export function RenderEdit(props) {
|
|
|
391
394
|
return fillable.length > 0 && fillable.every((field) => field.required);
|
|
392
395
|
}, [filteredSections]);
|
|
393
396
|
const showsAllRequiredHint = allFieldsRequired && hideSectionTitles !== true && !isWizard;
|
|
394
|
-
// true when editable fields exist or an extension opted into composed submit
|
|
397
|
+
// true when editable fields exist or an extension opted into composed submit.
|
|
395
398
|
const isFormEditable = hasEditableSection(filteredSections);
|
|
396
|
-
// A fieldless form (input-less secretMint mint
|
|
399
|
+
// A fieldless form (input-less secretMint mint — the secret is
|
|
397
400
|
// server-generated, the mint step is only its submit button) has no
|
|
398
401
|
// section to gate on: it IS the action, so it always shows the submit
|
|
399
402
|
// button, and it can never go dirty either.
|
|
@@ -404,8 +407,8 @@ export function RenderEdit(props) {
|
|
|
404
407
|
// reaches this line and `filteredSections[0]` stays the active section)
|
|
405
408
|
// needs its own tab panel capped at the available height so its table
|
|
406
409
|
// scrolls inside the panel instead of the whole page stretching to the
|
|
407
|
-
// row count
|
|
408
|
-
// of stretching the panel to the bottom
|
|
410
|
+
// row count — a short table still sizes to its content instead
|
|
411
|
+
// of stretching the panel to the bottom. Any other layout —
|
|
409
412
|
// multiple sections, a non-relatedList tab, stacked (non-tabs) forms —
|
|
410
413
|
// keeps normal document-flow height untouched.
|
|
411
414
|
const fillHeight = fillScreenHeight === true ||
|
|
@@ -415,9 +418,9 @@ export function RenderEdit(props) {
|
|
|
415
418
|
// instead of sitting in the section's title row.
|
|
416
419
|
const isLoneWriteFormTab = hideSectionTitles === true && filteredSections[0]?.kind === "writeForm";
|
|
417
420
|
const [writeFormFooterAction, setWriteFormFooterAction] = useState(undefined);
|
|
418
|
-
//
|
|
419
|
-
// false
|
|
420
|
-
//
|
|
421
|
+
// Persist all composed extension sections using the resolved entityId.
|
|
422
|
+
// false means a section failed and its i18n key is shown in the banner. Without
|
|
423
|
+
// entity context (create mode without a route id), there is nothing to write.
|
|
421
424
|
async function persistExtensions() {
|
|
422
425
|
const entityId = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
423
426
|
if (entityId === null)
|
|
@@ -495,7 +498,7 @@ export function RenderEdit(props) {
|
|
|
495
498
|
function handleWizardNext() {
|
|
496
499
|
const next = Math.min(currentStep + 1, lastStepIndex);
|
|
497
500
|
hasNavigatedRef.current = true;
|
|
498
|
-
// Locked state
|
|
501
|
+
// Locked state: `disabled` means "no input/no write", not "no
|
|
499
502
|
// navigation" — a disabled wizard must still be steppable so Back/Next
|
|
500
503
|
// reach every step. But neither validate() (would block on an empty
|
|
501
504
|
// required field nobody can fill in) nor saveDraft() (would mint a
|
|
@@ -569,7 +572,7 @@ export function RenderEdit(props) {
|
|
|
569
572
|
handleWizardNext();
|
|
570
573
|
return;
|
|
571
574
|
}
|
|
572
|
-
// Locked state
|
|
575
|
+
// Locked state: the submit button is visibly disabled, but a
|
|
573
576
|
// native form submit (Enter key) reaches this handler regardless of the
|
|
574
577
|
// button's disabled attribute — block it here too, not just in the UI.
|
|
575
578
|
if (disabled || submitBlocked)
|
|
@@ -582,9 +585,9 @@ export function RenderEdit(props) {
|
|
|
582
585
|
setIsSubmitting(true);
|
|
583
586
|
setExtensionErrorKey(null);
|
|
584
587
|
try {
|
|
585
|
-
// Extension-only: only a section is dirty
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
+
// Extension-only: only a section is dirty and the main form is unchanged.
|
|
589
|
+
// Skip the entity write (it would send an empty changes payload) and only
|
|
590
|
+
// run the section handlers.
|
|
588
591
|
if (snapshot.isUnchanged && extensionDirty) {
|
|
589
592
|
// Same discard as the main path: an extension-only save is still a
|
|
590
593
|
// successful submit, so the draft must not survive it.
|
|
@@ -597,22 +600,19 @@ export function RenderEdit(props) {
|
|
|
597
600
|
}
|
|
598
601
|
let result;
|
|
599
602
|
if (customSubmit !== undefined) {
|
|
600
|
-
// customSubmit path (e.g. configEdit, which
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
// erfolgreichen Submit (isUnchanged blieb sonst false).
|
|
603
|
+
// customSubmit path (e.g. configEdit, which performs a separate write per
|
|
604
|
+
// field). Validate on the client first, then call the caller; on success,
|
|
605
|
+
// explicitly rebase the form state because controller.submit() normally
|
|
606
|
+
// does this itself, and the controller otherwise knows nothing about a
|
|
607
|
+
// successful custom submission (isUnchanged would remain false).
|
|
606
608
|
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
// immer aktuell — der Controller ist die Source-of-Truth, die
|
|
615
|
-
// React state is only a mirror for rendering.
|
|
609
|
+
// Read the snapshot directly from the controller rather than React state.
|
|
610
|
+
// With rapid fill-then-click input, React batching may not have committed
|
|
611
|
+
// state updates when the submit click fires. handleSubmit's closure could otherwise
|
|
612
|
+
// see stale snapshot.changes={} and perform no writes, then return success
|
|
613
|
+
// and rebase the form, making it appear that the data was saved.
|
|
614
|
+
// controller.getSnapshot() is always current: the controller is the
|
|
615
|
+
// source of truth, while React state is only a rendering mirror.
|
|
616
616
|
const valid = controller.validate(scopeFieldNames);
|
|
617
617
|
if (!valid) {
|
|
618
618
|
// Field order matters: validationBlocked:true is its own
|
|
@@ -642,7 +642,7 @@ export function RenderEdit(props) {
|
|
|
642
642
|
// `isNoOp: true` (payloadMode "changes", pre-filled form submitted
|
|
643
643
|
// untouched) means controller.submit() never called dispatcher.write
|
|
644
644
|
// — nothing to discard, and discarding here would delete a draft the
|
|
645
|
-
// form never persisted
|
|
645
|
+
// form never persisted. Awaited, not fire-and-forget:
|
|
646
646
|
// `onSubmit` typically navigates away and unmounts this form, which
|
|
647
647
|
// would abort an in-flight discard and leave the draft behind after
|
|
648
648
|
// a successful submit.
|
|
@@ -679,7 +679,7 @@ export function RenderEdit(props) {
|
|
|
679
679
|
// otherwise invisible — jump to the first step that contains one of
|
|
680
680
|
// the errored fields. For tabs this is what keeps the submit from
|
|
681
681
|
// blocking in silence, the failure mode the boot-validator used to
|
|
682
|
-
// prevent by rejecting the layout outright
|
|
682
|
+
// prevent by rejecting the layout outright. An issue no
|
|
683
683
|
// rendered field can show (`version`, `id`, a root-level refine, a
|
|
684
684
|
// hidden field) keeps the banner, jump or not.
|
|
685
685
|
if (isStepped) {
|
|
@@ -850,14 +850,14 @@ export function RenderEdit(props) {
|
|
|
850
850
|
: hasDirtyFooter
|
|
851
851
|
? "kumiko.form.saveChanges"
|
|
852
852
|
: "kumiko.actions.save")) })), writeFormFooterAction] }));
|
|
853
|
-
// Title + Subtitle, create/edit-
|
|
853
|
+
// Title + Subtitle, create/edit-aware. i18n-Keys (mode = "create"|"edit"):
|
|
854
854
|
// screen:<id>.<mode>.title / .<mode>.subtitle (mode = "create"|"edit"|"view")
|
|
855
|
-
// Fallback
|
|
855
|
+
// Fallback chain: mode-specific → generic (screen:<id>.title/.subtitle).
|
|
856
856
|
// title falls back to screenId; subtitle falls back to screen.description,
|
|
857
857
|
// itself run through translate() so an i18n key (the established
|
|
858
858
|
// convention for description, e.g. bundled-features) resolves instead of
|
|
859
859
|
// showing the raw key — translate() returns the input unchanged for plain
|
|
860
|
-
// prose, same as it does for an unknown key
|
|
860
|
+
// prose, same as it does for an unknown key. Same slot
|
|
861
861
|
// section.description already fills for a section. Falls back to
|
|
862
862
|
// undefined (no subtitle) when neither is set.
|
|
863
863
|
const formMode = isCreate ? "create" : viewMode ? "view" : "edit";
|
|
@@ -984,7 +984,7 @@ export function RenderEdit(props) {
|
|
|
984
984
|
// survives navigating past its step — otherwise Finish only ran the
|
|
985
985
|
// last mounted step's handler and silently dropped earlier steps'
|
|
986
986
|
// writes. For tabs this is also what makes ONE submit cover every
|
|
987
|
-
// tab, including tabs the user never opened
|
|
987
|
+
// tab, including tabs the user never opened.
|
|
988
988
|
const stepHidden = isStepped && sectionIndex !== currentStep;
|
|
989
989
|
const wrapWizardStep = (key, el) => {
|
|
990
990
|
if (!isStepped)
|
|
@@ -1025,13 +1025,13 @@ export function RenderEdit(props) {
|
|
|
1025
1025
|
}
|
|
1026
1026
|
if (section.kind === "relatedList") {
|
|
1027
1027
|
// parentId is the displayed record's id — without it there's no
|
|
1028
|
-
// parent row whose related rows could be queried
|
|
1028
|
+
// parent row whose related rows could be queried.
|
|
1029
1029
|
// Rejected at boot in wizard layouts, so no WizardStepGroup here.
|
|
1030
1030
|
const parentId = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
1031
1031
|
if (parentId === null)
|
|
1032
1032
|
return null;
|
|
1033
1033
|
const mount = (_jsx(RelatedListSection, { section: section, parentId: parentId,
|
|
1034
|
-
// @cast-boundary form-values: TValues
|
|
1034
|
+
// @cast-boundary form-values: TValues is structurally a Record.
|
|
1035
1035
|
record: snapshot.values, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, ...(refreshToken !== undefined && { refreshToken }), ...(fillScreenHeight === true && { grow: true }), ...(hideSectionTitles !== true &&
|
|
1036
1036
|
sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(onRelatedListDrawerAction !== undefined && {
|
|
1037
1037
|
onOpenDrawer: onRelatedListDrawerAction,
|
|
@@ -1048,9 +1048,10 @@ export function RenderEdit(props) {
|
|
|
1048
1048
|
}
|
|
1049
1049
|
if (!section.visible)
|
|
1050
1050
|
return null;
|
|
1051
|
-
//
|
|
1051
|
+
// Title-less sections would otherwise collide on key/testId, so use the index
|
|
1052
|
+
// as a fallback.
|
|
1052
1053
|
const sectionKey = section.title ?? `section-${sectionIndex}`;
|
|
1053
|
-
const renderFieldGrid = (fields, columns, gridKey) => (_jsx(Grid, { columns: columns, ...(isSettingsList ? { list: true } : usesFieldFlow && { flow: true }), children: fields.map((field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(!isSettingsList && usesFieldFlow && { flow: true }), ...(isSettingsList && {
|
|
1054
|
+
const renderFieldGrid = (fields, columns, gridKey, columnsDeclared) => (_jsx(Grid, { columns: usesFieldFlow && columnsDeclared !== true ? 1 : columns, ...(isSettingsList ? { list: true } : usesFieldFlow && { flow: true }), children: fields.map((field) => (_jsx(GridCellForField, { field: disabled ? { ...field, readOnly: true } : field, columns: columns, ...(!isSettingsList && usesFieldFlow && { flow: true }), ...(isSettingsList && {
|
|
1054
1055
|
layout: "row",
|
|
1055
1056
|
accent: fieldAccent?.(field.field) === true,
|
|
1056
1057
|
}), issues: snapshot.errors[field.field], onChange: (v) => {
|
|
@@ -1077,7 +1078,7 @@ export function RenderEdit(props) {
|
|
|
1077
1078
|
// cannot stand in for the tabs-mode card here.
|
|
1078
1079
|
if (hideSectionTitles === true) {
|
|
1079
1080
|
if (section.groups !== undefined) {
|
|
1080
|
-
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(group.fields, group.columns, `${sectionKey}-group-${groupIndex}-grid
|
|
1081
|
+
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(group.fields, group.columns, `${sectionKey}-group-${groupIndex}-grid`, group.columnsDeclared) }, group.title))) }, `${sectionKey}-groups`));
|
|
1081
1082
|
// `actions` on a `groups` section has no group of its own to
|
|
1082
1083
|
// sit in and would need an extra title-less Card around the
|
|
1083
1084
|
// group cards (Card-in-Card) — the boot-validator rejects that
|
|
@@ -1085,7 +1086,7 @@ export function RenderEdit(props) {
|
|
|
1085
1086
|
// undefined here at runtime.
|
|
1086
1087
|
return wrapWizardStep(sectionKey, _jsx(Card, { options: { framed: false }, children: groupsGrid }, sectionKey));
|
|
1087
1088
|
}
|
|
1088
|
-
// No section title here
|
|
1089
|
+
// No section title here — the Tab strip right above
|
|
1089
1090
|
// already names this panel, so a repeated Card title would just
|
|
1090
1091
|
// duplicate it. The header row only exists to carry the section
|
|
1091
1092
|
// description and actions, never the title text.
|
|
@@ -1094,7 +1095,7 @@ export function RenderEdit(props) {
|
|
|
1094
1095
|
...(section.description !== undefined && { subtitle: section.description }),
|
|
1095
1096
|
...(sectionActionsEl !== undefined && { headerActions: sectionActionsEl }),
|
|
1096
1097
|
},
|
|
1097
|
-
}), testId: `section-${sectionKey}`, children: renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid
|
|
1098
|
+
}), testId: `section-${sectionKey}`, children: renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`, section.columnsDeclared) }, sectionKey));
|
|
1098
1099
|
return wrapWizardStep(sectionKey, cardEl);
|
|
1099
1100
|
}
|
|
1100
1101
|
// Suppress the section header when it would just repeat the form
|
|
@@ -1111,7 +1112,7 @@ export function RenderEdit(props) {
|
|
|
1111
1112
|
const sectionSubtitle = section.description ?? (headerFallback ? formSubtitle : undefined);
|
|
1112
1113
|
const sectionEl = (_jsxs(Section, { ...(sectionNavItems.some((item) => item.id === formSectionDomId(sectionIndex)) && {
|
|
1113
1114
|
id: formSectionDomId(sectionIndex),
|
|
1114
|
-
}), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(sectionSubtitle !== undefined && { subtitle: sectionSubtitle }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(isSettingsList && { layout: "settings-list" }), testId: `section-${sectionKey}`, children: [showsAllRequiredHint && insideDrawer && sectionIndex === 0 && allRequiredHint, renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid
|
|
1115
|
+
}), ...(sectionTitle !== undefined && { title: sectionTitle }), ...(sectionSubtitle !== undefined && { subtitle: sectionSubtitle }), ...(section.icon !== undefined && { icon: section.icon }), ...(sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(isSettingsList && { layout: "settings-list" }), testId: `section-${sectionKey}`, children: [showsAllRequiredHint && insideDrawer && sectionIndex === 0 && allRequiredHint, renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`, section.columnsDeclared)] }, sectionKey));
|
|
1115
1116
|
return wrapWizardStep(sectionKey, sectionEl);
|
|
1116
1117
|
}), formError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-form-error", actions: formError.code === "version_conflict" && onReload !== undefined ? (_jsx(Button, { variant: "secondary", onClick: () => {
|
|
1117
1118
|
onReload();
|
|
@@ -5,14 +5,14 @@ export type RenderFieldProps = {
|
|
|
5
5
|
readonly field: EditFieldViewModel;
|
|
6
6
|
readonly issues?: readonly FieldIssue[];
|
|
7
7
|
readonly onChange: (val: unknown) => void;
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
/** Relevant only for type:"reference": the feature name for the lookup
|
|
9
|
+
* query QN (`<feature>:query:<refEntity>:list`). Other field types
|
|
10
|
+
* ignore this prop. */
|
|
11
11
|
readonly featureName?: string;
|
|
12
|
-
/**
|
|
12
|
+
/** Optional content rendered after the label (for example,
|
|
13
13
|
* ConfigSourceBadge). */
|
|
14
14
|
readonly labelAppendix?: ReactNode;
|
|
15
|
-
/**
|
|
15
|
+
/** Optional content rendered after the input (for example,
|
|
16
16
|
* ConfigCascade). */
|
|
17
17
|
readonly fieldAppendix?: ReactNode;
|
|
18
18
|
readonly appendixPlacement?: FieldProps["appendixPlacement"];
|
|
@@ -26,12 +26,12 @@ export type RenderFieldProps = {
|
|
|
26
26
|
/** "form" (default) renders every field as its editable Input, disabled
|
|
27
27
|
* when `field.readOnly` — unchanged behavior. "text" renders a
|
|
28
28
|
* `field.readOnly` field as plain text instead (projectionDetail's read
|
|
29
|
-
* view
|
|
29
|
+
* view); editable fields are untouched by this prop either way. */
|
|
30
30
|
readonly valueDisplay?: "form" | "text";
|
|
31
31
|
/** Current form values, keyed by field name. Consulted when
|
|
32
32
|
* `field.renderer` resolves to a `{ react: { __component } }` registry
|
|
33
33
|
* component (passed through as `ColumnRendererProps.row`, same contract
|
|
34
|
-
* as list-column renderers
|
|
34
|
+
* as list-column renderers), and to resolve a `type: "number"`
|
|
35
35
|
* field's sibling-field `unit`. Omitted → falls back to a single-key
|
|
36
36
|
* `{ [field.field]: field.value }` row. */
|
|
37
37
|
readonly row?: Readonly<Record<string, unknown>>;
|