@cosmicdrift/kumiko-renderer 0.367.0 → 0.369.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/kumiko-screen.js +70 -12
- package/dist/primitives.d.ts +3 -0
- package/package.json +5 -5
- package/src/changes.json +6 -0
|
@@ -93,8 +93,7 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
|
|
|
93
93
|
return _jsx(ReturnHostProvider, { value: ownHost, children: body });
|
|
94
94
|
}
|
|
95
95
|
function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, children, }) {
|
|
96
|
-
const { Banner
|
|
97
|
-
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
96
|
+
const { Banner } = usePrimitives();
|
|
98
97
|
const result = useQuery(visibleWhen.query, {}, { live: true });
|
|
99
98
|
const wasShown = useRef(false);
|
|
100
99
|
const verdict = evalVisibleWhenAfterShown(visibleWhen, result, wasShown.current);
|
|
@@ -107,9 +106,19 @@ function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, c
|
|
|
107
106
|
if (verdict === "loading") {
|
|
108
107
|
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
109
108
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
if (fallback !== undefined) {
|
|
110
|
+
return _jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate });
|
|
111
|
+
}
|
|
112
|
+
return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, {}), _jsx(ScreenUnavailableBanner, {})] }));
|
|
113
|
+
}
|
|
114
|
+
// The route still names the locked screen; its breadcrumb must not label what stands in for it.
|
|
115
|
+
// With a stand-in the header is labelled with that screen instead, none if it has no title.
|
|
116
|
+
function LockedScreenHeader({ standInScreenId }) {
|
|
117
|
+
const { PageHeader } = usePrimitives();
|
|
118
|
+
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
119
|
+
if (PageHeader === undefined || !pageHeaderSlotAvailable)
|
|
120
|
+
return null;
|
|
121
|
+
return standInScreenId !== undefined ? (_jsx(PageHeader, { hideBreadcrumb: true, breadcrumbScreenId: standInScreenId })) : (_jsx(PageHeader, { hideBreadcrumb: true }));
|
|
113
122
|
}
|
|
114
123
|
function ScreenMissingHomeButton({ homeScreenId, testId, }) {
|
|
115
124
|
const { Button } = usePrimitives();
|
|
@@ -144,9 +153,10 @@ function FallbackScreen({ featureName, fallback, translate, }) {
|
|
|
144
153
|
const target = useEmbeddedScreen(featureName, fallback);
|
|
145
154
|
// Also undefined when the user lacks access to the fallback screen, which is
|
|
146
155
|
// not a framework error.
|
|
147
|
-
if (target === undefined)
|
|
148
|
-
return _jsx(ScreenUnavailableBanner, {});
|
|
149
|
-
|
|
156
|
+
if (target === undefined) {
|
|
157
|
+
return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, {}), _jsx(ScreenUnavailableBanner, {})] }));
|
|
158
|
+
}
|
|
159
|
+
return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, { standInScreenId: lastSegment(target.qn) }), _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate })] }));
|
|
150
160
|
}
|
|
151
161
|
function renderScreenBody({ schema, screen, translate, entityId, onRowClick, onCopyLink, }) {
|
|
152
162
|
switch (screen.type) {
|
|
@@ -2645,6 +2655,13 @@ function buildConfigEditDefaults(fields) {
|
|
|
2645
2655
|
}
|
|
2646
2656
|
return defaults;
|
|
2647
2657
|
}
|
|
2658
|
+
// A field hidden by its `visible` condition (a provider's settings while another
|
|
2659
|
+
// provider is picked) is not saved: the form saves what it shows.
|
|
2660
|
+
function hiddenLayoutFieldNames(screen, values) {
|
|
2661
|
+
return new Set(layoutEditFields(screen)
|
|
2662
|
+
.filter((spec) => spec.visible !== undefined && !evalFieldCondition(spec.visible, values))
|
|
2663
|
+
.map((spec) => spec.field));
|
|
2664
|
+
}
|
|
2648
2665
|
function isEmptyNumericFormValue(value) {
|
|
2649
2666
|
if (value === undefined || value === null || value === "")
|
|
2650
2667
|
return true;
|
|
@@ -2660,6 +2677,8 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2660
2677
|
// unsere Form-Field-Werte.
|
|
2661
2678
|
const valuesQuery = useQuery("config:query:values", {});
|
|
2662
2679
|
const cascadeQuery = useQuery("config:query:cascade", {});
|
|
2680
|
+
const secretKeys = screen.secretKeys;
|
|
2681
|
+
const secretsQuery = useQuery("secrets:query:list", {}, { enabled: secretKeys !== undefined });
|
|
2663
2682
|
const synthEntity = useMemo(() => synthesizeConfigEditEntity(screen.fields), [screen.fields]);
|
|
2664
2683
|
const synthScreen = useMemo(() => synthesizeConfigEditScreen(screen), [screen]);
|
|
2665
2684
|
const boundsSchema = useMemo(() => buildConfigEditSchema(screen.fields), [screen.fields]);
|
|
@@ -2671,9 +2690,17 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2671
2690
|
const initial = useMemo(() => {
|
|
2672
2691
|
if (valuesQuery.data === null)
|
|
2673
2692
|
return null;
|
|
2693
|
+
if (secretKeys !== undefined && secretsQuery.data === null)
|
|
2694
|
+
return null;
|
|
2695
|
+
const storedSecretKeys = new Set((secretsQuery.data ?? []).map((row) => row.key));
|
|
2674
2696
|
const out = {};
|
|
2675
2697
|
const defaults = buildConfigEditDefaults(screen.fields);
|
|
2676
2698
|
for (const [shortName, fieldDef] of Object.entries(screen.fields)) {
|
|
2699
|
+
const secretKey = secretKeys?.[shortName];
|
|
2700
|
+
if (secretKey !== undefined) {
|
|
2701
|
+
out[shortName] = storedSecretKeys.has(secretKey) ? true : null;
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2677
2704
|
const qualified = screen.configKeys[shortName];
|
|
2678
2705
|
if (qualified === undefined) {
|
|
2679
2706
|
// Author hat ein Field deklariert ohne Mapping — nimm Default.
|
|
@@ -2709,7 +2736,14 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2709
2736
|
}
|
|
2710
2737
|
}
|
|
2711
2738
|
return out;
|
|
2712
|
-
}, [
|
|
2739
|
+
}, [
|
|
2740
|
+
valuesQuery.data,
|
|
2741
|
+
secretsQuery.data,
|
|
2742
|
+
secretKeys,
|
|
2743
|
+
screen.fields,
|
|
2744
|
+
screen.configKeys,
|
|
2745
|
+
screen.scope,
|
|
2746
|
+
]);
|
|
2713
2747
|
// Cascade-Lookup: qualifiedKey → ConfigCascade für die
|
|
2714
2748
|
// Cascade-Anzeige unter jedem Feld. Defensive `levels`-Shape-Check
|
|
2715
2749
|
// damit fremde Query-Mocks (z.B. configEdit-Unit-Tests die nur
|
|
@@ -2739,7 +2773,23 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2739
2773
|
const customSubmit = useCallback(async (snapshot) => {
|
|
2740
2774
|
const commands = [];
|
|
2741
2775
|
const commandFieldNames = [];
|
|
2776
|
+
const hiddenFieldNames = hiddenLayoutFieldNames(synthScreen, snapshot.values);
|
|
2742
2777
|
for (const [shortName, value] of Object.entries(snapshot.changes)) {
|
|
2778
|
+
if (hiddenFieldNames.has(shortName))
|
|
2779
|
+
continue;
|
|
2780
|
+
const secretKey = secretKeys?.[shortName];
|
|
2781
|
+
if (secretKey !== undefined) {
|
|
2782
|
+
// Same wire shape as a writeOnly config field: null removes, a string replaces.
|
|
2783
|
+
if (value === null) {
|
|
2784
|
+
commandFieldNames.push(shortName);
|
|
2785
|
+
commands.push({ type: "secrets:write:delete", payload: { key: secretKey } });
|
|
2786
|
+
}
|
|
2787
|
+
else if (typeof value === "string" && value.trim() !== "") {
|
|
2788
|
+
commandFieldNames.push(shortName);
|
|
2789
|
+
commands.push({ type: "secrets:write:set", payload: { key: secretKey, value } });
|
|
2790
|
+
}
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2743
2793
|
const qualified = screen.configKeys[shortName];
|
|
2744
2794
|
if (qualified === undefined)
|
|
2745
2795
|
continue;
|
|
@@ -2811,20 +2861,28 @@ function ConfigEditBody({ schema, screen, translate, }) {
|
|
|
2811
2861
|
// bleibt die Anzeige stale bis Reload — gleiche refetch-Calls wie onReset.
|
|
2812
2862
|
// allSettled: der Write ist schon committed, ein gescheitertes Refetch
|
|
2813
2863
|
// (stale Anzeige) darf das Erfolgsergebnis nicht in einen Fehler kippen.
|
|
2814
|
-
await Promise.allSettled([
|
|
2864
|
+
await Promise.allSettled([
|
|
2865
|
+
valuesQuery.refetch?.(),
|
|
2866
|
+
cascadeQuery.refetch?.(),
|
|
2867
|
+
...(secretKeys !== undefined ? [secretsQuery.refetch?.()] : []),
|
|
2868
|
+
]);
|
|
2815
2869
|
return { validationBlocked: false, isSuccess: true, data: undefined };
|
|
2816
2870
|
}, [
|
|
2817
2871
|
dispatcher,
|
|
2872
|
+
synthScreen,
|
|
2873
|
+
secretKeys,
|
|
2818
2874
|
screen.configKeys,
|
|
2819
2875
|
screen.fields,
|
|
2820
2876
|
screen.scope,
|
|
2821
2877
|
valuesQuery.refetch,
|
|
2822
2878
|
cascadeQuery.refetch,
|
|
2879
|
+
secretsQuery.refetch,
|
|
2823
2880
|
]);
|
|
2824
2881
|
if (valuesQuery.loading && valuesQuery.data === null)
|
|
2825
2882
|
return _jsx(QueryLoadingBanner, {});
|
|
2826
|
-
|
|
2827
|
-
|
|
2883
|
+
const failedQuery = valuesQuery.error ? valuesQuery : secretsQuery.error ? secretsQuery : null;
|
|
2884
|
+
if (failedQuery?.error) {
|
|
2885
|
+
return (_jsx(QueryErrorBanner, { error: failedQuery.error, translate: effectiveTranslate, onRetry: failedQuery.refetch }));
|
|
2828
2886
|
}
|
|
2829
2887
|
if (initial === null)
|
|
2830
2888
|
return _jsx(QueryLoadingBanner, {});
|
package/dist/primitives.d.ts
CHANGED
|
@@ -1353,6 +1353,9 @@ export type PageHeaderProps = {
|
|
|
1353
1353
|
/** Hide the shell header's breadcrumb while mounted, for a screen that stands in
|
|
1354
1354
|
* for the one the route names (a locked screen's fallback). */
|
|
1355
1355
|
readonly hideBreadcrumb?: boolean;
|
|
1356
|
+
/** Label the breadcrumb with this screen (id as in the route) instead of the one
|
|
1357
|
+
* the route names, for a locked screen's fallback. Wins over `hideBreadcrumb`. */
|
|
1358
|
+
readonly breadcrumbScreenId?: string;
|
|
1356
1359
|
};
|
|
1357
1360
|
/** One item in an `ActionOverflowMenu` (A7: header/row actions beyond the
|
|
1358
1361
|
* primary one collapse into this menu instead of a wall of buttons). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.369.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.369.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.369.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.369.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.369.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.368.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "A screen that falls back via visibleWhen shows the fallback's title",
|
|
6
|
+
"detail": "When visibleWhen locks a screen and its fallback renders instead, the header no longer stays empty. It shows the title of the fallback screen (resolved like any other screen title, from its nav label), never the locked screen's. A fallback without a title still leaves the header empty. PageHeader takes a new breadcrumbScreenId prop for this."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.367.0",
|
|
4
10
|
"type": "fix",
|