@cosmicdrift/kumiko-renderer 0.386.0 → 0.387.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 +2 -2
- package/dist/app/action-form-shim.d.ts +8 -8
- package/dist/app/action-form-shim.js +20 -22
- 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 +1 -1
- 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.js +9 -9
- 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 +24 -25
- package/dist/app/extension-sections.js +10 -10
- package/dist/app/feature-schema.js +11 -11
- package/dist/app/form-schema.js +1 -1
- package/dist/app/header-actions-bar.js +1 -1
- 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/embedded-list-field.js +4 -4
- 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 +1 -1
- 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 +63 -65
- package/dist/components/render-field.d.ts +7 -7
- package/dist/components/render-field.js +57 -59
- package/dist/components/render-list.d.ts +42 -43
- package/dist/components/render-list.js +63 -67
- 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/headless/apex/index.d.ts +1 -2
- package/dist/headless/apex/index.js +5 -6
- package/dist/headless/form/form-controller.js +6 -6
- package/dist/headless/form/zod-bridge.js +2 -2
- package/dist/headless/format/money.js +1 -1
- package/dist/headless/index.js +2 -2
- package/dist/headless/view-model/edit.js +2 -2
- package/dist/headless/view-model/list.js +3 -4
- package/dist/headless/view-model/types.d.ts +3 -3
- 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.js +16 -17
- package/dist/hooks/use-store.js +18 -22
- package/dist/hooks/use-stream-handler.js +4 -2
- package/dist/i18n-defaults.js +2 -8
- package/dist/i18n.d.ts +10 -10
- package/dist/i18n.js +25 -25
- package/dist/index.js +5 -4
- package/dist/live/dispatcher-live.js +2 -2
- package/dist/primitives.d.ts +225 -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 +21 -22
- 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/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/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/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/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/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 +2 -3
- 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 +1 -1
- package/dist/web/primitives/field.js +9 -8
- package/dist/web/primitives/form.d.ts +1 -1
- package/dist/web/primitives/form.js +6 -6
- package/dist/web/primitives/input.js +2 -2
- package/dist/web/primitives/layout-elements.js +1 -1
- 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/pager.js +1 -1
- package/dist/web/primitives/row-actions.js +3 -4
- 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/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/charts.d.ts +10 -10
- package/dist/web/widgets/charts.js +8 -8
- package/dist/web/widgets/collapsible-section.d.ts +3 -3
- package/dist/web/widgets/collapsible-section.js +6 -6
- package/dist/web/widgets/detail-list.d.ts +1 -3
- package/dist/web/widgets/detail-list.js +1 -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.js +4 -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/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/use-draft.d.ts +7 -7
- package/dist/web/widgets/use-draft.js +7 -7
- package/package.json +6 -6
|
@@ -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;
|
|
@@ -250,9 +250,9 @@ export function RenderEdit(props) {
|
|
|
250
250
|
};
|
|
251
251
|
}, []);
|
|
252
252
|
// patch() (controlled mode, extension sections) applies immediately —
|
|
253
|
-
// only the resulting draft save is debounced
|
|
253
|
+
// only the resulting draft save is debounced, so a patch() burst
|
|
254
254
|
// (VIN-decode filling several fields, or onChange fanning a keystroke out
|
|
255
|
-
// through patch(), see the
|
|
255
|
+
// through patch(), see the VIN-decode test above) collapses into one save.
|
|
256
256
|
const patchAndScheduleDraftSave = useCallback((partial) => {
|
|
257
257
|
controller.setValues(partial);
|
|
258
258
|
if (!draftEnabled || dispatcher === undefined || disabled || submittedRef.current)
|
|
@@ -270,7 +270,7 @@ export function RenderEdit(props) {
|
|
|
270
270
|
// execution order) so that an onChange fired on mount always sees
|
|
271
271
|
// `controls !== undefined` — both effects depend only on mount-stable
|
|
272
272
|
// values (controller/scopedValidate/patchAndScheduleDraftSave), so the
|
|
273
|
-
// swap is otherwise a no-op. See the
|
|
273
|
+
// swap is otherwise a no-op. See the VIN-decode test: an initial
|
|
274
274
|
// value that should derive dependent fields on mount needs controls.patch
|
|
275
275
|
// available on the very first onChange call, not just from the second
|
|
276
276
|
// keystroke onward.
|
|
@@ -377,7 +377,7 @@ export function RenderEdit(props) {
|
|
|
377
377
|
// run their own query / own submit), so they always pass through. A
|
|
378
378
|
// section with no fields at all (e.g. a review-only step) has
|
|
379
379
|
// `visible: fields.some(...)` = false vacuously; that's "no fields to
|
|
380
|
-
// hide", not "hidden", so it stays too
|
|
380
|
+
// hide", not "hidden", so it stays too. Unless the create
|
|
381
381
|
// exclusion emptied it: that would leave an empty wizard step / tab.
|
|
382
382
|
() => filterEditSections(vm.sections, fieldsFilter).filter((section) => section.kind === "extension" ||
|
|
383
383
|
section.kind === "relatedList" ||
|
|
@@ -391,9 +391,9 @@ export function RenderEdit(props) {
|
|
|
391
391
|
return fillable.length > 0 && fillable.every((field) => field.required);
|
|
392
392
|
}, [filteredSections]);
|
|
393
393
|
const showsAllRequiredHint = allFieldsRequired && hideSectionTitles !== true && !isWizard;
|
|
394
|
-
// true when editable fields exist or an extension opted into composed submit
|
|
394
|
+
// true when editable fields exist or an extension opted into composed submit.
|
|
395
395
|
const isFormEditable = hasEditableSection(filteredSections);
|
|
396
|
-
// A fieldless form (input-less secretMint mint
|
|
396
|
+
// A fieldless form (input-less secretMint mint — the secret is
|
|
397
397
|
// server-generated, the mint step is only its submit button) has no
|
|
398
398
|
// section to gate on: it IS the action, so it always shows the submit
|
|
399
399
|
// button, and it can never go dirty either.
|
|
@@ -404,8 +404,8 @@ export function RenderEdit(props) {
|
|
|
404
404
|
// reaches this line and `filteredSections[0]` stays the active section)
|
|
405
405
|
// needs its own tab panel capped at the available height so its table
|
|
406
406
|
// scrolls inside the panel instead of the whole page stretching to the
|
|
407
|
-
// row count
|
|
408
|
-
// of stretching the panel to the bottom
|
|
407
|
+
// row count — a short table still sizes to its content instead
|
|
408
|
+
// of stretching the panel to the bottom. Any other layout —
|
|
409
409
|
// multiple sections, a non-relatedList tab, stacked (non-tabs) forms —
|
|
410
410
|
// keeps normal document-flow height untouched.
|
|
411
411
|
const fillHeight = fillScreenHeight === true ||
|
|
@@ -415,9 +415,9 @@ export function RenderEdit(props) {
|
|
|
415
415
|
// instead of sitting in the section's title row.
|
|
416
416
|
const isLoneWriteFormTab = hideSectionTitles === true && filteredSections[0]?.kind === "writeForm";
|
|
417
417
|
const [writeFormFooterAction, setWriteFormFooterAction] = useState(undefined);
|
|
418
|
-
//
|
|
419
|
-
// false
|
|
420
|
-
//
|
|
418
|
+
// Persist all composed extension sections using the resolved entityId.
|
|
419
|
+
// false means a section failed and its i18n key is shown in the banner. Without
|
|
420
|
+
// entity context (create mode without a route id), there is nothing to write.
|
|
421
421
|
async function persistExtensions() {
|
|
422
422
|
const entityId = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
423
423
|
if (entityId === null)
|
|
@@ -495,7 +495,7 @@ export function RenderEdit(props) {
|
|
|
495
495
|
function handleWizardNext() {
|
|
496
496
|
const next = Math.min(currentStep + 1, lastStepIndex);
|
|
497
497
|
hasNavigatedRef.current = true;
|
|
498
|
-
// Locked state
|
|
498
|
+
// Locked state: `disabled` means "no input/no write", not "no
|
|
499
499
|
// navigation" — a disabled wizard must still be steppable so Back/Next
|
|
500
500
|
// reach every step. But neither validate() (would block on an empty
|
|
501
501
|
// required field nobody can fill in) nor saveDraft() (would mint a
|
|
@@ -569,7 +569,7 @@ export function RenderEdit(props) {
|
|
|
569
569
|
handleWizardNext();
|
|
570
570
|
return;
|
|
571
571
|
}
|
|
572
|
-
// Locked state
|
|
572
|
+
// Locked state: the submit button is visibly disabled, but a
|
|
573
573
|
// native form submit (Enter key) reaches this handler regardless of the
|
|
574
574
|
// button's disabled attribute — block it here too, not just in the UI.
|
|
575
575
|
if (disabled || submitBlocked)
|
|
@@ -582,9 +582,9 @@ export function RenderEdit(props) {
|
|
|
582
582
|
setIsSubmitting(true);
|
|
583
583
|
setExtensionErrorKey(null);
|
|
584
584
|
try {
|
|
585
|
-
// Extension-only: only a section is dirty
|
|
586
|
-
//
|
|
587
|
-
//
|
|
585
|
+
// Extension-only: only a section is dirty and the main form is unchanged.
|
|
586
|
+
// Skip the entity write (it would send an empty changes payload) and only
|
|
587
|
+
// run the section handlers.
|
|
588
588
|
if (snapshot.isUnchanged && extensionDirty) {
|
|
589
589
|
// Same discard as the main path: an extension-only save is still a
|
|
590
590
|
// successful submit, so the draft must not survive it.
|
|
@@ -597,22 +597,19 @@ export function RenderEdit(props) {
|
|
|
597
597
|
}
|
|
598
598
|
let result;
|
|
599
599
|
if (customSubmit !== undefined) {
|
|
600
|
-
// customSubmit path (e.g. configEdit, which
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
// erfolgreichen Submit (isUnchanged blieb sonst false).
|
|
600
|
+
// customSubmit path (e.g. configEdit, which performs a separate write per
|
|
601
|
+
// field). Validate on the client first, then call the caller; on success,
|
|
602
|
+
// explicitly rebase the form state because controller.submit() normally
|
|
603
|
+
// does this itself, and the controller otherwise knows nothing about a
|
|
604
|
+
// successful custom submission (isUnchanged would remain false).
|
|
606
605
|
//
|
|
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.
|
|
606
|
+
// Read the snapshot directly from the controller rather than React state.
|
|
607
|
+
// With rapid fill-then-click input, React batching may not have committed
|
|
608
|
+
// state updates when the submit click fires. handleSubmit's closure could otherwise
|
|
609
|
+
// see stale snapshot.changes={} and perform no writes, then return success
|
|
610
|
+
// and rebase the form, making it appear that the data was saved.
|
|
611
|
+
// controller.getSnapshot() is always current: the controller is the
|
|
612
|
+
// source of truth, while React state is only a rendering mirror.
|
|
616
613
|
const valid = controller.validate(scopeFieldNames);
|
|
617
614
|
if (!valid) {
|
|
618
615
|
// Field order matters: validationBlocked:true is its own
|
|
@@ -642,7 +639,7 @@ export function RenderEdit(props) {
|
|
|
642
639
|
// `isNoOp: true` (payloadMode "changes", pre-filled form submitted
|
|
643
640
|
// untouched) means controller.submit() never called dispatcher.write
|
|
644
641
|
// — nothing to discard, and discarding here would delete a draft the
|
|
645
|
-
// form never persisted
|
|
642
|
+
// form never persisted. Awaited, not fire-and-forget:
|
|
646
643
|
// `onSubmit` typically navigates away and unmounts this form, which
|
|
647
644
|
// would abort an in-flight discard and leave the draft behind after
|
|
648
645
|
// a successful submit.
|
|
@@ -679,7 +676,7 @@ export function RenderEdit(props) {
|
|
|
679
676
|
// otherwise invisible — jump to the first step that contains one of
|
|
680
677
|
// the errored fields. For tabs this is what keeps the submit from
|
|
681
678
|
// blocking in silence, the failure mode the boot-validator used to
|
|
682
|
-
// prevent by rejecting the layout outright
|
|
679
|
+
// prevent by rejecting the layout outright. An issue no
|
|
683
680
|
// rendered field can show (`version`, `id`, a root-level refine, a
|
|
684
681
|
// hidden field) keeps the banner, jump or not.
|
|
685
682
|
if (isStepped) {
|
|
@@ -850,14 +847,14 @@ export function RenderEdit(props) {
|
|
|
850
847
|
: hasDirtyFooter
|
|
851
848
|
? "kumiko.form.saveChanges"
|
|
852
849
|
: "kumiko.actions.save")) })), writeFormFooterAction] }));
|
|
853
|
-
// Title + Subtitle, create/edit-
|
|
850
|
+
// Title + Subtitle, create/edit-aware. i18n-Keys (mode = "create"|"edit"):
|
|
854
851
|
// screen:<id>.<mode>.title / .<mode>.subtitle (mode = "create"|"edit"|"view")
|
|
855
|
-
// Fallback
|
|
852
|
+
// Fallback chain: mode-specific → generic (screen:<id>.title/.subtitle).
|
|
856
853
|
// title falls back to screenId; subtitle falls back to screen.description,
|
|
857
854
|
// itself run through translate() so an i18n key (the established
|
|
858
855
|
// convention for description, e.g. bundled-features) resolves instead of
|
|
859
856
|
// showing the raw key — translate() returns the input unchanged for plain
|
|
860
|
-
// prose, same as it does for an unknown key
|
|
857
|
+
// prose, same as it does for an unknown key. Same slot
|
|
861
858
|
// section.description already fills for a section. Falls back to
|
|
862
859
|
// undefined (no subtitle) when neither is set.
|
|
863
860
|
const formMode = isCreate ? "create" : viewMode ? "view" : "edit";
|
|
@@ -984,7 +981,7 @@ export function RenderEdit(props) {
|
|
|
984
981
|
// survives navigating past its step — otherwise Finish only ran the
|
|
985
982
|
// last mounted step's handler and silently dropped earlier steps'
|
|
986
983
|
// writes. For tabs this is also what makes ONE submit cover every
|
|
987
|
-
// tab, including tabs the user never opened
|
|
984
|
+
// tab, including tabs the user never opened.
|
|
988
985
|
const stepHidden = isStepped && sectionIndex !== currentStep;
|
|
989
986
|
const wrapWizardStep = (key, el) => {
|
|
990
987
|
if (!isStepped)
|
|
@@ -1025,13 +1022,13 @@ export function RenderEdit(props) {
|
|
|
1025
1022
|
}
|
|
1026
1023
|
if (section.kind === "relatedList") {
|
|
1027
1024
|
// parentId is the displayed record's id — without it there's no
|
|
1028
|
-
// parent row whose related rows could be queried
|
|
1025
|
+
// parent row whose related rows could be queried.
|
|
1029
1026
|
// Rejected at boot in wizard layouts, so no WizardStepGroup here.
|
|
1030
1027
|
const parentId = resolveExtensionEntityId(entityIdProp, vm.id);
|
|
1031
1028
|
if (parentId === null)
|
|
1032
1029
|
return null;
|
|
1033
1030
|
const mount = (_jsx(RelatedListSection, { section: section, parentId: parentId,
|
|
1034
|
-
// @cast-boundary form-values: TValues
|
|
1031
|
+
// @cast-boundary form-values: TValues is structurally a Record.
|
|
1035
1032
|
record: snapshot.values, featureName: featureName, translate: translate, hideTitle: hideSectionTitles, ...(refreshToken !== undefined && { refreshToken }), ...(fillScreenHeight === true && { grow: true }), ...(hideSectionTitles !== true &&
|
|
1036
1033
|
sectionActionsEl !== undefined && { actions: sectionActionsEl }), ...(onRelatedListDrawerAction !== undefined && {
|
|
1037
1034
|
onOpenDrawer: onRelatedListDrawerAction,
|
|
@@ -1048,7 +1045,8 @@ export function RenderEdit(props) {
|
|
|
1048
1045
|
}
|
|
1049
1046
|
if (!section.visible)
|
|
1050
1047
|
return null;
|
|
1051
|
-
//
|
|
1048
|
+
// Title-less sections would otherwise collide on key/testId, so use the index
|
|
1049
|
+
// as a fallback.
|
|
1052
1050
|
const sectionKey = section.title ?? `section-${sectionIndex}`;
|
|
1053
1051
|
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
1052
|
layout: "row",
|
|
@@ -1085,7 +1083,7 @@ export function RenderEdit(props) {
|
|
|
1085
1083
|
// undefined here at runtime.
|
|
1086
1084
|
return wrapWizardStep(sectionKey, _jsx(Card, { options: { framed: false }, children: groupsGrid }, sectionKey));
|
|
1087
1085
|
}
|
|
1088
|
-
// No section title here
|
|
1086
|
+
// No section title here — the Tab strip right above
|
|
1089
1087
|
// already names this panel, so a repeated Card title would just
|
|
1090
1088
|
// duplicate it. The header row only exists to carry the section
|
|
1091
1089
|
// description and actions, never the title text.
|
|
@@ -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>>;
|
|
@@ -29,11 +29,10 @@ const AllFieldsRequiredContext = createContext(false);
|
|
|
29
29
|
export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
|
|
30
30
|
export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay = "form", row, changed, layout, accent, fillDisabled, hideLabel, }) {
|
|
31
31
|
const { Field, Input, Banner, Text, JsonView } = usePrimitives();
|
|
32
|
-
//
|
|
33
|
-
// navigator.language
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// Standalone-Consumer/Tests müssen wrappen (createKumikoApp tut es).
|
|
32
|
+
// The app locale is required for money/date inputs; otherwise they fall back
|
|
33
|
+
// to navigator.language instead of the selected app language. RenderField is
|
|
34
|
+
// publicly exported and therefore requires a LocaleProvider; standalone
|
|
35
|
+
// consumers and tests must wrap it (createKumikoApp does this).
|
|
37
36
|
const appLocale = useLocale().locale();
|
|
38
37
|
const t = useTranslation();
|
|
39
38
|
const allFieldsRequired = useContext(AllFieldsRequiredContext);
|
|
@@ -45,13 +44,13 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
45
44
|
// the list-column path (render-list.tsx). Only takes effect once the field
|
|
46
45
|
// is actually readOnly — a FormatSpec/PlatformComponent renderer has no
|
|
47
46
|
// editable widget of its own, so applying it to an editable field would
|
|
48
|
-
// silently make that field un-editable
|
|
47
|
+
// silently make that field un-editable.
|
|
49
48
|
const readOnlyText = valueDisplay === "text" && field.readOnly && field.renderer === undefined;
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// The list input prints its own list-level issues (empty state
|
|
54
|
-
// the Field wrapper must not print them a second time.
|
|
49
|
+
// The reference field renders its own component because it uses useQuery()
|
|
50
|
+
// for the live lookup and must therefore be mounted as a React component,
|
|
51
|
+
// not called as a pure render function.
|
|
52
|
+
// The list input prints its own list-level issues (including the empty state),
|
|
53
|
+
// so the Field wrapper must not print them a second time.
|
|
55
54
|
const listPrintsOwnIssues = field.type === "embedded" &&
|
|
56
55
|
field.embeddedListCells !== undefined &&
|
|
57
56
|
!(field.readOnly && field.renderer !== undefined);
|
|
@@ -168,38 +167,35 @@ function WriteOnlyTextInput({ field, id, hasError, onChange, changed, }) {
|
|
|
168
167
|
},
|
|
169
168
|
}) }));
|
|
170
169
|
}
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// -
|
|
176
|
-
// -
|
|
177
|
-
//
|
|
178
|
-
// - Multi-Mode (Tier 2.7e-Multi via field.refMultiple): value ist
|
|
179
|
-
// ein UUID-Array, Combobox rendert Selected-Tags.
|
|
170
|
+
// ReferenceInput renders a searchable combobox populated by a live query for
|
|
171
|
+
// the referenced entity. With the default limit of 200, larger datasets miss
|
|
172
|
+
// the tail in the dropdown; a server-side debounced search query comes later.
|
|
173
|
+
// - Display = row[refLabelField], default labelField "id".
|
|
174
|
+
// - During loading, the dropdown is empty and the field is disabled.
|
|
175
|
+
// - In multiple mode (field.refMultiple), value is a UUID array and the
|
|
176
|
+
// combobox renders tags.
|
|
180
177
|
//
|
|
181
|
-
// Storage: UI
|
|
182
|
-
// Server
|
|
183
|
-
// REFERENCE_COMBOBOX_LIMIT
|
|
184
|
-
// (
|
|
178
|
+
// Storage: the UI value is a UUID (row.id), or a UUID array in multiple mode.
|
|
179
|
+
// Server schema: z.uuid() or z.array(z.uuid()).
|
|
180
|
+
// REFERENCE_COMBOBOX_LIMIT is centralized in hooks/reference-limits.ts
|
|
181
|
+
// (see there for the rationale behind the default values).
|
|
185
182
|
function ReferenceInput({ field, id, hasError, onChange, Input, featureName, }) {
|
|
186
183
|
const { Banner } = usePrimitives();
|
|
187
184
|
const refEntity = field.refEntity ?? "";
|
|
188
185
|
const refFeature = field.refFeature ?? featureName;
|
|
189
186
|
const isMultiple = field.refMultiple === true;
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
187
|
+
// refFeature may differ from featureName (for example,
|
|
188
|
+
// items.assignee → users:query:user:list). The default is the same feature,
|
|
189
|
+
// as provided by the ViewModel (parseRefTarget).
|
|
190
|
+
// refOptionsQuery replaces that source with an author-supplied handler whose
|
|
191
|
+
// rows already carry a composed `label`.
|
|
195
192
|
const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// refFeature
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// Combobox rendert dann ohne den Footer.
|
|
193
|
+
// "+ Neu" in the combobox opens the referenced entity's create screen in a
|
|
194
|
+
// dialog instead of leaving the current form.
|
|
195
|
+
// refFeature may differ from the currently rendered feature; appFeatures
|
|
196
|
+
// (createKumikoApp) knows all feature schemas, not only the active screen.
|
|
197
|
+
// If the feature, screen, or entity is not registered, or allowCreate:false,
|
|
198
|
+
// onCreate remains undefined and the combobox renders without the footer.
|
|
203
199
|
const appFeatures = useAppFeatures();
|
|
204
200
|
const userRoles = useUserRoles();
|
|
205
201
|
const t = useTranslation();
|
|
@@ -210,10 +206,10 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
|
|
|
210
206
|
s.allowCreate !== false &&
|
|
211
207
|
screenAccessAllows(s.access, userRoles));
|
|
212
208
|
const refEntityDef = refTargetSchema?.entities[refEntity];
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
209
|
+
// When the user types in the combobox, the server filters through the
|
|
210
|
+
// existing list-payload `search` parameter. The combobox debounces the
|
|
211
|
+
// keystroke itself (300ms) and calls onSearchChange.
|
|
212
|
+
// An empty initial state requests the first 50 items in default order.
|
|
217
213
|
const [searchTerm, setSearchTerm] = useState("");
|
|
218
214
|
const queryPayload = useMemo(() => searchTerm === ""
|
|
219
215
|
? { limit: REFERENCE_COMBOBOX_LIMIT }
|
|
@@ -254,13 +250,14 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
|
|
|
254
250
|
return { value: idVal, label };
|
|
255
251
|
});
|
|
256
252
|
}, [queryResult.data, labelKey]);
|
|
257
|
-
// Single
|
|
258
|
-
//
|
|
259
|
-
//
|
|
253
|
+
// Single mode uses a String/null value; multiple mode uses an array. Coerce
|
|
254
|
+
// to the shape expected by the combobox so storage drift (for example, an
|
|
255
|
+
// old String from the server where an array is now expected) cannot crash it.
|
|
260
256
|
const baseInputProps = {
|
|
261
257
|
id,
|
|
262
258
|
name: field.field,
|
|
263
|
-
//
|
|
259
|
+
// Disable the input during the initial load; subsequent loads show a
|
|
260
|
+
// loading indicator in the popover.
|
|
264
261
|
disabled: field.readOnly || (queryResult.loading && options.length === 0),
|
|
265
262
|
required: field.required,
|
|
266
263
|
hasError,
|
|
@@ -295,7 +292,7 @@ function FieldRendererOutput({ field, renderer, row, appLocale, }) {
|
|
|
295
292
|
if (isFormatSpec(renderer)) {
|
|
296
293
|
// format:"json" wants the raw value (JsonView stringifies + highlights
|
|
297
294
|
// itself) — applyFormatSpec's already-indented string is only the
|
|
298
|
-
// fallback for primitives-providers without JsonView
|
|
295
|
+
// fallback for primitives-providers without JsonView.
|
|
299
296
|
if (renderer.format === "json" && JsonView !== undefined) {
|
|
300
297
|
return (_jsx(JsonView, { value: field.value, indent: renderer.indent, testId: `field-value-${field.field}` }));
|
|
301
298
|
}
|
|
@@ -308,8 +305,8 @@ function FieldRendererOutput({ field, renderer, row, appLocale, }) {
|
|
|
308
305
|
// App locale as default when the FormatSpec declares none of its own —
|
|
309
306
|
// otherwise locale-sensitive formats (timestamp/date/number/decimal/
|
|
310
307
|
// bigInt/unit) fell back to Intl's runtime default instead of the app
|
|
311
|
-
// language chosen via LocaleProvider
|
|
312
|
-
// when set; coalesce undefined (spread override) back to appLocale
|
|
308
|
+
// language chosen via LocaleProvider. Prefer renderer.locale
|
|
309
|
+
// when set; coalesce undefined (spread override) back to appLocale.
|
|
313
310
|
return (_jsx(Text, { testId: `field-value-${field.field}`, children: applyFormatSpec({
|
|
314
311
|
...renderer,
|
|
315
312
|
locale: stringPropertyOf(renderer, "locale") ?? appLocale,
|
|
@@ -417,7 +414,7 @@ function resolveFieldIcon(field) {
|
|
|
417
414
|
// from the EditFieldViewModel (computeEditViewModel pulls them from
|
|
418
415
|
// SelectFieldDef.options). Structural types without a widget (embedded,
|
|
419
416
|
// jsonb, multiSelect) render read-only instead of a data-destroying text
|
|
420
|
-
// fallback
|
|
417
|
+
// fallback; truly unknown scalar types still fall back to text.
|
|
421
418
|
function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner, Text, JsonView, t, row, fill, }) {
|
|
422
419
|
const common = {
|
|
423
420
|
id,
|
|
@@ -438,7 +435,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
438
435
|
// decimal/bigInt are both plain numbers on the wire (fieldToZod:
|
|
439
436
|
// z.number() / z.number().int().safe()) — the number input's onChange
|
|
440
437
|
// already emits `number | undefined`, so no extra coercion is needed
|
|
441
|
-
// beyond what "number" already does
|
|
438
|
+
// beyond what "number" already does.
|
|
442
439
|
case "number":
|
|
443
440
|
case "bigInt": {
|
|
444
441
|
const icon = resolveFieldIcon(field);
|
|
@@ -472,11 +469,11 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
472
469
|
case "locatedTimestamp":
|
|
473
470
|
return (_jsx(Input, { kind: "locatedTimestamp", ...common, value: locatedValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
|
|
474
471
|
case "select": {
|
|
475
|
-
// Translated
|
|
476
|
-
// (computeEditViewModel,
|
|
472
|
+
// Translated option labels come from the ViewModel builder
|
|
473
|
+
// (computeEditViewModel, convention key
|
|
477
474
|
// `<feature>:entity:<entity>:field:<field>:option:<value>`).
|
|
478
|
-
//
|
|
479
|
-
//
|
|
475
|
+
// If no translations are registered, fall back to the raw value as the
|
|
476
|
+
// label, which ComboboxInput then shows unchanged.
|
|
480
477
|
const selectOptions = field.optionLabels !== undefined ? staticSelectOptions(field) : (field.options ?? []);
|
|
481
478
|
// `display` is shared with multiSelect's "checkboxes", which is not a
|
|
482
479
|
// select presentation — narrow to the two a select field can request.
|
|
@@ -495,11 +492,11 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
495
492
|
case "images":
|
|
496
493
|
return (_jsx(FileListField, { field: field, id: id, kind: field.type === "images" ? "image" : "file", hasError: hasError, onChange: (ids) => onChange(ids) }));
|
|
497
494
|
// embedded (without embeddedListCells — that's embeddedList, which has
|
|
498
|
-
// had its own EmbeddedListField widget
|
|
495
|
+
// had its own EmbeddedListField widget) and jsonb carry
|
|
499
496
|
// arbitrary objects. Without a dedicated widget these must NOT fall
|
|
500
497
|
// through to a text input: stringValue() turns them into
|
|
501
498
|
// "[object Object]" and saving that overwrites the real data with the
|
|
502
|
-
// mangled string
|
|
499
|
+
// mangled string. A `required: true` on either is caught loudly
|
|
503
500
|
// at boot (validateNoWidgetRequiredField in the framework package)
|
|
504
501
|
// instead of silently failing here.
|
|
505
502
|
case "embedded":
|
|
@@ -519,7 +516,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
519
516
|
// is set (the view-model carries it), the renderer switches to
|
|
520
517
|
// textarea. longText always renders a textarea — that's the point of
|
|
521
518
|
// the type — regardless of whether `multiline` is set; `multiline`
|
|
522
|
-
// only supplies an optional `{ rows }` override for it
|
|
519
|
+
// only supplies an optional `{ rows }` override for it.
|
|
523
520
|
if (field.type === "longText" || (field.type === "text" && field.multiline)) {
|
|
524
521
|
const rows = typeof field.multiline === "object" ? field.multiline.rows : undefined;
|
|
525
522
|
return (_jsx(Input, { kind: "textarea", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(rows !== undefined && { rows }), ...placeholderProp }));
|
|
@@ -628,8 +625,9 @@ function moneyPayload(minorUnits, currency) {
|
|
|
628
625
|
return undefined;
|
|
629
626
|
return { amount: minorUnits / 10 ** currencyDecimals(currency), currency };
|
|
630
627
|
}
|
|
631
|
-
// locatedTimestamp
|
|
632
|
-
//
|
|
628
|
+
// A locatedTimestamp field value is supplied by the read wrapper as
|
|
629
|
+
// `{ at, tz, utc }`; an unset value becomes "" as the empty sentinel,
|
|
630
|
+
// consistent with money and timestamp fields.
|
|
633
631
|
function locatedValue(v) {
|
|
634
632
|
if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
|
|
635
633
|
const utc = propertyOf(v, "utc");
|
|
@@ -644,7 +642,7 @@ function locatedValue(v) {
|
|
|
644
642
|
// Read-only text for a `field.readOnly` field without its own `renderer` —
|
|
645
643
|
// per-type formatting that reuses the same value-shaping helpers as the
|
|
646
644
|
// editable widgets above, so a text value and its would-be Input widget stay
|
|
647
|
-
// derived from the identical parse
|
|
645
|
+
// derived from the identical parse. `type: "reference"` isn't
|
|
648
646
|
// covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
|
|
649
647
|
// `isComplexFieldType` types aren't covered either — callers keep those on
|
|
650
648
|
// `renderInput`'s existing Banner fallback.
|