@cosmicdrift/kumiko-renderer 0.307.0 → 0.308.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 -5
- package/src/app/form-schema.ts +1 -1
- package/src/app/kumiko-screen.tsx +91 -259
- package/src/app/row-actions.ts +215 -56
- package/src/changes.json +7 -0
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +143 -0
- package/src/components/__tests__/related-list-section.test.tsx +2 -2
- package/src/components/__tests__/render-field-number-grouping.test.tsx +76 -0
- package/src/components/__tests__/write-form-section.test.tsx +54 -1
- package/src/components/related-list-section.tsx +94 -15
- package/src/components/render-edit-types.ts +13 -2
- package/src/components/render-edit.tsx +147 -36
- package/src/components/render-field.tsx +3 -1
- package/src/components/render-list.tsx +6 -4
- package/src/components/write-form-section.tsx +37 -4
- package/src/primitives.tsx +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.308.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.308.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.308.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.308.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.308.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/app/form-schema.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
6
|
import { evalFieldCondition, NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
7
|
import { I18N_KEY_PARAM } from "@cosmicdrift/kumiko-headless";
|
|
8
|
-
import
|
|
8
|
+
import * as z from "zod";
|
|
9
9
|
import { layoutEditFields } from "./layout-fields";
|
|
10
10
|
|
|
11
11
|
// `required` means "has a value", not "is truthy" — `false` and `0` count
|
|
@@ -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 { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
35
36
|
import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
36
37
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
37
38
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog";
|
|
@@ -99,10 +100,10 @@ import {
|
|
|
99
100
|
buildDefaultEditRowAction,
|
|
100
101
|
buildProjectionRowActions,
|
|
101
102
|
buildProjectionToolbarActions,
|
|
103
|
+
buildRecordActions,
|
|
102
104
|
evalRowExtractor,
|
|
103
105
|
isWriteHandlerRowAction,
|
|
104
106
|
refetchAfterWrite,
|
|
105
|
-
resolveActionIcon,
|
|
106
107
|
runProjectionRowNavigate,
|
|
107
108
|
stringifyNavParams,
|
|
108
109
|
} from "./row-actions";
|
|
@@ -1218,127 +1219,25 @@ function EntityEditUpdateForm({
|
|
|
1218
1219
|
// pattern as ProjectionDetailBody.headerActions above (the edited record
|
|
1219
1220
|
// stands in for the "row"), minus the cross-feature defaultEditAction
|
|
1220
1221
|
// lookup that doesn't apply here (this screen already IS the edit form).
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
id: action.id,
|
|
1241
|
-
label: effectiveTranslate(action.label),
|
|
1242
|
-
...(action.style !== undefined && { style: action.style }),
|
|
1243
|
-
confirmRequired: false,
|
|
1244
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
1245
|
-
onPress: () => {
|
|
1246
|
-
if (id === "") return;
|
|
1247
|
-
nav.navigate({ entity: targetEntity, id });
|
|
1248
|
-
runParams();
|
|
1249
|
-
},
|
|
1250
|
-
});
|
|
1251
|
-
} else if (action.screen !== undefined) {
|
|
1252
|
-
// Default entityId for a screen-target: the currently edited
|
|
1253
|
-
// record's own id — an entityEdit header action has no other
|
|
1254
|
-
// "row" to derive one from (unlike ProjectionDetailBody, which
|
|
1255
|
-
// needs the same-entity detailFor lookup instead).
|
|
1256
|
-
const explicit =
|
|
1257
|
-
action.entityId !== undefined ? String(record[action.entityId] ?? "") : undefined;
|
|
1258
|
-
const navEntityId = explicit ?? entityId;
|
|
1259
|
-
const targetScreen = action.screen;
|
|
1260
|
-
out.push({
|
|
1261
|
-
id: action.id,
|
|
1262
|
-
label: effectiveTranslate(action.label),
|
|
1263
|
-
...(action.style !== undefined && { style: action.style }),
|
|
1264
|
-
confirmRequired: false,
|
|
1265
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
1266
|
-
onPress: () => {
|
|
1267
|
-
const target: ScreenTarget = {
|
|
1268
|
-
screenId: targetScreen,
|
|
1269
|
-
...(navEntityId !== undefined && navEntityId !== "" && { entityId: navEntityId }),
|
|
1270
|
-
};
|
|
1271
|
-
const params =
|
|
1272
|
-
action.params !== undefined
|
|
1273
|
-
? stringifyNavParams(evalRowExtractor(action.params, record))
|
|
1274
|
-
: undefined;
|
|
1275
|
-
navigateWithReturnTo(nav, target, host, params);
|
|
1276
|
-
},
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
if (action.kind === "drawer") {
|
|
1282
|
-
const drawerActionEntry = action;
|
|
1283
|
-
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
1284
|
-
out.push({
|
|
1285
|
-
id: action.id,
|
|
1286
|
-
label: effectiveTranslate(action.label),
|
|
1287
|
-
...(action.style !== undefined && { style: action.style }),
|
|
1288
|
-
confirmRequired: false,
|
|
1289
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
1290
|
-
onPress: () => {
|
|
1291
|
-
const initialValues =
|
|
1292
|
-
drawerActionEntry.params !== undefined
|
|
1293
|
-
? evalRowExtractor(drawerActionEntry.params, record)
|
|
1294
|
-
: undefined;
|
|
1295
|
-
openDrawer(drawerActionEntry, initialValues);
|
|
1296
|
-
},
|
|
1297
|
-
});
|
|
1298
|
-
continue;
|
|
1299
|
-
}
|
|
1300
|
-
// writeHandler — same dispatch/reload/failure-surfacing pattern as
|
|
1301
|
-
// ProjectionDetailBody's headerActions above.
|
|
1302
|
-
const writeAction = action;
|
|
1303
|
-
out.push({
|
|
1304
|
-
id: writeAction.id,
|
|
1305
|
-
label: effectiveTranslate(writeAction.label),
|
|
1306
|
-
...(writeAction.style !== undefined && { style: writeAction.style }),
|
|
1307
|
-
icon: resolveActionIcon(writeAction.id, writeAction.icon),
|
|
1308
|
-
...(writeAction.confirm !== undefined && {
|
|
1309
|
-
confirm: effectiveTranslate(writeAction.confirm),
|
|
1310
|
-
}),
|
|
1311
|
-
...(writeAction.confirmLabel !== undefined && {
|
|
1312
|
-
confirmLabel: effectiveTranslate(writeAction.confirmLabel),
|
|
1313
|
-
}),
|
|
1314
|
-
onPress: async () => {
|
|
1315
|
-
const payload =
|
|
1316
|
-
writeAction.payload !== undefined
|
|
1317
|
-
? evalRowExtractor(writeAction.payload, record)
|
|
1318
|
-
: { id: entityId };
|
|
1319
|
-
const result = await dispatcher.write(writeAction.handler, payload);
|
|
1320
|
-
if (!result.isSuccess) {
|
|
1321
|
-
throw new WriteFailedError(
|
|
1322
|
-
result.error,
|
|
1323
|
-
dispatcherErrorText(result.error, effectiveTranslate),
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1326
|
-
await onReload();
|
|
1327
|
-
},
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
return out.length > 0 ? out : undefined;
|
|
1331
|
-
}, [
|
|
1332
|
-
screen.actions,
|
|
1333
|
-
effectiveTranslate,
|
|
1334
|
-
nav,
|
|
1335
|
-
dispatcher,
|
|
1336
|
-
record,
|
|
1337
|
-
entityId,
|
|
1338
|
-
onReload,
|
|
1339
|
-
openDrawer,
|
|
1340
|
-
host,
|
|
1341
|
-
]);
|
|
1222
|
+
const buildSectionActions = useCallback(
|
|
1223
|
+
(actions: readonly RowAction[]): readonly RenderEditAction[] | undefined =>
|
|
1224
|
+
buildRecordActions({
|
|
1225
|
+
actions,
|
|
1226
|
+
record,
|
|
1227
|
+
translate: effectiveTranslate,
|
|
1228
|
+
nav,
|
|
1229
|
+
host,
|
|
1230
|
+
dispatcher,
|
|
1231
|
+
openDrawer,
|
|
1232
|
+
onWriteSuccess: onReload,
|
|
1233
|
+
defaultScreenTargetEntityId: entityId,
|
|
1234
|
+
}),
|
|
1235
|
+
[record, effectiveTranslate, nav, host, dispatcher, openDrawer, onReload, entityId],
|
|
1236
|
+
);
|
|
1237
|
+
const headerActions = useMemo(
|
|
1238
|
+
(): readonly RenderEditAction[] | undefined => buildSectionActions(screen.actions ?? []),
|
|
1239
|
+
[buildSectionActions, screen.actions],
|
|
1240
|
+
);
|
|
1342
1241
|
const handleSubmitted = useCallback(
|
|
1343
1242
|
(result: SubmitResult<unknown>) => {
|
|
1344
1243
|
if (!result.isSuccess) return;
|
|
@@ -1429,6 +1328,7 @@ function EntityEditUpdateForm({
|
|
|
1429
1328
|
{...(translate !== undefined && { translate })}
|
|
1430
1329
|
{...(onCopyLink !== undefined && { onCopyLink })}
|
|
1431
1330
|
{...(headerActions !== undefined && { actions: headerActions })}
|
|
1331
|
+
buildSectionActions={buildSectionActions}
|
|
1432
1332
|
/>
|
|
1433
1333
|
<DrawerHost
|
|
1434
1334
|
schema={schema}
|
|
@@ -2954,144 +2854,69 @@ function ProjectionDetailBody({
|
|
|
2954
2854
|
};
|
|
2955
2855
|
}, [editScreen, effectiveTranslate, nav, effectiveEntityId, host]);
|
|
2956
2856
|
|
|
2857
|
+
// a navigate-screen action targeting an entityEdit screen for the
|
|
2858
|
+
// SAME entity this projectionDetail shows (screen.detailFor) defaults its
|
|
2859
|
+
// entityId to the shown record's own id — without this fallback the target
|
|
2860
|
+
// opens an empty create-form instead of the shown record, silently.
|
|
2861
|
+
// Searched cross-feature, consistent with editScreen above.
|
|
2862
|
+
const sameEntityScreenId = useCallback(
|
|
2863
|
+
(targetScreen: string): string | undefined => {
|
|
2864
|
+
const record = detailQuery.data ?? {};
|
|
2865
|
+
const targetIsEntityEditSameEntity =
|
|
2866
|
+
screen.detailFor !== undefined &&
|
|
2867
|
+
appFeatures.some((feature) =>
|
|
2868
|
+
feature.screens.some(
|
|
2869
|
+
(s) =>
|
|
2870
|
+
s.type === "entityEdit" &&
|
|
2871
|
+
s.entity === screen.detailFor &&
|
|
2872
|
+
lastSegment(s.id) === targetScreen,
|
|
2873
|
+
),
|
|
2874
|
+
);
|
|
2875
|
+
return targetIsEntityEditSameEntity ? String(record["id"] ?? "") : undefined;
|
|
2876
|
+
},
|
|
2877
|
+
[detailQuery.data, screen.detailFor, appFeatures],
|
|
2878
|
+
);
|
|
2879
|
+
const buildSectionActions = useCallback(
|
|
2880
|
+
(actions: readonly RowAction[]): readonly RenderEditAction[] | undefined =>
|
|
2881
|
+
buildRecordActions({
|
|
2882
|
+
actions,
|
|
2883
|
+
record: detailQuery.data ?? {},
|
|
2884
|
+
translate: effectiveTranslate,
|
|
2885
|
+
nav,
|
|
2886
|
+
host,
|
|
2887
|
+
dispatcher,
|
|
2888
|
+
openDrawer,
|
|
2889
|
+
onWriteSuccess: detailQuery.refetch,
|
|
2890
|
+
sameEntityScreenId,
|
|
2891
|
+
}),
|
|
2892
|
+
[
|
|
2893
|
+
detailQuery.data,
|
|
2894
|
+
detailQuery.refetch,
|
|
2895
|
+
effectiveTranslate,
|
|
2896
|
+
nav,
|
|
2897
|
+
host,
|
|
2898
|
+
dispatcher,
|
|
2899
|
+
openDrawer,
|
|
2900
|
+
sameEntityScreenId,
|
|
2901
|
+
],
|
|
2902
|
+
);
|
|
2957
2903
|
const headerActions = useMemo((): readonly RenderEditAction[] | undefined => {
|
|
2958
|
-
const record = detailQuery.data ?? {};
|
|
2959
2904
|
const declaredHasEdit = screen.actions?.some((a) => a.id === "edit") === true;
|
|
2960
|
-
const
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
nav.setSearchParams(stringifyNavParams(params));
|
|
2974
|
-
}
|
|
2975
|
-
};
|
|
2976
|
-
if (action.entity !== undefined) {
|
|
2977
|
-
// Entity-Targets (fw#2228) — Auflösung passiert in der NavApi-Impl
|
|
2978
|
-
// (siehe EntityListBody.runNavigate), nicht hier. KEIN record["id"]-
|
|
2979
|
-
// Fallback: record kommt aus einer beliebigen Detail-Query ohne
|
|
2980
|
-
// garantiertes "id"-Feld, der Boot-Validator erzwingt deshalb einen
|
|
2981
|
-
// expliziten entityId für projectionDetail-entity-Targets.
|
|
2982
|
-
const targetEntity = action.entity;
|
|
2983
|
-
const id = action.entityId !== undefined ? String(record[action.entityId] ?? "") : "";
|
|
2984
|
-
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
2985
|
-
out.push({
|
|
2986
|
-
id: action.id,
|
|
2987
|
-
label: effectiveTranslate(action.label),
|
|
2988
|
-
...(action.style !== undefined && { style: action.style }),
|
|
2989
|
-
confirmRequired: false,
|
|
2990
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
2991
|
-
onPress: () => {
|
|
2992
|
-
if (id === "") return;
|
|
2993
|
-
nav.navigate({ entity: targetEntity, id });
|
|
2994
|
-
runParams();
|
|
2995
|
-
},
|
|
2996
|
-
});
|
|
2997
|
-
} else if (action.screen !== undefined) {
|
|
2998
|
-
// Default entityId for an entityEdit target of the SAME entity
|
|
2999
|
-
// (screen.detailFor plays entity's role here, like screen.entity
|
|
3000
|
-
// does for entityList's runNavigate) — without this fallback the
|
|
3001
|
-
// target opens an empty create-form instead of the shown record,
|
|
3002
|
-
// silently. Searched cross-feature, consistent with editScreen above.
|
|
3003
|
-
const explicit =
|
|
3004
|
-
action.entityId !== undefined ? String(record[action.entityId] ?? "") : undefined;
|
|
3005
|
-
const targetIsEntityEditSameEntity =
|
|
3006
|
-
screen.detailFor !== undefined &&
|
|
3007
|
-
appFeatures.some((feature) =>
|
|
3008
|
-
feature.screens.some(
|
|
3009
|
-
(s) =>
|
|
3010
|
-
s.type === "entityEdit" &&
|
|
3011
|
-
s.entity === screen.detailFor &&
|
|
3012
|
-
lastSegment(s.id) === action.screen,
|
|
3013
|
-
),
|
|
3014
|
-
);
|
|
3015
|
-
const fallback = targetIsEntityEditSameEntity ? String(record["id"] ?? "") : undefined;
|
|
3016
|
-
const navEntityId = explicit ?? fallback;
|
|
3017
|
-
const targetScreen = action.screen;
|
|
3018
|
-
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
3019
|
-
out.push({
|
|
3020
|
-
id: action.id,
|
|
3021
|
-
label: effectiveTranslate(action.label),
|
|
3022
|
-
...(action.style !== undefined && { style: action.style }),
|
|
3023
|
-
confirmRequired: false,
|
|
3024
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
3025
|
-
onPress: () => {
|
|
3026
|
-
const target: ScreenTarget = {
|
|
3027
|
-
screenId: targetScreen,
|
|
3028
|
-
...(navEntityId !== undefined && navEntityId !== "" && { entityId: navEntityId }),
|
|
3029
|
-
};
|
|
3030
|
-
const params =
|
|
3031
|
-
action.params !== undefined
|
|
3032
|
-
? stringifyNavParams(evalRowExtractor(action.params, record))
|
|
3033
|
-
: undefined;
|
|
3034
|
-
navigateWithReturnTo(nav, target, host, params);
|
|
3035
|
-
},
|
|
3036
|
-
});
|
|
3037
|
-
}
|
|
3038
|
-
continue;
|
|
3039
|
-
}
|
|
3040
|
-
if (action.kind === "drawer") {
|
|
3041
|
-
const drawerActionEntry = action;
|
|
3042
|
-
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
3043
|
-
out.push({
|
|
3044
|
-
id: action.id,
|
|
3045
|
-
label: effectiveTranslate(action.label),
|
|
3046
|
-
...(action.style !== undefined && { style: action.style }),
|
|
3047
|
-
confirmRequired: false,
|
|
3048
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
3049
|
-
onPress: () => {
|
|
3050
|
-
const initialValues =
|
|
3051
|
-
drawerActionEntry.params !== undefined
|
|
3052
|
-
? evalRowExtractor(drawerActionEntry.params, record)
|
|
3053
|
-
: undefined;
|
|
3054
|
-
openDrawer(drawerActionEntry, initialValues);
|
|
3055
|
-
},
|
|
3056
|
-
});
|
|
3057
|
-
continue;
|
|
3058
|
-
}
|
|
3059
|
-
// writeHandler — same dispatch/refetch/failure-surfacing pattern as
|
|
3060
|
-
// ProjectionListBody's rowActions/toolbarActions above.
|
|
3061
|
-
if (dispatcher === undefined) continue;
|
|
3062
|
-
const writeAction = action;
|
|
3063
|
-
out.push({
|
|
3064
|
-
id: writeAction.id,
|
|
3065
|
-
label: effectiveTranslate(writeAction.label),
|
|
3066
|
-
...(writeAction.style !== undefined && { style: writeAction.style }),
|
|
3067
|
-
icon: resolveActionIcon(writeAction.id, writeAction.icon),
|
|
3068
|
-
...(writeAction.confirm !== undefined && {
|
|
3069
|
-
confirm: effectiveTranslate(writeAction.confirm),
|
|
3070
|
-
}),
|
|
3071
|
-
...(writeAction.confirmLabel !== undefined && {
|
|
3072
|
-
confirmLabel: effectiveTranslate(writeAction.confirmLabel),
|
|
3073
|
-
}),
|
|
3074
|
-
onPress: async () => {
|
|
3075
|
-
const payload =
|
|
3076
|
-
writeAction.payload !== undefined
|
|
3077
|
-
? evalRowExtractor(writeAction.payload, record)
|
|
3078
|
-
: { id: record["id"] };
|
|
3079
|
-
const result = await dispatcher.write(writeAction.handler, payload);
|
|
3080
|
-
if (!result.isSuccess) {
|
|
3081
|
-
throw new WriteFailedError(
|
|
3082
|
-
result.error,
|
|
3083
|
-
dispatcherErrorText(result.error, effectiveTranslate),
|
|
3084
|
-
);
|
|
3085
|
-
}
|
|
3086
|
-
await detailQuery.refetch();
|
|
3087
|
-
},
|
|
3088
|
-
});
|
|
3089
|
-
}
|
|
3090
|
-
return out.length > 0 ? out : undefined;
|
|
2905
|
+
const resolved = buildRecordActions({
|
|
2906
|
+
actions: screen.actions ?? [],
|
|
2907
|
+
record: detailQuery.data ?? {},
|
|
2908
|
+
translate: effectiveTranslate,
|
|
2909
|
+
nav,
|
|
2910
|
+
host,
|
|
2911
|
+
dispatcher,
|
|
2912
|
+
openDrawer,
|
|
2913
|
+
onWriteSuccess: detailQuery.refetch,
|
|
2914
|
+
sameEntityScreenId,
|
|
2915
|
+
});
|
|
2916
|
+
if (defaultEditAction === undefined || declaredHasEdit) return resolved;
|
|
2917
|
+
return [defaultEditAction, ...(resolved ?? [])];
|
|
3091
2918
|
}, [
|
|
3092
2919
|
screen.actions,
|
|
3093
|
-
screen.detailFor,
|
|
3094
|
-
appFeatures,
|
|
3095
2920
|
defaultEditAction,
|
|
3096
2921
|
effectiveTranslate,
|
|
3097
2922
|
nav,
|
|
@@ -3100,6 +2925,7 @@ function ProjectionDetailBody({
|
|
|
3100
2925
|
detailQuery.data,
|
|
3101
2926
|
detailQuery.refetch,
|
|
3102
2927
|
openDrawer,
|
|
2928
|
+
sameEntityScreenId,
|
|
3103
2929
|
]);
|
|
3104
2930
|
|
|
3105
2931
|
if (effectiveEntityId === undefined && screen.singleton !== true) {
|
|
@@ -3205,13 +3031,16 @@ function ProjectionDetailBody({
|
|
|
3205
3031
|
</Text>
|
|
3206
3032
|
)
|
|
3207
3033
|
) : undefined;
|
|
3208
|
-
|
|
3034
|
+
// slots.header shares the header card with the actions (one card, actions
|
|
3035
|
+
// top right) instead of rendering as an unframed block above it.
|
|
3036
|
+
const renderHeaderContent = (headerSlot: ReactNode | undefined): ReactNode => (
|
|
3209
3037
|
<>
|
|
3210
|
-
{hasHeaderCard && (
|
|
3038
|
+
{(hasHeaderCard || headerSlot !== undefined) && (
|
|
3211
3039
|
<Card
|
|
3212
3040
|
slots={{
|
|
3213
3041
|
...(headerTitleSlot !== undefined && { title: headerTitleSlot }),
|
|
3214
3042
|
...(headerSubtitleSlot !== undefined && { subtitle: headerSubtitleSlot }),
|
|
3043
|
+
...(headerSlot !== undefined && { headerContent: headerSlot }),
|
|
3215
3044
|
...(hasHeaderActions && { headerActions: headerActionsContent }),
|
|
3216
3045
|
}}
|
|
3217
3046
|
>
|
|
@@ -3293,7 +3122,10 @@ function ProjectionDetailBody({
|
|
|
3293
3122
|
onRelatedListDrawerAction={openDrawer}
|
|
3294
3123
|
{...(translate !== undefined && { translate })}
|
|
3295
3124
|
{...(hasTabs && { hideSectionTitles: true })}
|
|
3296
|
-
{...((hasHeaderCard || hasTabs) && {
|
|
3125
|
+
{...((hasHeaderCard || hasTabs || screen.slots?.header !== undefined) && {
|
|
3126
|
+
headerRegion: renderHeaderContent,
|
|
3127
|
+
})}
|
|
3128
|
+
buildSectionActions={buildSectionActions}
|
|
3297
3129
|
valueDisplay={screen.valueDisplay ?? "text"}
|
|
3298
3130
|
/>
|
|
3299
3131
|
<DrawerHost
|