@cosmicdrift/kumiko-renderer 0.197.1 → 0.199.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.199.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,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.199.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.199.0",
|
|
20
20
|
"react": "^19.2.6",
|
|
21
21
|
"temporal-polyfill": "^0.3.2",
|
|
22
22
|
"zod": "^4.4.3"
|
|
@@ -198,7 +198,7 @@ export function KumikoScreen({
|
|
|
198
198
|
case "configEdit":
|
|
199
199
|
return <ConfigEditBody schema={schema} screen={screen} translate={translate} />;
|
|
200
200
|
case "custom":
|
|
201
|
-
return <CustomScreenBody screenId={screen.id} />;
|
|
201
|
+
return <CustomScreenBody screenId={screen.id} featureName={schema.featureName} />;
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -228,16 +228,27 @@ function DashboardScreenBody({
|
|
|
228
228
|
// Lookup-Body für custom-screens: schaut die Component aus dem
|
|
229
229
|
// CustomScreens-Context (gefüttert von clientFeatures.components in
|
|
230
230
|
// createKumikoApp). Wenn weder Provider gemounted noch screenId
|
|
231
|
-
// registriert ist, fällt es auf einen Banner zurück
|
|
232
|
-
//
|
|
233
|
-
|
|
231
|
+
// registriert ist, fällt es auf einen error-Banner zurück statt leer zu
|
|
232
|
+
// rendern — der Screen ist per URL erreichbar sobald das Server-Feature
|
|
233
|
+
// gemountet ist, unabhängig davon ob ein Client-Plugin existiert
|
|
234
|
+
// (kumiko-framework#2025).
|
|
235
|
+
function CustomScreenBody({
|
|
236
|
+
screenId,
|
|
237
|
+
featureName,
|
|
238
|
+
}: {
|
|
239
|
+
readonly screenId: string;
|
|
240
|
+
readonly featureName: string;
|
|
241
|
+
}): ReactNode {
|
|
234
242
|
const { Banner, Text } = usePrimitives();
|
|
235
243
|
const Component = useCustomScreenComponent(screenId);
|
|
236
244
|
if (Component === undefined) {
|
|
237
245
|
return (
|
|
238
|
-
<Banner padded variant="
|
|
239
|
-
Custom screen <Text variant="code">{screenId}</Text>
|
|
240
|
-
<Text variant="code">
|
|
246
|
+
<Banner padded variant="error" testId="kumiko-screen-custom-placeholder">
|
|
247
|
+
Custom screen <Text variant="code">{screenId}</Text> (Feature{" "}
|
|
248
|
+
<Text variant="code">{featureName}</Text>) hat keine Component in{" "}
|
|
249
|
+
<Text variant="code">clientFeatures.components</Text> — das Web-Client-Plugin des Features
|
|
250
|
+
(siehe <Text variant="code">{featureName}/web/client-plugin.tsx</Text>) fehlt in{" "}
|
|
251
|
+
<Text variant="code">clientFeatures</Text>.
|
|
241
252
|
</Banner>
|
|
242
253
|
);
|
|
243
254
|
}
|
|
@@ -438,11 +449,20 @@ function EntityEditCreateBody({
|
|
|
438
449
|
screen,
|
|
439
450
|
entity,
|
|
440
451
|
translate,
|
|
452
|
+
onSaved,
|
|
441
453
|
}: {
|
|
442
454
|
readonly schema: FeatureSchema;
|
|
443
455
|
readonly screen: EntityEditScreenDefinition;
|
|
444
456
|
readonly entity: EntityDefinition;
|
|
445
457
|
readonly translate?: Translate;
|
|
458
|
+
// Singleton screens (kumiko-screen#1944) run without a wrapping
|
|
459
|
+
// entityList screen, so `navigateToList` below is a silent no-op there —
|
|
460
|
+
// without this callback, a successful create left the form stuck showing
|
|
461
|
+
// the just-submitted values, and a second submit created a duplicate
|
|
462
|
+
// record. Wired by EntityEditSingletonBody to re-run its list(limit:1)
|
|
463
|
+
// query, which flips the branch to EntityEditUpdateBody once it sees the
|
|
464
|
+
// new row.
|
|
465
|
+
readonly onSaved?: () => void;
|
|
446
466
|
}): ReactNode {
|
|
447
467
|
const nav = useNav();
|
|
448
468
|
const initial = useMemo(
|
|
@@ -470,8 +490,9 @@ function EntityEditCreateBody({
|
|
|
470
490
|
return;
|
|
471
491
|
}
|
|
472
492
|
navigateToList();
|
|
493
|
+
onSaved?.();
|
|
473
494
|
},
|
|
474
|
-
[nav, screen.redirect, navigateToList],
|
|
495
|
+
[nav, screen.redirect, navigateToList, onSaved],
|
|
475
496
|
);
|
|
476
497
|
return (
|
|
477
498
|
<RenderEdit
|
|
@@ -500,6 +521,8 @@ function EntityEditUpdateBody({
|
|
|
500
521
|
entityId,
|
|
501
522
|
translate,
|
|
502
523
|
onCopyLink,
|
|
524
|
+
autoReloadOnSave,
|
|
525
|
+
onDeleted,
|
|
503
526
|
}: {
|
|
504
527
|
readonly schema: FeatureSchema;
|
|
505
528
|
readonly screen: EntityEditScreenDefinition;
|
|
@@ -507,6 +530,18 @@ function EntityEditUpdateBody({
|
|
|
507
530
|
readonly entityId: string;
|
|
508
531
|
readonly translate?: Translate;
|
|
509
532
|
readonly onCopyLink?: () => Promise<void> | void;
|
|
533
|
+
// Singleton screens (kumiko-screen#1944): `navigateToList` below is a
|
|
534
|
+
// no-op there (no wrapping entityList screen), so a successful update
|
|
535
|
+
// left the form holding the now-stale `recordVersion` it captured at
|
|
536
|
+
// mount — the next submit hit a version conflict. Set by
|
|
537
|
+
// EntityEditSingletonBody to re-fetch this body's own detail query after
|
|
538
|
+
// a successful save, remounting the form (via the version-keyed `key`
|
|
539
|
+
// below) with the fresh version.
|
|
540
|
+
readonly autoReloadOnSave?: boolean;
|
|
541
|
+
// Wired by EntityEditSingletonBody to re-run its list(limit:1) query
|
|
542
|
+
// after a successful delete, so the branch flips back to the create form
|
|
543
|
+
// instead of the update form continuing to display the deleted record.
|
|
544
|
+
readonly onDeleted?: () => void;
|
|
510
545
|
}): ReactNode {
|
|
511
546
|
const { Banner, Text } = usePrimitives();
|
|
512
547
|
const t = useTranslation();
|
|
@@ -554,6 +589,8 @@ function EntityEditUpdateBody({
|
|
|
554
589
|
onReload={detailQuery.refetch}
|
|
555
590
|
{...(translate !== undefined && { translate })}
|
|
556
591
|
{...(onCopyLink !== undefined && { onCopyLink })}
|
|
592
|
+
{...(autoReloadOnSave === true && { onSaved: () => void detailQuery.refetch() })}
|
|
593
|
+
{...(onDeleted !== undefined && { onDeleted })}
|
|
557
594
|
/>
|
|
558
595
|
);
|
|
559
596
|
}
|
|
@@ -567,6 +604,8 @@ function EntityEditUpdateForm({
|
|
|
567
604
|
onReload,
|
|
568
605
|
translate,
|
|
569
606
|
onCopyLink,
|
|
607
|
+
onSaved,
|
|
608
|
+
onDeleted,
|
|
570
609
|
}: {
|
|
571
610
|
readonly schema: FeatureSchema;
|
|
572
611
|
readonly screen: EntityEditScreenDefinition;
|
|
@@ -576,6 +615,10 @@ function EntityEditUpdateForm({
|
|
|
576
615
|
readonly onReload: () => Promise<void> | void;
|
|
577
616
|
readonly translate?: Translate;
|
|
578
617
|
readonly onCopyLink?: () => Promise<void> | void;
|
|
618
|
+
// See EntityEditUpdateBody's autoReloadOnSave/onDeleted docs — both are
|
|
619
|
+
// no-ops unless the caller (singleton screens) wires them.
|
|
620
|
+
readonly onSaved?: () => void;
|
|
621
|
+
readonly onDeleted?: () => void;
|
|
579
622
|
}): ReactNode {
|
|
580
623
|
// Seed the form with the server values for the entity's declared
|
|
581
624
|
// fields; anything else (id, tenant_id, created_at…) stays out of
|
|
@@ -628,13 +671,17 @@ function EntityEditUpdateForm({
|
|
|
628
671
|
return;
|
|
629
672
|
}
|
|
630
673
|
navigateToList();
|
|
674
|
+
onSaved?.();
|
|
631
675
|
},
|
|
632
|
-
[nav, screen.redirect, navigateToList],
|
|
676
|
+
[nav, screen.redirect, navigateToList, onSaved],
|
|
633
677
|
);
|
|
634
678
|
const handleDelete = useCallback(async () => {
|
|
635
679
|
const res = await dispatcher.write(deleteCommand, { id: entityId });
|
|
636
|
-
if (res.isSuccess)
|
|
637
|
-
|
|
680
|
+
if (res.isSuccess) {
|
|
681
|
+
navigateToList();
|
|
682
|
+
onDeleted?.();
|
|
683
|
+
}
|
|
684
|
+
}, [dispatcher, deleteCommand, entityId, navigateToList, onDeleted]);
|
|
638
685
|
|
|
639
686
|
return (
|
|
640
687
|
<RenderEdit
|
|
@@ -715,6 +762,8 @@ function EntityEditSingletonBody({
|
|
|
715
762
|
entityId={existingId}
|
|
716
763
|
{...(translate !== undefined && { translate })}
|
|
717
764
|
{...(onCopyLink !== undefined && { onCopyLink })}
|
|
765
|
+
autoReloadOnSave
|
|
766
|
+
onDeleted={() => void listQuery.refetch()}
|
|
718
767
|
/>
|
|
719
768
|
);
|
|
720
769
|
}
|
|
@@ -724,6 +773,7 @@ function EntityEditSingletonBody({
|
|
|
724
773
|
screen={screen}
|
|
725
774
|
entity={entity}
|
|
726
775
|
{...(translate !== undefined && { translate })}
|
|
776
|
+
onSaved={() => void listQuery.refetch()}
|
|
727
777
|
/>
|
|
728
778
|
);
|
|
729
779
|
}
|
|
@@ -737,11 +787,13 @@ function EntityEditCreateOrDisabled({
|
|
|
737
787
|
screen,
|
|
738
788
|
entity,
|
|
739
789
|
translate,
|
|
790
|
+
onSaved,
|
|
740
791
|
}: {
|
|
741
792
|
readonly schema: FeatureSchema;
|
|
742
793
|
readonly screen: EntityEditScreenDefinition;
|
|
743
794
|
readonly entity: EntityDefinition;
|
|
744
795
|
readonly translate?: Translate;
|
|
796
|
+
readonly onSaved?: () => void;
|
|
745
797
|
}): ReactNode {
|
|
746
798
|
const { Banner, Text } = usePrimitives();
|
|
747
799
|
if (screen.allowCreate === false) {
|
|
@@ -758,6 +810,7 @@ function EntityEditCreateOrDisabled({
|
|
|
758
810
|
screen={screen}
|
|
759
811
|
entity={entity}
|
|
760
812
|
{...(translate !== undefined && { translate })}
|
|
813
|
+
{...(onSaved !== undefined && { onSaved })}
|
|
761
814
|
/>
|
|
762
815
|
);
|
|
763
816
|
}
|
|
@@ -1229,12 +1282,16 @@ function EntityListBody({
|
|
|
1229
1282
|
? (row: ListRowViewModel) => onRowClick(row, screen.entity)
|
|
1230
1283
|
: undefined;
|
|
1231
1284
|
|
|
1232
|
-
// Searchable
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1285
|
+
// Searchable default: explicit author choice wins, otherwise auto-on when
|
|
1286
|
+
// the entity has searchable fields (dead toolbar slot otherwise — the
|
|
1287
|
+
// server search index has nothing to filter). Gated on
|
|
1288
|
+
// schema.searchAdapterMissing !== true regardless of source (explicit or
|
|
1289
|
+
// auto) — a search bar the server can't serve would 422 at request-time
|
|
1290
|
+
// (#2032); matches the boot-time check in api/server.ts (#2051).
|
|
1235
1291
|
const searchable =
|
|
1236
|
-
|
|
1237
|
-
|
|
1292
|
+
schema.searchAdapterMissing !== true &&
|
|
1293
|
+
(screen.searchable ??
|
|
1294
|
+
Object.values(entity.fields).some((f) => "searchable" in f && f.searchable === true));
|
|
1238
1295
|
|
|
1239
1296
|
// Pager-Props nur bei pagination="pages" zusammenstellen. Server-
|
|
1240
1297
|
// total kommt async — bis es da ist, rendert RenderList die Tabelle
|
|
@@ -392,6 +392,55 @@ describe("EmbeddedListField — paste coercion", () => {
|
|
|
392
392
|
]);
|
|
393
393
|
});
|
|
394
394
|
});
|
|
395
|
+
|
|
396
|
+
// kumiko-framework#1838: a naive `replace(",", ".")` on a DE-locale paste
|
|
397
|
+
// with a thousands separator turned "1.234,56" into "1.234.56" → NaN →
|
|
398
|
+
// the cell silently cleared instead of keeping the pasted amount.
|
|
399
|
+
test("pastes a DE-locale money value with a thousands separator (1.234,56)", () => {
|
|
400
|
+
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
401
|
+
let lastValue: unknown;
|
|
402
|
+
renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
403
|
+
lastValue = v;
|
|
404
|
+
});
|
|
405
|
+
// Column index 3 = "unitPrice".
|
|
406
|
+
captured?.onPasteCells?.(0, 3, [["1.234,56"]]);
|
|
407
|
+
const result = lastValue as readonly Record<string, unknown>[];
|
|
408
|
+
expect(result[0]?.["unitPrice"]).toBe(123456);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("pastes an en-US-locale money value with a thousands separator (1,234.56)", () => {
|
|
412
|
+
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
413
|
+
let lastValue: unknown;
|
|
414
|
+
renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
415
|
+
lastValue = v;
|
|
416
|
+
});
|
|
417
|
+
captured?.onPasteCells?.(0, 3, [["1,234.56"]]);
|
|
418
|
+
const result = lastValue as readonly Record<string, unknown>[];
|
|
419
|
+
expect(result[0]?.["unitPrice"]).toBe(123456);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("an unparseable money paste leaves the cell unchanged and surfaces a listIssue instead of clearing it", async () => {
|
|
423
|
+
const rows = [{ product: "p1", unit: "pcs", quantity: 1, unitPrice: 100, amount: 100 }];
|
|
424
|
+
let lastValue: unknown;
|
|
425
|
+
renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
|
|
426
|
+
lastValue = v;
|
|
427
|
+
});
|
|
428
|
+
act(() => {
|
|
429
|
+
captured?.onPasteCells?.(0, 3, [["not-a-number"]]);
|
|
430
|
+
});
|
|
431
|
+
const result = lastValue as readonly Record<string, unknown>[];
|
|
432
|
+
expect(result[0]?.["unitPrice"]).toBe(100);
|
|
433
|
+
await waitFor(() => {
|
|
434
|
+
expect(captured?.listIssues).toEqual([
|
|
435
|
+
{
|
|
436
|
+
path: "lines",
|
|
437
|
+
code: "paste-cells-unmatched",
|
|
438
|
+
i18nKey: "kumiko.field.embedded-list.paste-cells-unmatched",
|
|
439
|
+
params: { count: 1 },
|
|
440
|
+
},
|
|
441
|
+
]);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
395
444
|
});
|
|
396
445
|
|
|
397
446
|
describe("EmbeddedListField — reference column populated via useQuery", () => {
|
|
@@ -55,6 +55,30 @@ function withRecomputedDerived(
|
|
|
55
55
|
return result;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Locale-aware decimal parse for pasted text: strips space/NBSP thousands
|
|
59
|
+
// separators outright, then treats whichever of "," or "." appears LAST in
|
|
60
|
+
// the string as the decimal separator and strips the other one — handles
|
|
61
|
+
// both "1.234,56" (de) and "1,234.56" (en-US) pastes. Returns undefined for
|
|
62
|
+
// anything that still doesn't parse as a finite number.
|
|
63
|
+
function parseLocaleAwareDecimal(raw: string): number | undefined {
|
|
64
|
+
const stripped = raw.replace(/[\s ]/g, "");
|
|
65
|
+
if (stripped === "") return undefined;
|
|
66
|
+
const lastComma = stripped.lastIndexOf(",");
|
|
67
|
+
const lastDot = stripped.lastIndexOf(".");
|
|
68
|
+
const decimalIndex = Math.max(lastComma, lastDot);
|
|
69
|
+
if (decimalIndex === -1) {
|
|
70
|
+
const n = Number(stripped);
|
|
71
|
+
return Number.isFinite(n) ? n : undefined;
|
|
72
|
+
}
|
|
73
|
+
const integerPart = stripped.slice(0, decimalIndex).replace(/[.,]/g, "");
|
|
74
|
+
const fractionPart = stripped.slice(decimalIndex + 1);
|
|
75
|
+
if (!/^\d*$/.test(integerPart) || !/^\d*$/.test(fractionPart)) return undefined;
|
|
76
|
+
const n = Number(
|
|
77
|
+
`${integerPart === "" ? "0" : integerPart}.${fractionPart === "" ? "0" : fractionPart}`,
|
|
78
|
+
);
|
|
79
|
+
return Number.isFinite(n) ? n : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
function coerceCellValue(column: EmbeddedListColumn, text: string, currency: string): unknown {
|
|
59
83
|
switch (column.type) {
|
|
60
84
|
case "text":
|
|
@@ -73,9 +97,13 @@ function coerceCellValue(column: EmbeddedListColumn, text: string, currency: str
|
|
|
73
97
|
// currencyDecimals) — a hardcoded ×100 here diverged for zero-/three-
|
|
74
98
|
// decimal currencies (JPY, BHD, ...), landing a pasted value 100x off
|
|
75
99
|
// from the same value typed by hand (kumiko-framework#1972).
|
|
100
|
+
// parseLocaleAwareDecimal handles both "1.234,56" (de) and
|
|
101
|
+
// "1,234.56" (en-US) thousands/decimal conventions — a naive
|
|
102
|
+
// `replace(",", ".")` mangled DE-locale pastes with thousands
|
|
103
|
+
// separators into NaN (kumiko-framework#1838).
|
|
76
104
|
if (text.trim() === "") return undefined;
|
|
77
|
-
const n =
|
|
78
|
-
return
|
|
105
|
+
const n = parseLocaleAwareDecimal(text);
|
|
106
|
+
return n === undefined ? undefined : Math.round(n * 10 ** currencyDecimals(currency));
|
|
79
107
|
}
|
|
80
108
|
case "boolean":
|
|
81
109
|
return ["true", "1", "yes", "y", "ja"].includes(text.trim().toLowerCase());
|
|
@@ -263,11 +291,14 @@ export function EmbeddedListField({
|
|
|
263
291
|
const column = columns[columnIndex + gridColOffset];
|
|
264
292
|
if (column === undefined) return;
|
|
265
293
|
const coerced = coerceCellValue(column, text, field.embeddedListCurrency ?? "EUR");
|
|
266
|
-
|
|
267
|
-
|
|
294
|
+
// Also covers an unparseable money paste (e.g. mangled thousands
|
|
295
|
+
// separators) — the cell must stay unchanged instead of silently
|
|
296
|
+
// clearing to undefined (kumiko-framework#1838).
|
|
297
|
+
const isUnmatchedOrUnparseable =
|
|
298
|
+
(column.type === "select" || column.type === "reference" || column.type === "money") &&
|
|
268
299
|
coerced === undefined &&
|
|
269
300
|
text.trim() !== "";
|
|
270
|
-
if (
|
|
301
|
+
if (isUnmatchedOrUnparseable) {
|
|
271
302
|
unmatchedCells += 1;
|
|
272
303
|
return;
|
|
273
304
|
}
|
|
@@ -415,11 +415,15 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
415
415
|
// freezes it on first render) — safe because a section's field-name set is
|
|
416
416
|
// value-independent, so it matches filterEditSections(vm.sections, fieldsFilter).
|
|
417
417
|
// undefined = unscoped, since scoping would silently drop root-level .refine() issues.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
)
|
|
418
|
+
// A `fieldsFilter` that matches nothing in `fields` (typo, renamed field)
|
|
419
|
+
// must fall back to unscoped too — an empty scope array would filter out
|
|
420
|
+
// ALL validation issues (kumiko-framework#1907), letting submit() through
|
|
421
|
+
// unvalidated on a form that also renders no sections.
|
|
422
|
+
const scopeFieldNames = useMemo(() => {
|
|
423
|
+
if (fieldsFilter === undefined) return undefined;
|
|
424
|
+
const matched = fieldsFilter.filter((f) => Object.hasOwn(fields, f));
|
|
425
|
+
return matched.length === 0 ? undefined : matched;
|
|
426
|
+
}, [fieldsFilter, fields]);
|
|
423
427
|
|
|
424
428
|
// Submit-Config nur wenn der Caller einen writeCommand mitgibt; bei
|
|
425
429
|
// customSubmit-Pfad kommt der Form-Controller ohne Submit-Wiring,
|
|
@@ -1097,7 +1101,13 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
1097
1101
|
</Button>,
|
|
1098
1102
|
]}
|
|
1099
1103
|
>
|
|
1100
|
-
<Text>
|
|
1104
|
+
<Text>
|
|
1105
|
+
{translate(
|
|
1106
|
+
draftCandidates.length === 1
|
|
1107
|
+
? "kumiko.form.draft.resume-single"
|
|
1108
|
+
: "kumiko.form.draft.resume-multiple",
|
|
1109
|
+
)}
|
|
1110
|
+
</Text>
|
|
1101
1111
|
</Banner>
|
|
1102
1112
|
)}
|
|
1103
1113
|
{isWizard && (
|
package/src/i18n-defaults.ts
CHANGED
|
@@ -162,6 +162,9 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
162
162
|
"kumiko.form.extension.save-failed": "Ein Zusatzfeld konnte nicht gespeichert werden.",
|
|
163
163
|
"kumiko.form.draft.resume-multiple":
|
|
164
164
|
"Mehrere offene Entwürfe für dieses Formular gefunden. Welchen möchtest du fortsetzen?",
|
|
165
|
+
"kumiko.form.draft.resume-single":
|
|
166
|
+
"Ein offener Entwurf für dieses Formular gefunden. Möchtest du ihn fortsetzen?",
|
|
167
|
+
"kumiko.form.draft.start-new": "Neu beginnen",
|
|
165
168
|
|
|
166
169
|
// Validation — Default-Reason-Codes aus dem Framework. App-Code
|
|
167
170
|
// kann eigene Codes via Validation-Hooks reinwerfen; die hier sind
|
|
@@ -343,6 +346,9 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
343
346
|
"kumiko.form.extension.save-failed": "A custom field could not be saved.",
|
|
344
347
|
"kumiko.form.draft.resume-multiple":
|
|
345
348
|
"Found multiple open drafts for this form. Which one do you want to resume?",
|
|
349
|
+
"kumiko.form.draft.resume-single":
|
|
350
|
+
"Found an open draft for this form. Do you want to resume it?",
|
|
351
|
+
"kumiko.form.draft.start-new": "Start new",
|
|
346
352
|
|
|
347
353
|
"kumiko.validation.required": "Required.",
|
|
348
354
|
"kumiko.validation.invalid": "Invalid value.",
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// eigenen Bootstrap schreiben will. Normale Samples gehen über
|
|
10
10
|
// @cosmicdrift/kumiko-renderer-web/createKumikoApp, das alle Provider verdrahtet.
|
|
11
11
|
|
|
12
|
+
export { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./app/action-form-shim";
|
|
12
13
|
export type { AppFeaturesProviderProps } from "./app/app-features-context";
|
|
13
14
|
export { AppFeaturesProvider, useAppFeatures } from "./app/app-features-context";
|
|
14
15
|
export type {
|