@cosmicdrift/kumiko-renderer 0.333.0 → 0.335.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/config-edit-shim.d.ts +7 -0
- package/dist/app/config-edit-shim.js +39 -0
- package/dist/app/content-preview.js +7 -9
- package/dist/app/extension-sections.d.ts +6 -0
- package/dist/app/form-schema.d.ts +2 -1
- package/dist/app/form-schema.js +11 -2
- package/dist/app/kumiko-screen.d.ts +5 -0
- package/dist/app/kumiko-screen.js +259 -101
- package/dist/app/nav.d.ts +5 -0
- package/dist/app/nav.js +17 -4
- package/dist/app/query-state-banners.d.ts +8 -0
- package/dist/app/query-state-banners.js +14 -0
- package/dist/app/reference-facet-bridge.js +8 -1
- package/dist/app/row-actions.d.ts +12 -0
- package/dist/app/row-actions.js +24 -7
- package/dist/app/screen-access.js +13 -9
- package/dist/app/secret-mint-body.js +41 -7
- package/dist/app/secrets-edit-body.js +89 -33
- package/dist/components/embedded-list-field.js +2 -4
- package/dist/components/grid-cell-for-field.d.ts +7 -2
- package/dist/components/grid-cell-for-field.js +2 -2
- package/dist/components/multi-select-checkboxes.js +4 -1
- package/dist/components/query-options-select.d.ts +12 -3
- package/dist/components/query-options-select.js +45 -2
- package/dist/components/reference-create-dialog.js +9 -4
- package/dist/components/related-list-section.js +8 -10
- package/dist/components/render-edit-action-button.d.ts +1 -1
- package/dist/components/render-edit-action-button.js +5 -1
- package/dist/components/render-edit-logic.d.ts +2 -1
- package/dist/components/render-edit-logic.js +8 -5
- package/dist/components/render-edit-types.d.ts +30 -1
- package/dist/components/render-edit.js +248 -76
- package/dist/components/render-field.d.ts +7 -1
- package/dist/components/render-field.js +40 -15
- package/dist/components/render-list.js +6 -6
- package/dist/components/write-form-section.d.ts +5 -1
- package/dist/components/write-form-section.js +29 -25
- package/dist/context/embedded-screen-context.d.ts +5 -0
- package/dist/context/embedded-screen-context.js +12 -0
- package/dist/hooks/use-reference-lookup.d.ts +7 -0
- package/dist/hooks/use-reference-lookup.js +10 -0
- package/dist/i18n-defaults.js +30 -5
- package/dist/i18n.js +8 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +70 -19
- package/dist/session/session-ended.d.ts +16 -0
- package/dist/session/session-ended.js +73 -0
- package/dist/sort-by-accessor.d.ts +6 -2
- package/dist/sort-by-accessor.js +13 -3
- package/package.json +5 -5
- package/src/changes.json +40 -0
|
@@ -10,7 +10,7 @@ import { screenAccessAllows } from "../app/screen-access.js";
|
|
|
10
10
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
11
11
|
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
|
|
12
12
|
import { useQuery } from "../hooks/use-query.js";
|
|
13
|
-
import {
|
|
13
|
+
import { referenceOptionSource } from "../hooks/use-reference-lookup.js";
|
|
14
14
|
import { useLocale, useTranslation } from "../i18n.js";
|
|
15
15
|
import { usePrimitives } from "../primitives.js";
|
|
16
16
|
import { EmbeddedListField } from "./embedded-list-field.js";
|
|
@@ -19,7 +19,7 @@ import { QueryOptionsSelect } from "./query-options-select.js";
|
|
|
19
19
|
import { ReferenceCreateDialog } from "./reference-create-dialog.js";
|
|
20
20
|
const AllFieldsRequiredContext = createContext(false);
|
|
21
21
|
export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
|
|
22
|
-
export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay = "form", row, changed, }) {
|
|
22
|
+
export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay = "form", row, changed, layout, accent, }) {
|
|
23
23
|
const { Field, Input, Banner, Text, JsonView } = usePrimitives();
|
|
24
24
|
// App-Locale (i18n) für money/date-Inputs — sonst fielen sie auf
|
|
25
25
|
// navigator.language (Browser-Sprache) zurück statt der gewählten
|
|
@@ -42,7 +42,7 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
42
42
|
// Reference-Field rendert eine eigene Component — sie nutzt
|
|
43
43
|
// useQuery() für den Live-Lookup, also muss sie als React-
|
|
44
44
|
// Komponente gemountet werden (nicht als pure render-Call).
|
|
45
|
-
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, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale) })) : (renderInput({
|
|
45
|
+
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 })) : readOnlyText && !isComplexFieldType(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 })) : (renderInput({
|
|
46
46
|
field,
|
|
47
47
|
id,
|
|
48
48
|
hasError,
|
|
@@ -55,17 +55,33 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
55
55
|
t,
|
|
56
56
|
row,
|
|
57
57
|
}));
|
|
58
|
-
return (_jsx(Field, { id: id, label: field.label, required: field.required && field.type !== "boolean" && !allFieldsRequired, ...(issues !== undefined && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(changed === true && { changed }), testId: `field-${field.field}`, children: control }));
|
|
58
|
+
return (_jsx(Field, { id: id, label: field.label, required: field.required && field.type !== "boolean" && !allFieldsRequired, ...(issues !== undefined && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(appendixPlacement !== undefined && { appendixPlacement }), ...(description !== undefined && { description }), ...(status !== undefined && { status }), ...(changed === true && { changed }), ...(layout !== undefined && { layout }), ...(accent === true && { accent }), testId: `field-${field.field}`, children: control }));
|
|
59
59
|
}
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// (
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
// Wire value of a writeOnly field is `true` (set) / `null` (empty), never the
|
|
61
|
+
// secret. Typing sets a new value; an untouched or emptied input submits
|
|
62
|
+
// nothing / "" which the server reads as "keep". `null` after a loaded `true`
|
|
63
|
+
// (changed) means "remove on save".
|
|
64
|
+
function WriteOnlyTextInput({ field, id, hasError, onChange, changed, }) {
|
|
65
|
+
const { Input } = usePrimitives();
|
|
66
|
+
const t = useTranslation();
|
|
67
|
+
const isSet = field.value === true;
|
|
68
|
+
const isMarkedForRemoval = field.value === null && changed;
|
|
69
|
+
if (isMarkedForRemoval) {
|
|
70
|
+
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: {
|
|
71
|
+
icon: "undo",
|
|
72
|
+
label: t("kumiko.field.writeOnly.undo"),
|
|
73
|
+
onPress: () => onChange(true),
|
|
74
|
+
} }));
|
|
67
75
|
}
|
|
68
|
-
return
|
|
76
|
+
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 &&
|
|
77
|
+
!field.required &&
|
|
78
|
+
!field.readOnly && {
|
|
79
|
+
trailingAction: {
|
|
80
|
+
icon: "clear",
|
|
81
|
+
label: t("kumiko.field.writeOnly.remove"),
|
|
82
|
+
onPress: () => onChange(null),
|
|
83
|
+
},
|
|
84
|
+
}) }));
|
|
69
85
|
}
|
|
70
86
|
// Tier 2.7e-3 + 2.1c: Reference-Input rendert eine Searchable Combobox
|
|
71
87
|
// gefüllt aus einer Live-Query auf die referenced Entity. Default-
|
|
@@ -438,15 +454,22 @@ function resolveMoneyCurrency(value, fieldCurrency) {
|
|
|
438
454
|
}
|
|
439
455
|
return fieldCurrency ?? "EUR";
|
|
440
456
|
}
|
|
457
|
+
// The suffix span is absolutely positioned inside a fixed right padding sized
|
|
458
|
+
// for short symbols; a free-text sibling value longer than any real unit
|
|
459
|
+
// symbol would overflow the number.
|
|
460
|
+
const MAX_SIBLING_UNIT_LENGTH = 8;
|
|
441
461
|
// Static unit → used as-is. Sibling-field reference → read the live row
|
|
442
|
-
// value; missing/empty/non-string sibling means no suffix, never a guess.
|
|
462
|
+
// value; missing/empty/non-string/overlong sibling means no suffix, never a guess.
|
|
443
463
|
function resolveNumberUnit(unit, row) {
|
|
444
464
|
if (unit === undefined)
|
|
445
465
|
return undefined;
|
|
446
466
|
if (typeof unit === "string")
|
|
447
467
|
return unit;
|
|
448
468
|
const sibling = row?.[unit.field];
|
|
449
|
-
|
|
469
|
+
if (typeof sibling !== "string")
|
|
470
|
+
return undefined;
|
|
471
|
+
const trimmed = sibling.trim();
|
|
472
|
+
return trimmed.length > 0 && trimmed.length <= MAX_SIBLING_UNIT_LENGTH ? trimmed : undefined;
|
|
450
473
|
}
|
|
451
474
|
function moneyMinorValue(v, currency) {
|
|
452
475
|
if (v === undefined || v === null || v === "")
|
|
@@ -488,10 +511,12 @@ function locatedValue(v) {
|
|
|
488
511
|
// covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
|
|
489
512
|
// `isComplexFieldType` types aren't covered either — callers keep those on
|
|
490
513
|
// `renderInput`'s existing Banner fallback.
|
|
491
|
-
function readOnlyDisplayText(field, appLocale) {
|
|
514
|
+
function readOnlyDisplayText(field, appLocale, t) {
|
|
492
515
|
const { type, value } = field;
|
|
493
516
|
if (value === undefined || value === null || value === "")
|
|
494
517
|
return "—";
|
|
518
|
+
if (type === "text" && field.writeOnly === true)
|
|
519
|
+
return t("kumiko.field.writeOnly.set");
|
|
495
520
|
if (type === "text" && field.format === "password")
|
|
496
521
|
return "••••••••";
|
|
497
522
|
switch (type) {
|
|
@@ -6,11 +6,11 @@ import { useReferenceLookup } from "../hooks/use-reference-lookup.js";
|
|
|
6
6
|
import { useTranslation } from "../i18n.js";
|
|
7
7
|
import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header-slot.js";
|
|
8
8
|
import { shouldRenderActionsIconOnly, usePrimitives, } from "../primitives.js";
|
|
9
|
+
import { needsActionConfirm } from "./render-edit-action-button.js";
|
|
9
10
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
10
|
-
//
|
|
11
|
-
// page header without losing the confirm dialog or busy/error surfacing.
|
|
11
|
+
// Icon-less actions are promoted too (with a "plus" fallback at the call site).
|
|
12
12
|
function isHeaderPromotableToolbarAction(action) {
|
|
13
|
-
return
|
|
13
|
+
return action.style === "primary" && !needsActionConfirm(action);
|
|
14
14
|
}
|
|
15
15
|
export function RenderList(props) {
|
|
16
16
|
const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode, rowGrouping, rowTone, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, } = props;
|
|
@@ -147,7 +147,7 @@ export function RenderList(props) {
|
|
|
147
147
|
pageHeaderCompact
|
|
148
148
|
? toolbarActions?.find(isHeaderPromotableToolbarAction)
|
|
149
149
|
: undefined;
|
|
150
|
-
const promotedActionButton = promotedToolbarAction !== undefined ? (_jsx(
|
|
150
|
+
const promotedActionButton = promotedToolbarAction !== undefined ? (_jsx(ToolbarActionView, { action: { ...promotedToolbarAction, icon: promotedToolbarAction.icon ?? "plus" }, iconOnly: true, Button: Button, Dialog: Dialog, Banner: Banner })) : undefined;
|
|
151
151
|
const toolbarActionsInToolbar = toolbarActions?.filter((a) => a !== promotedToolbarAction) ?? [];
|
|
152
152
|
const hasToolbarActionsInToolbar = toolbarActionsInToolbar.length > 0;
|
|
153
153
|
const toolbarEnd = hasHeaderSlot ||
|
|
@@ -239,9 +239,9 @@ function ToolbarActionView({ action, iconOnly = false, Button, Dialog, Banner, }
|
|
|
239
239
|
}
|
|
240
240
|
};
|
|
241
241
|
const variant = action.style ?? "secondary";
|
|
242
|
-
const needsConfirm =
|
|
242
|
+
const needsConfirm = needsActionConfirm(action);
|
|
243
243
|
const showIconOnly = iconOnly && action.icon !== undefined;
|
|
244
|
-
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: variant, loading: busy, ...(action.icon !== undefined && { icon: action.icon }), size: showIconOnly ? "icon" : "sm", ...(showIconOnly && { ariaLabel: action.label }), onClick: () => {
|
|
244
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: variant, loading: busy, ...(action.icon !== undefined && { icon: action.icon }), size: showIconOnly ? "icon" : "sm", ...(showIconOnly && { ariaLabel: action.label, title: action.label }), onClick: () => {
|
|
245
245
|
if (needsConfirm) {
|
|
246
246
|
setConfirmOpen(true);
|
|
247
247
|
}
|
|
@@ -13,5 +13,9 @@ export type WriteFormSectionProps = {
|
|
|
13
13
|
* rendered alongside (before) the section's own submit button in the
|
|
14
14
|
* title-row actions slot. */
|
|
15
15
|
readonly actions?: ReactNode;
|
|
16
|
+
/** Set when this section is a whole tab: the submit button is handed to
|
|
17
|
+
* the host form's footer (pinned like every other screen's save) instead
|
|
18
|
+
* of the section's title row. Called with undefined on unmount. */
|
|
19
|
+
readonly onFooterAction?: (action: ReactNode | undefined) => void;
|
|
16
20
|
};
|
|
17
|
-
export declare function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, }: WriteFormSectionProps): ReactNode;
|
|
21
|
+
export declare function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, onFooterAction, }: WriteFormSectionProps): ReactNode;
|
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { I18N_KEY_PARAM } from "@cosmicdrift/kumiko-headless";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
-
import { REQUIRED_FIELD_I18N_KEY } from "../app/form-schema.js";
|
|
5
|
+
import { isPresent, REQUIRED_FIELD_I18N_KEY } from "../app/form-schema.js";
|
|
6
6
|
import { dispatcherErrorText } from "../app/write-failed-error.js";
|
|
7
7
|
import { useForm } from "../hooks/use-form.js";
|
|
8
8
|
import { useTranslation } from "../i18n.js";
|
|
9
|
-
import { usePrimitives } from "../primitives.js";
|
|
9
|
+
import { STICKY_PRIMARY_ACTION_PROP, usePrimitives } from "../primitives.js";
|
|
10
10
|
import { GridCellForField } from "./grid-cell-for-field.js";
|
|
11
|
-
|
|
12
|
-
// — duplicated because that helper walks raw EditFieldSpec + EntityDefinition,
|
|
13
|
-
// unavailable here: this section only ever sees computeEditViewModel's
|
|
14
|
-
// already-resolved EditFieldViewModel[] (required/readOnly/visible are plain
|
|
15
|
-
// booleans by the time render-edit.tsx hands the section to this component).
|
|
16
|
-
function isPresent(value) {
|
|
17
|
-
if (value === undefined || value === null || value === "")
|
|
18
|
-
return false;
|
|
19
|
-
if (Array.isArray(value) && value.length === 0)
|
|
20
|
-
return false;
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
11
|
+
import { hasIssueWithoutVisibleField } from "./render-edit-logic.js";
|
|
23
12
|
function buildWriteFormSchema(fields) {
|
|
24
13
|
return z
|
|
25
14
|
.object({})
|
|
@@ -45,7 +34,7 @@ function buildWriteFormSchema(fields) {
|
|
|
45
34
|
// would render a second, invalid nested <form> inside the host's own form
|
|
46
35
|
// element — so this mounts its own independent useForm() controller and
|
|
47
36
|
// submits via a plain button click instead of native form submission.
|
|
48
|
-
export function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, }) {
|
|
37
|
+
export function WriteFormSection({ section, featureName, translate, hideTitle, onSubmitted, actions, onFooterAction, }) {
|
|
49
38
|
const { Section, Card, Grid, GridCell, Button, Banner } = usePrimitives();
|
|
50
39
|
const t = useTranslation();
|
|
51
40
|
const effectiveTranslate = translate ?? t;
|
|
@@ -71,11 +60,14 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
71
60
|
}
|
|
72
61
|
if (result.validationBlocked)
|
|
73
62
|
return;
|
|
74
|
-
// Field-level issues
|
|
75
|
-
// GridCellForField/RenderField path RenderEdit uses) — the banner
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
63
|
+
// Field-level issues surface inline via snapshot.errors (same
|
|
64
|
+
// GridCellForField/RenderField path RenderEdit uses) — the banner stays
|
|
65
|
+
// for anything no rendered field can show (`version`, `id`, a root
|
|
66
|
+
// refine, a hidden field).
|
|
67
|
+
const issuePaths = (result.error.details?.fields ?? []).map((i) => i.path);
|
|
68
|
+
setError(hasIssueWithoutVisibleField(issuePaths, section.fields)
|
|
69
|
+
? dispatcherErrorText(result.error, effectiveTranslate)
|
|
70
|
+
: null);
|
|
79
71
|
}
|
|
80
72
|
finally {
|
|
81
73
|
setIsSubmitting(false);
|
|
@@ -85,8 +77,20 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
85
77
|
// type="button" (not "submit") is load-bearing: this section is deliberately
|
|
86
78
|
// NOT a nested <form> (see the component doc above), so a "submit" type
|
|
87
79
|
// would instead trigger the host RenderEdit's own form submit.
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
// The footer copy of the button outlives this render, so it calls the
|
|
81
|
+
// latest handleSubmit through a ref instead of capturing a stale closure.
|
|
82
|
+
const handleSubmitRef = useRef(handleSubmit);
|
|
83
|
+
handleSubmitRef.current = handleSubmit;
|
|
84
|
+
const submitLabel = section.submitLabel ?? effectiveTranslate("kumiko.actions.save");
|
|
85
|
+
const submitButton = (_jsx(Button, { type: "button", variant: "primary", disabled: isSubmitting, loading: isSubmitting, onClick: () => void handleSubmitRef.current(), testId: "write-form-section-submit", [STICKY_PRIMARY_ACTION_PROP]: true, children: submitLabel }));
|
|
86
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: submitButton is rebuilt every render; isSubmitting and submitLabel are everything it shows.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (onFooterAction === undefined)
|
|
89
|
+
return;
|
|
90
|
+
onFooterAction(submitButton);
|
|
91
|
+
return () => onFooterAction(undefined);
|
|
92
|
+
}, [onFooterAction, isSubmitting, submitLabel]);
|
|
93
|
+
const titleRowActions = onFooterAction !== undefined ? (actions) : actions !== undefined ? (_jsxs(_Fragment, { children: [actions, submitButton] })) : (submitButton);
|
|
90
94
|
// Section always flattens to a borderless divider when rendered inside
|
|
91
95
|
// RenderEdit's own <Form> in tabs mode (hideTitle) — an unframed Card panel
|
|
92
96
|
// stands in, like render-edit.tsx's tabs-mode fields/extension branches.
|
|
@@ -94,10 +98,10 @@ export function WriteFormSection({ section, featureName, translate, hideTitle, o
|
|
|
94
98
|
return (_jsx(Card, { options: { framed: false }, slots: {
|
|
95
99
|
...(section.description !== undefined && { subtitle: section.description }),
|
|
96
100
|
headerActions: titleRowActions,
|
|
97
|
-
}, testId: `write-form-${section.title ??
|
|
101
|
+
}, testId: `write-form-${section.title ?? section.handler}`, children: content }));
|
|
98
102
|
}
|
|
99
103
|
// Routed through Section's `actions` slot (same mechanism render-edit.tsx
|
|
100
104
|
// uses via Form's `actions`) so the button gets the established right-
|
|
101
105
|
// aligned footer treatment instead of stretching full-width inline.
|
|
102
|
-
return (_jsx(Section, { ...(section.title !== undefined && { title: section.title }), ...(section.description !== undefined && { subtitle: section.description }), ...(section.icon !== undefined && { icon: section.icon }), actions: titleRowActions, testId: `write-form-${section.title ??
|
|
106
|
+
return (_jsx(Section, { ...(section.title !== undefined && { title: section.title }), ...(section.description !== undefined && { subtitle: section.description }), ...(section.icon !== undefined && { icon: section.icon }), actions: titleRowActions, testId: `write-form-${section.title ?? section.handler}`, children: content }));
|
|
103
107
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
// A host that already frames and pads the embedded screen (dashboard panel
|
|
4
|
+
// with `chromeless`) marks the subtree, so list screens drop their own
|
|
5
|
+
// screen padding and card frame instead of stacking it on the host's.
|
|
6
|
+
const EmbeddedScreenContext = createContext(false);
|
|
7
|
+
export function EmbeddedScreenProvider({ children }) {
|
|
8
|
+
return _jsx(EmbeddedScreenContext, { value: true, children: children });
|
|
9
|
+
}
|
|
10
|
+
export function useIsEmbeddedScreen() {
|
|
11
|
+
return useContext(EmbeddedScreenContext);
|
|
12
|
+
}
|
|
@@ -6,6 +6,13 @@ export declare function referenceLookupSource(refFeature: string, refEntity: str
|
|
|
6
6
|
readonly queryQn: string;
|
|
7
7
|
readonly labelKey: string;
|
|
8
8
|
};
|
|
9
|
+
export declare function referenceOptionSource(field: {
|
|
10
|
+
readonly refLabelField?: string;
|
|
11
|
+
readonly refOptionsQuery?: string;
|
|
12
|
+
}, refFeature: string, refEntity: string): {
|
|
13
|
+
readonly queryQn: string;
|
|
14
|
+
readonly labelKey: string;
|
|
15
|
+
};
|
|
9
16
|
/** Empty while the query loads (or fails), so callers fall back to the raw id.
|
|
10
17
|
* `featureName` is the reference's target feature, not the screen's. */
|
|
11
18
|
export declare function useReferenceLookup(featureName: string, refEntity: string, labelField: string): {
|
|
@@ -34,6 +34,16 @@ export function referenceLookupSource(refFeature, refEntity, labelField) {
|
|
|
34
34
|
labelKey: labelField,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
// Where a reference field's option rows come from, and which of their keys
|
|
38
|
+
// holds the display text — the entity's own list handler plus `refLabelField`
|
|
39
|
+
// by default, or an author-supplied handler whose rows carry `label`
|
|
40
|
+
// (fw#2780), for targets with no readable column of their own.
|
|
41
|
+
export function referenceOptionSource(field, refFeature, refEntity) {
|
|
42
|
+
if (field.refOptionsQuery !== undefined) {
|
|
43
|
+
return { queryQn: field.refOptionsQuery, labelKey: "label" };
|
|
44
|
+
}
|
|
45
|
+
return referenceLookupSource(refFeature, refEntity, field.refLabelField ?? "id");
|
|
46
|
+
}
|
|
37
47
|
/** Empty while the query loads (or fails), so callers fall back to the raw id.
|
|
38
48
|
* `featureName` is the reference's target feature, not the screen's. */
|
|
39
49
|
export function useReferenceLookup(featureName, refEntity, labelField) {
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -14,6 +14,7 @@ export const kumikoDefaultTranslations = {
|
|
|
14
14
|
"kumiko.actions.delete": "Delete",
|
|
15
15
|
"kumiko.actions.delete-confirm": "Confirm delete?",
|
|
16
16
|
"kumiko.actions.reload": "Reload",
|
|
17
|
+
"kumiko.actions.retry": "Try again",
|
|
17
18
|
"kumiko.actions.create": "New",
|
|
18
19
|
"kumiko.actions.edit": "Edit",
|
|
19
20
|
"kumiko.actions.view": "View",
|
|
@@ -36,6 +37,11 @@ export const kumikoDefaultTranslations = {
|
|
|
36
37
|
"kumiko.field.locatedTzHint": "Time local to the given location",
|
|
37
38
|
"kumiko.field.reference-created-no-id": "Record was created but could not be selected automatically. Please select it manually.",
|
|
38
39
|
"kumiko.field.unsupported": "This field type can't be edited here yet.",
|
|
40
|
+
"kumiko.field.writeOnly.remove": "Remove stored value",
|
|
41
|
+
"kumiko.field.writeOnly.set": "Set",
|
|
42
|
+
"kumiko.field.writeOnly.setPlaceholder": "Set — leave empty to keep it",
|
|
43
|
+
"kumiko.field.writeOnly.undo": "Undo",
|
|
44
|
+
"kumiko.field.writeOnly.willRemove": "Removed on save",
|
|
39
45
|
"kumiko.field.embedded-list.add-row": "Add row",
|
|
40
46
|
"kumiko.field.embedded-list.remove-row": "Remove row",
|
|
41
47
|
"kumiko.field.embedded-list.duplicate-row": "Duplicate row",
|
|
@@ -77,6 +83,9 @@ export const kumikoDefaultTranslations = {
|
|
|
77
83
|
"kumiko.list.sort.by": "Sorted by {column}",
|
|
78
84
|
"kumiko.form.changed": "changed",
|
|
79
85
|
"kumiko.form.on-this-page": "On this page",
|
|
86
|
+
"kumiko.form.noUnsavedChanges": "No unsaved changes",
|
|
87
|
+
"kumiko.form.discard": "Discard",
|
|
88
|
+
"kumiko.form.saveChanges": "Save changes",
|
|
80
89
|
"kumiko.form.unsaved.one": "1 unsaved change",
|
|
81
90
|
"kumiko.form.unsaved.other": "{count} unsaved changes",
|
|
82
91
|
"kumiko.wizard.next-with-title": "Next: {title}",
|
|
@@ -153,16 +162,30 @@ export const kumikoDefaultTranslations = {
|
|
|
153
162
|
"kumiko.contentEditor.heading2": "Heading 2",
|
|
154
163
|
"kumiko.contentEditor.bulletList": "Bullet list",
|
|
155
164
|
"kumiko.contentEditor.orderedList": "Numbered list",
|
|
156
|
-
"kumiko.config.source.user": "
|
|
165
|
+
"kumiko.config.source.user": "Your value",
|
|
157
166
|
"kumiko.config.source.tenant": "Tenant",
|
|
158
|
-
"kumiko.config.source.system": "
|
|
167
|
+
"kumiko.config.source.system": "Platform",
|
|
159
168
|
"kumiko.config.source.appOverride": "App override",
|
|
160
169
|
"kumiko.config.source.computed": "Computed",
|
|
161
170
|
"kumiko.config.source.default": "Default",
|
|
162
171
|
"kumiko.config.source.missing": "Missing",
|
|
163
|
-
"kumiko.config.cascade.noValue": "
|
|
164
|
-
"kumiko.config.cascade.
|
|
165
|
-
"kumiko.config.cascade.
|
|
172
|
+
"kumiko.config.cascade.noValue": "Not set",
|
|
173
|
+
"kumiko.config.cascade.noDefault": "not set",
|
|
174
|
+
"kumiko.config.cascade.notSetRequired": "Not set. Required.",
|
|
175
|
+
"kumiko.config.cascade.activeMarker": "in use",
|
|
176
|
+
"kumiko.config.cascade.resetTo": "Reset to default",
|
|
177
|
+
"kumiko.config.cascade.resetToApp": "Reset to app value",
|
|
178
|
+
"kumiko.config.cascade.showLevels": "Show all levels",
|
|
179
|
+
"kumiko.config.cascade.hideLevels": "Hide levels",
|
|
180
|
+
"kumiko.config.cascade.on": "On",
|
|
181
|
+
"kumiko.config.cascade.off": "Off",
|
|
182
|
+
"kumiko.config.cascade.origin.tenant": "Set for this tenant. Default is {value}.",
|
|
183
|
+
"kumiko.config.cascade.origin.user": "Set by you. Default is {value}.",
|
|
184
|
+
"kumiko.config.cascade.originNoDefault.tenant": "Set for this tenant.",
|
|
185
|
+
"kumiko.config.cascade.originNoDefault.user": "Set by you.",
|
|
186
|
+
"kumiko.config.cascade.originNoDefault.system": "Set for the platform.",
|
|
187
|
+
"kumiko.config.cascade.origin.system": "Set for the platform. App default is {value}.",
|
|
188
|
+
"kumiko.config.cascade.origin.default": "Uses the default",
|
|
166
189
|
"kumiko.form.error.generic": "Something went wrong.",
|
|
167
190
|
"kumiko.form.error.version-conflict": "Record was modified in the meantime. Reload and try again.",
|
|
168
191
|
"kumiko.form.extension.save-failed": "A custom field could not be saved.",
|
|
@@ -183,6 +206,8 @@ export const kumikoDefaultTranslations = {
|
|
|
183
206
|
"errors.validation.invalid_type": "Invalid value.",
|
|
184
207
|
"errors.validation.too_small": "Too small or too short (minimum: {minimum}).",
|
|
185
208
|
"errors.validation.too_big": "Too big or too long (maximum: {maximum}).",
|
|
209
|
+
"kumiko.validation.number.min": "Must be at least {minimum}.",
|
|
210
|
+
"kumiko.validation.number.max": "Must be {maximum} or less.",
|
|
186
211
|
"errors.validation.invalid_format": "Invalid format.",
|
|
187
212
|
"errors.validation.not_multiple_of": "Must be a multiple of {divisor}.",
|
|
188
213
|
"errors.validation.unrecognized_keys": "Unknown fields.",
|
package/dist/i18n.js
CHANGED
|
@@ -65,7 +65,8 @@ export function useLocale() {
|
|
|
65
65
|
}
|
|
66
66
|
// Subscribe + current locale-snapshot. Wir brauchen den Rückgabewert
|
|
67
67
|
// selbst nicht — wichtig ist nur der re-render-trigger.
|
|
68
|
-
|
|
68
|
+
const getLocaleSnapshot = () => ctx.resolver.locale();
|
|
69
|
+
useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
|
|
69
70
|
return ctx.resolver;
|
|
70
71
|
}
|
|
71
72
|
/** `t("key", params)` resolves in this order:
|
|
@@ -83,7 +84,8 @@ export function useTranslation() {
|
|
|
83
84
|
// Re-Render bei Sprach-Wechsel. `ctx.resolver.subscribe` ist bereits
|
|
84
85
|
// eine stable-reference aus dem Resolver, daher hier keine eigene
|
|
85
86
|
// Memoization der Subscribe-Callback nötig.
|
|
86
|
-
const
|
|
87
|
+
const getLocaleSnapshot = () => ctx.resolver.locale();
|
|
88
|
+
const locale = useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
|
|
87
89
|
// `t` MUSS referenz-stabil sein solange sich Resolver/Bundles/Locale
|
|
88
90
|
// nicht ändern — Consumer nutzen `t` regelmäßig in useEffect-Deps
|
|
89
91
|
// (z.B. um Queries neu zu laden wenn sich die Sprache ändert). Ein neu
|
|
@@ -132,7 +134,8 @@ function bundleLocaleTiers(locale, fallbackLocale, formality) {
|
|
|
132
134
|
* Used by DataTable FormatCell so plain tables without a provider do not crash. */
|
|
133
135
|
export function useOptionalLocale() {
|
|
134
136
|
const ctx = useContext(LocaleContext);
|
|
135
|
-
|
|
137
|
+
const getLocaleSnapshot = () => (ctx ? ctx.resolver.locale() : "en");
|
|
138
|
+
useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), getLocaleSnapshot, getLocaleSnapshot);
|
|
136
139
|
return ctx === undefined ? undefined : ctx.resolver.locale();
|
|
137
140
|
}
|
|
138
141
|
/** Non-throwing time-zone read — "UTC" outside LocaleProvider, matching
|
|
@@ -144,7 +147,8 @@ export function useOptionalTimeZone() {
|
|
|
144
147
|
/** Non-throwing translate — undefined outside LocaleProvider. */
|
|
145
148
|
export function useOptionalTranslation() {
|
|
146
149
|
const ctx = useContext(LocaleContext);
|
|
147
|
-
const
|
|
150
|
+
const getLocaleSnapshot = () => (ctx ? ctx.resolver.locale() : "en");
|
|
151
|
+
const locale = useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), getLocaleSnapshot, getLocaleSnapshot);
|
|
148
152
|
const formality = useContext(FormalityContext);
|
|
149
153
|
const t = useCallback((key, params) => ctx === undefined ? key : translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
|
|
150
154
|
return ctx === undefined ? undefined : t;
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error.
|
|
|
35
35
|
export { RelatedListSection } from "./components/related-list-section.js";
|
|
36
36
|
export type { RenderEditAction, RenderEditChangeState, RenderEditControls, RenderEditProps, } from "./components/render-edit.js";
|
|
37
37
|
export { RenderEdit } from "./components/render-edit.js";
|
|
38
|
+
export { needsActionConfirm } from "./components/render-edit-action-button.js";
|
|
38
39
|
export type { RenderFieldProps } from "./components/render-field.js";
|
|
39
40
|
export { RenderField } from "./components/render-field.js";
|
|
40
41
|
export type { RenderListProps } from "./components/render-list.js";
|
|
@@ -43,6 +44,7 @@ export type { DispatcherProviderProps } from "./context/dispatcher-context.js";
|
|
|
43
44
|
export { DispatcherProvider, useDispatcher, useDispatcherStatus, useOptionalDispatcher, } from "./context/dispatcher-context.js";
|
|
44
45
|
export type { DraftStorage, DraftStorageProviderProps } from "./context/draft-storage-context.js";
|
|
45
46
|
export { DraftStorageProvider, useDraftStorage } from "./context/draft-storage-context.js";
|
|
47
|
+
export { EmbeddedScreenProvider, useIsEmbeddedScreen } from "./context/embedded-screen-context.js";
|
|
46
48
|
export type { UserRolesProviderProps } from "./context/user-roles-context.js";
|
|
47
49
|
export { UserRolesProvider, useUserRoles } from "./context/user-roles-context.js";
|
|
48
50
|
export { formatWhen } from "./format-when.js";
|
|
@@ -67,8 +69,10 @@ export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTra
|
|
|
67
69
|
export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
68
70
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
69
71
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
|
70
|
-
export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FillContainerProps, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, WizardStepGroupProps, } from "./primitives.js";
|
|
72
|
+
export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FillContainerProps, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.js";
|
|
71
73
|
export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
|
|
74
|
+
export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.js";
|
|
75
|
+
export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
|
|
72
76
|
export { buildWhatsAppShareUrl } from "./share-links.js";
|
|
73
77
|
export { sortByAccessor } from "./sort-by-accessor.js";
|
|
74
78
|
export type { LiveEvent, LiveEventSubscriber, LiveEventsProviderProps } from "./sse/live-events.js";
|
package/dist/index.js
CHANGED
|
@@ -29,10 +29,12 @@ export { VariableChips } from "./app/variable-chips.js";
|
|
|
29
29
|
export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error.js";
|
|
30
30
|
export { RelatedListSection } from "./components/related-list-section.js";
|
|
31
31
|
export { RenderEdit } from "./components/render-edit.js";
|
|
32
|
+
export { needsActionConfirm } from "./components/render-edit-action-button.js";
|
|
32
33
|
export { RenderField } from "./components/render-field.js";
|
|
33
34
|
export { RenderList } from "./components/render-list.js";
|
|
34
35
|
export { DispatcherProvider, useDispatcher, useDispatcherStatus, useOptionalDispatcher, } from "./context/dispatcher-context.js";
|
|
35
36
|
export { DraftStorageProvider, useDraftStorage } from "./context/draft-storage-context.js";
|
|
37
|
+
export { EmbeddedScreenProvider, useIsEmbeddedScreen } from "./context/embedded-screen-context.js";
|
|
36
38
|
export { UserRolesProvider, useUserRoles } from "./context/user-roles-context.js";
|
|
37
39
|
export { formatWhen } from "./format-when.js";
|
|
38
40
|
export { REFERENCE_COMBOBOX_LIMIT, REFERENCE_LIST_LOOKUP_LIMIT, REFERENCE_SEARCH_DEBOUNCE_MS, } from "./hooks/reference-limits.js";
|
|
@@ -49,6 +51,7 @@ export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
|
49
51
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
50
52
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
|
51
53
|
export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
|
|
54
|
+
export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
|
|
52
55
|
export { buildWhatsAppShareUrl } from "./share-links.js";
|
|
53
56
|
export { sortByAccessor } from "./sort-by-accessor.js";
|
|
54
57
|
export { LiveEventsProvider, useLiveEvents } from "./sse/live-events.js";
|