@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/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 +9 -9
- package/src/changes.json +54 -0
|
@@ -1,32 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
|
-
import { createContext, Fragment, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
5
|
-
import { useAppFeatures } from "../app/app-features-context.js";
|
|
6
|
-
import { useColumnRenderer } from "../app/column-renderers.js";
|
|
7
|
-
import { extensionSectionName } from "../app/extension-sections.js";
|
|
8
|
-
import { toKebab } from "../app/qn.js";
|
|
9
|
-
import { useReferenceLinkResolver } from "../app/reference-link.js";
|
|
10
|
-
import { evalRowExtractor } from "../app/row-actions.js";
|
|
11
|
-
import { screenAccessAllows } from "../app/screen-access.js";
|
|
12
|
-
import { useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
13
|
-
import { useUserRoles } from "../context/user-roles-context.js";
|
|
14
|
-
import { rendererError, rendererWarn } from "../dev-console.js";
|
|
15
|
-
import { fileIdsOf } from "../file-ids-of.js";
|
|
16
|
-
import { applyFormatSpec, currencyDecimals, EMPTY_VALUE_PLACEHOLDER, } from "../headless/index.js";
|
|
17
|
-
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
|
|
18
|
-
import { useQuery } from "../hooks/use-query.js";
|
|
19
|
-
import { isCompleteRowPage, referenceOptionSource } from "../hooks/use-reference-lookup.js";
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
20
3
|
import { useLocale, useTranslation } from "../i18n.js";
|
|
21
4
|
import { usePrimitives } from "../primitives.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import { FileListField } from "./file-list-field.js";
|
|
25
|
-
import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
|
|
26
|
-
import { QueryOptionsSelect } from "./query-options-select.js";
|
|
27
|
-
import { ReferenceCreateDialog } from "./reference-create-dialog.js";
|
|
5
|
+
import { listPrintsOwnIssues, renderControl } from "./render-field-control.js";
|
|
6
|
+
import { inputId } from "./render-field-meta.js";
|
|
28
7
|
const AllFieldsRequiredContext = createContext(false);
|
|
29
8
|
export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
|
|
9
|
+
function fieldOptionalProps({ issues, printsIssues, labelAppendix, fieldAppendix, appendixPlacement, description, status, changed, layout, accent, hideLabel, }) {
|
|
10
|
+
return {
|
|
11
|
+
...(issues !== undefined && printsIssues && { issues }),
|
|
12
|
+
...(labelAppendix !== undefined && { labelAppendix }),
|
|
13
|
+
...(fieldAppendix !== undefined && { fieldAppendix }),
|
|
14
|
+
...(appendixPlacement !== undefined && { appendixPlacement }),
|
|
15
|
+
...(description !== undefined && { description }),
|
|
16
|
+
...(status !== undefined && { status }),
|
|
17
|
+
...(changed === true && { changed }),
|
|
18
|
+
...(layout !== undefined && { layout }),
|
|
19
|
+
...(accent === true && { accent }),
|
|
20
|
+
...(hideLabel === true && { hideLabel }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
30
23
|
export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay = "form", row, changed, layout, accent, fillDisabled, hideLabel, }) {
|
|
31
24
|
const { Field, Input, Banner, Text, JsonView } = usePrimitives();
|
|
32
25
|
// The app locale is required for money/date inputs; otherwise they fall back
|
|
@@ -39,40 +32,16 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
39
32
|
if (!field.visible)
|
|
40
33
|
return null;
|
|
41
34
|
const id = inputId(field);
|
|
42
|
-
const hasError = issues !== undefined && issues.length > 0;
|
|
43
35
|
// An author-declared renderer always wins over the input widget, same as
|
|
44
36
|
// the list-column path (render-list.tsx). Only takes effect once the field
|
|
45
37
|
// is actually readOnly — a FormatSpec/PlatformComponent renderer has no
|
|
46
38
|
// editable widget of its own, so applying it to an editable field would
|
|
47
39
|
// silently make that field un-editable.
|
|
48
40
|
const readOnlyText = valueDisplay === "text" && field.readOnly && field.renderer === undefined;
|
|
49
|
-
|
|
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.
|
|
54
|
-
const listPrintsOwnIssues = field.type === "embedded" &&
|
|
55
|
-
field.embeddedListCells !== undefined &&
|
|
56
|
-
!(field.readOnly && field.renderer !== undefined);
|
|
57
|
-
const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : field.type === "select" &&
|
|
58
|
-
field.selectOptionsAvailabilityQuery !== undefined &&
|
|
59
|
-
!readOnlyText ? (_jsx(AvailabilityOptionsSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, payload: field.selectOptionsAvailabilityQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) && !isFileFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale, t) })) : field.type === "text" && field.writeOnly === true && !readOnlyText ? (_jsx(WriteOnlyTextInput, { field: field, id: id, hasError: hasError, onChange: onChange, changed: changed === true })) : field.fillAction !== undefined ? (_jsx(FillActionControl, { field: field, fillAction: field.fillAction, row: row ?? { [field.field]: field.value }, onChange: onChange, disabled: fillDisabled === true, Banner: Banner, renderControl: (fill) => renderInput({
|
|
60
|
-
field,
|
|
61
|
-
id,
|
|
62
|
-
hasError,
|
|
63
|
-
onChange,
|
|
64
|
-
Input,
|
|
65
|
-
appLocale,
|
|
66
|
-
Banner,
|
|
67
|
-
Text,
|
|
68
|
-
JsonView,
|
|
69
|
-
t,
|
|
70
|
-
row,
|
|
71
|
-
fill,
|
|
72
|
-
}) })) : (renderInput({
|
|
41
|
+
const control = renderControl({
|
|
73
42
|
field,
|
|
74
43
|
id,
|
|
75
|
-
hasError,
|
|
44
|
+
hasError: issues !== undefined && issues.length > 0,
|
|
76
45
|
onChange,
|
|
77
46
|
Input,
|
|
78
47
|
appLocale,
|
|
@@ -80,624 +49,24 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
80
49
|
Text,
|
|
81
50
|
JsonView,
|
|
82
51
|
t,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// number — a string would be sent as a wrong-typed value.
|
|
90
|
-
function fillValueFor(fieldType, result) {
|
|
91
|
-
if (NUMERIC_FIELD_TYPES.has(fieldType))
|
|
92
|
-
return typeof result === "number" ? result : undefined;
|
|
93
|
-
if (typeof result === "string")
|
|
94
|
-
return result;
|
|
95
|
-
return typeof result === "number" ? String(result) : undefined;
|
|
96
|
-
}
|
|
97
|
-
// Owns the query state of a fill button. A separate component because RenderField
|
|
98
|
-
// returns early for hidden fields and the dispatcher hook must not be conditional.
|
|
99
|
-
function FillActionControl({ field, fillAction, row, onChange, disabled, Banner, renderControl, }) {
|
|
100
|
-
const dispatcher = useOptionalDispatcher();
|
|
101
|
-
const t = useTranslation();
|
|
102
|
-
const [busy, setBusy] = useState(false);
|
|
103
|
-
const [failed, setFailed] = useState(false);
|
|
104
|
-
// A newer click or an unmount cancels the in-flight query so a late answer
|
|
105
|
-
// cannot overwrite a value the user has since typed.
|
|
106
|
-
const activeRun = useRef(null);
|
|
107
|
-
useEffect(() => () => activeRun.current?.abort(), []);
|
|
108
|
-
const run = async () => {
|
|
109
|
-
if (dispatcher === undefined) {
|
|
110
|
-
setFailed(true);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
activeRun.current?.abort();
|
|
114
|
-
const ctrl = new AbortController();
|
|
115
|
-
activeRun.current = ctrl;
|
|
116
|
-
setBusy(true);
|
|
117
|
-
setFailed(false);
|
|
118
|
-
const payload = fillAction.params === undefined ? {} : evalRowExtractor(fillAction.params, row);
|
|
119
|
-
const result = await dispatcher.query(fillAction.query, payload, {
|
|
120
|
-
signal: ctrl.signal,
|
|
121
|
-
});
|
|
122
|
-
if (ctrl.signal.aborted)
|
|
123
|
-
return;
|
|
124
|
-
setBusy(false);
|
|
125
|
-
if (!result.isSuccess) {
|
|
126
|
-
setFailed(true);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const value = fillValueFor(field.type, propertyOf(result.data, fillAction.field));
|
|
130
|
-
if (value === undefined) {
|
|
131
|
-
setFailed(true);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
onChange(value);
|
|
135
|
-
};
|
|
136
|
-
return (_jsxs(_Fragment, { children: [renderControl({
|
|
137
|
-
label: fillAction.label,
|
|
138
|
-
onPress: () => void run(),
|
|
139
|
-
busy,
|
|
140
|
-
disabled,
|
|
141
|
-
testId: `field-fill-${field.field}`,
|
|
142
|
-
}), failed && (_jsx(Banner, { variant: "error", testId: `field-fill-error-${field.field}`, children: t("kumiko.field.fillAction.failed") }))] }));
|
|
143
|
-
}
|
|
144
|
-
// Wire value of a writeOnly field is `true` (set) / `null` (empty), never the
|
|
145
|
-
// secret. Typing sets a new value; an untouched or emptied input submits
|
|
146
|
-
// nothing / "" which the server reads as "keep". `null` after a loaded `true`
|
|
147
|
-
// (changed) means "remove on save".
|
|
148
|
-
function WriteOnlyTextInput({ field, id, hasError, onChange, changed, }) {
|
|
149
|
-
const { Input } = usePrimitives();
|
|
150
|
-
const t = useTranslation();
|
|
151
|
-
const isSet = field.value === true;
|
|
152
|
-
const isMarkedForRemoval = field.value === null && changed;
|
|
153
|
-
if (isMarkedForRemoval) {
|
|
154
|
-
return (_jsx(Input, { kind: "password", id: id, name: field.field, disabled: true, hasError: hasError, value: "", onChange: () => { }, autoComplete: "new-password", placeholder: t("kumiko.field.writeOnly.willRemove"), testId: `field-write-only-removal-${field.field}`, trailingAction: {
|
|
155
|
-
icon: "undo",
|
|
156
|
-
label: t("kumiko.field.writeOnly.undo"),
|
|
157
|
-
onPress: () => onChange(true),
|
|
158
|
-
} }));
|
|
159
|
-
}
|
|
160
|
-
return (_jsx(Input, { kind: "password", id: id, name: field.field, disabled: field.readOnly, required: field.required && !isSet, hasError: hasError, value: typeof field.value === "string" ? field.value : "", onChange: (v) => onChange(v), autoComplete: "new-password", ...(isSet && { placeholder: t("kumiko.field.writeOnly.setPlaceholder") }), ...(isSet &&
|
|
161
|
-
!field.required &&
|
|
162
|
-
!field.readOnly && {
|
|
163
|
-
trailingAction: {
|
|
164
|
-
icon: "clear",
|
|
165
|
-
label: t("kumiko.field.writeOnly.remove"),
|
|
166
|
-
onPress: () => onChange(null),
|
|
167
|
-
},
|
|
168
|
-
}) }));
|
|
169
|
-
}
|
|
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.
|
|
177
|
-
//
|
|
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).
|
|
182
|
-
function ReferenceInput({ field, id, hasError, onChange, Input, featureName, }) {
|
|
183
|
-
const { Banner } = usePrimitives();
|
|
184
|
-
const refEntity = field.refEntity ?? "";
|
|
185
|
-
const refFeature = field.refFeature ?? featureName;
|
|
186
|
-
const isMultiple = field.refMultiple === true;
|
|
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`.
|
|
192
|
-
const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
|
|
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.
|
|
199
|
-
const appFeatures = useAppFeatures();
|
|
200
|
-
const userRoles = useUserRoles();
|
|
201
|
-
const t = useTranslation();
|
|
202
|
-
const [createOpen, setCreateOpen] = useState(false);
|
|
203
|
-
const refTargetSchema = appFeatures.find((f) => f.featureName === refFeature);
|
|
204
|
-
const refCreateScreen = refTargetSchema?.screens.find((s) => s.type === "entityEdit" &&
|
|
205
|
-
s.entity === refEntity &&
|
|
206
|
-
s.allowCreate !== false &&
|
|
207
|
-
screenAccessAllows(s.access, userRoles));
|
|
208
|
-
const refEntityDef = refTargetSchema?.entities[refEntity];
|
|
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.
|
|
213
|
-
const [searchTerm, setSearchTerm] = useState("");
|
|
214
|
-
const queryPayload = useMemo(() => searchTerm === ""
|
|
215
|
-
? { limit: REFERENCE_COMBOBOX_LIMIT }
|
|
216
|
-
: { limit: REFERENCE_COMBOBOX_LIMIT, search: searchTerm }, [searchTerm]);
|
|
217
|
-
const queryResult = useQuery(queryQn, queryPayload);
|
|
218
|
-
const handleSearchChange = useCallback((q) => setSearchTerm(q), []);
|
|
219
|
-
const canCreate = !field.readOnly && refCreateScreen !== undefined && refEntityDef !== undefined;
|
|
220
|
-
const [createdWithoutIdWarning, setCreatedWithoutIdWarning] = useState(false);
|
|
221
|
-
const handleCreated = useCallback((newId) => {
|
|
222
|
-
setCreateOpen(false);
|
|
223
|
-
// Clear the search filter before refetching — the just-created record
|
|
224
|
-
// won't match whatever the user had typed pre-create, and would then
|
|
225
|
-
// be missing from `options` (shown as a raw id instead of a label).
|
|
226
|
-
setSearchTerm("");
|
|
227
|
-
queryResult.refetch().catch((err) => {
|
|
228
|
-
rendererError("render-field: refetch after create failed", err);
|
|
229
|
-
});
|
|
230
|
-
if (newId === undefined) {
|
|
231
|
-
// Record was created server-side but the payload carried no id —
|
|
232
|
-
// can't auto-select it, surface that instead of failing silently.
|
|
233
|
-
setCreatedWithoutIdWarning(true);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
setCreatedWithoutIdWarning(false);
|
|
237
|
-
if (isMultiple) {
|
|
238
|
-
const current = stringArrayOf(field.value);
|
|
239
|
-
onChange([...current, newId]);
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
onChange(newId);
|
|
243
|
-
}
|
|
244
|
-
}, [isMultiple, field.value, onChange, queryResult.refetch]);
|
|
245
|
-
const options = useMemo(() => {
|
|
246
|
-
const rows = queryResult.data?.rows ?? [];
|
|
247
|
-
return rows.map((row) => {
|
|
248
|
-
const idVal = String(row["id"] ?? "");
|
|
249
|
-
const label = String(row[labelKey] ?? idVal);
|
|
250
|
-
return { value: idVal, label };
|
|
251
|
-
});
|
|
252
|
-
}, [queryResult.data, labelKey]);
|
|
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.
|
|
256
|
-
const baseInputProps = {
|
|
257
|
-
id,
|
|
258
|
-
name: field.field,
|
|
259
|
-
// Disable the input during the initial load; subsequent loads show a
|
|
260
|
-
// loading indicator in the popover.
|
|
261
|
-
disabled: field.readOnly || (queryResult.loading && options.length === 0),
|
|
262
|
-
required: field.required,
|
|
263
|
-
hasError,
|
|
264
|
-
options,
|
|
265
|
-
onSearchChange: handleSearchChange,
|
|
266
|
-
loading: queryResult.loading,
|
|
267
|
-
...(canCreate && {
|
|
268
|
-
onCreate: () => setCreateOpen(true),
|
|
269
|
-
createLabel: t("kumiko.actions.create"),
|
|
270
|
-
}),
|
|
271
|
-
};
|
|
272
|
-
const createDialog = (_jsxs(_Fragment, { children: [canCreate && refCreateScreen && refEntityDef && (_jsx(ReferenceCreateDialog, { open: createOpen, onClose: () => setCreateOpen(false), onCreated: handleCreated, featureName: refFeature, screen: refCreateScreen, entity: refEntityDef })), createdWithoutIdWarning && (_jsx(Banner, { variant: "error", children: t("kumiko.field.reference-created-no-id") }))] }));
|
|
273
|
-
if (isMultiple) {
|
|
274
|
-
const arrayValue = stringArrayOf(field.value);
|
|
275
|
-
return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, multiple: true, value: arrayValue, onChange: (v) => onChange(v) }), createDialog] }));
|
|
276
|
-
}
|
|
277
|
-
const stringValue = field.value === undefined || field.value === null ? "" : String(field.value);
|
|
278
|
-
return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, value: stringValue, onChange: (v) => onChange(v === "" ? null : v) }), createDialog] }));
|
|
279
|
-
}
|
|
280
|
-
function inputId(field) {
|
|
281
|
-
return `kumiko-edit-${field.field}`;
|
|
282
|
-
}
|
|
283
|
-
// Doesn't resolve the `string` (cross-feature QN) FieldRenderer variant — the
|
|
284
|
-
// list-column path (DataTableCell) doesn't either, so this stays in parity.
|
|
285
|
-
function FieldRendererOutput({ field, renderer, row, appLocale, }) {
|
|
286
|
-
const { Text, JsonView, FileValueList } = usePrimitives();
|
|
287
|
-
const t = useTranslation();
|
|
288
|
-
const valueTestId = `field-value-${field.field}`;
|
|
289
|
-
const componentName = !isFormatSpec(renderer) && typeof renderer === "object" && renderer !== null
|
|
290
|
-
? extensionSectionName(renderer)
|
|
291
|
-
: undefined;
|
|
292
|
-
const Component = useColumnRenderer(componentName);
|
|
293
|
-
if (isFormatSpec(renderer)) {
|
|
294
|
-
// format:"json" wants the raw value (JsonView stringifies + highlights
|
|
295
|
-
// itself) — applyFormatSpec's already-indented string is only the
|
|
296
|
-
// fallback for primitives-providers without JsonView.
|
|
297
|
-
if (renderer.format === "json" && JsonView !== undefined) {
|
|
298
|
-
return _jsx(JsonView, { value: field.value, indent: renderer.indent, testId: valueTestId });
|
|
299
|
-
}
|
|
300
|
-
if (renderer.format === "file" && FileValueList !== undefined) {
|
|
301
|
-
const fileIds = fileIdsOf(field.value);
|
|
302
|
-
if (fileIds.length === 0)
|
|
303
|
-
return _jsx(Text, { testId: valueTestId, children: EMPTY_VALUE_PLACEHOLDER });
|
|
304
|
-
return (_jsx(FileValueList, { fileIds: fileIds, previewOpenByDefault: renderer.previewOpenByDefault === true, testId: valueTestId }));
|
|
305
|
-
}
|
|
306
|
-
// App locale as default when the FormatSpec declares none of its own —
|
|
307
|
-
// otherwise locale-sensitive formats (timestamp/date/number/decimal/
|
|
308
|
-
// bigInt/unit) fell back to Intl's runtime default instead of the app
|
|
309
|
-
// language chosen via LocaleProvider. Prefer renderer.locale
|
|
310
|
-
// when set; coalesce undefined (spread override) back to appLocale.
|
|
311
|
-
return (_jsx(Text, { testId: valueTestId, children: applyFormatSpec({
|
|
312
|
-
...renderer,
|
|
313
|
-
locale: stringPropertyOf(renderer, "locale") ?? appLocale,
|
|
314
|
-
}, field.value, t) }));
|
|
315
|
-
}
|
|
316
|
-
if (componentName !== undefined) {
|
|
317
|
-
if (Component !== undefined) {
|
|
318
|
-
return (_jsx(Component, { value: field.value, row: row ?? { [field.field]: field.value }, column: { field: field.field } }));
|
|
319
|
-
}
|
|
320
|
-
rendererWarn(`[kumiko] fieldRenderer "${componentName}" not registered`);
|
|
321
|
-
}
|
|
322
|
-
return _jsx(Text, { testId: valueTestId, children: stringValue(field.value) });
|
|
323
|
-
}
|
|
324
|
-
// Read-only text for `type: "reference"` — resolves the referenced row's
|
|
325
|
-
// label via the same lookup query as ReferenceInput's combobox, but shows
|
|
326
|
-
// plain text instead of mounting an (unusable, disabled) combobox.
|
|
327
|
-
function ReadOnlyReferenceValue({ field, featureName, }) {
|
|
328
|
-
const { Text, Link } = usePrimitives();
|
|
329
|
-
const t = useTranslation();
|
|
330
|
-
const resolveReferenceLink = useReferenceLinkResolver();
|
|
331
|
-
const refEntity = field.refEntity ?? "";
|
|
332
|
-
const refFeature = field.refFeature ?? featureName;
|
|
333
|
-
const isMultiple = field.refMultiple === true;
|
|
334
|
-
const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
|
|
335
|
-
const queryResult = useQuery(queryQn, {
|
|
336
|
-
limit: REFERENCE_COMBOBOX_LIMIT,
|
|
52
|
+
readOnlyText,
|
|
53
|
+
featureName,
|
|
54
|
+
allIssues,
|
|
55
|
+
changed,
|
|
56
|
+
fillDisabled,
|
|
57
|
+
...(row !== undefined && { row }),
|
|
337
58
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const parts = ids.map((id) => {
|
|
352
|
-
if (systemLabel !== undefined && (id === systemLabel.id || systemLabel.aliases?.includes(id))) {
|
|
353
|
-
return { id, label: t(systemLabel.labelKey), link: undefined };
|
|
354
|
-
}
|
|
355
|
-
const row = rows.find((r) => String(r["id"] ?? "") === id);
|
|
356
|
-
if (row === undefined) {
|
|
357
|
-
const unknownKey = UNRESOLVED_REFERENCE_LABEL_KEYS[`${refFeature}:${refEntity}`];
|
|
358
|
-
const label = unknownKey !== undefined &&
|
|
359
|
-
!queryResult.loading &&
|
|
360
|
-
queryResult.error === null &&
|
|
361
|
-
isCompleteRowPage(queryResult.data)
|
|
362
|
-
? t(unknownKey)
|
|
363
|
-
: id;
|
|
364
|
-
return { id, label, link: undefined };
|
|
365
|
-
}
|
|
366
|
-
return { id, label: String(row[labelKey] ?? id), link: resolveReferenceLink(refEntity, id) };
|
|
367
|
-
});
|
|
368
|
-
return (_jsx(Text, { testId: `field-value-${field.field}`, children: parts.map(({ id, label, link }, index) => (_jsxs(Fragment, { children: [index > 0 && ", ", link === undefined ? (label) : (_jsx(Link, { href: link.href, onPress: link.onPress, testId: `field-link-${field.field}-${id}`, children: label }))] }, id))) }));
|
|
369
|
-
}
|
|
370
|
-
// Field types whose read display already isn't a boxed "disabled input" look
|
|
371
|
-
// (embedded/jsonb render as an info Banner) — `readOnlyDisplayText` doesn't
|
|
372
|
-
// cover these, they keep going through `renderInput`'s existing Banner branch
|
|
373
|
-
// in text-display mode too.
|
|
374
|
-
function isComplexFieldType(type) {
|
|
375
|
-
return type === "embedded" || type === "jsonb";
|
|
376
|
-
}
|
|
377
|
-
// Read-only file fields keep their (disabled) widget: it shows a thumbnail or
|
|
378
|
-
// the file name, where plain text could only show the FileRef UUID.
|
|
379
|
-
function isFileFieldType(type) {
|
|
380
|
-
return type === "file" || type === "image" || type === "files" || type === "images";
|
|
381
|
-
}
|
|
382
|
-
// Mirrors ACTION_ICON_BY_ID (kumiko-screen.tsx) for fields: only names with
|
|
383
|
-
// an unambiguous matching FieldIconKey are listed here, everything else
|
|
384
|
-
// stays iconless rather than guessing.
|
|
385
|
-
const FIELD_ICON_BY_NAME = {
|
|
386
|
-
email: "mail",
|
|
387
|
-
phone: "phone",
|
|
388
|
-
tel: "phone",
|
|
389
|
-
mobile: "phone",
|
|
390
|
-
url: "link",
|
|
391
|
-
website: "link",
|
|
392
|
-
link: "link",
|
|
393
|
-
password: "lock",
|
|
394
|
-
search: "search",
|
|
395
|
-
city: "map-pin",
|
|
396
|
-
address: "map-pin",
|
|
397
|
-
street: "map-pin",
|
|
398
|
-
user: "user",
|
|
399
|
-
owner: "user",
|
|
400
|
-
assignee: "user",
|
|
401
|
-
};
|
|
402
|
-
function kebabLastSegment(value) {
|
|
403
|
-
const kebab = toKebab(value);
|
|
404
|
-
const lastDash = kebab.lastIndexOf("-");
|
|
405
|
-
return lastDash === -1 ? kebab : kebab.slice(lastDash + 1);
|
|
406
|
-
}
|
|
407
|
-
// Derives a prefix icon when the field declares none: field.icon wins, then
|
|
408
|
-
// the field name. Deliberately no type-based fallback — a hash in front of a
|
|
409
|
-
// number field restates what the field already shows and reads as noise.
|
|
410
|
-
function resolveFieldIcon(field) {
|
|
411
|
-
if (field.icon !== undefined)
|
|
412
|
-
return field.icon;
|
|
413
|
-
const byName = FIELD_ICON_BY_NAME[field.field.toLowerCase()] ??
|
|
414
|
-
FIELD_ICON_BY_NAME[kebabLastSegment(field.field)];
|
|
415
|
-
return byName;
|
|
416
|
-
}
|
|
417
|
-
// Dispatches field.type → Input-kind. Select threads options through
|
|
418
|
-
// from the EditFieldViewModel (computeEditViewModel pulls them from
|
|
419
|
-
// SelectFieldDef.options). Structural types without a widget (embedded,
|
|
420
|
-
// jsonb, multiSelect) render read-only instead of a data-destroying text
|
|
421
|
-
// fallback; truly unknown scalar types still fall back to text.
|
|
422
|
-
function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner, Text, JsonView, t, row, fill, }) {
|
|
423
|
-
const common = {
|
|
424
|
-
id,
|
|
425
|
-
name: field.field,
|
|
426
|
-
disabled: field.readOnly,
|
|
427
|
-
required: field.required,
|
|
428
|
-
hasError,
|
|
429
|
-
};
|
|
430
|
-
const placeholderProp = field.placeholder !== undefined ? { placeholder: field.placeholder } : {};
|
|
431
|
-
const prefix = resolveAffix(field.prefix, row);
|
|
432
|
-
const suffix = resolveAffix(field.suffix, row);
|
|
433
|
-
const affixProps = {
|
|
434
|
-
...(prefix !== undefined && { prefix }),
|
|
435
|
-
...(suffix !== undefined && { suffix }),
|
|
436
|
-
...(fill !== undefined && { fill }),
|
|
437
|
-
};
|
|
438
|
-
switch (field.type) {
|
|
439
|
-
// decimal/bigInt are both plain numbers on the wire (fieldToZod:
|
|
440
|
-
// z.number() / z.number().int().safe()) — the number input's onChange
|
|
441
|
-
// already emits `number | undefined`, so no extra coercion is needed
|
|
442
|
-
// beyond what "number" already does.
|
|
443
|
-
case "number":
|
|
444
|
-
case "bigInt": {
|
|
445
|
-
const icon = resolveFieldIcon(field);
|
|
446
|
-
const unit = resolveNumberUnit(field.unit, row);
|
|
447
|
-
return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), locale: appLocale, ...(field.grouping !== undefined && { grouping: field.grouping }), ...(field.type === "bigInt" && { integer: true }), ...placeholderProp, ...affixProps, ...(icon !== undefined && { icon }), ...(unit !== undefined && { unit }) }));
|
|
448
|
-
}
|
|
449
|
-
case "decimal": {
|
|
450
|
-
const icon = resolveFieldIcon(field);
|
|
451
|
-
return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), locale: appLocale, ...(field.grouping !== undefined && { grouping: field.grouping }), ...placeholderProp, ...affixProps, ...(icon !== undefined && { icon }) }));
|
|
452
|
-
}
|
|
453
|
-
case "tz":
|
|
454
|
-
return (_jsx(Input, { kind: "tz", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
|
|
455
|
-
case "multiSelect": {
|
|
456
|
-
const arrayValue = stringArrayOf(field.value);
|
|
457
|
-
const renderMultiSelect = (options) => field.display === "checkboxes" ? (_jsx(MultiSelectCheckboxes, { field: field, id: id, options: options, value: arrayValue, onChange: (v) => onChange(v) })) : (_jsx(Input, { kind: "combobox", ...common, multiple: true, value: arrayValue, onChange: (v) => onChange(withPinnedValues(v, options, arrayValue)), options: options }));
|
|
458
|
-
if (field.selectOptionsAvailabilityQuery !== undefined && field.readOnly !== true) {
|
|
459
|
-
return (_jsx(AvailabilityOptionsMultiSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, initialValues: arrayValue, children: renderMultiSelect }));
|
|
460
|
-
}
|
|
461
|
-
return renderMultiSelect(staticSelectOptions(field));
|
|
462
|
-
}
|
|
463
|
-
case "money": {
|
|
464
|
-
const currency = resolveMoneyCurrency(field.value, field.currency);
|
|
465
|
-
return (_jsx(Input, { kind: "money", ...common, value: moneyMinorValue(field.value, currency), onChange: (v) => onChange(moneyPayload(v, currency)), currency: currency, locale: appLocale }));
|
|
466
|
-
}
|
|
467
|
-
case "boolean":
|
|
468
|
-
return (_jsx(Input, { kind: "boolean", ...common, value: field.value === true, onChange: (v) => onChange(v) }));
|
|
469
|
-
case "date":
|
|
470
|
-
return (_jsx(Input, { kind: "date", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
|
|
471
|
-
case "timestamp":
|
|
472
|
-
return (_jsx(Input, { kind: "timestamp", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.wallClock !== undefined && { wallClock: field.wallClock }), ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
|
|
473
|
-
case "locatedTimestamp":
|
|
474
|
-
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 }) }));
|
|
475
|
-
case "select": {
|
|
476
|
-
// Translated option labels come from the ViewModel builder
|
|
477
|
-
// (computeEditViewModel, convention key
|
|
478
|
-
// `<feature>:entity:<entity>:field:<field>:option:<value>`).
|
|
479
|
-
// If no translations are registered, fall back to the raw value as the
|
|
480
|
-
// label, which ComboboxInput then shows unchanged.
|
|
481
|
-
const selectOptions = field.optionLabels !== undefined ? staticSelectOptions(field) : (field.options ?? []);
|
|
482
|
-
// `display` is shared with multiSelect's "checkboxes", which is not a
|
|
483
|
-
// select presentation — narrow to the two a select field can request.
|
|
484
|
-
const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
|
|
485
|
-
return (_jsx(Input, { kind: "select", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), options: selectOptions, ...(display !== undefined && { display }) }));
|
|
486
|
-
}
|
|
487
|
-
case "file":
|
|
488
|
-
case "image": {
|
|
489
|
-
const kind = field.type === "image" ? "image" : "file";
|
|
490
|
-
const fileId = typeof field.value === "string" && field.value !== "" ? field.value : null;
|
|
491
|
-
return (_jsx(Input, { kind: kind, ...common, value: fileId, onChange: (v) => onChange(v), ...(field.accept !== undefined && { accept: field.accept }), ...(field.maxSize !== undefined && { maxSize: field.maxSize }), ...(field.entityType !== undefined && { entityType: field.entityType }), ...(field.uploadActionForm !== undefined && {
|
|
492
|
-
uploadActionForm: field.uploadActionForm,
|
|
493
|
-
}), ...(field.entityId !== undefined && { entityId: field.entityId }), ...(field.fieldName !== undefined && { fieldName: field.fieldName }), ...(field.imageVariant !== undefined && { imageVariant: field.imageVariant }), ...(field.capture !== undefined && { capture: field.capture }) }));
|
|
494
|
-
}
|
|
495
|
-
case "files":
|
|
496
|
-
case "images":
|
|
497
|
-
return (_jsx(FileListField, { field: field, id: id, kind: field.type === "images" ? "image" : "file", hasError: hasError, onChange: (ids) => onChange(ids) }));
|
|
498
|
-
// embedded (without embeddedListCells — that's embeddedList, which has
|
|
499
|
-
// had its own EmbeddedListField widget) and jsonb carry
|
|
500
|
-
// arbitrary objects. Without a dedicated widget these must NOT fall
|
|
501
|
-
// through to a text input: stringValue() turns them into
|
|
502
|
-
// "[object Object]" and saving that overwrites the real data with the
|
|
503
|
-
// mangled string. A `required: true` on either is caught loudly
|
|
504
|
-
// at boot (validateNoWidgetRequiredField in the framework package)
|
|
505
|
-
// instead of silently failing here.
|
|
506
|
-
case "embedded":
|
|
507
|
-
case "jsonb": {
|
|
508
|
-
const hasValue = field.value !== undefined && field.value !== null && field.value !== "";
|
|
509
|
-
return (_jsxs(Banner, { id: id, variant: "info", children: [t("kumiko.field.unsupported"), hasValue &&
|
|
510
|
-
(JsonView !== undefined ? (_jsx(JsonView, { value: field.value, testId: `field-value-${field.field}` })) : (_jsx(Text, { variant: "code", children: JSON.stringify(field.value) })))] }));
|
|
511
|
-
}
|
|
512
|
-
default: {
|
|
513
|
-
if (field.type === "text" && field.format === "password") {
|
|
514
|
-
return (_jsx(Input, { kind: "password", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
|
|
515
|
-
}
|
|
516
|
-
if (field.type === "text" && field.format === "month") {
|
|
517
|
-
return (_jsx(Input, { kind: "month", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
|
|
518
|
-
}
|
|
519
|
-
// text + unknown scalar type → text input. If TextFieldDef.multiline
|
|
520
|
-
// is set (the view-model carries it), the renderer switches to
|
|
521
|
-
// textarea. longText always renders a textarea — that's the point of
|
|
522
|
-
// the type — regardless of whether `multiline` is set; `multiline`
|
|
523
|
-
// only supplies an optional `{ rows }` override for it.
|
|
524
|
-
if (field.type === "longText" || (field.type === "text" && field.multiline)) {
|
|
525
|
-
const rows = typeof field.multiline === "object" ? field.multiline.rows : undefined;
|
|
526
|
-
return (_jsx(Input, { kind: "textarea", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(rows !== undefined && { rows }), ...placeholderProp }));
|
|
527
|
-
}
|
|
528
|
-
const icon = resolveFieldIcon(field);
|
|
529
|
-
return (_jsx(Input, { kind: "text", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...placeholderProp, ...affixProps, ...(icon !== undefined && { icon }), ...(field.autoComplete !== undefined && { autoComplete: field.autoComplete }) }));
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
function stringValue(v) {
|
|
534
|
-
if (v === undefined || v === null)
|
|
535
|
-
return "";
|
|
536
|
-
return typeof v === "string" ? v : String(v);
|
|
537
|
-
}
|
|
538
|
-
function numberValue(v) {
|
|
539
|
-
if (v === undefined || v === null || v === "")
|
|
540
|
-
return "";
|
|
541
|
-
return typeof v === "number" ? v : Number(v);
|
|
542
|
-
}
|
|
543
|
-
// Read/initial value → MoneyInput's minor-units contract. The server's
|
|
544
|
-
// read shape is `{amount, currency, amountMinor}` in MAJOR units — deliberately
|
|
545
|
-
// NOT reading `amountMinor` here: the server derives it via a flat
|
|
546
|
-
// MINOR_UNIT_SCALE=100 (money.ts), which disagrees with currencyDecimals for
|
|
547
|
-
// zero-decimal currencies like JPY, so it would double-scale JPY amounts.
|
|
548
|
-
// Deriving from `amount * 10**currencyDecimals(currency)` keeps this the
|
|
549
|
-
// exact inverse of moneyPayload below. A bare number (e.g. ConfigEditBody's
|
|
550
|
-
// stored-config coercion) is MAJOR units too — every producer in this repo
|
|
551
|
-
// hands rehydrateMoney's `{amount,…}` shape or a raw major-unit number, never
|
|
552
|
-
// pre-scaled minor units.
|
|
553
|
-
// A stored row can carry a legacy/imported currency that is not an ISO-4217
|
|
554
|
-
// shape; Intl.NumberFormat throws a RangeError on it and would crash the screen.
|
|
555
|
-
const ISO_CURRENCY_CODE = /^[A-Z]{3}$/;
|
|
556
|
-
function resolveMoneyCurrency(value, fieldCurrency) {
|
|
557
|
-
if (typeof value === "object" && value !== null && "currency" in value) {
|
|
558
|
-
const { currency } = value;
|
|
559
|
-
if (typeof currency === "string") {
|
|
560
|
-
const normalized = currency.toUpperCase();
|
|
561
|
-
if (ISO_CURRENCY_CODE.test(normalized))
|
|
562
|
-
return normalized;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return fieldCurrency ?? "EUR";
|
|
566
|
-
}
|
|
567
|
-
// The suffix span is absolutely positioned inside a fixed right padding sized
|
|
568
|
-
// for short symbols; a free-text sibling value longer than any real unit
|
|
569
|
-
// symbol would overflow the number.
|
|
570
|
-
const MAX_SIBLING_UNIT_LENGTH = 8;
|
|
571
|
-
// Static unit → used as-is. Sibling-field reference → read the live row
|
|
572
|
-
// value; missing/empty/non-string/overlong sibling means no suffix, never a guess.
|
|
573
|
-
function resolveNumberUnit(unit, row) {
|
|
574
|
-
if (unit === undefined)
|
|
575
|
-
return undefined;
|
|
576
|
-
if (typeof unit === "string")
|
|
577
|
-
return unit;
|
|
578
|
-
const sibling = row?.[unit.field];
|
|
579
|
-
if (typeof sibling !== "string")
|
|
580
|
-
return undefined;
|
|
581
|
-
const trimmed = sibling.trim();
|
|
582
|
-
return trimmed.length > 0 && trimmed.length <= MAX_SIBLING_UNIT_LENGTH ? trimmed : undefined;
|
|
583
|
-
}
|
|
584
|
-
// String affix → used as-is (the view model already translated it). Field
|
|
585
|
-
// affix → the live form value; missing, empty or non-scalar means no affix.
|
|
586
|
-
function resolveAffix(affix, row) {
|
|
587
|
-
if (affix === undefined)
|
|
588
|
-
return undefined;
|
|
589
|
-
if (typeof affix === "string")
|
|
590
|
-
return affix;
|
|
591
|
-
const value = row?.[affix.field];
|
|
592
|
-
const text = typeof value === "number" ? String(value) : value;
|
|
593
|
-
return typeof text === "string" && text.trim().length > 0 ? text.trim() : undefined;
|
|
594
|
-
}
|
|
595
|
-
function stringArrayOf(value) {
|
|
596
|
-
return Array.isArray(value)
|
|
597
|
-
? value.filter((item) => typeof item === "string")
|
|
598
|
-
: [];
|
|
599
|
-
}
|
|
600
|
-
// The combobox cannot disable a single option, so removals of disabled options
|
|
601
|
-
// the form already holds are merged back.
|
|
602
|
-
function withPinnedValues(next, options, current) {
|
|
603
|
-
const missingPinned = options
|
|
604
|
-
.filter((opt) => opt.disabled === true && current.includes(opt.value) && !next.includes(opt.value))
|
|
605
|
-
.map((opt) => opt.value);
|
|
606
|
-
return missingPinned.length === 0 ? next : [...next, ...missingPinned];
|
|
607
|
-
}
|
|
608
|
-
function stringPropertyOf(value, key) {
|
|
609
|
-
const property = propertyOf(value, key);
|
|
610
|
-
return typeof property === "string" ? property : undefined;
|
|
611
|
-
}
|
|
612
|
-
function moneyMinorValue(v, currency) {
|
|
613
|
-
if (v === undefined || v === null || v === "")
|
|
614
|
-
return "";
|
|
615
|
-
if (typeof v === "number")
|
|
616
|
-
return Math.round(v * 10 ** currencyDecimals(currency));
|
|
617
|
-
if (typeof v === "object") {
|
|
618
|
-
const amount = propertyOf(v, "amount");
|
|
619
|
-
if (typeof amount === "number")
|
|
620
|
-
return Math.round(amount * 10 ** currencyDecimals(currency));
|
|
621
|
-
}
|
|
622
|
-
return "";
|
|
623
|
-
}
|
|
624
|
-
// MoneyInput's minor-units onChange → the server payload shape
|
|
625
|
-
// (`z.object({amount, currency})`, schema-builder.ts) — MAJOR units, no
|
|
626
|
-
// `amountMinor` (the server derives that itself on write).
|
|
627
|
-
function moneyPayload(minorUnits, currency) {
|
|
628
|
-
if (minorUnits === undefined)
|
|
629
|
-
return undefined;
|
|
630
|
-
return { amount: minorUnits / 10 ** currencyDecimals(currency), currency };
|
|
631
|
-
}
|
|
632
|
-
// A locatedTimestamp field value is supplied by the read wrapper as
|
|
633
|
-
// `{ at, tz, utc }`; an unset value becomes "" as the empty sentinel,
|
|
634
|
-
// consistent with money and timestamp fields.
|
|
635
|
-
function locatedValue(v) {
|
|
636
|
-
if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
|
|
637
|
-
const utc = propertyOf(v, "utc");
|
|
638
|
-
return {
|
|
639
|
-
at: typeof v.at === "string" ? v.at : "",
|
|
640
|
-
tz: typeof v.tz === "string" ? v.tz : "",
|
|
641
|
-
...(typeof utc === "string" && { utc }),
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
return "";
|
|
645
|
-
}
|
|
646
|
-
// Read-only text for a `field.readOnly` field without its own `renderer` —
|
|
647
|
-
// per-type formatting that reuses the same value-shaping helpers as the
|
|
648
|
-
// editable widgets above, so a text value and its would-be Input widget stay
|
|
649
|
-
// derived from the identical parse. `type: "reference"` isn't
|
|
650
|
-
// covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
|
|
651
|
-
// `isComplexFieldType` types aren't covered either — callers keep those on
|
|
652
|
-
// `renderInput`'s existing Banner fallback.
|
|
653
|
-
function readOnlyDisplayText(field, appLocale, t) {
|
|
654
|
-
const { type, value } = field;
|
|
655
|
-
if (value === undefined || value === null || value === "")
|
|
656
|
-
return EMPTY_VALUE_PLACEHOLDER;
|
|
657
|
-
if (type === "text" && field.writeOnly === true)
|
|
658
|
-
return t("kumiko.field.writeOnly.set");
|
|
659
|
-
if (type === "text" && field.format === "password")
|
|
660
|
-
return "••••••••";
|
|
661
|
-
switch (type) {
|
|
662
|
-
case "boolean":
|
|
663
|
-
return applyFormatSpec({ format: "boolean" }, value);
|
|
664
|
-
case "date":
|
|
665
|
-
return applyFormatSpec({ format: "date", locale: field.dateLocale ?? appLocale }, value);
|
|
666
|
-
case "timestamp":
|
|
667
|
-
return applyFormatSpec({ format: "timestamp", locale: field.dateLocale ?? appLocale }, value);
|
|
668
|
-
case "locatedTimestamp": {
|
|
669
|
-
const located = locatedValue(value);
|
|
670
|
-
if (located === "" || located.at === "")
|
|
671
|
-
return EMPTY_VALUE_PLACEHOLDER;
|
|
672
|
-
return applyFormatSpec({ format: "timestamp", locale: field.dateLocale ?? appLocale }, located.utc ?? located.at);
|
|
673
|
-
}
|
|
674
|
-
case "number":
|
|
675
|
-
case "bigInt":
|
|
676
|
-
case "decimal": {
|
|
677
|
-
const n = numberValue(value);
|
|
678
|
-
return n === ""
|
|
679
|
-
? EMPTY_VALUE_PLACEHOLDER
|
|
680
|
-
: new Intl.NumberFormat(appLocale, { useGrouping: field.grouping ?? true }).format(n);
|
|
681
|
-
}
|
|
682
|
-
case "money": {
|
|
683
|
-
const currency = resolveMoneyCurrency(value, field.currency);
|
|
684
|
-
const minor = moneyMinorValue(value, currency);
|
|
685
|
-
if (minor === "")
|
|
686
|
-
return EMPTY_VALUE_PLACEHOLDER;
|
|
687
|
-
const major = minor / 10 ** currencyDecimals(currency);
|
|
688
|
-
return new Intl.NumberFormat(appLocale, { style: "currency", currency }).format(major);
|
|
689
|
-
}
|
|
690
|
-
case "select":
|
|
691
|
-
case "multiSelect": {
|
|
692
|
-
const labels = field.optionLabels;
|
|
693
|
-
const values = Array.isArray(value) ? value : [value];
|
|
694
|
-
if (values.length === 0)
|
|
695
|
-
return EMPTY_VALUE_PLACEHOLDER;
|
|
696
|
-
return values
|
|
697
|
-
.map((v) => (typeof v === "string" ? (labels?.[v] ?? v) : stringValue(v)))
|
|
698
|
-
.join(", ");
|
|
699
|
-
}
|
|
700
|
-
default:
|
|
701
|
-
return stringValue(value);
|
|
702
|
-
}
|
|
59
|
+
return (_jsx(Field, { id: id, label: field.label, required: field.required && field.type !== "boolean" && !allFieldsRequired, ...fieldOptionalProps({
|
|
60
|
+
issues,
|
|
61
|
+
printsIssues: !listPrintsOwnIssues(field),
|
|
62
|
+
labelAppendix,
|
|
63
|
+
fieldAppendix,
|
|
64
|
+
appendixPlacement,
|
|
65
|
+
description,
|
|
66
|
+
status,
|
|
67
|
+
changed,
|
|
68
|
+
layout,
|
|
69
|
+
accent,
|
|
70
|
+
hideLabel,
|
|
71
|
+
}), testId: `field-${field.field}`, children: control }));
|
|
703
72
|
}
|