@cosmicdrift/kumiko-renderer 0.362.0 → 0.364.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 +83 -39
- package/dist/app/secret-mint-body.js +2 -1
- package/dist/app/visible-when.d.ts +1 -0
- package/dist/app/visible-when.js +7 -0
- package/dist/components/availability-options-select.d.ts +4 -4
- package/dist/components/availability-options-select.js +13 -6
- package/dist/components/multi-select-checkboxes.d.ts +1 -0
- package/dist/components/multi-select-checkboxes.js +5 -2
- package/dist/components/reference-create-dialog.js +2 -1
- package/dist/components/render-edit.js +9 -4
- package/dist/components/render-field.js +35 -18
- 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 +24 -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";
|
|
@@ -50,7 +51,7 @@ import { findEditScreenFor, navigateTargetAllows, screenAccessAllows } from "./s
|
|
|
50
51
|
import { SecretMintBody } from "./secret-mint-body.js";
|
|
51
52
|
import { SecretsEditBody } from "./secrets-edit-body.js";
|
|
52
53
|
import { useEmbeddedScreen } from "./use-embedded-screen.js";
|
|
53
|
-
import {
|
|
54
|
+
import { evalVisibleWhenAfterShown } from "./visible-when.js";
|
|
54
55
|
import { dispatcherErrorText, WriteFailedError } from "./write-failed-error.js";
|
|
55
56
|
export { qualifyScreenId };
|
|
56
57
|
/** Mirrors qualifyScreenId for nav QNs (registry form `<feature>:nav:<short-id>`).
|
|
@@ -95,9 +96,13 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
|
|
|
95
96
|
function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, children, }) {
|
|
96
97
|
const { Banner, PageHeader } = usePrimitives();
|
|
97
98
|
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
98
|
-
const t = useTranslation();
|
|
99
99
|
const result = useQuery(visibleWhen.query, {}, { live: true });
|
|
100
|
-
const
|
|
100
|
+
const wasShown = useRef(false);
|
|
101
|
+
const verdict = evalVisibleWhenAfterShown(visibleWhen, result, wasShown.current);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (verdict === "visible")
|
|
104
|
+
wasShown.current = true;
|
|
105
|
+
}, [verdict]);
|
|
101
106
|
if (verdict === "visible")
|
|
102
107
|
return children;
|
|
103
108
|
if (verdict === "loading") {
|
|
@@ -105,14 +110,19 @@ function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, c
|
|
|
105
110
|
}
|
|
106
111
|
// The route still names the locked screen; its breadcrumb must not label what stands in for it.
|
|
107
112
|
const hideLockedBreadcrumb = PageHeader !== undefined && pageHeaderSlotAvailable ? _jsx(PageHeader, { hideBreadcrumb: true }) : null;
|
|
108
|
-
return (_jsxs(_Fragment, { children: [hideLockedBreadcrumb, fallback !== undefined ? (_jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate })) : (_jsx(
|
|
113
|
+
return (_jsxs(_Fragment, { children: [hideLockedBreadcrumb, fallback !== undefined ? (_jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate })) : (_jsx(ScreenUnavailableBanner, {}))] }));
|
|
114
|
+
}
|
|
115
|
+
function ScreenUnavailableBanner() {
|
|
116
|
+
const { Banner } = usePrimitives();
|
|
117
|
+
const t = useTranslation();
|
|
118
|
+
return (_jsx(Banner, { padded: true, variant: "info", testId: "kumiko-screen-unavailable", children: t("kumiko.screen.unavailable") }));
|
|
109
119
|
}
|
|
110
120
|
function FallbackScreen({ featureName, fallback, translate, }) {
|
|
111
121
|
const target = useEmbeddedScreen(featureName, fallback);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
// Also undefined when the user lacks access to the fallback screen, which is
|
|
123
|
+
// not a framework error.
|
|
124
|
+
if (target === undefined)
|
|
125
|
+
return _jsx(ScreenUnavailableBanner, {});
|
|
116
126
|
return _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate });
|
|
117
127
|
}
|
|
118
128
|
function renderScreenBody({ schema, screen, translate, entityId, onRowClick, onCopyLink, }) {
|
|
@@ -246,24 +256,29 @@ function useNavigateToCreateFor(schema, entityName, createScreen, createUnavaila
|
|
|
246
256
|
// — used for a `money` field that declares `currency: { kind: "tenant" }`, so
|
|
247
257
|
// its empty initial value carries the tenant's own currency instead of the
|
|
248
258
|
// entity-wide default.
|
|
259
|
+
function versionOf(record) {
|
|
260
|
+
const version = record["version"];
|
|
261
|
+
return typeof version === "number" ? version : 1;
|
|
262
|
+
}
|
|
249
263
|
export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrides) {
|
|
250
264
|
const out = {};
|
|
251
265
|
for (const [name, def] of Object.entries(fields)) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
266
|
+
const type = propertyOf(def, "type");
|
|
267
|
+
const defaultValue = propertyOf(def, "default");
|
|
268
|
+
if (defaultValue !== undefined) {
|
|
269
|
+
out[name] = defaultValue;
|
|
255
270
|
continue;
|
|
256
271
|
}
|
|
257
272
|
// An untouched optional number must not persist as a deliberate 0.
|
|
258
|
-
if (
|
|
273
|
+
if (type === "number" && propertyOf(def, "required") !== true) {
|
|
259
274
|
out[name] = undefined;
|
|
260
275
|
continue;
|
|
261
276
|
}
|
|
262
|
-
if (
|
|
277
|
+
if (type === "embedded" && propertyOf(def, "multiple") === true) {
|
|
263
278
|
out[name] = [];
|
|
264
279
|
continue;
|
|
265
280
|
}
|
|
266
|
-
if (
|
|
281
|
+
if (type === "money") {
|
|
267
282
|
const currency = moneyCurrencyOverrides?.[name] ?? defaultCurrency;
|
|
268
283
|
if (currency !== undefined) {
|
|
269
284
|
out[name] = { amount: 0, currency };
|
|
@@ -271,11 +286,11 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
|
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
288
|
out[name] =
|
|
274
|
-
|
|
289
|
+
type === "boolean"
|
|
275
290
|
? false
|
|
276
|
-
:
|
|
291
|
+
: type === "number" || type === "money"
|
|
277
292
|
? 0
|
|
278
|
-
:
|
|
293
|
+
: type === "multiSelect"
|
|
279
294
|
? []
|
|
280
295
|
: "";
|
|
281
296
|
}
|
|
@@ -290,9 +305,10 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
|
|
|
290
305
|
export function tenantCurrencyMoneyFieldNames(fields) {
|
|
291
306
|
const names = [];
|
|
292
307
|
for (const [name, def] of Object.entries(fields)) {
|
|
293
|
-
|
|
294
|
-
|
|
308
|
+
if (propertyOf(def, "type") === "money" &&
|
|
309
|
+
propertyOf(propertyOf(def, "currency"), "kind") === "tenant") {
|
|
295
310
|
names.push(name);
|
|
311
|
+
}
|
|
296
312
|
}
|
|
297
313
|
return names;
|
|
298
314
|
}
|
|
@@ -327,9 +343,11 @@ const UPDATE_CHANGES_KEY = "changes";
|
|
|
327
343
|
export function literalCurrencyOverrides(fields) {
|
|
328
344
|
const out = {};
|
|
329
345
|
for (const [name, def] of Object.entries(fields)) {
|
|
330
|
-
const
|
|
331
|
-
const code =
|
|
332
|
-
if (
|
|
346
|
+
const currency = propertyOf(def, "currency");
|
|
347
|
+
const code = propertyOf(currency, "code");
|
|
348
|
+
if (propertyOf(def, "type") === "money" &&
|
|
349
|
+
propertyOf(currency, "kind") === "literal" &&
|
|
350
|
+
typeof code === "string") {
|
|
333
351
|
out[name] = code;
|
|
334
352
|
}
|
|
335
353
|
}
|
|
@@ -375,7 +393,8 @@ const CURRENCY_CODE_PATTERN = /^[A-Za-z]{3}$/;
|
|
|
375
393
|
function isMoneyValue(value) {
|
|
376
394
|
if (typeof value !== "object" || value === null)
|
|
377
395
|
return false;
|
|
378
|
-
const
|
|
396
|
+
const amount = propertyOf(value, "amount");
|
|
397
|
+
const currency = propertyOf(value, "currency");
|
|
379
398
|
return (typeof amount === "number" &&
|
|
380
399
|
Number.isFinite(amount) &&
|
|
381
400
|
typeof currency === "string" &&
|
|
@@ -470,7 +489,11 @@ function isRepresentableAtScale(value, scale) {
|
|
|
470
489
|
const tolerance = Math.abs(scaled) * 8 * Number.EPSILON + Number.EPSILON;
|
|
471
490
|
return Math.abs(scaled - Math.round(scaled)) <= tolerance;
|
|
472
491
|
}
|
|
473
|
-
|
|
492
|
+
// Server schema (`z.iso.datetime()`) accepts only a `Z` suffix, while
|
|
493
|
+
// Temporal.Instant.from also takes numeric offsets.
|
|
494
|
+
function isUtcIsoInstant(value) {
|
|
495
|
+
if (!value.endsWith("Z"))
|
|
496
|
+
return false;
|
|
474
497
|
try {
|
|
475
498
|
Temporal.Instant.from(value);
|
|
476
499
|
return true;
|
|
@@ -480,6 +503,21 @@ function isIsoInstant(value) {
|
|
|
480
503
|
}
|
|
481
504
|
}
|
|
482
505
|
const EMBEDDED_LIST_PREFILL_ROW_LIMIT = 500;
|
|
506
|
+
// The server schema (`z.string().date()`) rejects calendar-invalid days like 2020-02-31.
|
|
507
|
+
function isValidIsoDate(value) {
|
|
508
|
+
if (!ISO_DATE_PATTERN.test(value))
|
|
509
|
+
return false;
|
|
510
|
+
try {
|
|
511
|
+
Temporal.PlainDate.from(value, { overflow: "reject" });
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function isDecimalWithinSafeRange(value, scale) {
|
|
519
|
+
return scale === undefined || Math.abs(value) <= Number.MAX_SAFE_INTEGER / 10 ** scale;
|
|
520
|
+
}
|
|
483
521
|
function coerceEmbeddedCell(value, cell) {
|
|
484
522
|
switch (cell.type) {
|
|
485
523
|
case "number":
|
|
@@ -487,13 +525,15 @@ function coerceEmbeddedCell(value, cell) {
|
|
|
487
525
|
case "decimal":
|
|
488
526
|
return typeof value === "number" &&
|
|
489
527
|
Number.isFinite(value) &&
|
|
490
|
-
(cell.scale === undefined ||
|
|
528
|
+
(cell.scale === undefined ||
|
|
529
|
+
(isRepresentableAtScale(value, cell.scale) &&
|
|
530
|
+
isDecimalWithinSafeRange(value, cell.scale)))
|
|
491
531
|
? value
|
|
492
532
|
: undefined;
|
|
493
533
|
case "date":
|
|
494
|
-
return typeof value === "string" &&
|
|
534
|
+
return typeof value === "string" && isValidIsoDate(value) ? value : undefined;
|
|
495
535
|
case "timestamp":
|
|
496
|
-
return typeof value === "string" &&
|
|
536
|
+
return typeof value === "string" && isUtcIsoInstant(value) ? value : undefined;
|
|
497
537
|
case "money":
|
|
498
538
|
// Row cells are signed minor units; the currency lives on the head, so a
|
|
499
539
|
// top-level `{amount, currency}` in major units would land 100x off.
|
|
@@ -526,7 +566,7 @@ function coerceEmbeddedListRows(raw, fieldName, schema, maxItems) {
|
|
|
526
566
|
}
|
|
527
567
|
const cellEntries = [];
|
|
528
568
|
for (const [cellName, cell] of Object.entries(schema)) {
|
|
529
|
-
const cellValue = candidate
|
|
569
|
+
const cellValue = propertyOf(candidate, cellName);
|
|
530
570
|
if (cellValue === undefined || cellValue === null)
|
|
531
571
|
continue;
|
|
532
572
|
const coerced = coerceEmbeddedCell(cellValue, cell);
|
|
@@ -542,6 +582,8 @@ function coerceEmbeddedListRows(raw, fieldName, schema, maxItems) {
|
|
|
542
582
|
}
|
|
543
583
|
function coercePrefillString(raw, name, shape, fallback, defaultCurrency, moneyCurrencyOverrides) {
|
|
544
584
|
if (shape.type === "number") {
|
|
585
|
+
if (raw.trim() === "")
|
|
586
|
+
return fallback;
|
|
545
587
|
const parsed = Number(raw);
|
|
546
588
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
547
589
|
}
|
|
@@ -762,7 +804,7 @@ function EntityEditUpdateBody({ schema, screen, entity, entityId, translate, onC
|
|
|
762
804
|
// der ursprünglichen version sitzen — der buildPayload würde
|
|
763
805
|
// weiter die stale version stampen und der Konflikt-Recovery wäre
|
|
764
806
|
// kaputt.
|
|
765
|
-
const recordKey = record
|
|
807
|
+
const recordKey = versionOf(record);
|
|
766
808
|
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
809
|
}
|
|
768
810
|
function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onReload, translate, onCopyLink, onSaved, onDeleted, }) {
|
|
@@ -773,7 +815,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
773
815
|
// captured once and stamped into every update payload — if a
|
|
774
816
|
// concurrent writer bumps it, the server returns a version-conflict
|
|
775
817
|
// error and the user reloads.
|
|
776
|
-
const recordVersion = record
|
|
818
|
+
const recordVersion = versionOf(record);
|
|
777
819
|
const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
778
820
|
// A money field declaring `currency: { kind: "tenant" }` (fw#2933) resolves
|
|
779
821
|
// its empty-value currency from the tenant-settings config key instead of
|
|
@@ -800,9 +842,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
800
842
|
// damit sie beim Edit den Bestand zeigen statt write-only zu sein.
|
|
801
843
|
const extensionInitialValues = useMemo(() => {
|
|
802
844
|
const cf = record["customFields"];
|
|
803
|
-
return cf
|
|
804
|
-
? cf
|
|
805
|
-
: undefined;
|
|
845
|
+
return isPlainObject(cf) ? cf : undefined;
|
|
806
846
|
}, [record]);
|
|
807
847
|
const writeCommand = entityWriteCommand(schema.featureName, screen.entity, "update");
|
|
808
848
|
const deleteCommand = entityWriteCommand(schema.featureName, screen.entity, "delete");
|
|
@@ -1217,8 +1257,8 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1217
1257
|
const filterPayload = useMemo(() => buildFilterPayload(urlState.filters, (field) => {
|
|
1218
1258
|
// entity.fields ist am Renderer-Layer schwach getypt (vom Schema
|
|
1219
1259
|
// deserialisiert) — Boundary-Cast wie buildInitialValues.
|
|
1220
|
-
const
|
|
1221
|
-
return
|
|
1260
|
+
const type = propertyOf(entity.fields[field], "type");
|
|
1261
|
+
return typeof type === "string" ? type : undefined;
|
|
1222
1262
|
}), [urlState.filters, entity.fields]);
|
|
1223
1263
|
// Entity-only additions (screen.filter, faceted filters) layer on top of
|
|
1224
1264
|
// the shared buildListQueryPayload — projectionList has neither.
|
|
@@ -2190,8 +2230,12 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2190
2230
|
(screen.detailFor !== undefined
|
|
2191
2231
|
? findEntityListScreen(schema, screen.detailFor)?.id
|
|
2192
2232
|
: undefined);
|
|
2193
|
-
return (_jsx(RecordNotFoundState, { translate: effectiveTranslate, ...(backListScreen !== undefined && {
|
|
2194
|
-
onBack: () => nav
|
|
2233
|
+
return (_jsx(RecordNotFoundState, { translate: effectiveTranslate, ...((returnTarget !== undefined || backListScreen !== undefined) && {
|
|
2234
|
+
onBack: () => navigateToReturnOr(nav, returnTarget, () => {
|
|
2235
|
+
if (backListScreen !== undefined) {
|
|
2236
|
+
nav.navigate({ screenId: lastSegment(backListScreen) });
|
|
2237
|
+
}
|
|
2238
|
+
}),
|
|
2195
2239
|
}) }));
|
|
2196
2240
|
}
|
|
2197
2241
|
// Grouped into the head Card alongside title/status/metrics (fw#2713):
|
|
@@ -2379,7 +2423,7 @@ function navigateAfterRedirect(options) {
|
|
|
2379
2423
|
function overlayUpdatedProjection(record, resultData) {
|
|
2380
2424
|
if (typeof resultData !== "object" || resultData === null)
|
|
2381
2425
|
return record;
|
|
2382
|
-
const projection = resultData
|
|
2426
|
+
const projection = propertyOf(resultData, "data");
|
|
2383
2427
|
if (typeof projection !== "object" || projection === null)
|
|
2384
2428
|
return record;
|
|
2385
2429
|
return { ...record, ...projection };
|
|
@@ -2628,7 +2672,7 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2628
2672
|
out[shortName] = defaults[shortName];
|
|
2629
2673
|
continue;
|
|
2630
2674
|
}
|
|
2631
|
-
const ftype = fieldDef
|
|
2675
|
+
const ftype = propertyOf(fieldDef, "type");
|
|
2632
2676
|
// Field-Type-Coercion: config-Werte sind string|number|boolean,
|
|
2633
2677
|
// aber der Form-State erwartet das passende Field-Native-Type.
|
|
2634
2678
|
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 = {};
|
|
@@ -2,3 +2,4 @@ import type { DashboardPanelVisibility } from "@cosmicdrift/kumiko-framework/ui-
|
|
|
2
2
|
import type { UseQueryResult } from "../hooks/use-query.js";
|
|
3
3
|
export type VisibleWhenVerdict = "loading" | "visible" | "hidden" | "error";
|
|
4
4
|
export declare function evalVisibleWhen(visibleWhen: DashboardPanelVisibility, result: UseQueryResult<Readonly<Record<string, unknown>>> | undefined): VisibleWhenVerdict;
|
|
5
|
+
export declare function evalVisibleWhenAfterShown(visibleWhen: DashboardPanelVisibility, result: UseQueryResult<Readonly<Record<string, unknown>>> | undefined, wasShown: boolean): VisibleWhenVerdict;
|
package/dist/app/visible-when.js
CHANGED
|
@@ -9,3 +9,10 @@ export function evalVisibleWhen(visibleWhen, result) {
|
|
|
9
9
|
return result.loading ? "loading" : "hidden";
|
|
10
10
|
return result.data[visibleWhen.field] === visibleWhen.eq ? "visible" : "hidden";
|
|
11
11
|
}
|
|
12
|
+
// Once the content was shown, a failed refetch must not unmount it (and drop unsaved
|
|
13
|
+
// form input) while the last good data still matches; the first evaluation stays fail-closed.
|
|
14
|
+
export function evalVisibleWhenAfterShown(visibleWhen, result, wasShown) {
|
|
15
|
+
const verdict = evalVisibleWhen(visibleWhen, result);
|
|
16
|
+
const lastGoodDataMatches = result?.data?.[visibleWhen.field] === visibleWhen.eq;
|
|
17
|
+
return verdict === "error" && wasShown && lastGoodDataMatches ? "visible" : verdict;
|
|
18
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
|
|
2
2
|
import type { OptionsQueryPayload } from "@cosmicdrift/kumiko-types/fields";
|
|
3
|
-
import type
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
4
|
export type SelectOptionEntry = {
|
|
5
5
|
readonly value: string;
|
|
6
6
|
readonly label: string;
|
|
@@ -9,14 +9,14 @@ export type SelectOptionEntry = {
|
|
|
9
9
|
};
|
|
10
10
|
export declare function staticSelectOptions(field: EditFieldViewModel): readonly SelectOptionEntry[];
|
|
11
11
|
export declare function mergeOptionAvailability(options: readonly SelectOptionEntry[], rows: readonly unknown[], current: string): readonly SelectOptionEntry[];
|
|
12
|
-
export declare function filterUnavailableOptions(options: readonly SelectOptionEntry[], rows: readonly unknown[],
|
|
12
|
+
export declare function filterUnavailableOptions(options: readonly SelectOptionEntry[], rows: readonly unknown[], heldValues: ReadonlySet<string>): readonly SelectOptionEntry[];
|
|
13
13
|
type AvailabilityOptionsMultiSelectProps = {
|
|
14
14
|
readonly field: EditFieldViewModel;
|
|
15
15
|
readonly query: string;
|
|
16
|
-
readonly
|
|
16
|
+
readonly initialValues: readonly string[];
|
|
17
17
|
readonly children: (options: readonly SelectOptionEntry[]) => ReactNode;
|
|
18
18
|
};
|
|
19
|
-
export declare function AvailabilityOptionsMultiSelect({ field, query,
|
|
19
|
+
export declare function AvailabilityOptionsMultiSelect({ field, query, initialValues, children, }: AvailabilityOptionsMultiSelectProps): ReactNode;
|
|
20
20
|
type AvailabilityOptionsSelectProps = {
|
|
21
21
|
readonly field: EditFieldViewModel;
|
|
22
22
|
readonly query: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from "react";
|
|
2
3
|
import { useQuery } from "../hooks/use-query.js";
|
|
3
4
|
import { usePrimitives } from "../primitives.js";
|
|
4
5
|
import { resolveOptionsQueryPayload } from "./query-options-select.js";
|
|
@@ -36,21 +37,27 @@ export function mergeOptionAvailability(options, rows, current) {
|
|
|
36
37
|
};
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
40
|
+
// Unavailable options are dropped unless the target already held them when the
|
|
41
|
+
// form mounted; those stay listed but disabled, because the write path keeps
|
|
42
|
+
// values the caller cannot grant and an unchecked one could never be re-checked.
|
|
43
|
+
export function filterUnavailableOptions(options, rows, heldValues) {
|
|
42
44
|
const unavailable = new Set();
|
|
43
45
|
for (const row of rows) {
|
|
44
46
|
if (isAvailabilityRow(row) && row.disabled === true)
|
|
45
47
|
unavailable.add(row.value);
|
|
46
48
|
}
|
|
47
|
-
return options.
|
|
49
|
+
return options.flatMap((option) => {
|
|
50
|
+
if (!unavailable.has(option.value))
|
|
51
|
+
return [option];
|
|
52
|
+
return heldValues.has(option.value) ? [{ ...option, disabled: true }] : [];
|
|
53
|
+
});
|
|
48
54
|
}
|
|
49
55
|
// Until the result arrives, and when the query fails, every static option is
|
|
50
56
|
// shown; the write path enforces the rule either way.
|
|
51
|
-
export function AvailabilityOptionsMultiSelect({ field, query,
|
|
57
|
+
export function AvailabilityOptionsMultiSelect({ field, query, initialValues, children, }) {
|
|
52
58
|
const { data } = useQuery(query, {});
|
|
53
|
-
|
|
59
|
+
const heldAtMount = useRef(new Set(initialValues));
|
|
60
|
+
return children(filterUnavailableOptions(staticSelectOptions(field), data?.rows ?? [], heldAtMount.current));
|
|
54
61
|
}
|
|
55
62
|
// Select with static options whose availability (SelectFieldDef.optionsAvailabilityQuery)
|
|
56
63
|
// comes from a query, so it has to be a mounted component for useQuery. Until the
|
|
@@ -14,8 +14,11 @@ export function MultiSelectCheckboxes({ field, id, options, value, onChange, })
|
|
|
14
14
|
const allSelected = options.length > 0 && options.every((opt) => selected.has(opt.value));
|
|
15
15
|
// Always emits in `field.options` order (MultiSelectFieldDef's documented
|
|
16
16
|
// ordering guarantee), never in click/selection order.
|
|
17
|
+
// Disabled options are pinned: they keep their current state whatever is toggled.
|
|
17
18
|
const emitOrdered = (next) => {
|
|
18
|
-
onChange(options
|
|
19
|
+
onChange(options
|
|
20
|
+
.filter((opt) => (opt.disabled === true ? selected.has(opt.value) : next.has(opt.value)))
|
|
21
|
+
.map((opt) => opt.value));
|
|
19
22
|
};
|
|
20
23
|
const toggleOption = (optionValue, checked) => {
|
|
21
24
|
const next = new Set(selected);
|
|
@@ -35,5 +38,5 @@ export function MultiSelectCheckboxes({ field, id, options, value, onChange, })
|
|
|
35
38
|
: t("kumiko.field.multiSelect.select-all") }), _jsx(Grid, { columns: field.columns ?? 2, testId: `${id}-checkboxes`,
|
|
36
39
|
// Field's label targets `id`, which no checkbox carries here; point the
|
|
37
40
|
// group at Field's `${id}-label` element instead.
|
|
38
|
-
ariaLabelledBy: `${id}-label`, ...(field.maxRows !== undefined && { maxRows: field.maxRows }), children: options.map((opt) => (_jsx(GridCell, { children: _jsx(Field, { id: `${id}-${opt.value}`, label: opt.label, layout: "inline", children: _jsx(Input, { kind: "boolean", id: `${id}-${opt.value}`, name: `${id}-${opt.value}`, value: selected.has(opt.value), onChange: (checked) => toggleOption(opt.value, checked), disabled: disabled }) }) }, opt.value))) })] }));
|
|
41
|
+
ariaLabelledBy: `${id}-label`, ...(field.maxRows !== undefined && { maxRows: field.maxRows }), children: options.map((opt) => (_jsx(GridCell, { children: _jsx(Field, { id: `${id}-${opt.value}`, label: opt.label, layout: "inline", children: _jsx(Input, { kind: "boolean", id: `${id}-${opt.value}`, name: `${id}-${opt.value}`, value: selected.has(opt.value), onChange: (checked) => toggleOption(opt.value, checked), disabled: disabled || opt.disabled === true }) }) }, opt.value))) })] }));
|
|
39
42
|
}
|
|
@@ -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,10 +351,10 @@ 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 =
|
|
358
|
-
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 }));
|
|
354
|
+
const arrayValue = stringArrayOf(field.value);
|
|
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(withPinnedValues(v, options, arrayValue)), options: options }));
|
|
359
356
|
if (field.selectOptionsAvailabilityQuery !== undefined && field.readOnly !== true) {
|
|
360
|
-
return (_jsx(AvailabilityOptionsMultiSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query,
|
|
357
|
+
return (_jsx(AvailabilityOptionsMultiSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, initialValues: arrayValue, children: renderMultiSelect }));
|
|
361
358
|
}
|
|
362
359
|
return renderMultiSelect(staticSelectOptions(field));
|
|
363
360
|
}
|
|
@@ -452,8 +449,11 @@ const ISO_CURRENCY_CODE = /^[A-Z]{3}$/;
|
|
|
452
449
|
function resolveMoneyCurrency(value, fieldCurrency) {
|
|
453
450
|
if (typeof value === "object" && value !== null && "currency" in value) {
|
|
454
451
|
const { currency } = value;
|
|
455
|
-
if (typeof currency === "string"
|
|
456
|
-
|
|
452
|
+
if (typeof currency === "string") {
|
|
453
|
+
const normalized = currency.toUpperCase();
|
|
454
|
+
if (ISO_CURRENCY_CODE.test(normalized))
|
|
455
|
+
return normalized;
|
|
456
|
+
}
|
|
457
457
|
}
|
|
458
458
|
return fieldCurrency ?? "EUR";
|
|
459
459
|
}
|
|
@@ -474,13 +474,30 @@ function resolveNumberUnit(unit, row) {
|
|
|
474
474
|
const trimmed = sibling.trim();
|
|
475
475
|
return trimmed.length > 0 && trimmed.length <= MAX_SIBLING_UNIT_LENGTH ? trimmed : undefined;
|
|
476
476
|
}
|
|
477
|
+
function stringArrayOf(value) {
|
|
478
|
+
return Array.isArray(value)
|
|
479
|
+
? value.filter((item) => typeof item === "string")
|
|
480
|
+
: [];
|
|
481
|
+
}
|
|
482
|
+
// The combobox cannot disable a single option, so removals of disabled options
|
|
483
|
+
// the form already holds are merged back.
|
|
484
|
+
function withPinnedValues(next, options, current) {
|
|
485
|
+
const missingPinned = options
|
|
486
|
+
.filter((opt) => opt.disabled === true && current.includes(opt.value) && !next.includes(opt.value))
|
|
487
|
+
.map((opt) => opt.value);
|
|
488
|
+
return missingPinned.length === 0 ? next : [...next, ...missingPinned];
|
|
489
|
+
}
|
|
490
|
+
function stringPropertyOf(value, key) {
|
|
491
|
+
const property = propertyOf(value, key);
|
|
492
|
+
return typeof property === "string" ? property : undefined;
|
|
493
|
+
}
|
|
477
494
|
function moneyMinorValue(v, currency) {
|
|
478
495
|
if (v === undefined || v === null || v === "")
|
|
479
496
|
return "";
|
|
480
497
|
if (typeof v === "number")
|
|
481
498
|
return Math.round(v * 10 ** currencyDecimals(currency));
|
|
482
499
|
if (typeof v === "object") {
|
|
483
|
-
const amount = v
|
|
500
|
+
const amount = propertyOf(v, "amount");
|
|
484
501
|
if (typeof amount === "number")
|
|
485
502
|
return Math.round(amount * 10 ** currencyDecimals(currency));
|
|
486
503
|
}
|
|
@@ -498,11 +515,11 @@ function moneyPayload(minorUnits, currency) {
|
|
|
498
515
|
// (noch nicht gesetzt) → "" als Empty-Sentinel, analog money/timestamp.
|
|
499
516
|
function locatedValue(v) {
|
|
500
517
|
if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
|
|
501
|
-
const
|
|
518
|
+
const utc = propertyOf(v, "utc");
|
|
502
519
|
return {
|
|
503
|
-
at: typeof
|
|
504
|
-
tz: typeof
|
|
505
|
-
...(typeof
|
|
520
|
+
at: typeof v.at === "string" ? v.at : "",
|
|
521
|
+
tz: typeof v.tz === "string" ? v.tz : "",
|
|
522
|
+
...(typeof utc === "string" && { utc }),
|
|
506
523
|
};
|
|
507
524
|
}
|
|
508
525
|
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.364.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.364.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.364.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.364.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.364.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.364.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "A failed refetch of a visibleWhen gate no longer unmounts a screen that is already showing",
|
|
6
|
+
"detail": "ScreenVisibilityGate used to fail closed on every error, so one failed live refetch (SSE reconnect, a short 5xx) removed the content and reset unsaved form input. Only the first evaluation fails closed now; after the content was shown, an error keeps it mounted as long as the last good data still matches the condition. The dashboard panel retry path is unchanged."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.364.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "A held multiSelect value that optionsAvailabilityQuery marks unavailable stays listed but cannot be removed",
|
|
12
|
+
"detail": "The keep-listed set is taken from the values present when the field mounted, not from the live value. In the checkbox grid the entry is rendered disabled and select-all/deselect-all leave it as is; in the combobox a removal is merged back into the value. Previously unchecking such a role hid it for good while the write path kept it, so the form reported a removal that never happened. A stored lower-case currency code such as usd is normalised to upper case instead of falling back to the field currency, and array values with a non-string element keep their string entries instead of being treated as empty."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.364.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Screen fallback access, projectionDetail back navigation and stricter prefill",
|
|
18
|
+
"detail": "A visibleWhen fallback screen the user may not access now shows the standard unavailable notice instead of a \"Screen not found\" error. The not-found state of a projectionDetail honours returnTo for its back button. Embedded-list prefill rejects calendar-invalid dates, timestamps with numeric offsets and decimals beyond the safe range, and an empty number param keeps the field default instead of becoming 0."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.363.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "propertyOf type guard and fewer unchecked casts in the UI packages",
|
|
24
|
+
"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."
|
|
25
|
+
},
|
|
2
26
|
{
|
|
3
27
|
"version": "0.362.0",
|
|
4
28
|
"type": "improvement",
|