@cosmicdrift/kumiko-renderer 0.327.0 → 0.328.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -44
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -38
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/dist/inside-drawer.js +9 -0
- package/dist/page-header-slot.d.ts +2 -0
- package/dist/page-header-slot.js +9 -0
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/changes.json +52 -0
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -111
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3614
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -85
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -639
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1669
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -60
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -467
- package/src/components/render-edit-action-button.tsx +0 -106
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -216
- package/src/components/render-edit.tsx +0 -1700
- package/src/components/render-field.tsx +0 -942
- package/src/components/render-list.tsx +0 -561
- package/src/components/write-form-section.tsx +0 -199
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -171
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -132
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -220
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -272
- package/src/primitives.tsx +0 -1446
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type EditFieldViewModel, type FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type RenderFieldProps = {
|
|
4
|
+
readonly field: EditFieldViewModel;
|
|
5
|
+
readonly issues?: readonly FieldIssue[];
|
|
6
|
+
readonly onChange: (val: unknown) => void;
|
|
7
|
+
/** Nur bei type:"reference" relevant — Feature-Name für die Lookup-
|
|
8
|
+
* Query-QN (`<feature>:query:<refEntity>:list`). Andere Field-Types
|
|
9
|
+
* ignorieren das Prop. */
|
|
10
|
+
readonly featureName?: string;
|
|
11
|
+
/** Optionaler Zusatz-Inhalt der nach dem Label gerendert wird (z.B.
|
|
12
|
+
* ConfigSourceBadge). */
|
|
13
|
+
readonly labelAppendix?: ReactNode;
|
|
14
|
+
/** Optionaler Zusatz-Inhalt der nach dem Input gerendert wird (z.B.
|
|
15
|
+
* ConfigCascade). */
|
|
16
|
+
readonly fieldAppendix?: ReactNode;
|
|
17
|
+
/** Flat issues-by-path map (FormSnapshot.errors) — only relevant for
|
|
18
|
+
* type:"embedded" with embeddedListCells, to bucket row-/cell-issues
|
|
19
|
+
* (`${field}.${rowIndex}` / `${field}.${rowIndex}.${cellField}`).
|
|
20
|
+
* Other field types ignore this prop. */
|
|
21
|
+
readonly allIssues?: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
22
|
+
/** "form" (default) renders every field as its editable Input, disabled
|
|
23
|
+
* when `field.readOnly` — unchanged behavior. "text" renders a
|
|
24
|
+
* `field.readOnly` field as plain text instead (projectionDetail's read
|
|
25
|
+
* view, fw#2245); editable fields are untouched by this prop either way. */
|
|
26
|
+
readonly valueDisplay?: "form" | "text";
|
|
27
|
+
/** Current form values, keyed by field name. Consulted when
|
|
28
|
+
* `field.renderer` resolves to a `{ react: { __component } }` registry
|
|
29
|
+
* component (passed through as `ColumnRendererProps.row`, same contract
|
|
30
|
+
* as list-column renderers, fw#2245), and to resolve a `type: "number"`
|
|
31
|
+
* field's sibling-field `unit`. Omitted → falls back to a single-key
|
|
32
|
+
* `{ [field.field]: field.value }` row. */
|
|
33
|
+
readonly row?: Readonly<Record<string, unknown>>;
|
|
34
|
+
/** Marks the field as modified against the loaded record (edit mode only). */
|
|
35
|
+
readonly changed?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare const AllFieldsRequiredProvider: import("react").Provider<boolean>;
|
|
38
|
+
export declare function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, }: RenderFieldProps): ReactNode;
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { isFormatSpec, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
+
import { applyFormatSpec, currencyDecimals, } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { createContext, useCallback, useContext, useMemo, 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 { screenAccessAllows } from "../app/screen-access.js";
|
|
10
|
+
import { useUserRoles } from "../context/user-roles-context.js";
|
|
11
|
+
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
|
|
12
|
+
import { useQuery } from "../hooks/use-query.js";
|
|
13
|
+
import { referenceLookupSource } from "../hooks/use-reference-lookup.js";
|
|
14
|
+
import { useLocale, useTranslation } from "../i18n.js";
|
|
15
|
+
import { usePrimitives } from "../primitives.js";
|
|
16
|
+
import { EmbeddedListField } from "./embedded-list-field.js";
|
|
17
|
+
import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
|
|
18
|
+
import { ReferenceCreateDialog } from "./reference-create-dialog.js";
|
|
19
|
+
const AllFieldsRequiredContext = createContext(false);
|
|
20
|
+
export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
|
|
21
|
+
export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay = "form", row, changed, }) {
|
|
22
|
+
const { Field, Input, Banner, Text, JsonView } = usePrimitives();
|
|
23
|
+
// App-Locale (i18n) für money/date-Inputs — sonst fielen sie auf
|
|
24
|
+
// navigator.language (Browser-Sprache) zurück statt der gewählten
|
|
25
|
+
// App-Sprache. BEWUSSTE API-Verschärfung (seit 0.38): RenderField ist
|
|
26
|
+
// public exportiert und verlangt jetzt einen LocaleProvider —
|
|
27
|
+
// Standalone-Consumer/Tests müssen wrappen (createKumikoApp tut es).
|
|
28
|
+
const appLocale = useLocale().locale();
|
|
29
|
+
const t = useTranslation();
|
|
30
|
+
const allFieldsRequired = useContext(AllFieldsRequiredContext);
|
|
31
|
+
if (!field.visible)
|
|
32
|
+
return null;
|
|
33
|
+
const id = inputId(field);
|
|
34
|
+
const hasError = issues !== undefined && issues.length > 0;
|
|
35
|
+
// An author-declared renderer always wins over the input widget, same as
|
|
36
|
+
// the list-column path (render-list.tsx). Only takes effect once the field
|
|
37
|
+
// is actually readOnly — a FormatSpec/PlatformComponent renderer has no
|
|
38
|
+
// editable widget of its own, so applying it to an editable field would
|
|
39
|
+
// silently make that field un-editable (fw#2245).
|
|
40
|
+
const readOnlyText = valueDisplay === "text" && field.readOnly && field.renderer === undefined;
|
|
41
|
+
// Reference-Field rendert eine eigene Component — sie nutzt
|
|
42
|
+
// useQuery() für den Live-Lookup, also muss sie als React-
|
|
43
|
+
// Komponente gemountet werden (nicht als pure render-Call).
|
|
44
|
+
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 ?? "" }))) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale) })) : (renderInput({
|
|
45
|
+
field,
|
|
46
|
+
id,
|
|
47
|
+
hasError,
|
|
48
|
+
onChange,
|
|
49
|
+
Input,
|
|
50
|
+
appLocale,
|
|
51
|
+
Banner,
|
|
52
|
+
Text,
|
|
53
|
+
JsonView,
|
|
54
|
+
t,
|
|
55
|
+
row,
|
|
56
|
+
}));
|
|
57
|
+
return (_jsx(Field, { id: id, label: field.label, required: field.required && !allFieldsRequired, ...(issues !== undefined && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(changed === true && { changed }), testId: `field-${field.field}`, children: control }));
|
|
58
|
+
}
|
|
59
|
+
// Where a reference field's option rows come from, and which of their keys
|
|
60
|
+
// holds the display text — the entity's own list handler plus `refLabelField`
|
|
61
|
+
// by default, or an author-supplied handler whose rows carry `label`
|
|
62
|
+
// (fw#2780), for targets with no readable column of their own.
|
|
63
|
+
function referenceOptionSource(field, refFeature, refEntity) {
|
|
64
|
+
if (field.refOptionsQuery !== undefined) {
|
|
65
|
+
return { queryQn: field.refOptionsQuery, labelKey: "label" };
|
|
66
|
+
}
|
|
67
|
+
return referenceLookupSource(refFeature, refEntity, field.refLabelField ?? "id");
|
|
68
|
+
}
|
|
69
|
+
// Tier 2.7e-3 + 2.1c: Reference-Input rendert eine Searchable Combobox
|
|
70
|
+
// gefüllt aus einer Live-Query auf die referenced Entity. Default-
|
|
71
|
+
// Limit: 200 — bei größeren Datasets fehlt der Tail im Dropdown
|
|
72
|
+
// (Tier 2.7e-Remote: server-side Search-Query mit debounce kommt später).
|
|
73
|
+
// - Display = row[refLabelField], Default labelField "id".
|
|
74
|
+
// - Loading-State: leeres Dropdown bis die rows da sind. Field
|
|
75
|
+
// ist disabled während useQuery läuft.
|
|
76
|
+
// - Multi-Mode (Tier 2.7e-Multi via field.refMultiple): value ist
|
|
77
|
+
// ein UUID-Array, Combobox rendert Selected-Tags.
|
|
78
|
+
//
|
|
79
|
+
// Storage: UI-Wert ist UUID (row.id) oder UUID-Array bei multiple.
|
|
80
|
+
// Server-Schema: z.uuid() bzw. z.array(z.uuid()).
|
|
81
|
+
// REFERENCE_COMBOBOX_LIMIT lebt zentral in hooks/reference-limits.ts
|
|
82
|
+
// (siehe dort für Begründung der Default-Werte).
|
|
83
|
+
function ReferenceInput({ field, id, hasError, onChange, Input, featureName, }) {
|
|
84
|
+
const { Banner } = usePrimitives();
|
|
85
|
+
const refEntity = field.refEntity ?? "";
|
|
86
|
+
const refFeature = field.refFeature ?? featureName;
|
|
87
|
+
const isMultiple = field.refMultiple === true;
|
|
88
|
+
// Tier 2.7e Cross-Feature: refFeature kann ≠ featureName sein
|
|
89
|
+
// (z.B. items.assignee → users:query:user:list). Default ist
|
|
90
|
+
// same-feature, kommt aus dem ViewModel (parseRefTarget).
|
|
91
|
+
// fw#2780: refOptionsQuery replaces that source with an author-supplied
|
|
92
|
+
// handler whose rows already carry a composed `label`.
|
|
93
|
+
const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
|
|
94
|
+
// Issue #1681: "+ Neu" in der Combobox öffnet den Create-Screen der
|
|
95
|
+
// referenced entity als Dialog, statt die aktuelle Form zu verlassen.
|
|
96
|
+
// refFeature kann ein anderes Feature als das aktuell gerenderte sein
|
|
97
|
+
// — appFeatures (createKumikoApp) kennt alle Feature-Schemas, nicht
|
|
98
|
+
// nur das der aktiven Screen. Kein Match (Feature/Screen/Entity nicht
|
|
99
|
+
// registriert, oder allowCreate:false) → onCreate bleibt undefined,
|
|
100
|
+
// Combobox rendert dann ohne den Footer.
|
|
101
|
+
const appFeatures = useAppFeatures();
|
|
102
|
+
const userRoles = useUserRoles();
|
|
103
|
+
const t = useTranslation();
|
|
104
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
105
|
+
const refTargetSchema = appFeatures.find((f) => f.featureName === refFeature);
|
|
106
|
+
const refCreateScreen = refTargetSchema?.screens.find((s) => s.type === "entityEdit" &&
|
|
107
|
+
s.entity === refEntity &&
|
|
108
|
+
s.allowCreate !== false &&
|
|
109
|
+
screenAccessAllows(s.access, userRoles));
|
|
110
|
+
const refEntityDef = refTargetSchema?.entities[refEntity];
|
|
111
|
+
// Tier 2.7e Remote-Search: User tippt im Combobox → Server filtert
|
|
112
|
+
// via existing list-payload `search`-Param (Tier 2.6c). Combobox
|
|
113
|
+
// debounced den keystroke selbst (300ms) und ruft onSearchChange.
|
|
114
|
+
// Initial-State leer → erste 50 Items vom Server (default-sortiert).
|
|
115
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
116
|
+
const queryPayload = useMemo(() => searchTerm === ""
|
|
117
|
+
? { limit: REFERENCE_COMBOBOX_LIMIT }
|
|
118
|
+
: { limit: REFERENCE_COMBOBOX_LIMIT, search: searchTerm }, [searchTerm]);
|
|
119
|
+
const queryResult = useQuery(queryQn, queryPayload);
|
|
120
|
+
const handleSearchChange = useCallback((q) => setSearchTerm(q), []);
|
|
121
|
+
const canCreate = !field.readOnly && refCreateScreen !== undefined && refEntityDef !== undefined;
|
|
122
|
+
const [createdWithoutIdWarning, setCreatedWithoutIdWarning] = useState(false);
|
|
123
|
+
const handleCreated = useCallback((newId) => {
|
|
124
|
+
setCreateOpen(false);
|
|
125
|
+
// Clear the search filter before refetching — the just-created record
|
|
126
|
+
// won't match whatever the user had typed pre-create, and would then
|
|
127
|
+
// be missing from `options` (shown as a raw id instead of a label).
|
|
128
|
+
setSearchTerm("");
|
|
129
|
+
queryResult.refetch().catch((err) => {
|
|
130
|
+
// biome-ignore lint/suspicious/noConsole: no error-surfacing path exists in this widget yet — at minimum, don't swallow it silently.
|
|
131
|
+
console.error("render-field: refetch after create failed", err);
|
|
132
|
+
});
|
|
133
|
+
if (newId === undefined) {
|
|
134
|
+
// Record was created server-side but the payload carried no id —
|
|
135
|
+
// can't auto-select it, surface that instead of failing silently.
|
|
136
|
+
setCreatedWithoutIdWarning(true);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
setCreatedWithoutIdWarning(false);
|
|
140
|
+
if (isMultiple) {
|
|
141
|
+
const current = Array.isArray(field.value) ? field.value : [];
|
|
142
|
+
onChange([...current, newId]);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
onChange(newId);
|
|
146
|
+
}
|
|
147
|
+
}, [isMultiple, field.value, onChange, queryResult.refetch]);
|
|
148
|
+
const options = useMemo(() => {
|
|
149
|
+
const rows = queryResult.data?.rows ?? [];
|
|
150
|
+
return rows.map((row) => {
|
|
151
|
+
const idVal = String(row["id"] ?? "");
|
|
152
|
+
const label = String(row[labelKey] ?? idVal);
|
|
153
|
+
return { value: idVal, label };
|
|
154
|
+
});
|
|
155
|
+
}, [queryResult.data, labelKey]);
|
|
156
|
+
// Single: value ist String/null; Multi: Array. Coerce auf das was
|
|
157
|
+
// der Combobox-Mode erwartet, damit Storage-Drift (Server liefert
|
|
158
|
+
// alten String wo jetzt Array erwartet wird) keine Crash auslöst.
|
|
159
|
+
const baseInputProps = {
|
|
160
|
+
id,
|
|
161
|
+
name: field.field,
|
|
162
|
+
// Initial-Load disabled — danach loading-Indicator im Popover.
|
|
163
|
+
disabled: field.readOnly || (queryResult.loading && options.length === 0),
|
|
164
|
+
required: field.required,
|
|
165
|
+
hasError,
|
|
166
|
+
options,
|
|
167
|
+
onSearchChange: handleSearchChange,
|
|
168
|
+
loading: queryResult.loading,
|
|
169
|
+
...(canCreate && {
|
|
170
|
+
onCreate: () => setCreateOpen(true),
|
|
171
|
+
createLabel: t("kumiko.actions.create"),
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
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") }))] }));
|
|
175
|
+
if (isMultiple) {
|
|
176
|
+
const arrayValue = Array.isArray(field.value)
|
|
177
|
+
? field.value
|
|
178
|
+
: [];
|
|
179
|
+
return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, multiple: true, value: arrayValue, onChange: (v) => onChange(v) }), createDialog] }));
|
|
180
|
+
}
|
|
181
|
+
const stringValue = field.value === undefined || field.value === null ? "" : String(field.value);
|
|
182
|
+
return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, value: stringValue, onChange: (v) => onChange(v === "" ? null : v) }), createDialog] }));
|
|
183
|
+
}
|
|
184
|
+
function inputId(field) {
|
|
185
|
+
return `kumiko-edit-${field.field}`;
|
|
186
|
+
}
|
|
187
|
+
// Doesn't resolve the `string` (cross-feature QN) FieldRenderer variant — the
|
|
188
|
+
// list-column path (DataTableCell) doesn't either, so this stays in parity.
|
|
189
|
+
function FieldRendererOutput({ field, renderer, row, appLocale, }) {
|
|
190
|
+
const { Text, JsonView } = usePrimitives();
|
|
191
|
+
const t = useTranslation();
|
|
192
|
+
const componentName = !isFormatSpec(renderer) && typeof renderer === "object" && renderer !== null
|
|
193
|
+
? extensionSectionName(renderer)
|
|
194
|
+
: undefined;
|
|
195
|
+
const Component = useColumnRenderer(componentName);
|
|
196
|
+
if (isFormatSpec(renderer)) {
|
|
197
|
+
// format:"json" wants the raw value (JsonView stringifies + highlights
|
|
198
|
+
// itself) — applyFormatSpec's already-indented string is only the
|
|
199
|
+
// fallback for primitives-providers without JsonView (fw#2312).
|
|
200
|
+
if (renderer.format === "json" && JsonView !== undefined) {
|
|
201
|
+
return (_jsx(JsonView, { value: field.value, indent: renderer.indent, testId: `field-value-${field.field}` }));
|
|
202
|
+
}
|
|
203
|
+
// App locale as default when the FormatSpec declares none of its own —
|
|
204
|
+
// otherwise locale-sensitive formats (timestamp/date/number/decimal/
|
|
205
|
+
// bigInt/unit) fell back to Intl's runtime default instead of the app
|
|
206
|
+
// language chosen via LocaleProvider (fw#2187). Prefer renderer.locale
|
|
207
|
+
// when set; coalesce undefined (spread override) back to appLocale (#2332).
|
|
208
|
+
return (_jsx(Text, { testId: `field-value-${field.field}`, children: applyFormatSpec({
|
|
209
|
+
...renderer,
|
|
210
|
+
locale: renderer.locale ?? appLocale,
|
|
211
|
+
}, field.value, t) }));
|
|
212
|
+
}
|
|
213
|
+
if (componentName !== undefined) {
|
|
214
|
+
if (Component !== undefined) {
|
|
215
|
+
return (_jsx(Component, { value: field.value, row: row ?? { [field.field]: field.value }, column: { field: field.field } }));
|
|
216
|
+
}
|
|
217
|
+
// biome-ignore lint/suspicious/noConsole: dev-warning for a registry mismatch, mirrors DataTableCell's columnRenderer warning.
|
|
218
|
+
console.warn(`[kumiko] fieldRenderer "${componentName}" not registered`);
|
|
219
|
+
}
|
|
220
|
+
return _jsx(Text, { testId: `field-value-${field.field}`, children: stringValue(field.value) });
|
|
221
|
+
}
|
|
222
|
+
// Read-only text for `type: "reference"` — resolves the referenced row's
|
|
223
|
+
// label via the same lookup query as ReferenceInput's combobox, but shows
|
|
224
|
+
// plain text instead of mounting an (unusable, disabled) combobox.
|
|
225
|
+
function ReadOnlyReferenceValue({ field, featureName, }) {
|
|
226
|
+
const { Text } = usePrimitives();
|
|
227
|
+
const t = useTranslation();
|
|
228
|
+
const refEntity = field.refEntity ?? "";
|
|
229
|
+
const refFeature = field.refFeature ?? featureName;
|
|
230
|
+
const isMultiple = field.refMultiple === true;
|
|
231
|
+
const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
|
|
232
|
+
const queryResult = useQuery(queryQn, {
|
|
233
|
+
limit: REFERENCE_COMBOBOX_LIMIT,
|
|
234
|
+
});
|
|
235
|
+
const ids = isMultiple
|
|
236
|
+
? Array.isArray(field.value)
|
|
237
|
+
? field.value
|
|
238
|
+
: []
|
|
239
|
+
: typeof field.value === "string" && field.value !== ""
|
|
240
|
+
? [field.value]
|
|
241
|
+
: [];
|
|
242
|
+
if (ids.length === 0)
|
|
243
|
+
return _jsx(Text, { testId: `field-value-${field.field}`, children: "\u2014" });
|
|
244
|
+
const rows = queryResult.data?.rows ?? [];
|
|
245
|
+
// System-scope ids (e.g. SYSTEM_TENANT_ID) never have a backing row —
|
|
246
|
+
// same central fallback as useReferenceLookup (render-list.tsx), so a
|
|
247
|
+
// reference field showing one on the detail path also gets a label
|
|
248
|
+
// instead of the raw id.
|
|
249
|
+
const systemLabel = SYSTEM_REFERENCE_LABELS[`${refFeature}:${refEntity}`];
|
|
250
|
+
const labels = ids.map((id) => {
|
|
251
|
+
if (systemLabel !== undefined && id === systemLabel.id)
|
|
252
|
+
return t(systemLabel.labelKey);
|
|
253
|
+
const row = rows.find((r) => String(r["id"] ?? "") === id);
|
|
254
|
+
return row !== undefined ? String(row[labelKey] ?? id) : id;
|
|
255
|
+
});
|
|
256
|
+
return _jsx(Text, { testId: `field-value-${field.field}`, children: labels.join(", ") });
|
|
257
|
+
}
|
|
258
|
+
// Field types whose read display already isn't a boxed "disabled input" look
|
|
259
|
+
// (embedded/jsonb render as an info Banner, files/images as an unsupported-
|
|
260
|
+
// type Banner) — `readOnlyDisplayText` doesn't cover these, they keep going
|
|
261
|
+
// through `renderInput`'s existing Banner branch in text-display mode too.
|
|
262
|
+
function isComplexFieldType(type) {
|
|
263
|
+
return type === "embedded" || type === "jsonb" || type === "files" || type === "images";
|
|
264
|
+
}
|
|
265
|
+
// Mirrors ACTION_ICON_BY_ID (kumiko-screen.tsx) for fields: only names with
|
|
266
|
+
// an unambiguous matching FieldIconKey are listed here, everything else
|
|
267
|
+
// stays iconless rather than guessing.
|
|
268
|
+
const FIELD_ICON_BY_NAME = {
|
|
269
|
+
email: "mail",
|
|
270
|
+
phone: "phone",
|
|
271
|
+
tel: "phone",
|
|
272
|
+
mobile: "phone",
|
|
273
|
+
url: "link",
|
|
274
|
+
website: "link",
|
|
275
|
+
link: "link",
|
|
276
|
+
password: "lock",
|
|
277
|
+
search: "search",
|
|
278
|
+
city: "map-pin",
|
|
279
|
+
address: "map-pin",
|
|
280
|
+
street: "map-pin",
|
|
281
|
+
user: "user",
|
|
282
|
+
owner: "user",
|
|
283
|
+
assignee: "user",
|
|
284
|
+
};
|
|
285
|
+
function kebabLastSegment(value) {
|
|
286
|
+
const kebab = toKebab(value);
|
|
287
|
+
const lastDash = kebab.lastIndexOf("-");
|
|
288
|
+
return lastDash === -1 ? kebab : kebab.slice(lastDash + 1);
|
|
289
|
+
}
|
|
290
|
+
// Derives a prefix icon when the field declares none: field.icon wins, then
|
|
291
|
+
// the field name. Deliberately no type-based fallback — a hash in front of a
|
|
292
|
+
// number field restates what the field already shows and reads as noise.
|
|
293
|
+
function resolveFieldIcon(field) {
|
|
294
|
+
if (field.icon !== undefined)
|
|
295
|
+
return field.icon;
|
|
296
|
+
const byName = FIELD_ICON_BY_NAME[field.field.toLowerCase()] ??
|
|
297
|
+
FIELD_ICON_BY_NAME[kebabLastSegment(field.field)];
|
|
298
|
+
return byName;
|
|
299
|
+
}
|
|
300
|
+
// Dispatches field.type → Input-kind. Select threads options through
|
|
301
|
+
// from the EditFieldViewModel (computeEditViewModel pulls them from
|
|
302
|
+
// SelectFieldDef.options). Structural types without a widget (embedded,
|
|
303
|
+
// jsonb, multiSelect) render read-only instead of a data-destroying text
|
|
304
|
+
// fallback (#1834); truly unknown scalar types still fall back to text.
|
|
305
|
+
function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner, Text, JsonView, t, row, }) {
|
|
306
|
+
const common = {
|
|
307
|
+
id,
|
|
308
|
+
name: field.field,
|
|
309
|
+
disabled: field.readOnly,
|
|
310
|
+
required: field.required,
|
|
311
|
+
hasError,
|
|
312
|
+
};
|
|
313
|
+
switch (field.type) {
|
|
314
|
+
// decimal/bigInt are both plain numbers on the wire (fieldToZod:
|
|
315
|
+
// z.number() / z.number().int().safe()) — the number input's onChange
|
|
316
|
+
// already emits `number | undefined`, so no extra coercion is needed
|
|
317
|
+
// beyond what "number" already does (#1925).
|
|
318
|
+
case "number":
|
|
319
|
+
case "bigInt": {
|
|
320
|
+
const icon = resolveFieldIcon(field);
|
|
321
|
+
const unit = resolveNumberUnit(field.unit, row);
|
|
322
|
+
return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), ...(icon !== undefined && { icon }), ...(unit !== undefined && { unit }) }));
|
|
323
|
+
}
|
|
324
|
+
case "decimal": {
|
|
325
|
+
// step="any" disables the native stepMismatch constraint — without it
|
|
326
|
+
// <input type="number"> defaults to step=1 and blocks form submit on
|
|
327
|
+
// any fractional value via silent browser-native validation.
|
|
328
|
+
const icon = resolveFieldIcon(field);
|
|
329
|
+
return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), step: "any", ...(icon !== undefined && { icon }) }));
|
|
330
|
+
}
|
|
331
|
+
case "tz":
|
|
332
|
+
return (_jsx(Input, { kind: "tz", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
|
|
333
|
+
case "multiSelect": {
|
|
334
|
+
const rawOptions = field.options ?? [];
|
|
335
|
+
const labels = field.optionLabels;
|
|
336
|
+
const multiSelectOptions = labels !== undefined
|
|
337
|
+
? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
|
|
338
|
+
: rawOptions.map((value) => ({ value, label: value }));
|
|
339
|
+
const arrayValue = Array.isArray(field.value) ? field.value : [];
|
|
340
|
+
if (field.display === "checkboxes") {
|
|
341
|
+
return (_jsx(MultiSelectCheckboxes, { field: field, id: id, options: multiSelectOptions, value: arrayValue, onChange: (v) => onChange(v) }));
|
|
342
|
+
}
|
|
343
|
+
return (_jsx(Input, { kind: "combobox", ...common, multiple: true, value: arrayValue, onChange: (v) => onChange(v), options: multiSelectOptions }));
|
|
344
|
+
}
|
|
345
|
+
case "money": {
|
|
346
|
+
const currency = resolveMoneyCurrency(field.value, field.currency);
|
|
347
|
+
return (_jsx(Input, { kind: "money", ...common, value: moneyMinorValue(field.value, currency), onChange: (v) => onChange(moneyPayload(v, currency)), currency: currency, locale: appLocale }));
|
|
348
|
+
}
|
|
349
|
+
case "boolean":
|
|
350
|
+
return (_jsx(Input, { kind: "boolean", ...common, value: field.value === true, onChange: (v) => onChange(v) }));
|
|
351
|
+
case "date":
|
|
352
|
+
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 }) }));
|
|
353
|
+
case "timestamp":
|
|
354
|
+
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 }) }));
|
|
355
|
+
case "locatedTimestamp":
|
|
356
|
+
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 }) }));
|
|
357
|
+
case "select": {
|
|
358
|
+
// Translated Option-Labels kommen aus dem ViewModel-Builder
|
|
359
|
+
// (computeEditViewModel, Convention-Key
|
|
360
|
+
// `<feature>:entity:<entity>:field:<field>:option:<value>`).
|
|
361
|
+
// Wenn keine Translations registriert sind, fallback auf raw
|
|
362
|
+
// value als Label — der ComboboxInput zeigt dann unverändert.
|
|
363
|
+
const rawOptions = field.options ?? [];
|
|
364
|
+
const labels = field.optionLabels;
|
|
365
|
+
const selectOptions = labels !== undefined
|
|
366
|
+
? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
|
|
367
|
+
: rawOptions;
|
|
368
|
+
// `display` is shared with multiSelect's "checkboxes", which is not a
|
|
369
|
+
// select presentation — narrow to the two a select field can request.
|
|
370
|
+
const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
|
|
371
|
+
return (_jsx(Input, { kind: "select", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), options: selectOptions, ...(display !== undefined && { display }) }));
|
|
372
|
+
}
|
|
373
|
+
case "file":
|
|
374
|
+
case "image": {
|
|
375
|
+
const kind = field.type === "image" ? "image" : "file";
|
|
376
|
+
const fileId = typeof field.value === "string" && field.value !== "" ? field.value : null;
|
|
377
|
+
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.fieldName !== undefined && { fieldName: field.fieldName }), ...(field.imageVariant !== undefined && { imageVariant: field.imageVariant }), ...(field.capture !== undefined && { capture: field.capture }) }));
|
|
378
|
+
}
|
|
379
|
+
// embedded (without embeddedListCells — that's embeddedList, which has
|
|
380
|
+
// had its own EmbeddedListField widget since #1838) and jsonb carry
|
|
381
|
+
// arbitrary objects; files/images carry a FileRef-UUID array and have
|
|
382
|
+
// no multi-upload widget yet (deliberately deferred, #1925). Without a
|
|
383
|
+
// dedicated widget these must NOT fall through to a text input:
|
|
384
|
+
// stringValue() turns them into "[object Object]" / a comma-joined
|
|
385
|
+
// string, and saving that overwrites the real data with the mangled
|
|
386
|
+
// string (#1834). A `required: true` on any of these is caught loudly
|
|
387
|
+
// at boot (validateNoWidgetRequiredField in the framework package)
|
|
388
|
+
// instead of silently failing here.
|
|
389
|
+
case "embedded":
|
|
390
|
+
case "jsonb":
|
|
391
|
+
case "files":
|
|
392
|
+
case "images": {
|
|
393
|
+
const hasValue = field.value !== undefined && field.value !== null && field.value !== "";
|
|
394
|
+
return (_jsxs(Banner, { id: id, variant: "info", children: [t("kumiko.field.unsupported"), hasValue &&
|
|
395
|
+
(JsonView !== undefined ? (_jsx(JsonView, { value: field.value, testId: `field-value-${field.field}` })) : (_jsx(Text, { variant: "code", children: JSON.stringify(field.value) })))] }));
|
|
396
|
+
}
|
|
397
|
+
default: {
|
|
398
|
+
if (field.type === "text" && field.format === "password") {
|
|
399
|
+
return (_jsx(Input, { kind: "password", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
|
|
400
|
+
}
|
|
401
|
+
// text + unknown scalar type → text input. If TextFieldDef.multiline
|
|
402
|
+
// is set (the view-model carries it), the renderer switches to
|
|
403
|
+
// textarea. longText always renders a textarea — that's the point of
|
|
404
|
+
// the type — regardless of whether `multiline` is set; `multiline`
|
|
405
|
+
// only supplies an optional `{ rows }` override for it (#1925).
|
|
406
|
+
if (field.type === "longText" || (field.type === "text" && field.multiline)) {
|
|
407
|
+
const rows = typeof field.multiline === "object" ? field.multiline.rows : undefined;
|
|
408
|
+
return (_jsx(Input, { kind: "textarea", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(rows !== undefined && { rows }) }));
|
|
409
|
+
}
|
|
410
|
+
const icon = resolveFieldIcon(field);
|
|
411
|
+
return (_jsx(Input, { kind: "text", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(icon !== undefined && { icon }) }));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function stringValue(v) {
|
|
416
|
+
if (v === undefined || v === null)
|
|
417
|
+
return "";
|
|
418
|
+
return typeof v === "string" ? v : String(v);
|
|
419
|
+
}
|
|
420
|
+
function numberValue(v) {
|
|
421
|
+
if (v === undefined || v === null || v === "")
|
|
422
|
+
return "";
|
|
423
|
+
return typeof v === "number" ? v : Number(v);
|
|
424
|
+
}
|
|
425
|
+
// Read/initial value → MoneyInput's minor-units contract. The server's
|
|
426
|
+
// read shape is `{amount, currency, amountMinor}` in MAJOR units — deliberately
|
|
427
|
+
// NOT reading `amountMinor` here: the server derives it via a flat
|
|
428
|
+
// MINOR_UNIT_SCALE=100 (money.ts), which disagrees with currencyDecimals for
|
|
429
|
+
// zero-decimal currencies like JPY, so it would double-scale JPY amounts.
|
|
430
|
+
// Deriving from `amount * 10**currencyDecimals(currency)` keeps this the
|
|
431
|
+
// exact inverse of moneyPayload below. A bare number (e.g. ConfigEditBody's
|
|
432
|
+
// stored-config coercion) is MAJOR units too — every producer in this repo
|
|
433
|
+
// hands rehydrateMoney's `{amount,…}` shape or a raw major-unit number, never
|
|
434
|
+
// pre-scaled minor units.
|
|
435
|
+
function resolveMoneyCurrency(value, fieldCurrency) {
|
|
436
|
+
if (typeof value === "object" &&
|
|
437
|
+
value !== null &&
|
|
438
|
+
typeof value.currency === "string") {
|
|
439
|
+
return value.currency;
|
|
440
|
+
}
|
|
441
|
+
return fieldCurrency ?? "EUR";
|
|
442
|
+
}
|
|
443
|
+
// Static unit → used as-is. Sibling-field reference → read the live row
|
|
444
|
+
// value; missing/empty/non-string sibling means no suffix, never a guess.
|
|
445
|
+
function resolveNumberUnit(unit, row) {
|
|
446
|
+
if (unit === undefined)
|
|
447
|
+
return undefined;
|
|
448
|
+
if (typeof unit === "string")
|
|
449
|
+
return unit;
|
|
450
|
+
const sibling = row?.[unit.field];
|
|
451
|
+
return typeof sibling === "string" && sibling.length > 0 ? sibling : undefined;
|
|
452
|
+
}
|
|
453
|
+
function moneyMinorValue(v, currency) {
|
|
454
|
+
if (v === undefined || v === null || v === "")
|
|
455
|
+
return "";
|
|
456
|
+
if (typeof v === "number")
|
|
457
|
+
return Math.round(v * 10 ** currencyDecimals(currency));
|
|
458
|
+
if (typeof v === "object") {
|
|
459
|
+
const amount = v.amount;
|
|
460
|
+
if (typeof amount === "number")
|
|
461
|
+
return Math.round(amount * 10 ** currencyDecimals(currency));
|
|
462
|
+
}
|
|
463
|
+
return "";
|
|
464
|
+
}
|
|
465
|
+
// MoneyInput's minor-units onChange → the server payload shape
|
|
466
|
+
// (`z.object({amount, currency})`, schema-builder.ts) — MAJOR units, no
|
|
467
|
+
// `amountMinor` (the server derives that itself on write).
|
|
468
|
+
function moneyPayload(minorUnits, currency) {
|
|
469
|
+
if (minorUnits === undefined)
|
|
470
|
+
return undefined;
|
|
471
|
+
return { amount: minorUnits / 10 ** currencyDecimals(currency), currency };
|
|
472
|
+
}
|
|
473
|
+
// locatedTimestamp-Feldwert: das Read-Wrapper liefert `{ at, tz, utc }`; leer
|
|
474
|
+
// (noch nicht gesetzt) → "" als Empty-Sentinel, analog money/timestamp.
|
|
475
|
+
function locatedValue(v) {
|
|
476
|
+
if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
|
|
477
|
+
const o = v;
|
|
478
|
+
return {
|
|
479
|
+
at: typeof o.at === "string" ? o.at : "",
|
|
480
|
+
tz: typeof o.tz === "string" ? o.tz : "",
|
|
481
|
+
...(typeof o.utc === "string" && { utc: o.utc }),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return "";
|
|
485
|
+
}
|
|
486
|
+
// Read-only text for a `field.readOnly` field without its own `renderer` —
|
|
487
|
+
// per-type formatting that reuses the same value-shaping helpers as the
|
|
488
|
+
// editable widgets above, so a text value and its would-be Input widget stay
|
|
489
|
+
// derived from the identical parse (fw#2245). `type: "reference"` isn't
|
|
490
|
+
// covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
|
|
491
|
+
// `isComplexFieldType` types aren't covered either — callers keep those on
|
|
492
|
+
// `renderInput`'s existing Banner fallback.
|
|
493
|
+
function readOnlyDisplayText(field, appLocale) {
|
|
494
|
+
const { type, value } = field;
|
|
495
|
+
if (value === undefined || value === null || value === "")
|
|
496
|
+
return "—";
|
|
497
|
+
if (type === "text" && field.format === "password")
|
|
498
|
+
return "••••••••";
|
|
499
|
+
switch (type) {
|
|
500
|
+
case "boolean":
|
|
501
|
+
return applyFormatSpec({ format: "boolean" }, value);
|
|
502
|
+
case "date":
|
|
503
|
+
return applyFormatSpec({ format: "date", locale: field.dateLocale ?? appLocale }, value);
|
|
504
|
+
case "timestamp":
|
|
505
|
+
return applyFormatSpec({ format: "timestamp", locale: field.dateLocale ?? appLocale }, value);
|
|
506
|
+
case "locatedTimestamp": {
|
|
507
|
+
const located = locatedValue(value);
|
|
508
|
+
if (located === "" || located.at === "")
|
|
509
|
+
return "—";
|
|
510
|
+
return applyFormatSpec({ format: "timestamp", locale: field.dateLocale ?? appLocale }, located.utc ?? located.at);
|
|
511
|
+
}
|
|
512
|
+
case "number":
|
|
513
|
+
case "bigInt":
|
|
514
|
+
case "decimal": {
|
|
515
|
+
const n = numberValue(value);
|
|
516
|
+
return n === ""
|
|
517
|
+
? "—"
|
|
518
|
+
: new Intl.NumberFormat(appLocale, { useGrouping: field.grouping ?? true }).format(n);
|
|
519
|
+
}
|
|
520
|
+
case "money": {
|
|
521
|
+
const currency = resolveMoneyCurrency(value, field.currency);
|
|
522
|
+
const minor = moneyMinorValue(value, currency);
|
|
523
|
+
if (minor === "")
|
|
524
|
+
return "—";
|
|
525
|
+
const major = minor / 10 ** currencyDecimals(currency);
|
|
526
|
+
return new Intl.NumberFormat(appLocale, { style: "currency", currency }).format(major);
|
|
527
|
+
}
|
|
528
|
+
case "select":
|
|
529
|
+
case "multiSelect": {
|
|
530
|
+
const labels = field.optionLabels;
|
|
531
|
+
const values = Array.isArray(value) ? value : [value];
|
|
532
|
+
if (values.length === 0)
|
|
533
|
+
return "—";
|
|
534
|
+
return values
|
|
535
|
+
.map((v) => (typeof v === "string" ? (labels?.[v] ?? v) : stringValue(v)))
|
|
536
|
+
.join(", ");
|
|
537
|
+
}
|
|
538
|
+
case "file":
|
|
539
|
+
case "image":
|
|
540
|
+
return typeof value === "string" ? value : "—";
|
|
541
|
+
default:
|
|
542
|
+
return stringValue(value);
|
|
543
|
+
}
|
|
544
|
+
}
|