@cosmicdrift/kumiko-renderer 0.361.0 → 0.363.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/content-preview.js +4 -1
- package/dist/app/header-actions.js +7 -7
- package/dist/app/kumiko-screen.d.ts +2 -2
- package/dist/app/kumiko-screen.js +42 -27
- package/dist/app/secret-mint-body.js +2 -1
- package/dist/components/reference-create-dialog.js +2 -1
- package/dist/components/render-edit.js +9 -4
- package/dist/components/render-field.js +18 -14
- package/dist/components/write-form-section.js +1 -2
- package/dist/i18n-defaults.js +1 -0
- package/package.json +5 -5
- package/src/changes.json +18 -0
|
@@ -24,5 +24,8 @@ export function ContentPreview({ content, variables, contentFormat, }) {
|
|
|
24
24
|
const safeVariables = contentFormat === "rich"
|
|
25
25
|
? Object.fromEntries(Object.entries(variables).map(([name, value]) => [name, escapeHtmlAttr(value)]))
|
|
26
26
|
: variables;
|
|
27
|
-
|
|
27
|
+
// "document" keeps `{{x}}` as-is: substituting it into a button/link href would make the
|
|
28
|
+
// schema drop that node; variable chips stay visible.
|
|
29
|
+
const value = contentFormat === "document" ? content : substituteVariables(content, safeVariables);
|
|
30
|
+
return _jsx(Editor, { id: id, value: value, onChange: noop, variables: [], readOnly: true });
|
|
28
31
|
}
|
|
@@ -9,19 +9,19 @@ export function primaryHeaderAction(actions, preferredId) {
|
|
|
9
9
|
actions.find((a) => a.style === "primary") ??
|
|
10
10
|
actions[0]);
|
|
11
11
|
}
|
|
12
|
-
//
|
|
13
|
-
//
|
|
12
|
+
// The undeclared fallback picks an action nobody chose for the header, so a
|
|
13
|
+
// destructive or confirm-gated one (a bare "x" reads as "close") may not stand alone.
|
|
14
14
|
function canStandAsIconButton(action) {
|
|
15
15
|
return action?.icon !== undefined && action.style !== "danger" && !needsActionConfirm(action);
|
|
16
16
|
}
|
|
17
|
-
// A declared, visible `primaryAction` is always the phone header's button
|
|
18
|
-
//
|
|
19
|
-
// and `edit` moves into the menu. Without one the
|
|
20
|
-
// stays visible, falling back to the precedence-only primary.
|
|
17
|
+
// A declared, visible `primaryAction` is always the phone header's button, icon
|
|
18
|
+
// only when it has an icon (the author chose the icon, the label is its aria-label),
|
|
19
|
+
// so it matches the desktop header and `edit` moves into the menu. Without one the
|
|
20
|
+
// pencil-style icon action stays visible, falling back to the precedence-only primary.
|
|
21
21
|
export function compactHeaderPrimary(actions, preferredId) {
|
|
22
22
|
const declared = preferredId !== undefined ? actions.find((a) => a.id === preferredId) : undefined;
|
|
23
23
|
if (declared !== undefined)
|
|
24
|
-
return { action: declared, iconOnly:
|
|
24
|
+
return { action: declared, iconOnly: declared.icon !== undefined };
|
|
25
25
|
const iconAction = primaryHeaderAction(actions);
|
|
26
26
|
return canStandAsIconButton(iconAction) ? { action: iconAction, iconOnly: true } : undefined;
|
|
27
27
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
-
import type { ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { FormValues, ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import { type ReactNode } from "react";
|
|
4
4
|
import { type ListSort } from "../hooks/use-list-url-state.js";
|
|
5
5
|
import type { FeatureSchema } from "./feature-schema.js";
|
|
@@ -19,7 +19,7 @@ export { qualifyScreenId };
|
|
|
19
19
|
export declare function qualifyNavId(featureName: string, navId: string): string;
|
|
20
20
|
export { screenAccessAllows };
|
|
21
21
|
export declare function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCopyLink, }: KumikoScreenProps): ReactNode;
|
|
22
|
-
export declare function buildInitialValues(fields: Readonly<Record<string, unknown>>, defaultCurrency?: string, moneyCurrencyOverrides?: Readonly<Record<string, string>>):
|
|
22
|
+
export declare function buildInitialValues(fields: Readonly<Record<string, unknown>>, defaultCurrency?: string, moneyCurrencyOverrides?: Readonly<Record<string, string>>): FormValues;
|
|
23
23
|
export declare function tenantCurrencyMoneyFieldNames(fields: Readonly<Record<string, unknown>>): readonly string[];
|
|
24
24
|
export declare function literalCurrencyOverrides(fields: Readonly<Record<string, unknown>>): Readonly<Record<string, string>>;
|
|
25
25
|
export declare function useMoneyCurrencyOverrides(fields: Readonly<Record<string, unknown>>, tenantFieldNames: readonly string[], fallback: string | undefined): {
|
|
@@ -4,6 +4,7 @@ import { applyFormatSpec, buildOptionLabels, computeRelatedListSectionViewModel,
|
|
|
4
4
|
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
5
5
|
import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
6
6
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
7
|
+
import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
8
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
9
|
import { QueryOptionLabel, resolveOptionsQueryPayload, } from "../components/query-options-select.js";
|
|
9
10
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
|
|
@@ -246,19 +247,29 @@ function useNavigateToCreateFor(schema, entityName, createScreen, createUnavaila
|
|
|
246
247
|
// — used for a `money` field that declares `currency: { kind: "tenant" }`, so
|
|
247
248
|
// its empty initial value carries the tenant's own currency instead of the
|
|
248
249
|
// entity-wide default.
|
|
250
|
+
function versionOf(record) {
|
|
251
|
+
const version = record["version"];
|
|
252
|
+
return typeof version === "number" ? version : 1;
|
|
253
|
+
}
|
|
249
254
|
export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrides) {
|
|
250
255
|
const out = {};
|
|
251
256
|
for (const [name, def] of Object.entries(fields)) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
const type = propertyOf(def, "type");
|
|
258
|
+
const defaultValue = propertyOf(def, "default");
|
|
259
|
+
if (defaultValue !== undefined) {
|
|
260
|
+
out[name] = defaultValue;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// An untouched optional number must not persist as a deliberate 0.
|
|
264
|
+
if (type === "number" && propertyOf(def, "required") !== true) {
|
|
265
|
+
out[name] = undefined;
|
|
255
266
|
continue;
|
|
256
267
|
}
|
|
257
|
-
if (
|
|
268
|
+
if (type === "embedded" && propertyOf(def, "multiple") === true) {
|
|
258
269
|
out[name] = [];
|
|
259
270
|
continue;
|
|
260
271
|
}
|
|
261
|
-
if (
|
|
272
|
+
if (type === "money") {
|
|
262
273
|
const currency = moneyCurrencyOverrides?.[name] ?? defaultCurrency;
|
|
263
274
|
if (currency !== undefined) {
|
|
264
275
|
out[name] = { amount: 0, currency };
|
|
@@ -266,11 +277,11 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
|
|
|
266
277
|
}
|
|
267
278
|
}
|
|
268
279
|
out[name] =
|
|
269
|
-
|
|
280
|
+
type === "boolean"
|
|
270
281
|
? false
|
|
271
|
-
:
|
|
282
|
+
: type === "number" || type === "money"
|
|
272
283
|
? 0
|
|
273
|
-
:
|
|
284
|
+
: type === "multiSelect"
|
|
274
285
|
? []
|
|
275
286
|
: "";
|
|
276
287
|
}
|
|
@@ -285,9 +296,10 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
|
|
|
285
296
|
export function tenantCurrencyMoneyFieldNames(fields) {
|
|
286
297
|
const names = [];
|
|
287
298
|
for (const [name, def] of Object.entries(fields)) {
|
|
288
|
-
|
|
289
|
-
|
|
299
|
+
if (propertyOf(def, "type") === "money" &&
|
|
300
|
+
propertyOf(propertyOf(def, "currency"), "kind") === "tenant") {
|
|
290
301
|
names.push(name);
|
|
302
|
+
}
|
|
291
303
|
}
|
|
292
304
|
return names;
|
|
293
305
|
}
|
|
@@ -322,9 +334,11 @@ const UPDATE_CHANGES_KEY = "changes";
|
|
|
322
334
|
export function literalCurrencyOverrides(fields) {
|
|
323
335
|
const out = {};
|
|
324
336
|
for (const [name, def] of Object.entries(fields)) {
|
|
325
|
-
const
|
|
326
|
-
const code =
|
|
327
|
-
if (
|
|
337
|
+
const currency = propertyOf(def, "currency");
|
|
338
|
+
const code = propertyOf(currency, "code");
|
|
339
|
+
if (propertyOf(def, "type") === "money" &&
|
|
340
|
+
propertyOf(currency, "kind") === "literal" &&
|
|
341
|
+
typeof code === "string") {
|
|
328
342
|
out[name] = code;
|
|
329
343
|
}
|
|
330
344
|
}
|
|
@@ -370,7 +384,8 @@ const CURRENCY_CODE_PATTERN = /^[A-Za-z]{3}$/;
|
|
|
370
384
|
function isMoneyValue(value) {
|
|
371
385
|
if (typeof value !== "object" || value === null)
|
|
372
386
|
return false;
|
|
373
|
-
const
|
|
387
|
+
const amount = propertyOf(value, "amount");
|
|
388
|
+
const currency = propertyOf(value, "currency");
|
|
374
389
|
return (typeof amount === "number" &&
|
|
375
390
|
Number.isFinite(amount) &&
|
|
376
391
|
typeof currency === "string" &&
|
|
@@ -521,7 +536,7 @@ function coerceEmbeddedListRows(raw, fieldName, schema, maxItems) {
|
|
|
521
536
|
}
|
|
522
537
|
const cellEntries = [];
|
|
523
538
|
for (const [cellName, cell] of Object.entries(schema)) {
|
|
524
|
-
const cellValue = candidate
|
|
539
|
+
const cellValue = propertyOf(candidate, cellName);
|
|
525
540
|
if (cellValue === undefined || cellValue === null)
|
|
526
541
|
continue;
|
|
527
542
|
const coerced = coerceEmbeddedCell(cellValue, cell);
|
|
@@ -757,7 +772,7 @@ function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onC
|
|
|
757
772
|
// der ursprünglichen version sitzen — der buildPayload würde
|
|
758
773
|
// weiter die stale version stampen und der Konflikt-Recovery wäre
|
|
759
774
|
// kaputt.
|
|
760
|
-
const recordKey = record
|
|
775
|
+
const recordKey = versionOf(record);
|
|
761
776
|
return (_jsx(EntityEditUpdateForm, { schema: schema, screen: screen, entity: entity, entityId: entityId, record: record, onReload: detailQuery.refetch, ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), ...(autoReloadOnSave === true && { onSaved: () => void detailQuery.refetch() }), ...(onDeleted !== undefined && { onDeleted }) }, `${entityId}:${recordKey}`));
|
|
762
777
|
}
|
|
763
778
|
function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onReload, translate, onCopyLink, onSaved, onDeleted, }) {
|
|
@@ -768,7 +783,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
768
783
|
// captured once and stamped into every update payload — if a
|
|
769
784
|
// concurrent writer bumps it, the server returns a version-conflict
|
|
770
785
|
// error and the user reloads.
|
|
771
|
-
const recordVersion = record
|
|
786
|
+
const recordVersion = versionOf(record);
|
|
772
787
|
const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
773
788
|
// A money field declaring `currency: { kind: "tenant" }` (fw#2933) resolves
|
|
774
789
|
// its empty-value currency from the tenant-settings config key instead of
|
|
@@ -795,9 +810,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
795
810
|
// damit sie beim Edit den Bestand zeigen statt write-only zu sein.
|
|
796
811
|
const extensionInitialValues = useMemo(() => {
|
|
797
812
|
const cf = record["customFields"];
|
|
798
|
-
return cf
|
|
799
|
-
? cf
|
|
800
|
-
: undefined;
|
|
813
|
+
return isPlainObject(cf) ? cf : undefined;
|
|
801
814
|
}, [record]);
|
|
802
815
|
const writeCommand = entityWriteCommand(schema.featureName, screen.entity, "update");
|
|
803
816
|
const deleteCommand = entityWriteCommand(schema.featureName, screen.entity, "delete");
|
|
@@ -1161,7 +1174,8 @@ function EntityListScreen({ schema, screen, translate, onRowClick, }) {
|
|
|
1161
1174
|
function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
1162
1175
|
const isEmbeddedScreen = useIsEmbeddedScreen();
|
|
1163
1176
|
const featureName = schema.featureName;
|
|
1164
|
-
const
|
|
1177
|
+
const resolvedCreate = useNavigateToCreateFor(schema, screen.entity, screen.createScreen, screen.createUnavailable);
|
|
1178
|
+
const onCreate = screen.allowCreate === false ? undefined : resolvedCreate;
|
|
1165
1179
|
const { Banner } = usePrimitives();
|
|
1166
1180
|
const queryType = entityQueryCommand(featureName, screen.entity, "list");
|
|
1167
1181
|
const nav = useNav();
|
|
@@ -1211,8 +1225,8 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1211
1225
|
const filterPayload = useMemo(() => buildFilterPayload(urlState.filters, (field) => {
|
|
1212
1226
|
// entity.fields ist am Renderer-Layer schwach getypt (vom Schema
|
|
1213
1227
|
// deserialisiert) — Boundary-Cast wie buildInitialValues.
|
|
1214
|
-
const
|
|
1215
|
-
return
|
|
1228
|
+
const type = propertyOf(entity.fields[field], "type");
|
|
1229
|
+
return typeof type === "string" ? type : undefined;
|
|
1216
1230
|
}), [urlState.filters, entity.fields]);
|
|
1217
1231
|
// Entity-only additions (screen.filter, faceted filters) layer on top of
|
|
1218
1232
|
// the shared buildListQueryPayload — projectionList has neither.
|
|
@@ -2373,7 +2387,7 @@ function navigateAfterRedirect(options) {
|
|
|
2373
2387
|
function overlayUpdatedProjection(record, resultData) {
|
|
2374
2388
|
if (typeof resultData !== "object" || resultData === null)
|
|
2375
2389
|
return record;
|
|
2376
|
-
const projection = resultData
|
|
2390
|
+
const projection = propertyOf(resultData, "data");
|
|
2377
2391
|
if (typeof projection !== "object" || projection === null)
|
|
2378
2392
|
return record;
|
|
2379
2393
|
return { ...record, ...projection };
|
|
@@ -2559,8 +2573,9 @@ function mapFailedConfigCommandIssuesToField(error, failedIndex, commandFieldNam
|
|
|
2559
2573
|
},
|
|
2560
2574
|
};
|
|
2561
2575
|
}
|
|
2562
|
-
// A
|
|
2563
|
-
// maps it to 0, which a save would persist as a deliberate "0" override
|
|
2576
|
+
// A money field without a declared default starts empty too (buildInitialValues
|
|
2577
|
+
// maps it to 0, which a save would persist as a deliberate "0" override); numbers
|
|
2578
|
+
// are handled there, except a required one, which config fields never are.
|
|
2564
2579
|
function buildConfigEditDefaults(fields) {
|
|
2565
2580
|
const defaults = buildInitialValues(fields); // @cast-boundary render-helper
|
|
2566
2581
|
for (const [shortName, fieldDef] of Object.entries(fields)) {
|
|
@@ -2621,7 +2636,7 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2621
2636
|
out[shortName] = defaults[shortName];
|
|
2622
2637
|
continue;
|
|
2623
2638
|
}
|
|
2624
|
-
const ftype = fieldDef
|
|
2639
|
+
const ftype = propertyOf(fieldDef, "type");
|
|
2625
2640
|
// Field-Type-Coercion: config-Werte sind string|number|boolean,
|
|
2626
2641
|
// aber der Form-State erwartet das passende Field-Native-Type.
|
|
2627
2642
|
if (ftype === "number" || ftype === "money") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { RenderEdit } from "../components/render-edit.js";
|
|
4
5
|
import { useTranslation } from "../i18n.js";
|
|
@@ -14,7 +15,7 @@ function extractRevealValue(data, field) {
|
|
|
14
15
|
return undefined;
|
|
15
16
|
if (!Object.hasOwn(data, field))
|
|
16
17
|
return undefined;
|
|
17
|
-
return data
|
|
18
|
+
return propertyOf(data, field);
|
|
18
19
|
}
|
|
19
20
|
function extractCarriedValues(data, carry) {
|
|
20
21
|
const carried = {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { useMemo } from "react";
|
|
3
4
|
import { buildInitialValues, tenantCurrencyMoneyFieldNames, useMoneyCurrencyOverrides, } from "../app/kumiko-screen.js";
|
|
4
5
|
import { toKebab } from "../app/qn.js";
|
|
@@ -24,7 +25,7 @@ export function extractIdField(data, field) {
|
|
|
24
25
|
return undefined;
|
|
25
26
|
if (!Object.hasOwn(data, field))
|
|
26
27
|
return undefined;
|
|
27
|
-
const value = data
|
|
28
|
+
const value = propertyOf(data, field);
|
|
28
29
|
return typeof value === "string" ? value : undefined;
|
|
29
30
|
}
|
|
30
31
|
export function extractCreatedId(data) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import { computeEditViewModel, groupIssuesByPath, relevantFieldIssues, zodErrorToFieldIssues, } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
5
|
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
5
6
|
import { AllFieldsRequiredProvider } from "./render-field.js";
|
|
6
7
|
import { useActionMenuItems } from "./use-action-menu-items.js";
|
|
@@ -49,9 +50,13 @@ function newDraftPrefix(screenId) {
|
|
|
49
50
|
// `crypto.randomUUID` is missing in non-secure contexts and React Native/Hermes
|
|
50
51
|
// without a polyfill — guard like dispatcher-live.ts's generateRequestId.
|
|
51
52
|
function mintDraftId() {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const webCrypto = propertyOf(globalThis, "crypto");
|
|
54
|
+
const randomUUID = propertyOf(webCrypto, "randomUUID");
|
|
55
|
+
if (typeof randomUUID === "function") {
|
|
56
|
+
const id = Reflect.apply(randomUUID, webCrypto, []);
|
|
57
|
+
if (typeof id === "string")
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
55
60
|
return `draft-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
56
61
|
}
|
|
57
62
|
// End-to-end renderer für einen entityEdit screen. Rendert aus-
|
|
@@ -551,7 +556,7 @@ export function RenderEdit(props) {
|
|
|
551
556
|
// was in flight — their input wins over the stored draft.
|
|
552
557
|
if (controller.getSnapshot().isDirty || hasNavigatedRef.current)
|
|
553
558
|
return;
|
|
554
|
-
// @cast-boundary form-draft blob: `values` round-trips through an opaque
|
|
559
|
+
// @cast-boundary form-values draft blob: `values` round-trips through an opaque
|
|
555
560
|
// jsonb column and comes back untyped.
|
|
556
561
|
controller.setValues(draft.values);
|
|
557
562
|
setRawStep(Math.max(draft.stepIndex, 0));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { isFormatSpec, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import { applyFormatSpec, currencyDecimals, } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
5
|
import { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
5
6
|
import { useAppFeatures } from "../app/app-features-context.js";
|
|
6
7
|
import { useColumnRenderer } from "../app/column-renderers.js";
|
|
@@ -159,7 +160,7 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
|
|
|
159
160
|
}
|
|
160
161
|
setCreatedWithoutIdWarning(false);
|
|
161
162
|
if (isMultiple) {
|
|
162
|
-
const current =
|
|
163
|
+
const current = stringArrayOf(field.value);
|
|
163
164
|
onChange([...current, newId]);
|
|
164
165
|
}
|
|
165
166
|
else {
|
|
@@ -194,9 +195,7 @@ function ReferenceInput({ field, id, hasError, onChange, Input, featureName, })
|
|
|
194
195
|
};
|
|
195
196
|
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") }))] }));
|
|
196
197
|
if (isMultiple) {
|
|
197
|
-
const arrayValue =
|
|
198
|
-
? field.value
|
|
199
|
-
: [];
|
|
198
|
+
const arrayValue = stringArrayOf(field.value);
|
|
200
199
|
return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, multiple: true, value: arrayValue, onChange: (v) => onChange(v) }), createDialog] }));
|
|
201
200
|
}
|
|
202
201
|
const stringValue = field.value === undefined || field.value === null ? "" : String(field.value);
|
|
@@ -228,7 +227,7 @@ function FieldRendererOutput({ field, renderer, row, appLocale, }) {
|
|
|
228
227
|
// when set; coalesce undefined (spread override) back to appLocale (#2332).
|
|
229
228
|
return (_jsx(Text, { testId: `field-value-${field.field}`, children: applyFormatSpec({
|
|
230
229
|
...renderer,
|
|
231
|
-
locale: renderer
|
|
230
|
+
locale: stringPropertyOf(renderer, "locale") ?? appLocale,
|
|
232
231
|
}, field.value, t) }));
|
|
233
232
|
}
|
|
234
233
|
if (componentName !== undefined) {
|
|
@@ -254,9 +253,7 @@ function ReadOnlyReferenceValue({ field, featureName, }) {
|
|
|
254
253
|
limit: REFERENCE_COMBOBOX_LIMIT,
|
|
255
254
|
});
|
|
256
255
|
const ids = isMultiple
|
|
257
|
-
?
|
|
258
|
-
? field.value
|
|
259
|
-
: []
|
|
256
|
+
? stringArrayOf(field.value)
|
|
260
257
|
: typeof field.value === "string" && field.value !== ""
|
|
261
258
|
? [field.value]
|
|
262
259
|
: [];
|
|
@@ -354,7 +351,7 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
354
351
|
case "tz":
|
|
355
352
|
return (_jsx(Input, { kind: "tz", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
|
|
356
353
|
case "multiSelect": {
|
|
357
|
-
const arrayValue =
|
|
354
|
+
const arrayValue = stringArrayOf(field.value);
|
|
358
355
|
const renderMultiSelect = (options) => field.display === "checkboxes" ? (_jsx(MultiSelectCheckboxes, { field: field, id: id, options: options, value: arrayValue, onChange: (v) => onChange(v) })) : (_jsx(Input, { kind: "combobox", ...common, multiple: true, value: arrayValue, onChange: (v) => onChange(v), options: options }));
|
|
359
356
|
if (field.selectOptionsAvailabilityQuery !== undefined && field.readOnly !== true) {
|
|
360
357
|
return (_jsx(AvailabilityOptionsMultiSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, currentValues: arrayValue, children: renderMultiSelect }));
|
|
@@ -474,13 +471,20 @@ function resolveNumberUnit(unit, row) {
|
|
|
474
471
|
const trimmed = sibling.trim();
|
|
475
472
|
return trimmed.length > 0 && trimmed.length <= MAX_SIBLING_UNIT_LENGTH ? trimmed : undefined;
|
|
476
473
|
}
|
|
474
|
+
function stringArrayOf(value) {
|
|
475
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : [];
|
|
476
|
+
}
|
|
477
|
+
function stringPropertyOf(value, key) {
|
|
478
|
+
const property = propertyOf(value, key);
|
|
479
|
+
return typeof property === "string" ? property : undefined;
|
|
480
|
+
}
|
|
477
481
|
function moneyMinorValue(v, currency) {
|
|
478
482
|
if (v === undefined || v === null || v === "")
|
|
479
483
|
return "";
|
|
480
484
|
if (typeof v === "number")
|
|
481
485
|
return Math.round(v * 10 ** currencyDecimals(currency));
|
|
482
486
|
if (typeof v === "object") {
|
|
483
|
-
const amount = v
|
|
487
|
+
const amount = propertyOf(v, "amount");
|
|
484
488
|
if (typeof amount === "number")
|
|
485
489
|
return Math.round(amount * 10 ** currencyDecimals(currency));
|
|
486
490
|
}
|
|
@@ -498,11 +502,11 @@ function moneyPayload(minorUnits, currency) {
|
|
|
498
502
|
// (noch nicht gesetzt) → "" als Empty-Sentinel, analog money/timestamp.
|
|
499
503
|
function locatedValue(v) {
|
|
500
504
|
if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
|
|
501
|
-
const
|
|
505
|
+
const utc = propertyOf(v, "utc");
|
|
502
506
|
return {
|
|
503
|
-
at: typeof
|
|
504
|
-
tz: typeof
|
|
505
|
-
...(typeof
|
|
507
|
+
at: typeof v.at === "string" ? v.at : "",
|
|
508
|
+
tz: typeof v.tz === "string" ? v.tz : "",
|
|
509
|
+
...(typeof utc === "string" && { utc }),
|
|
506
510
|
};
|
|
507
511
|
}
|
|
508
512
|
return "";
|
|
@@ -14,11 +14,10 @@ function buildWriteFormSchema(fields) {
|
|
|
14
14
|
.object({})
|
|
15
15
|
.passthrough()
|
|
16
16
|
.superRefine((values, ctx) => {
|
|
17
|
-
const record = values;
|
|
18
17
|
for (const field of fields) {
|
|
19
18
|
if (field.readOnly || !field.visible || !field.required)
|
|
20
19
|
continue;
|
|
21
|
-
if (isPresent(
|
|
20
|
+
if (isPresent(values[field.field]))
|
|
22
21
|
continue;
|
|
23
22
|
ctx.addIssue({
|
|
24
23
|
code: "custom",
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -261,6 +261,7 @@ export const kumikoDefaultTranslations = {
|
|
|
261
261
|
"errors.internal": "Something went wrong. Please try again later.",
|
|
262
262
|
"errors.rate_limited": "Too many requests. Please try again shortly.",
|
|
263
263
|
"errors.rate_limit_unavailable": "Rate limiting is temporarily unavailable. Please try again shortly.",
|
|
264
|
+
"errors.puppeteer_unavailable": "The PDF service is not reachable. PDFs cannot be created right now.",
|
|
264
265
|
"errors.cap.exceeded": "Limit reached. Upgrade your plan or wait for the next period.",
|
|
265
266
|
"errors.download.urlMissing": "Download unavailable — please try again.",
|
|
266
267
|
"auth.errors.originNotAllowed": "Requests from this origin are not allowed.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.363.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.363.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.363.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.363.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.363.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.363.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "propertyOf type guard and fewer unchecked casts in the UI packages",
|
|
6
|
+
"detail": "\"@cosmicdrift/kumiko-types/type-guards\" exports propertyOf, which reads one property off an unknown value. Headless, renderer, renderer-web and dispatcher-live replace unchecked as-casts and globalThis probes with it and with narrowing. Behavior stays the same for well-formed input; a format spec, upload response or stream event with a wrongly typed property is now ignored instead of passed through."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.362.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "entityList honours allowCreate false",
|
|
12
|
+
"detail": "EntityListBody passes no onCreate when the list sets allowCreate false."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.362.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "A declared primaryAction with an icon is icon-only in the phone header",
|
|
18
|
+
"detail": "Regardless of confirm or style; the label is the button's aria-label and title. Without an icon it stays a text button. The ui-walkthrough terminate sample uses the circle-stop icon, since a bare x reads as close."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.361.0",
|
|
4
22
|
"type": "improvement",
|