@cosmicdrift/kumiko-renderer 0.351.0 → 0.353.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/kumiko-screen.js +3 -2
- package/dist/app/projection-list-shim.d.ts +4 -3
- package/dist/app/projection-list-shim.js +9 -4
- package/dist/app/secret-mint-body.js +31 -8
- package/dist/components/availability-options-select.d.ts +22 -0
- package/dist/components/availability-options-select.js +49 -0
- package/dist/components/render-edit.js +10 -7
- package/dist/components/render-field.js +5 -6
- package/dist/hooks/use-reference-lookup.js +2 -1
- package/dist/i18n-defaults.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/primitives.d.ts +9 -3
- package/dist/primitives.js +8 -1
- package/package.json +5 -5
- package/src/changes.json +11 -0
|
@@ -319,7 +319,7 @@ export function literalCurrencyOverrides(fields) {
|
|
|
319
319
|
// names themselves because an edit form only needs the fetch for fields the
|
|
320
320
|
// record has no stored value for.
|
|
321
321
|
export function useMoneyCurrencyOverrides(fields, tenantFieldNames, fallback) {
|
|
322
|
-
const query = useQuery("config:query:values", {}, { enabled: tenantFieldNames.length > 0 });
|
|
322
|
+
const query = useQuery("config:query:values", { keys: [TENANT_CURRENCY_CONFIG_KEY] }, { enabled: tenantFieldNames.length > 0 });
|
|
323
323
|
const resolution = resolveTenantCurrency(tenantFieldNames, query, fallback);
|
|
324
324
|
const resolvedTenantCurrency = resolution.status === "ready" ? resolution.currency : undefined;
|
|
325
325
|
const overrides = useMemo(() => {
|
|
@@ -1830,7 +1830,8 @@ function headerStatusTone(header, record) {
|
|
|
1830
1830
|
const declared = header.statusTones !== undefined && Object.hasOwn(header.statusTones, value)
|
|
1831
1831
|
? header.statusTones[value]
|
|
1832
1832
|
: undefined;
|
|
1833
|
-
|
|
1833
|
+
const declaredTone = declared !== undefined ? statusToneForOptionTone(declared) : undefined;
|
|
1834
|
+
return declaredTone ?? statusToneForValue(value);
|
|
1834
1835
|
}
|
|
1835
1836
|
function resolveSubtitleHref(header, record) {
|
|
1836
1837
|
if (header?.subtitleHref === undefined)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { EntityDefinition, EntityListScreenDefinition, ListColumnSpec, ProjectionListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
/** Minimal EntityDefinition from the column list: every field is a text
|
|
3
|
-
* field. `sortable` applies
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* field. `sortable` applies to all columns — buildAppSchema derives it from
|
|
4
|
+
* the query's Zod schema (`screen.sortable`, fw#2165); the query itself has
|
|
5
|
+
* no per-field server-sort guarantee, so a column opts out with
|
|
6
|
+
* `sortable: false` when the handler cannot order by it.
|
|
6
7
|
* computeListViewModel only reads `fields[<col>].type` — text is enough,
|
|
7
8
|
* presentation comes from the column renderer + explicit label. */
|
|
8
9
|
export declare function synthesizeProjectionEntity(columns: readonly ListColumnSpec[], sortable: boolean): EntityDefinition;
|
|
@@ -16,15 +16,20 @@
|
|
|
16
16
|
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
17
17
|
const PROJECTION_PSEUDO_ENTITY = "__projection__";
|
|
18
18
|
/** Minimal EntityDefinition from the column list: every field is a text
|
|
19
|
-
* field. `sortable` applies
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* field. `sortable` applies to all columns — buildAppSchema derives it from
|
|
20
|
+
* the query's Zod schema (`screen.sortable`, fw#2165); the query itself has
|
|
21
|
+
* no per-field server-sort guarantee, so a column opts out with
|
|
22
|
+
* `sortable: false` when the handler cannot order by it.
|
|
22
23
|
* computeListViewModel only reads `fields[<col>].type` — text is enough,
|
|
23
24
|
* presentation comes from the column renderer + explicit label. */
|
|
24
25
|
export function synthesizeProjectionEntity(columns, sortable) {
|
|
25
26
|
const fields = {};
|
|
26
27
|
for (const col of columns) {
|
|
27
|
-
|
|
28
|
+
const normalized = normalizeListColumn(col);
|
|
29
|
+
fields[normalized.field] = {
|
|
30
|
+
type: "text",
|
|
31
|
+
sortable: sortable && normalized.sortable !== false,
|
|
32
|
+
};
|
|
28
33
|
}
|
|
29
34
|
return { fields };
|
|
30
35
|
}
|
|
@@ -4,7 +4,7 @@ import { RenderEdit } from "../components/render-edit.js";
|
|
|
4
4
|
import { useTranslation } from "../i18n.js";
|
|
5
5
|
import { usePrimitives } from "../primitives.js";
|
|
6
6
|
import { synthesizeActionFormEntity, synthesizeActionFormScreen, synthesizeSecretMintConfirmScreen, } from "./action-form-shim.js";
|
|
7
|
-
import { buildInitialValues,
|
|
7
|
+
import { buildInitialValues, mergeSearchParamsIntoInitial, tenantCurrencyMoneyFieldNames, useMoneyCurrencyOverrides, } from "./kumiko-screen.js";
|
|
8
8
|
import { layoutFieldNames } from "./layout-fields.js";
|
|
9
9
|
import { useInitialValuesHandoff, useNav } from "./nav.js";
|
|
10
10
|
import { lastSegment } from "./qn.js";
|
|
@@ -26,6 +26,9 @@ function extractCarriedValues(data, carry) {
|
|
|
26
26
|
}
|
|
27
27
|
return carried;
|
|
28
28
|
}
|
|
29
|
+
// Entity-less forms have no `entity.defaultCurrency`; same fallback as actionForm.
|
|
30
|
+
const SECRET_MINT_CURRENCY_FALLBACK = "EUR";
|
|
31
|
+
const NO_FIELDS = {};
|
|
29
32
|
function isBlank(value) {
|
|
30
33
|
if (value === undefined || value === null || value === "")
|
|
31
34
|
return true;
|
|
@@ -50,17 +53,26 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
50
53
|
const synthEntity = useMemo(() => synthesizeActionFormEntity(screen.fields), [screen.fields]);
|
|
51
54
|
const synthScreen = useMemo(() => synthesizeActionFormScreen(screen), [screen]);
|
|
52
55
|
const handoffValues = useInitialValuesHandoff(screen.id);
|
|
56
|
+
const mintTenantCurrencyFields = useMemo(() => tenantCurrencyMoneyFieldNames(screen.fields), [screen.fields]);
|
|
57
|
+
const { overrides: mintCurrencyOverrides, loading: mintCurrencyLoading } = useMoneyCurrencyOverrides(screen.fields, mintTenantCurrencyFields, SECRET_MINT_CURRENCY_FALLBACK);
|
|
53
58
|
const initial = useMemo(() => mergeSearchParamsIntoInitial(screen.fields, {
|
|
54
59
|
searchParams: nav.searchParams,
|
|
55
60
|
urlPrefillFields: screen.urlPrefillFields,
|
|
56
61
|
renderableFields: layoutFieldNames(synthScreen),
|
|
57
62
|
// A mint form has no entity either, so its money fields name their own
|
|
58
|
-
// currency source (fw#2839).
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
// currency source (fw#2839, fw#2933).
|
|
64
|
+
...(mintCurrencyOverrides !== undefined && {
|
|
65
|
+
moneyCurrencyOverrides: mintCurrencyOverrides,
|
|
66
|
+
}),
|
|
62
67
|
...(handoffValues !== undefined && { handoffValues }),
|
|
63
|
-
}), [
|
|
68
|
+
}), [
|
|
69
|
+
screen.fields,
|
|
70
|
+
screen.urlPrefillFields,
|
|
71
|
+
nav.searchParams,
|
|
72
|
+
synthScreen,
|
|
73
|
+
handoffValues,
|
|
74
|
+
mintCurrencyOverrides,
|
|
75
|
+
]);
|
|
64
76
|
const [revealed, setRevealed] = useState(null);
|
|
65
77
|
const [done, setDone] = useState(false);
|
|
66
78
|
// The secret was minted server-side but none of the declared reveal fields
|
|
@@ -72,9 +84,12 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
72
84
|
const confirm = screen.confirm;
|
|
73
85
|
const confirmEntity = useMemo(() => (confirm !== undefined ? synthesizeActionFormEntity(confirm.fields) : undefined), [confirm]);
|
|
74
86
|
const confirmScreen = useMemo(() => (confirm !== undefined ? synthesizeSecretMintConfirmScreen(screen, confirm) : undefined), [screen, confirm]);
|
|
87
|
+
const confirmFields = confirm?.fields ?? NO_FIELDS;
|
|
88
|
+
const confirmTenantCurrencyFields = useMemo(() => tenantCurrencyMoneyFieldNames(confirmFields), [confirmFields]);
|
|
89
|
+
const { overrides: confirmCurrencyOverrides, loading: confirmCurrencyLoading } = useMoneyCurrencyOverrides(confirmFields, confirmTenantCurrencyFields, SECRET_MINT_CURRENCY_FALLBACK);
|
|
75
90
|
const confirmInitial = useMemo(() => confirm !== undefined
|
|
76
|
-
? buildInitialValues(confirm.fields, undefined,
|
|
77
|
-
: undefined, [confirm]);
|
|
91
|
+
? buildInitialValues(confirm.fields, undefined, confirmCurrencyOverrides)
|
|
92
|
+
: undefined, [confirm, confirmCurrencyOverrides]);
|
|
78
93
|
const handleSubmitted = useCallback((result) => {
|
|
79
94
|
if (!result.isSuccess)
|
|
80
95
|
return;
|
|
@@ -166,9 +181,17 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
166
181
|
// keys stop the mint and confirm forms sharing one instance (and its
|
|
167
182
|
// submit state) across phases.
|
|
168
183
|
if (confirm !== undefined && confirmEntity !== undefined && confirmScreen !== undefined) {
|
|
184
|
+
// Hold until a tenant-declared currency lands, else the form would seed (and
|
|
185
|
+
// on a fast click submit) the fallback currency (fw#2933).
|
|
186
|
+
if (confirmCurrencyLoading) {
|
|
187
|
+
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
188
|
+
}
|
|
169
189
|
return (_jsx(RenderEdit, { screen: confirmScreen, i18nScreenId: screen.id, entity: confirmEntity, featureName: schema.featureName, initial: confirmInitial ?? {}, writeCommand: confirm.handler, payloadMode: "values", buildPayload: (snapshot) => ({ ...snapshot.values, ...carriedRef.current }), onSubmit: handleConfirmSubmitted, onCancel: handleCancel ?? restartMint, fillScreenHeight: true, leadContent: _jsx(Section, { testId: "kumiko-screen-secret-mint-card", children: revealContent }), ...(translate !== undefined && { translate }), ...(confirm.submitLabel !== undefined && { submitLabel: confirm.submitLabel }) }, "confirm"));
|
|
170
190
|
}
|
|
171
191
|
return (_jsxs(Card, { options: { screenBody: true }, testId: "kumiko-screen-secret-mint-card", children: [revealContent, _jsx(Button, { type: "button", variant: "primary", onClick: finishMint, testId: "kumiko-screen-secret-mint-confirm", children: effectiveTranslate(screen.reveal.confirmLabel ?? "kumiko.secretMint.confirm") })] }));
|
|
172
192
|
}
|
|
193
|
+
if (mintCurrencyLoading) {
|
|
194
|
+
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
195
|
+
}
|
|
173
196
|
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, extensionInitialValues: initial, writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, fillScreenHeight: true, ...(handleCancel !== undefined && { onCancel: handleCancel }), ...(translate !== undefined && { translate }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) }, "mint"));
|
|
174
197
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { OptionsQueryPayload } from "@cosmicdrift/kumiko-types/fields";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
export type SelectOptionEntry = {
|
|
5
|
+
readonly value: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly description?: string;
|
|
8
|
+
readonly disabled?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function staticSelectOptions(field: EditFieldViewModel): readonly SelectOptionEntry[];
|
|
11
|
+
export declare function mergeOptionAvailability(options: readonly SelectOptionEntry[], rows: readonly unknown[], current: string): readonly SelectOptionEntry[];
|
|
12
|
+
type AvailabilityOptionsSelectProps = {
|
|
13
|
+
readonly field: EditFieldViewModel;
|
|
14
|
+
readonly query: string;
|
|
15
|
+
readonly payload: OptionsQueryPayload;
|
|
16
|
+
readonly row: Readonly<Record<string, unknown>>;
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly hasError: boolean;
|
|
19
|
+
readonly onChange: (value: unknown) => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function AvailabilityOptionsSelect({ field, query, payload, row, id, hasError, onChange, }: AvailabilityOptionsSelectProps): ReactNode;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from "../hooks/use-query.js";
|
|
3
|
+
import { usePrimitives } from "../primitives.js";
|
|
4
|
+
import { resolveOptionsQueryPayload } from "./query-options-select.js";
|
|
5
|
+
// Rows come from a query handler, so they are checked once here instead of trusted.
|
|
6
|
+
function isAvailabilityRow(row) {
|
|
7
|
+
return (typeof row === "object" &&
|
|
8
|
+
row !== null &&
|
|
9
|
+
"value" in row &&
|
|
10
|
+
typeof row.value === "string" &&
|
|
11
|
+
(!("disabled" in row) || row.disabled === undefined || typeof row.disabled === "boolean") &&
|
|
12
|
+
(!("hint" in row) || row.hint === undefined || typeof row.hint === "string"));
|
|
13
|
+
}
|
|
14
|
+
// Static options with the translated labels of the view-model.
|
|
15
|
+
export function staticSelectOptions(field) {
|
|
16
|
+
const labels = field.optionLabels;
|
|
17
|
+
return (field.options ?? []).map((value) => ({ value, label: labels?.[value] ?? value }));
|
|
18
|
+
}
|
|
19
|
+
// Static options stay authoritative: rows for unknown values are ignored, and the
|
|
20
|
+
// stored value is never disabled so a downgraded tenant keeps seeing its choice.
|
|
21
|
+
export function mergeOptionAvailability(options, rows, current) {
|
|
22
|
+
const byValue = new Map();
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
if (isAvailabilityRow(row))
|
|
25
|
+
byValue.set(row.value, row);
|
|
26
|
+
}
|
|
27
|
+
return options.map((option) => {
|
|
28
|
+
const row = byValue.get(option.value);
|
|
29
|
+
if (row === undefined)
|
|
30
|
+
return option;
|
|
31
|
+
const disabled = row.disabled === true && option.value !== current;
|
|
32
|
+
return {
|
|
33
|
+
...option,
|
|
34
|
+
...(row.hint !== undefined && { description: row.hint }),
|
|
35
|
+
...(disabled && { disabled: true }),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// Select with static options whose availability (SelectFieldDef.optionsAvailabilityQuery)
|
|
40
|
+
// comes from a query, so it has to be a mounted component for useQuery. Until the
|
|
41
|
+
// result arrives, and when the query fails, every option stays choosable; the write
|
|
42
|
+
// path enforces the rule either way.
|
|
43
|
+
export function AvailabilityOptionsSelect({ field, query, payload, row, id, hasError, onChange, }) {
|
|
44
|
+
const { Input } = usePrimitives();
|
|
45
|
+
const { data } = useQuery(query, resolveOptionsQueryPayload(payload, row));
|
|
46
|
+
const current = typeof field.value === "string" ? field.value : "";
|
|
47
|
+
const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
|
|
48
|
+
return (_jsx(Input, { kind: "select", id: id, name: field.field, disabled: field.readOnly, required: field.required, hasError: hasError, value: current, onChange: (v) => onChange(v), options: mergeOptionAvailability(staticSelectOptions(field), data?.rows ?? [], current), ...(display !== undefined && { display }) }));
|
|
49
|
+
}
|
|
@@ -752,10 +752,10 @@ export function RenderEdit(props) {
|
|
|
752
752
|
}
|
|
753
753
|
const secretFieldNames = new Set(vm.sections.flatMap((section) => section.kind === "fields"
|
|
754
754
|
? section.fields
|
|
755
|
-
.filter((f) => f.format === "password" || f.writeOnly === true)
|
|
755
|
+
.filter((f) => f.format === "password" || f.writeOnly === true || f.sensitive === true)
|
|
756
756
|
.map((f) => f.field)
|
|
757
757
|
: []));
|
|
758
|
-
// A password or
|
|
758
|
+
// A password, writeOnly or sensitive field never enters the persisted draft blob — it would be stored in the clear and restored on resume.
|
|
759
759
|
const draftValues = Object.fromEntries(Object.entries(controller.getSnapshot().values).filter(([field]) => !secretFieldNames.has(field)));
|
|
760
760
|
void dispatcher
|
|
761
761
|
.write(FORM_DRAFT_SAVE, {
|
|
@@ -1131,7 +1131,7 @@ export function RenderEdit(props) {
|
|
|
1131
1131
|
footerActions?.map((action) => (_jsx(Button, { type: "button", variant: action.variant ?? "secondary", ...(action.icon !== undefined && { icon: action.icon }), disabled: isSubmitting || disabled, onClick: () => {
|
|
1132
1132
|
controller.setValues(action.patch); // @cast-boundary form-values
|
|
1133
1133
|
void handleSubmit();
|
|
1134
|
-
}, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), hasDirtyFooter && showsSubmit && unsavedCount !== undefined && unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.form.discard") })), showSubmit && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged && !extensionDirty && !
|
|
1134
|
+
}, testId: `render-edit-footer-action-${action.id}`, children: translate(action.label) }, action.id))), hasDirtyFooter && showsSubmit && unsavedCount !== undefined && unsavedCount > 0 && (_jsx(Button, { type: "button", variant: "ghost", disabled: isSubmitting || disabled, onClick: () => controller.reset(), testId: "render-edit-discard", [STICKY_PRIMARY_ACTION_PROP]: true, children: translate("kumiko.form.discard") })), showSubmit && (_jsx(Button, { type: "submit", disabled: (snapshot.isUnchanged && !extensionDirty && !isFieldlessSubmitForm && !insideDrawer) ||
|
|
1135
1135
|
isSubmitting ||
|
|
1136
1136
|
disabled, loading: isSubmitting, variant: submitVariant ?? "primary", testId: "render-edit-submit", children: translate(submitLabel ??
|
|
1137
1137
|
(isWizard
|
|
@@ -1367,10 +1367,13 @@ export function RenderEdit(props) {
|
|
|
1367
1367
|
}
|
|
1368
1368
|
// No section title here (fw#3218) — the Tab strip right above
|
|
1369
1369
|
// already names this panel, so a repeated Card title would just
|
|
1370
|
-
// duplicate it. The
|
|
1371
|
-
//
|
|
1372
|
-
const cardEl = (_jsx(Card, { options: { framed: false }, ...(sectionActionsEl !== undefined && {
|
|
1373
|
-
slots: {
|
|
1370
|
+
// duplicate it. The header row only exists to carry the section
|
|
1371
|
+
// description and actions, never the title text.
|
|
1372
|
+
const cardEl = (_jsx(Card, { options: { framed: false }, ...((sectionActionsEl !== undefined || section.description !== undefined) && {
|
|
1373
|
+
slots: {
|
|
1374
|
+
...(section.description !== undefined && { subtitle: section.description }),
|
|
1375
|
+
...(sectionActionsEl !== undefined && { headerActions: sectionActionsEl }),
|
|
1376
|
+
},
|
|
1374
1377
|
}), testId: `section-${sectionKey}`, children: renderFieldGrid(section.fields, section.columns, `${sectionKey}-grid`) }, sectionKey));
|
|
1375
1378
|
return wrapWizardStep(sectionKey, cardEl);
|
|
1376
1379
|
}
|
|
@@ -13,6 +13,7 @@ import { useQuery } from "../hooks/use-query.js";
|
|
|
13
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
|
+
import { AvailabilityOptionsSelect, staticSelectOptions } from "./availability-options-select.js";
|
|
16
17
|
import { EmbeddedListField } from "./embedded-list-field.js";
|
|
17
18
|
import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
|
|
18
19
|
import { QueryOptionsSelect } from "./query-options-select.js";
|
|
@@ -42,7 +43,9 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
|
|
|
42
43
|
// Reference-Field rendert eine eigene Component — sie nutzt
|
|
43
44
|
// useQuery() für den Live-Lookup, also muss sie als React-
|
|
44
45
|
// 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, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) :
|
|
46
|
+
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" &&
|
|
47
|
+
field.selectOptionsAvailabilityQuery !== undefined &&
|
|
48
|
+
!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) ? (_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
49
|
field,
|
|
47
50
|
id,
|
|
48
51
|
hasError,
|
|
@@ -374,11 +377,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
374
377
|
// `<feature>:entity:<entity>:field:<field>:option:<value>`).
|
|
375
378
|
// Wenn keine Translations registriert sind, fallback auf raw
|
|
376
379
|
// value als Label — der ComboboxInput zeigt dann unverändert.
|
|
377
|
-
const
|
|
378
|
-
const labels = field.optionLabels;
|
|
379
|
-
const selectOptions = labels !== undefined
|
|
380
|
-
? rawOptions.map((value) => ({ value, label: labels[value] ?? value }))
|
|
381
|
-
: rawOptions;
|
|
380
|
+
const selectOptions = field.optionLabels !== undefined ? staticSelectOptions(field) : (field.options ?? []);
|
|
382
381
|
// `display` is shared with multiSelect's "checkboxes", which is not a
|
|
383
382
|
// select presentation — narrow to the two a select field can request.
|
|
384
383
|
const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// internen Cache shared zwischen List + Edit-Form für die gleiche
|
|
18
18
|
// Entity, Live-Updates kommen via SSE (use-query-live).
|
|
19
19
|
import { REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
20
|
+
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-types/kms-adapter-types";
|
|
20
21
|
import { useMemo } from "react";
|
|
21
22
|
import { toKebab } from "../app/qn.js";
|
|
22
23
|
import { useTranslation } from "../i18n.js";
|
|
@@ -61,7 +62,7 @@ export function useReferenceLookup(featureName, refEntity, labelField) {
|
|
|
61
62
|
continue;
|
|
62
63
|
const idStr = String(id);
|
|
63
64
|
const label = row[labelKey] ?? id;
|
|
64
|
-
out.set(idStr, String(label));
|
|
65
|
+
out.set(idStr, label === PII_ERASED_SENTINEL ? translate("kumiko.pii.erased") : String(label));
|
|
65
66
|
}
|
|
66
67
|
// System-scope ids (e.g. SYSTEM_TENANT_ID) never have a backing row, so
|
|
67
68
|
// the bulk lookup above never covers them — inject the label directly.
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -41,6 +41,7 @@ export const kumikoDefaultTranslations = {
|
|
|
41
41
|
"kumiko.field.unsupported": "This field type can't be edited here yet.",
|
|
42
42
|
"kumiko.field.writeOnly.remove": "Remove stored value",
|
|
43
43
|
"kumiko.field.writeOnly.set": "Set",
|
|
44
|
+
"kumiko.pii.erased": "Deleted",
|
|
44
45
|
"kumiko.field.writeOnly.setPlaceholder": "Set — leave empty to keep it",
|
|
45
46
|
"kumiko.field.writeOnly.undo": "Undo",
|
|
46
47
|
"kumiko.field.writeOnly.willRemove": "Removed on save",
|
|
@@ -107,6 +108,8 @@ export const kumikoDefaultTranslations = {
|
|
|
107
108
|
"kumiko.dashboard.panel.error.title": "{label} could not be loaded",
|
|
108
109
|
"kumiko.dashboard.time-range": "Time range",
|
|
109
110
|
"kumiko.screen.unavailable": "This page is not available right now.",
|
|
111
|
+
"kumiko.dashboard.brush-end": "Window end",
|
|
112
|
+
"kumiko.dashboard.brush-start": "Window start",
|
|
110
113
|
"kumiko.dashboard.today": "Today",
|
|
111
114
|
"kumiko.dashboard.updated-at": "As of {time}",
|
|
112
115
|
"kumiko.app-boot.loading": "Loading…",
|
package/dist/index.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
|
72
72
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
73
73
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
|
74
74
|
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, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxImage, 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";
|
|
75
|
-
export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
|
|
75
|
+
export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.js";
|
|
76
76
|
export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.js";
|
|
77
77
|
export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
|
|
78
78
|
export { buildWhatsAppShareUrl } from "./share-links.js";
|
package/dist/index.js
CHANGED
|
@@ -52,7 +52,7 @@ export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTra
|
|
|
52
52
|
export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
53
53
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
54
54
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
|
55
|
-
export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
|
|
55
|
+
export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.js";
|
|
56
56
|
export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
|
|
57
57
|
export { buildWhatsAppShareUrl } from "./share-links.js";
|
|
58
58
|
export { sortByAccessor } from "./sort-by-accessor.js";
|
package/dist/primitives.d.ts
CHANGED
|
@@ -347,6 +347,9 @@ export type InputProps = {
|
|
|
347
347
|
* without a group stay ungrouped and come before all groups. Options
|
|
348
348
|
* with description or group never render as a segmented control. */
|
|
349
349
|
readonly group?: string;
|
|
350
|
+
/** Not choosable and visibly muted. A disabled option that is the
|
|
351
|
+
* current `value` stays displayed as the selection. */
|
|
352
|
+
readonly disabled?: boolean;
|
|
350
353
|
}[];
|
|
351
354
|
readonly disabled?: boolean;
|
|
352
355
|
readonly required?: boolean;
|
|
@@ -1031,8 +1034,9 @@ export type DialogProps = {
|
|
|
1031
1034
|
readonly cancelLabel?: string;
|
|
1032
1035
|
/** `default` = Confirm primary, `danger` = Confirm danger. */
|
|
1033
1036
|
readonly variant?: "default" | "danger";
|
|
1034
|
-
/** Which button takes focus on open. Default: Confirm for a bare dialog
|
|
1035
|
-
*
|
|
1037
|
+
/** Which button takes focus on open. Default: Confirm for a bare dialog
|
|
1038
|
+
* (Cancel when `variant` is `danger`), the browser/Radix default when
|
|
1039
|
+
* `children` are present. `"cancel"` for
|
|
1036
1040
|
* dialogs where the safe choice must be the one Enter triggers. */
|
|
1037
1041
|
readonly initialFocus?: "confirm" | "cancel";
|
|
1038
1042
|
/** Wird gefeuert wenn der User Confirm drückt. Async-Funktion ist
|
|
@@ -1267,7 +1271,7 @@ export type StatusBadgeProps = {
|
|
|
1267
1271
|
readonly tone?: StatusTone;
|
|
1268
1272
|
readonly testId?: string;
|
|
1269
1273
|
};
|
|
1270
|
-
export declare function statusToneForOptionTone(tone: SelectOptionTone): StatusTone;
|
|
1274
|
+
export declare function statusToneForOptionTone(tone: SelectOptionTone): StatusTone | undefined;
|
|
1271
1275
|
export declare function statusToneForValue(value: string): StatusTone | undefined;
|
|
1272
1276
|
/** Compact label/value tile (record-detail metrics band). `testId` is the
|
|
1273
1277
|
* tile's own id — the impl derives `${testId}-label`/`${testId}-value` for
|
|
@@ -1520,4 +1524,6 @@ export type PrimitivesProviderProps = {
|
|
|
1520
1524
|
readonly value: PrimitivesRegistry;
|
|
1521
1525
|
};
|
|
1522
1526
|
export declare function PrimitivesProvider({ children, value }: PrimitivesProviderProps): ReactNode;
|
|
1527
|
+
/** Like `usePrimitives`, but `undefined` outside a provider — for components that also render standalone. */
|
|
1528
|
+
export declare function useOptionalPrimitives(): PrimitivesRegistry | undefined;
|
|
1523
1529
|
export declare function usePrimitives(): PrimitivesRegistry;
|
package/dist/primitives.js
CHANGED
|
@@ -52,8 +52,11 @@ const STATUS_TONE_BY_OPTION_TONE = {
|
|
|
52
52
|
bad: "bad",
|
|
53
53
|
neutral: "muted",
|
|
54
54
|
};
|
|
55
|
+
// Own-key check: an unknown tone from an untyped definition must yield undefined so callers fall back to the value heuristic.
|
|
55
56
|
export function statusToneForOptionTone(tone) {
|
|
56
|
-
return STATUS_TONE_BY_OPTION_TONE
|
|
57
|
+
return Object.hasOwn(STATUS_TONE_BY_OPTION_TONE, tone)
|
|
58
|
+
? STATUS_TONE_BY_OPTION_TONE[tone]
|
|
59
|
+
: undefined;
|
|
57
60
|
}
|
|
58
61
|
export function statusToneForValue(value) {
|
|
59
62
|
const slug = value
|
|
@@ -69,6 +72,10 @@ const PrimitivesContext = createContext(undefined);
|
|
|
69
72
|
export function PrimitivesProvider({ children, value }) {
|
|
70
73
|
return _jsx(PrimitivesContext.Provider, { value: value, children: children });
|
|
71
74
|
}
|
|
75
|
+
/** Like `usePrimitives`, but `undefined` outside a provider — for components that also render standalone. */
|
|
76
|
+
export function useOptionalPrimitives() {
|
|
77
|
+
return useContext(PrimitivesContext);
|
|
78
|
+
}
|
|
72
79
|
export function usePrimitives() {
|
|
73
80
|
const registry = useContext(PrimitivesContext);
|
|
74
81
|
if (registry === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.353.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.353.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.353.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.353.0",
|
|
21
21
|
"react": "^19.2.6",
|
|
22
22
|
"temporal-polyfill": "^0.3.2",
|
|
23
23
|
"zod": "^4.4.3"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^29.1.1",
|
|
30
30
|
"react-dom": "^19.2.6",
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.353.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.353.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "secretMint with tenant currency waits for the tenant currency in mint and confirm step",
|
|
6
|
+
"detail": "Number inputs no longer keep a typed prefix, secretMint honours tenant currency, refEntity respects real field types\nA `NumberInput` draft that does not parse (\"12abc\", \"12,5\" on an integer field) now clears the value instead of leaving the last parseable prefix (\"12\") to be submitted. A `secretMint` screen with `currency: { kind: \"tenant\" }` money fields now waits for the tenant currency in both the mint and the confirm step, like `actionForm`, instead of seeding a bare `0`. `refEntity` on an `entityList` column or `entityEdit` field no longer overrides a real non-text entity field (a multi-reference stays multi, a sortable column stays sortable). The Tailwind scan skips compiled `__tests__` in `dist`."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.352.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Select inputs accept disabled options and load their availability from optionsAvailabilityQuery"
|
|
12
|
+
},
|
|
2
13
|
{
|
|
3
14
|
"version": "0.348.0",
|
|
4
15
|
"type": "improvement",
|