@cosmicdrift/kumiko-renderer 0.362.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/kumiko-screen.d.ts +2 -2
- package/dist/app/kumiko-screen.js +33 -25
- 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 +6 -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
|
}
|
|
@@ -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,24 +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;
|
|
255
261
|
continue;
|
|
256
262
|
}
|
|
257
263
|
// An untouched optional number must not persist as a deliberate 0.
|
|
258
|
-
if (
|
|
264
|
+
if (type === "number" && propertyOf(def, "required") !== true) {
|
|
259
265
|
out[name] = undefined;
|
|
260
266
|
continue;
|
|
261
267
|
}
|
|
262
|
-
if (
|
|
268
|
+
if (type === "embedded" && propertyOf(def, "multiple") === true) {
|
|
263
269
|
out[name] = [];
|
|
264
270
|
continue;
|
|
265
271
|
}
|
|
266
|
-
if (
|
|
272
|
+
if (type === "money") {
|
|
267
273
|
const currency = moneyCurrencyOverrides?.[name] ?? defaultCurrency;
|
|
268
274
|
if (currency !== undefined) {
|
|
269
275
|
out[name] = { amount: 0, currency };
|
|
@@ -271,11 +277,11 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
|
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
out[name] =
|
|
274
|
-
|
|
280
|
+
type === "boolean"
|
|
275
281
|
? false
|
|
276
|
-
:
|
|
282
|
+
: type === "number" || type === "money"
|
|
277
283
|
? 0
|
|
278
|
-
:
|
|
284
|
+
: type === "multiSelect"
|
|
279
285
|
? []
|
|
280
286
|
: "";
|
|
281
287
|
}
|
|
@@ -290,9 +296,10 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
|
|
|
290
296
|
export function tenantCurrencyMoneyFieldNames(fields) {
|
|
291
297
|
const names = [];
|
|
292
298
|
for (const [name, def] of Object.entries(fields)) {
|
|
293
|
-
|
|
294
|
-
|
|
299
|
+
if (propertyOf(def, "type") === "money" &&
|
|
300
|
+
propertyOf(propertyOf(def, "currency"), "kind") === "tenant") {
|
|
295
301
|
names.push(name);
|
|
302
|
+
}
|
|
296
303
|
}
|
|
297
304
|
return names;
|
|
298
305
|
}
|
|
@@ -327,9 +334,11 @@ const UPDATE_CHANGES_KEY = "changes";
|
|
|
327
334
|
export function literalCurrencyOverrides(fields) {
|
|
328
335
|
const out = {};
|
|
329
336
|
for (const [name, def] of Object.entries(fields)) {
|
|
330
|
-
const
|
|
331
|
-
const code =
|
|
332
|
-
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") {
|
|
333
342
|
out[name] = code;
|
|
334
343
|
}
|
|
335
344
|
}
|
|
@@ -375,7 +384,8 @@ const CURRENCY_CODE_PATTERN = /^[A-Za-z]{3}$/;
|
|
|
375
384
|
function isMoneyValue(value) {
|
|
376
385
|
if (typeof value !== "object" || value === null)
|
|
377
386
|
return false;
|
|
378
|
-
const
|
|
387
|
+
const amount = propertyOf(value, "amount");
|
|
388
|
+
const currency = propertyOf(value, "currency");
|
|
379
389
|
return (typeof amount === "number" &&
|
|
380
390
|
Number.isFinite(amount) &&
|
|
381
391
|
typeof currency === "string" &&
|
|
@@ -526,7 +536,7 @@ function coerceEmbeddedListRows(raw, fieldName, schema, maxItems) {
|
|
|
526
536
|
}
|
|
527
537
|
const cellEntries = [];
|
|
528
538
|
for (const [cellName, cell] of Object.entries(schema)) {
|
|
529
|
-
const cellValue = candidate
|
|
539
|
+
const cellValue = propertyOf(candidate, cellName);
|
|
530
540
|
if (cellValue === undefined || cellValue === null)
|
|
531
541
|
continue;
|
|
532
542
|
const coerced = coerceEmbeddedCell(cellValue, cell);
|
|
@@ -762,7 +772,7 @@ function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onC
|
|
|
762
772
|
// der ursprünglichen version sitzen — der buildPayload würde
|
|
763
773
|
// weiter die stale version stampen und der Konflikt-Recovery wäre
|
|
764
774
|
// kaputt.
|
|
765
|
-
const recordKey = record
|
|
775
|
+
const recordKey = versionOf(record);
|
|
766
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}`));
|
|
767
777
|
}
|
|
768
778
|
function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onReload, translate, onCopyLink, onSaved, onDeleted, }) {
|
|
@@ -773,7 +783,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
773
783
|
// captured once and stamped into every update payload — if a
|
|
774
784
|
// concurrent writer bumps it, the server returns a version-conflict
|
|
775
785
|
// error and the user reloads.
|
|
776
|
-
const recordVersion = record
|
|
786
|
+
const recordVersion = versionOf(record);
|
|
777
787
|
const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
778
788
|
// A money field declaring `currency: { kind: "tenant" }` (fw#2933) resolves
|
|
779
789
|
// its empty-value currency from the tenant-settings config key instead of
|
|
@@ -800,9 +810,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
800
810
|
// damit sie beim Edit den Bestand zeigen statt write-only zu sein.
|
|
801
811
|
const extensionInitialValues = useMemo(() => {
|
|
802
812
|
const cf = record["customFields"];
|
|
803
|
-
return cf
|
|
804
|
-
? cf
|
|
805
|
-
: undefined;
|
|
813
|
+
return isPlainObject(cf) ? cf : undefined;
|
|
806
814
|
}, [record]);
|
|
807
815
|
const writeCommand = entityWriteCommand(schema.featureName, screen.entity, "update");
|
|
808
816
|
const deleteCommand = entityWriteCommand(schema.featureName, screen.entity, "delete");
|
|
@@ -1217,8 +1225,8 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1217
1225
|
const filterPayload = useMemo(() => buildFilterPayload(urlState.filters, (field) => {
|
|
1218
1226
|
// entity.fields ist am Renderer-Layer schwach getypt (vom Schema
|
|
1219
1227
|
// deserialisiert) — Boundary-Cast wie buildInitialValues.
|
|
1220
|
-
const
|
|
1221
|
-
return
|
|
1228
|
+
const type = propertyOf(entity.fields[field], "type");
|
|
1229
|
+
return typeof type === "string" ? type : undefined;
|
|
1222
1230
|
}), [urlState.filters, entity.fields]);
|
|
1223
1231
|
// Entity-only additions (screen.filter, faceted filters) layer on top of
|
|
1224
1232
|
// the shared buildListQueryPayload — projectionList has neither.
|
|
@@ -2379,7 +2387,7 @@ function navigateAfterRedirect(options) {
|
|
|
2379
2387
|
function overlayUpdatedProjection(record, resultData) {
|
|
2380
2388
|
if (typeof resultData !== "object" || resultData === null)
|
|
2381
2389
|
return record;
|
|
2382
|
-
const projection = resultData
|
|
2390
|
+
const projection = propertyOf(resultData, "data");
|
|
2383
2391
|
if (typeof projection !== "object" || projection === null)
|
|
2384
2392
|
return record;
|
|
2385
2393
|
return { ...record, ...projection };
|
|
@@ -2628,7 +2636,7 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2628
2636
|
out[shortName] = defaults[shortName];
|
|
2629
2637
|
continue;
|
|
2630
2638
|
}
|
|
2631
|
-
const ftype = fieldDef
|
|
2639
|
+
const ftype = propertyOf(fieldDef, "type");
|
|
2632
2640
|
// Field-Type-Coercion: config-Werte sind string|number|boolean,
|
|
2633
2641
|
// aber der Form-State erwartet das passende Field-Native-Type.
|
|
2634
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,10 @@
|
|
|
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
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.362.0",
|
|
4
10
|
"type": "improvement",
|