@cosmicdrift/kumiko-renderer 0.355.0 → 0.356.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 +8 -43
- package/dist/components/render-edit-types.d.ts +5 -0
- package/dist/components/render-edit.js +8 -3
- package/dist/components/use-action-menu-items.d.ts +10 -0
- package/dist/components/use-action-menu-items.js +47 -0
- package/package.json +5 -5
- package/src/changes.json +5 -0
|
@@ -8,8 +8,9 @@ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "rea
|
|
|
8
8
|
import { QueryOptionLabel, resolveOptionsQueryPayload, } from "../components/query-options-select.js";
|
|
9
9
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
|
|
10
10
|
import { RenderEdit, resolveRecordTitle, } from "../components/render-edit.js";
|
|
11
|
-
import { needsActionConfirm, RenderEditActionButton,
|
|
11
|
+
import { needsActionConfirm, RenderEditActionButton, } from "../components/render-edit-action-button.js";
|
|
12
12
|
import { RenderList } from "../components/render-list.js";
|
|
13
|
+
import { useActionMenuItems } from "../components/use-action-menu-items.js";
|
|
13
14
|
import { useDispatcher, useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
14
15
|
import { useIsEmbeddedScreen } from "../context/embedded-screen-context.js";
|
|
15
16
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
@@ -877,7 +878,8 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
877
878
|
sameEntityScreenId,
|
|
878
879
|
entityId,
|
|
879
880
|
]);
|
|
880
|
-
const headerActions = useMemo(() => buildSectionActions(screen.actions ?? []), [buildSectionActions, screen.actions]);
|
|
881
|
+
const headerActions = useMemo(() => buildSectionActions((screen.actions ?? []).filter((a) => a.placement !== "menu")), [buildSectionActions, screen.actions]);
|
|
882
|
+
const headerMenuActions = useMemo(() => buildSectionActions((screen.actions ?? []).filter((a) => a.placement === "menu")), [buildSectionActions, screen.actions]);
|
|
881
883
|
const handleSubmitted = useCallback((result) => {
|
|
882
884
|
if (!result.isSuccess)
|
|
883
885
|
return;
|
|
@@ -926,7 +928,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
926
928
|
entityId: entityId,
|
|
927
929
|
// Stored customFields for the extension section, so editing shows the
|
|
928
930
|
// persisted values instead of behaving write-only.
|
|
929
|
-
extensionInitialValues: extensionInitialValues, schema: formSchema, writeCommand: writeCommand, payloadMode: "changes", buildPayload: buildPayload, serverFieldPathPrefix: `${UPDATE_CHANGES_KEY}.`, onSubmit: handleSubmitted, ...(screen.allowDelete !== false && { onDelete: handleDelete }), onCancel: handleCancel, onReload: () => void onReload(), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }), ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), ...(headerActions !== undefined && { actions: headerActions }), buildSectionActions: buildSectionActions }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
931
|
+
extensionInitialValues: extensionInitialValues, schema: formSchema, writeCommand: writeCommand, payloadMode: "changes", buildPayload: buildPayload, serverFieldPathPrefix: `${UPDATE_CHANGES_KEY}.`, onSubmit: handleSubmitted, ...(screen.allowDelete !== false && { onDelete: handleDelete }), onCancel: handleCancel, onReload: () => void onReload(), ...(screenFillsHeight(screen) && { fillScreenHeight: true }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }), ...(translate !== undefined && { translate }), ...(onCopyLink !== undefined && { onCopyLink }), ...(headerActions !== undefined && { actions: headerActions }), ...(headerMenuActions !== undefined && { menuActions: headerMenuActions }), buildSectionActions: buildSectionActions }), _jsx(DrawerHost, { schema: schema, drawerAction: drawerAction, drawerScreen: drawerScreen, ...(drawerInitialValues !== undefined && { drawerInitialValues }), userRoles: userRoles, ...(translate !== undefined && { translate }), onClose: closeDrawer, onSuccess: () => {
|
|
930
932
|
closeDrawer();
|
|
931
933
|
void onReload();
|
|
932
934
|
} })] }));
|
|
@@ -1894,47 +1896,10 @@ function primaryHeaderAction(actions) {
|
|
|
1894
1896
|
// no button of its own to carry one.
|
|
1895
1897
|
function HeaderActionsBar({ actions, Button, Dialog, ActionOverflowMenu, onError, collapseAfterPrimary = false, compact = false, PageHeader, }) {
|
|
1896
1898
|
const t = useTranslation();
|
|
1897
|
-
const
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
// same writeHandler twice.
|
|
1901
|
-
const [busyActionId, setBusyActionId] = useState(null);
|
|
1902
|
-
const trigger = async (action) => {
|
|
1903
|
-
onError(null);
|
|
1904
|
-
setBusyActionId(action.id);
|
|
1905
|
-
try {
|
|
1906
|
-
await action.onPress();
|
|
1907
|
-
}
|
|
1908
|
-
catch (e) {
|
|
1909
|
-
onError(e instanceof Error ? e.message : String(e));
|
|
1910
|
-
}
|
|
1911
|
-
finally {
|
|
1912
|
-
setBusyActionId(null);
|
|
1913
|
-
}
|
|
1914
|
-
};
|
|
1915
|
-
const toMenuItem = (action) => ({
|
|
1916
|
-
id: action.id,
|
|
1917
|
-
label: action.label,
|
|
1918
|
-
...(action.icon !== undefined && { icon: action.icon }),
|
|
1919
|
-
variant: action.style === "danger" ? "danger" : "default",
|
|
1920
|
-
...(busyActionId !== null && { disabled: true }),
|
|
1921
|
-
onSelect: () => {
|
|
1922
|
-
if (needsActionConfirm(action)) {
|
|
1923
|
-
setPendingAction(action);
|
|
1924
|
-
}
|
|
1925
|
-
else {
|
|
1926
|
-
void trigger(action);
|
|
1927
|
-
}
|
|
1928
|
-
},
|
|
1899
|
+
const { toMenuItem, confirmDialog: pendingActionDialog } = useActionMenuItems({
|
|
1900
|
+
Dialog,
|
|
1901
|
+
onError,
|
|
1929
1902
|
});
|
|
1930
|
-
const pendingActionDialog = pendingAction !== null && (_jsx(RenderEditActionConfirmDialog, { action: pendingAction, open: true, onOpenChange: (open) => {
|
|
1931
|
-
if (!open)
|
|
1932
|
-
setPendingAction(null);
|
|
1933
|
-
}, onConfirm: async () => {
|
|
1934
|
-
const action = pendingAction;
|
|
1935
|
-
setPendingAction(null);
|
|
1936
|
-
await trigger(action);
|
|
1937
|
-
}, Dialog: Dialog }));
|
|
1938
1903
|
if (compact && PageHeader !== undefined) {
|
|
1939
1904
|
const primary = primaryHeaderAction(actions);
|
|
1940
1905
|
// A bare icon for a destructive or confirm-gated action (e.g. "x" for terminate)
|
|
@@ -64,6 +64,11 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
64
64
|
* same split as `onCopyLink`) — RenderEdit only wires the button, its
|
|
65
65
|
* busy state and its confirm dialog. */
|
|
66
66
|
readonly actions?: readonly RenderEditAction[];
|
|
67
|
+
/** Record actions declared with `placement: "menu"`, resolved like `actions`.
|
|
68
|
+
* Rendered in the header "…" menu ahead of copy-link/delete; without a
|
|
69
|
+
* header menu (drawer, card form, shell without a header slot) they fall
|
|
70
|
+
* back to buttons next to `actions`. */
|
|
71
|
+
readonly menuActions?: readonly RenderEditAction[];
|
|
67
72
|
/** Opens a relatedList section row's drawer-kind action (fw#2710).
|
|
68
73
|
* RenderEdit has no `schema` to resolve the target actionForm itself —
|
|
69
74
|
* the caller (ProjectionDetailBody, which does have schema) supplies the
|
|
@@ -3,6 +3,7 @@ import { evalFieldCondition, isFieldsEditSection, normalizeEditField, PROJECTION
|
|
|
3
3
|
import { computeEditViewModel, groupIssuesByPath, relevantFieldIssues, zodErrorToFieldIssues, } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
5
5
|
import { AllFieldsRequiredProvider } from "./render-field.js";
|
|
6
|
+
import { useActionMenuItems } from "./use-action-menu-items.js";
|
|
6
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
8
|
import { ExtensionFormRegistryProvider, useExtensionFormHost, } from "../app/extension-form-submit.js";
|
|
8
9
|
import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections.js";
|
|
@@ -223,7 +224,7 @@ function resolveTitleTemplate(template, values, translate, fallback) {
|
|
|
223
224
|
return translate(template, values);
|
|
224
225
|
}
|
|
225
226
|
export function RenderEdit(props) {
|
|
226
|
-
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, onRelatedListDrawerAction, submitLabel, submitVariant, submitWithoutChanges, footerActions, summary, labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription, fieldAccent, markChangedFields = false, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, dirtyFooter, validateOnChange, headerRegion, buildSectionActions, leadContent, } = props;
|
|
227
|
+
const { screen, entity, featureName, initial, writeCommand, translate: translateProp, ctx, schema, onSubmit, payloadMode = "values", buildPayload, serverFieldPathPrefix, onDelete, onCancel, onReload, onDirtyChange, onCopyLink, actions, menuActions, onRelatedListDrawerAction, submitLabel, submitVariant, submitWithoutChanges, footerActions, summary, labelAppendix, fieldAppendix, fieldAppendixPlacement, fieldDescription, fieldAccent, markChangedFields = false, entityId: entityIdProp, extensionInitialValues, onChange, onControlsReady, fields: fieldsFilter, disabled = false, hideActions, valueDisplay = "form", hideSectionTitles, fillScreenHeight, dirtyFooter, validateOnChange, headerRegion, buildSectionActions, leadContent, } = props;
|
|
227
228
|
const i18nScreenId = props.i18nScreenId ?? screen.id;
|
|
228
229
|
const { customSubmit } = props;
|
|
229
230
|
// Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
|
|
@@ -300,6 +301,7 @@ export function RenderEdit(props) {
|
|
|
300
301
|
setHasExtensionRegistrations(hasRegistrations);
|
|
301
302
|
});
|
|
302
303
|
const { Button, Banner, Card, Dialog, Form, Section, Grid, GridCell, Text, Progress, StepBar, WizardStepGroup, Tabs, PageHeader, ActionOverflowMenu, } = usePrimitives();
|
|
304
|
+
const { toMenuItem: menuActionToItem, confirmDialog: menuActionConfirmDialog } = useActionMenuItems({ Dialog, onError: setActionError });
|
|
303
305
|
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
304
306
|
const pageHeaderCompact = usePageHeaderCompact();
|
|
305
307
|
const insideDrawer = useInsideDrawer();
|
|
@@ -1041,6 +1043,7 @@ export function RenderEdit(props) {
|
|
|
1041
1043
|
// action.
|
|
1042
1044
|
const hasSecondaryFormActions = (!menuPageHeaderActions && (onDelete !== undefined || onCopyLink !== undefined)) ||
|
|
1043
1045
|
(actions !== undefined && actions.length > 0) ||
|
|
1046
|
+
(!menuPageHeaderActions && menuActions !== undefined && menuActions.length > 0) ||
|
|
1044
1047
|
(onCancel !== undefined && !cancelInHeaderMenu);
|
|
1045
1048
|
// Teil-C collapse rule: driven by the custom `actions` group only — a
|
|
1046
1049
|
// resolved true also folds the Copy-Link button (same visual group in the
|
|
@@ -1048,6 +1051,7 @@ export function RenderEdit(props) {
|
|
|
1048
1051
|
const iconOnlyMidActions = actions !== undefined && shouldRenderActionsIconOnly(actions);
|
|
1049
1052
|
const headerMenuItems = menuPageHeaderActions
|
|
1050
1053
|
? [
|
|
1054
|
+
...(menuActions ?? []).map(menuActionToItem),
|
|
1051
1055
|
...(onCopyLink !== undefined
|
|
1052
1056
|
? [
|
|
1053
1057
|
{
|
|
@@ -1091,7 +1095,8 @@ export function RenderEdit(props) {
|
|
|
1091
1095
|
setLinkCopied(true);
|
|
1092
1096
|
}, children: iconOnlyMidActions
|
|
1093
1097
|
? null
|
|
1094
|
-
: translate(linkCopied ? "kumiko.actions.copyLinkCopied" : "kumiko.actions.copyLink") })), actions?.map((action) => (_jsx(RenderEditActionButton, { action: action, iconOnly: iconOnlyMidActions, Button: Button, Dialog: Dialog, onError: setActionError }, action.id))),
|
|
1098
|
+
: translate(linkCopied ? "kumiko.actions.copyLinkCopied" : "kumiko.actions.copyLink") })), actions?.map((action) => (_jsx(RenderEditActionButton, { action: action, iconOnly: iconOnlyMidActions, Button: Button, Dialog: Dialog, onError: setActionError }, action.id))), !menuPageHeaderActions &&
|
|
1099
|
+
menuActions?.map((action) => (_jsx(RenderEditActionButton, { action: action, Button: Button, Dialog: Dialog, onError: setActionError }, action.id))), onCancel !== undefined && !cancelInHeaderMenu && (_jsx(Button, { type: "button", variant: "secondary", onClick: () => onCancel(), testId: "render-edit-cancel", children: translate("kumiko.actions.cancel") }))] }));
|
|
1095
1100
|
const headerSlot = screen.slots?.header;
|
|
1096
1101
|
const headerSlotMount = headerSlot !== undefined ? (_jsx(EditSlotMount, { slot: headerSlot, slotName: "header", screenId: screen.id, entityName: vm.entityName, entityId: resolveExtensionEntityId(entityIdProp, vm.id), values: snapshot.values })) : undefined;
|
|
1097
1102
|
// A function headerRegion places the slot itself (projectionDetail puts it
|
|
@@ -1227,7 +1232,7 @@ export function RenderEdit(props) {
|
|
|
1227
1232
|
})
|
|
1228
1233
|
: translate("kumiko.wizard.step", { current: currentStep + 1, total: lastStepIndex + 1 });
|
|
1229
1234
|
})();
|
|
1230
|
-
return (_jsxs(ExtensionFormRegistryProvider, { value: extensionFormRegistry, children: [titleInShell && (_jsx(PageHeader, { title: formTitle, ...(recordTitle !== undefined && { recordTitle }), ...(headerMenuItems.length > 0 &&
|
|
1235
|
+
return (_jsxs(ExtensionFormRegistryProvider, { value: extensionFormRegistry, children: [menuActionConfirmDialog, titleInShell && (_jsx(PageHeader, { title: formTitle, ...(recordTitle !== undefined && { recordTitle }), ...(headerMenuItems.length > 0 &&
|
|
1231
1236
|
pageHeaderCompact && { overflowItems: headerMenuItems }), ...(headerMenuItems.length > 0 &&
|
|
1232
1237
|
!pageHeaderCompact &&
|
|
1233
1238
|
ActionOverflowMenu !== undefined && {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ActionMenuItemSpec, usePrimitives } from "../primitives.js";
|
|
3
|
+
import type { RenderEditAction } from "./render-edit-types.js";
|
|
4
|
+
export declare function useActionMenuItems({ Dialog, onError, }: {
|
|
5
|
+
readonly Dialog: ReturnType<typeof usePrimitives>["Dialog"];
|
|
6
|
+
readonly onError: (text: string | null) => void;
|
|
7
|
+
}): {
|
|
8
|
+
readonly toMenuItem: (action: RenderEditAction) => ActionMenuItemSpec;
|
|
9
|
+
readonly confirmDialog: ReactNode;
|
|
10
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { needsActionConfirm, RenderEditActionConfirmDialog } from "./render-edit-action-button.js";
|
|
4
|
+
// A menu item has no button of its own to carry RenderEditActionButton's busy
|
|
5
|
+
// and confirm state, so this hook owns both for every item it creates.
|
|
6
|
+
export function useActionMenuItems({ Dialog, onError, }) {
|
|
7
|
+
const [pendingAction, setPendingAction] = useState(null);
|
|
8
|
+
// Without it the menu can be reopened mid-write and fire the same writeHandler twice.
|
|
9
|
+
const [busyActionId, setBusyActionId] = useState(null);
|
|
10
|
+
const trigger = async (action) => {
|
|
11
|
+
onError(null);
|
|
12
|
+
setBusyActionId(action.id);
|
|
13
|
+
try {
|
|
14
|
+
await action.onPress();
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
onError(e instanceof Error ? e.message : String(e));
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
setBusyActionId(null);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const toMenuItem = (action) => ({
|
|
24
|
+
id: action.id,
|
|
25
|
+
label: action.label,
|
|
26
|
+
...(action.icon !== undefined && { icon: action.icon }),
|
|
27
|
+
variant: action.style === "danger" ? "danger" : "default",
|
|
28
|
+
...(busyActionId !== null && { disabled: true }),
|
|
29
|
+
onSelect: () => {
|
|
30
|
+
if (needsActionConfirm(action)) {
|
|
31
|
+
setPendingAction(action);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
void trigger(action);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const confirmDialog = pendingAction !== null && (_jsx(RenderEditActionConfirmDialog, { action: pendingAction, open: true, onOpenChange: (open) => {
|
|
39
|
+
if (!open)
|
|
40
|
+
setPendingAction(null);
|
|
41
|
+
}, onConfirm: async () => {
|
|
42
|
+
const action = pendingAction;
|
|
43
|
+
setPendingAction(null);
|
|
44
|
+
await trigger(action);
|
|
45
|
+
}, Dialog: Dialog }));
|
|
46
|
+
return { toMenuItem, confirmDialog };
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.356.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.356.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.356.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.356.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.356.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|