@cosmicdrift/kumiko-renderer 0.274.0 → 0.276.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 +5 -4
- package/src/app/kumiko-screen.tsx +195 -15
- package/src/components/render-edit.tsx +14 -1
- package/src/primitives.tsx +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.276.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,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.276.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.276.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.276.0",
|
|
20
21
|
"react": "^19.2.6",
|
|
21
22
|
"temporal-polyfill": "^0.3.2",
|
|
22
23
|
"zod": "^4.4.3"
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"@types/react-dom": "^19.2.3",
|
|
28
29
|
"jsdom": "^29.1.1",
|
|
29
30
|
"react-dom": "^19.2.6",
|
|
30
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.276.0"
|
|
31
32
|
},
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
Translate,
|
|
33
33
|
} from "@cosmicdrift/kumiko-headless";
|
|
34
34
|
import { fieldLabelKey, fieldOptionLabelKey, isSafeHref } from "@cosmicdrift/kumiko-headless";
|
|
35
|
+
import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
35
36
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
36
37
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog";
|
|
37
38
|
import {
|
|
@@ -48,7 +49,7 @@ import { RenderList } from "../components/render-list";
|
|
|
48
49
|
import { useDispatcher, useOptionalDispatcher } from "../context/dispatcher-context";
|
|
49
50
|
import { useUserRoles } from "../context/user-roles-context";
|
|
50
51
|
import { type ListSort, useListUrlState } from "../hooks/use-list-url-state";
|
|
51
|
-
import { useQuery } from "../hooks/use-query";
|
|
52
|
+
import { type UseQueryResult, useQuery } from "../hooks/use-query";
|
|
52
53
|
import { useTranslation } from "../i18n";
|
|
53
54
|
import {
|
|
54
55
|
type DataTableFacet,
|
|
@@ -404,9 +405,14 @@ function useNavigateToCreateFor(
|
|
|
404
405
|
|
|
405
406
|
// `defaultCurrency` is entityEdit-only — it enables the `{amount, currency}`
|
|
406
407
|
// write shape (fw#1923); config-edit's plain-number contract needs bare `0`.
|
|
408
|
+
// `moneyCurrencyOverrides` (fw#2933) replaces `defaultCurrency` per field name
|
|
409
|
+
// — used for a `money` field that declares `currency: { kind: "tenant" }`, so
|
|
410
|
+
// its empty initial value carries the tenant's own currency instead of the
|
|
411
|
+
// entity-wide default.
|
|
407
412
|
export function buildInitialValues(
|
|
408
413
|
fields: Readonly<Record<string, unknown>>,
|
|
409
414
|
defaultCurrency?: string,
|
|
415
|
+
moneyCurrencyOverrides?: Readonly<Record<string, string>>,
|
|
410
416
|
): Readonly<Record<string, unknown>> {
|
|
411
417
|
const out: Record<string, unknown> = {};
|
|
412
418
|
for (const [name, def] of Object.entries(fields)) {
|
|
@@ -419,9 +425,12 @@ export function buildInitialValues(
|
|
|
419
425
|
out[name] = [];
|
|
420
426
|
continue;
|
|
421
427
|
}
|
|
422
|
-
if (shape.type === "money"
|
|
423
|
-
|
|
424
|
-
|
|
428
|
+
if (shape.type === "money") {
|
|
429
|
+
const currency = moneyCurrencyOverrides?.[name] ?? defaultCurrency;
|
|
430
|
+
if (currency !== undefined) {
|
|
431
|
+
out[name] = { amount: 0, currency };
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
425
434
|
}
|
|
426
435
|
out[name] =
|
|
427
436
|
shape.type === "boolean"
|
|
@@ -435,6 +444,49 @@ export function buildInitialValues(
|
|
|
435
444
|
return out;
|
|
436
445
|
}
|
|
437
446
|
|
|
447
|
+
// A `money` field opts a currently-empty value into the tenant-settings
|
|
448
|
+
// bundle's per-tenant currency (fw#2933) via `currency: { kind: "tenant" }`
|
|
449
|
+
// on its FieldDefinition. Read structurally — renderer only depends on the
|
|
450
|
+
// client-safe FieldDefinition subset, not the concrete MoneyFieldDef/
|
|
451
|
+
// MoneyCurrencySource types, same idiom as the other field-shape narrowings
|
|
452
|
+
// in this file (PrefillFieldShape etc.).
|
|
453
|
+
function tenantCurrencyMoneyFieldNames(
|
|
454
|
+
fields: Readonly<Record<string, unknown>>,
|
|
455
|
+
): readonly string[] {
|
|
456
|
+
const names: string[] = [];
|
|
457
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
458
|
+
const shape = def as { readonly type?: string; readonly currency?: { readonly kind?: string } };
|
|
459
|
+
if (shape.type === "money" && shape.currency?.kind === "tenant") names.push(name);
|
|
460
|
+
}
|
|
461
|
+
return names;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
type TenantConfigValuesResponse = Readonly<
|
|
465
|
+
Record<string, { readonly value: string | number | boolean | undefined }>
|
|
466
|
+
>;
|
|
467
|
+
|
|
468
|
+
type TenantCurrencyResolution =
|
|
469
|
+
| { readonly status: "not-needed" }
|
|
470
|
+
| { readonly status: "loading" }
|
|
471
|
+
| { readonly status: "ready"; readonly currency: string };
|
|
472
|
+
|
|
473
|
+
// Resolves what a tenant-declared money field's currency should be, from the
|
|
474
|
+
// same `config:query:values` query ConfigEditBody already uses. Never falls
|
|
475
|
+
// back to "EUR" while the query is genuinely still in flight (fw#2933) — a
|
|
476
|
+
// screen with no tenant-declared money field, or a query that errored or
|
|
477
|
+
// returned no value for the key, resolves immediately instead of blocking.
|
|
478
|
+
function resolveTenantCurrency(
|
|
479
|
+
fieldNames: readonly string[],
|
|
480
|
+
query: UseQueryResult<TenantConfigValuesResponse>,
|
|
481
|
+
fallback: string,
|
|
482
|
+
): TenantCurrencyResolution {
|
|
483
|
+
if (fieldNames.length === 0) return { status: "not-needed" };
|
|
484
|
+
if (query.error) return { status: "ready", currency: fallback };
|
|
485
|
+
if (query.data === null) return { status: "loading" };
|
|
486
|
+
const raw = query.data[TENANT_CURRENCY_CONFIG_KEY]?.value;
|
|
487
|
+
return { status: "ready", currency: typeof raw === "string" && raw !== "" ? raw : fallback };
|
|
488
|
+
}
|
|
489
|
+
|
|
438
490
|
function multiSelectOptionValues(shape: {
|
|
439
491
|
readonly options?: readonly (string | { readonly value: string })[];
|
|
440
492
|
}): ReadonlySet<string> | undefined {
|
|
@@ -586,6 +638,9 @@ export type InitialValueSources = {
|
|
|
586
638
|
readonly urlPrefillFields: readonly string[] | undefined;
|
|
587
639
|
readonly renderableFields?: ReadonlySet<string>;
|
|
588
640
|
readonly defaultCurrency?: string;
|
|
641
|
+
// Per-field override of `defaultCurrency` for money fields declaring
|
|
642
|
+
// `currency: { kind: "tenant" }` (fw#2933) — see buildInitialValues.
|
|
643
|
+
readonly moneyCurrencyOverrides?: Readonly<Record<string, string>>;
|
|
589
644
|
// Drawer-kind row actions (fw#2710) prefill from the clicked row's
|
|
590
645
|
// already-typed values; wins over every other source.
|
|
591
646
|
readonly drawerOverrides?: Readonly<Record<string, unknown>>;
|
|
@@ -600,13 +655,14 @@ function coercePrefillString(
|
|
|
600
655
|
shape: PrefillFieldShape,
|
|
601
656
|
fallback: unknown,
|
|
602
657
|
defaultCurrency: string | undefined,
|
|
658
|
+
moneyCurrencyOverrides: Readonly<Record<string, string>> | undefined,
|
|
603
659
|
): unknown {
|
|
604
660
|
if (shape.type === "number") {
|
|
605
661
|
const parsed = Number(raw);
|
|
606
662
|
return Number.isNaN(parsed) ? fallback : parsed;
|
|
607
663
|
}
|
|
608
664
|
if (shape.type === "money") {
|
|
609
|
-
const coerced = coerceMoneyValue(raw, name, defaultCurrency);
|
|
665
|
+
const coerced = coerceMoneyValue(raw, name, moneyCurrencyOverrides?.[name] ?? defaultCurrency);
|
|
610
666
|
return coerced === undefined ? fallback : coerced;
|
|
611
667
|
}
|
|
612
668
|
if (shape.type === "boolean") return raw === "true";
|
|
@@ -648,10 +704,19 @@ export function mergeSearchParamsIntoInitial(
|
|
|
648
704
|
fields: Readonly<Record<string, unknown>>,
|
|
649
705
|
sources: InitialValueSources,
|
|
650
706
|
): Record<string, unknown> {
|
|
651
|
-
const {
|
|
652
|
-
|
|
707
|
+
const {
|
|
708
|
+
searchParams,
|
|
709
|
+
renderableFields,
|
|
710
|
+
defaultCurrency,
|
|
711
|
+
moneyCurrencyOverrides,
|
|
712
|
+
drawerOverrides,
|
|
713
|
+
handoffValues,
|
|
714
|
+
} = sources;
|
|
653
715
|
const urlPrefillFields = new Set(sources.urlPrefillFields ?? []);
|
|
654
|
-
const defaults = buildInitialValues(fields, defaultCurrency) as Record<
|
|
716
|
+
const defaults = buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrides) as Record<
|
|
717
|
+
string,
|
|
718
|
+
unknown
|
|
719
|
+
>;
|
|
655
720
|
const merged: Record<string, unknown> = { ...defaults };
|
|
656
721
|
for (const [name, fieldDef] of Object.entries(fields)) {
|
|
657
722
|
if (renderableFields !== undefined && !renderableFields.has(name)) continue;
|
|
@@ -670,14 +735,28 @@ export function mergeSearchParamsIntoInitial(
|
|
|
670
735
|
if (handedOff !== undefined) {
|
|
671
736
|
merged[name] =
|
|
672
737
|
typeof handedOff === "string"
|
|
673
|
-
? coercePrefillString(
|
|
738
|
+
? coercePrefillString(
|
|
739
|
+
handedOff,
|
|
740
|
+
name,
|
|
741
|
+
shape,
|
|
742
|
+
defaults[name],
|
|
743
|
+
defaultCurrency,
|
|
744
|
+
moneyCurrencyOverrides,
|
|
745
|
+
)
|
|
674
746
|
: handedOff;
|
|
675
747
|
continue;
|
|
676
748
|
}
|
|
677
749
|
if (!urlPrefillFields.has(name)) continue;
|
|
678
750
|
const raw = searchParams[name];
|
|
679
751
|
if (raw === undefined) continue;
|
|
680
|
-
merged[name] = coercePrefillString(
|
|
752
|
+
merged[name] = coercePrefillString(
|
|
753
|
+
raw,
|
|
754
|
+
name,
|
|
755
|
+
shape,
|
|
756
|
+
defaults[name],
|
|
757
|
+
defaultCurrency,
|
|
758
|
+
moneyCurrencyOverrides,
|
|
759
|
+
);
|
|
681
760
|
}
|
|
682
761
|
return merged;
|
|
683
762
|
}
|
|
@@ -761,6 +840,7 @@ function EntityEditCreateBody({
|
|
|
761
840
|
// new row.
|
|
762
841
|
readonly onSaved?: () => void;
|
|
763
842
|
}): ReactNode {
|
|
843
|
+
const { Banner } = usePrimitives();
|
|
764
844
|
const nav = useNav();
|
|
765
845
|
const handoffValues = useInitialValuesHandoff(screen.id);
|
|
766
846
|
const appFeatures = useAppFeatures();
|
|
@@ -768,16 +848,53 @@ function EntityEditCreateBody({
|
|
|
768
848
|
// A singleton stays on its own screen after saving (onSaved/refetch) —
|
|
769
849
|
// returnTo would fight that.
|
|
770
850
|
const returnTarget = screen.singleton === true ? undefined : returnTargetParam;
|
|
851
|
+
const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
852
|
+
// A money field declaring `currency: { kind: "tenant" }` (fw#2933) resolves
|
|
853
|
+
// its empty-value currency from the tenant-settings config key instead of
|
|
854
|
+
// entityDefaultCurrency — `enabled` keeps the query a no-op for entities
|
|
855
|
+
// without any such field.
|
|
856
|
+
const tenantCurrencyFieldNames = useMemo(
|
|
857
|
+
() => tenantCurrencyMoneyFieldNames(entity.fields),
|
|
858
|
+
[entity.fields],
|
|
859
|
+
);
|
|
860
|
+
const needsTenantCurrency = tenantCurrencyFieldNames.length > 0;
|
|
861
|
+
const tenantCurrencyQuery = useQuery<TenantConfigValuesResponse>(
|
|
862
|
+
"config:query:values",
|
|
863
|
+
{},
|
|
864
|
+
{ enabled: needsTenantCurrency },
|
|
865
|
+
);
|
|
866
|
+
const tenantCurrencyResolution = resolveTenantCurrency(
|
|
867
|
+
tenantCurrencyFieldNames,
|
|
868
|
+
tenantCurrencyQuery,
|
|
869
|
+
entityDefaultCurrency,
|
|
870
|
+
);
|
|
871
|
+
const resolvedTenantCurrency =
|
|
872
|
+
tenantCurrencyResolution.status === "ready" ? tenantCurrencyResolution.currency : undefined;
|
|
873
|
+
const moneyCurrencyOverrides = useMemo(
|
|
874
|
+
() =>
|
|
875
|
+
resolvedTenantCurrency !== undefined
|
|
876
|
+
? Object.fromEntries(tenantCurrencyFieldNames.map((name) => [name, resolvedTenantCurrency]))
|
|
877
|
+
: undefined,
|
|
878
|
+
[tenantCurrencyFieldNames, resolvedTenantCurrency],
|
|
879
|
+
);
|
|
771
880
|
const initial = useMemo(
|
|
772
881
|
() =>
|
|
773
882
|
mergeSearchParamsIntoInitial(entity.fields, {
|
|
774
883
|
searchParams: nav.searchParams,
|
|
775
884
|
urlPrefillFields: screen.urlPrefillFields,
|
|
776
885
|
renderableFields: layoutFieldNames(screen),
|
|
777
|
-
defaultCurrency:
|
|
886
|
+
defaultCurrency: entityDefaultCurrency,
|
|
887
|
+
...(moneyCurrencyOverrides !== undefined && { moneyCurrencyOverrides }),
|
|
778
888
|
...(handoffValues !== undefined && { handoffValues }),
|
|
779
889
|
}) as FormValues,
|
|
780
|
-
[
|
|
890
|
+
[
|
|
891
|
+
entity.fields,
|
|
892
|
+
nav.searchParams,
|
|
893
|
+
screen,
|
|
894
|
+
entityDefaultCurrency,
|
|
895
|
+
moneyCurrencyOverrides,
|
|
896
|
+
handoffValues,
|
|
897
|
+
],
|
|
781
898
|
);
|
|
782
899
|
const formSchema = useMemo(() => buildFormSchema(entity, screen), [entity, screen]);
|
|
783
900
|
const writeCommand = entityWriteCommand(schema.featureName, screen.entity, "create");
|
|
@@ -827,6 +944,15 @@ function EntityEditCreateBody({
|
|
|
827
944
|
},
|
|
828
945
|
[nav, screen.redirect, schema, appFeatures, navigateToList, onSaved, returnTarget],
|
|
829
946
|
);
|
|
947
|
+
// Never seed a tenant-declared money field with entityDefaultCurrency while
|
|
948
|
+
// its real tenant currency is still in flight (fw#2933) — wait instead.
|
|
949
|
+
if (needsTenantCurrency && tenantCurrencyResolution.status === "loading") {
|
|
950
|
+
return (
|
|
951
|
+
<Banner padded variant="loading" testId="kumiko-screen-loading">
|
|
952
|
+
Loading…
|
|
953
|
+
</Banner>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
830
956
|
// Deliberately no `actions` prop here: `screen.actions` targets an
|
|
831
957
|
// EXISTING record (publish/archive/duplicate and friends), which the
|
|
832
958
|
// create branch has none of yet — see EntityEditUpdateForm for the
|
|
@@ -958,6 +1084,7 @@ function EntityEditUpdateForm({
|
|
|
958
1084
|
readonly onSaved?: () => void;
|
|
959
1085
|
readonly onDeleted?: () => void;
|
|
960
1086
|
}): ReactNode {
|
|
1087
|
+
const { Banner } = usePrimitives();
|
|
961
1088
|
// Seed the form with the server values for the entity's declared
|
|
962
1089
|
// fields; anything else (id, tenant_id, created_at…) stays out of
|
|
963
1090
|
// the form and lives in the closure. The record's `version` is
|
|
@@ -965,15 +1092,56 @@ function EntityEditUpdateForm({
|
|
|
965
1092
|
// concurrent writer bumps it, the server returns a version-conflict
|
|
966
1093
|
// error and the user reloads.
|
|
967
1094
|
const recordVersion = (record as { version?: number }).version ?? 1;
|
|
1095
|
+
const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
1096
|
+
// A money field declaring `currency: { kind: "tenant" }` (fw#2933) resolves
|
|
1097
|
+
// its empty-value currency from the tenant-settings config key instead of
|
|
1098
|
+
// entityDefaultCurrency — `enabled` keeps the query a no-op for entities
|
|
1099
|
+
// without any such field. A record's own stored value (below) always wins
|
|
1100
|
+
// over this, regardless of the declaration.
|
|
1101
|
+
//
|
|
1102
|
+
// Only fields the record itself has no value for need the fetch at all —
|
|
1103
|
+
// an already-set tenant-declared field keeps its own stored currency and
|
|
1104
|
+
// must never block the form on this query.
|
|
1105
|
+
const tenantCurrencyFieldNames = useMemo(
|
|
1106
|
+
() =>
|
|
1107
|
+
tenantCurrencyMoneyFieldNames(entity.fields).filter(
|
|
1108
|
+
(name) => record[name] === null || record[name] === undefined,
|
|
1109
|
+
),
|
|
1110
|
+
[entity.fields, record],
|
|
1111
|
+
);
|
|
1112
|
+
const needsTenantCurrency = tenantCurrencyFieldNames.length > 0;
|
|
1113
|
+
const tenantCurrencyQuery = useQuery<TenantConfigValuesResponse>(
|
|
1114
|
+
"config:query:values",
|
|
1115
|
+
{},
|
|
1116
|
+
{ enabled: needsTenantCurrency },
|
|
1117
|
+
);
|
|
1118
|
+
const tenantCurrencyResolution = resolveTenantCurrency(
|
|
1119
|
+
tenantCurrencyFieldNames,
|
|
1120
|
+
tenantCurrencyQuery,
|
|
1121
|
+
entityDefaultCurrency,
|
|
1122
|
+
);
|
|
1123
|
+
const resolvedTenantCurrency =
|
|
1124
|
+
tenantCurrencyResolution.status === "ready" ? tenantCurrencyResolution.currency : undefined;
|
|
1125
|
+
const moneyCurrencyOverrides = useMemo(
|
|
1126
|
+
() =>
|
|
1127
|
+
resolvedTenantCurrency !== undefined
|
|
1128
|
+
? Object.fromEntries(tenantCurrencyFieldNames.map((name) => [name, resolvedTenantCurrency]))
|
|
1129
|
+
: undefined,
|
|
1130
|
+
[tenantCurrencyFieldNames, resolvedTenantCurrency],
|
|
1131
|
+
);
|
|
968
1132
|
const initial = useMemo(() => {
|
|
969
1133
|
const out: Record<string, unknown> = {};
|
|
970
|
-
const defaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
971
1134
|
for (const name of Object.keys(entity.fields)) {
|
|
972
1135
|
out[name] =
|
|
973
|
-
record[name] ??
|
|
1136
|
+
record[name] ??
|
|
1137
|
+
buildInitialValues(
|
|
1138
|
+
{ [name]: entity.fields[name] },
|
|
1139
|
+
entityDefaultCurrency,
|
|
1140
|
+
moneyCurrencyOverrides,
|
|
1141
|
+
)[name];
|
|
974
1142
|
}
|
|
975
1143
|
return out as FormValues;
|
|
976
|
-
}, [entity.fields,
|
|
1144
|
+
}, [entity.fields, entityDefaultCurrency, moneyCurrencyOverrides, record]);
|
|
977
1145
|
|
|
978
1146
|
const formSchema = useMemo(() => buildFormSchema(entity, screen), [entity, screen]);
|
|
979
1147
|
|
|
@@ -1186,6 +1354,18 @@ function EntityEditUpdateForm({
|
|
|
1186
1354
|
}
|
|
1187
1355
|
}, [dispatcher, deleteCommand, entityId, navigateToList, onDeleted, returnTarget, nav]);
|
|
1188
1356
|
|
|
1357
|
+
// Never seed a tenant-declared money field with entityDefaultCurrency while
|
|
1358
|
+
// its real tenant currency is still in flight (fw#2933) — wait instead. A
|
|
1359
|
+
// stored (non-empty) value is unaffected — it never reaches this branch's
|
|
1360
|
+
// fallback because `initial` above already prefers `record[name]`.
|
|
1361
|
+
if (needsTenantCurrency && tenantCurrencyResolution.status === "loading") {
|
|
1362
|
+
return (
|
|
1363
|
+
<Banner padded variant="loading" testId="kumiko-screen-loading">
|
|
1364
|
+
Loading…
|
|
1365
|
+
</Banner>
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1189
1369
|
return (
|
|
1190
1370
|
<>
|
|
1191
1371
|
<RenderEdit
|
|
@@ -208,7 +208,7 @@ function EditSlotMount({
|
|
|
208
208
|
wizardStep,
|
|
209
209
|
}: {
|
|
210
210
|
readonly slot: PlatformComponent;
|
|
211
|
-
readonly slotName: "header" | "footer";
|
|
211
|
+
readonly slotName: "header" | "titleAction" | "footer";
|
|
212
212
|
readonly screenId: string;
|
|
213
213
|
readonly entityName: string;
|
|
214
214
|
readonly entityId: string | null;
|
|
@@ -1088,6 +1088,18 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
1088
1088
|
) : (
|
|
1089
1089
|
headerRegion
|
|
1090
1090
|
);
|
|
1091
|
+
const titleActionSlot = screen.slots?.titleAction;
|
|
1092
|
+
const titleActionMount =
|
|
1093
|
+
titleActionSlot !== undefined ? (
|
|
1094
|
+
<EditSlotMount
|
|
1095
|
+
slot={titleActionSlot}
|
|
1096
|
+
slotName="titleAction"
|
|
1097
|
+
screenId={screen.id}
|
|
1098
|
+
entityName={vm.entityName}
|
|
1099
|
+
entityId={resolveExtensionEntityId(entityIdProp, vm.id)}
|
|
1100
|
+
values={snapshot.values}
|
|
1101
|
+
/>
|
|
1102
|
+
) : undefined;
|
|
1091
1103
|
const footerSlot = screen.slots?.footer;
|
|
1092
1104
|
// Mirrors every branch inside formActions below — without this guard
|
|
1093
1105
|
// DefaultForm renders an empty footer strip (border + padding, no content)
|
|
@@ -1199,6 +1211,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
1199
1211
|
stickyActions={isWizard}
|
|
1200
1212
|
{...(screen.layout.width !== undefined && { width: screen.layout.width })}
|
|
1201
1213
|
{...(formHeaderRegion !== undefined && { headerRegion: formHeaderRegion })}
|
|
1214
|
+
{...(titleActionMount !== undefined && { titleAction: titleActionMount })}
|
|
1202
1215
|
{...(fillHeight && { fillHeight })}
|
|
1203
1216
|
{...(hideSectionTitles === true && { chromeless: true })}
|
|
1204
1217
|
>
|
package/src/primitives.tsx
CHANGED
|
@@ -811,6 +811,10 @@ export type FormProps = {
|
|
|
811
811
|
* instead of rendering as unpadded siblings before it. Native impls may
|
|
812
812
|
* ignore this prop. */
|
|
813
813
|
readonly headerRegion?: ReactNode;
|
|
814
|
+
/** Compact content on the right of the form title, same row — status
|
|
815
|
+
* chips or allowance badges that belong to the screen, not to a field.
|
|
816
|
+
* Native impls may ignore this prop. */
|
|
817
|
+
readonly titleAction?: ReactNode;
|
|
814
818
|
/** Sizes the form to fill its container's height (instead of the page's
|
|
815
819
|
* natural content height) so a single scrolling child — a lone
|
|
816
820
|
* relatedList tab's table — can scroll internally instead of stretching
|