@cosmicdrift/kumiko-renderer 0.373.0 → 0.374.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/action-form-support.d.ts +51 -0
- package/dist/app/action-form-support.js +74 -0
- package/dist/app/client-actions.d.ts +46 -0
- package/dist/app/client-actions.js +60 -0
- package/dist/app/entity-list-expanded-row.js +4 -0
- package/dist/app/header-actions-bar.d.ts +19 -0
- package/dist/app/header-actions-bar.js +30 -0
- package/dist/app/kumiko-screen.js +129 -62
- package/dist/app/list-empty-content.d.ts +1 -1
- package/dist/app/list-empty-content.js +3 -2
- package/dist/app/row-actions.d.ts +9 -1
- package/dist/app/row-actions.js +85 -6
- package/dist/app/use-record-actions.d.ts +12 -0
- package/dist/app/use-record-actions.js +30 -0
- package/dist/components/grid-cell-for-field.d.ts +3 -1
- package/dist/components/grid-cell-for-field.js +2 -2
- package/dist/components/related-list-section.js +16 -3
- package/dist/components/render-edit.js +1 -1
- package/dist/components/render-field.d.ts +4 -1
- package/dist/components/render-field.js +99 -8
- package/dist/i18n-defaults.js +7 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/primitives.d.ts +18 -0
- package/package.json +5 -5
- package/src/changes.json +24 -0
|
@@ -9,9 +9,7 @@ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "rea
|
|
|
9
9
|
import { QueryOptionLabel, resolveOptionsQueryPayload, } from "../components/query-options-select.js";
|
|
10
10
|
import { extractCreatedId, extractIdField } from "../components/reference-create-dialog.js";
|
|
11
11
|
import { RenderEdit, resolveRecordTitle, } from "../components/render-edit.js";
|
|
12
|
-
import { RenderEditActionButton } from "../components/render-edit-action-button.js";
|
|
13
12
|
import { RenderList } from "../components/render-list.js";
|
|
14
|
-
import { useActionMenuItems } from "../components/use-action-menu-items.js";
|
|
15
13
|
import { useDispatcher, useOptionalDispatcher } from "../context/dispatcher-context.js";
|
|
16
14
|
import { useIsEmbeddedScreen } from "../context/embedded-screen-context.js";
|
|
17
15
|
import { useUserRoles } from "../context/user-roles-context.js";
|
|
@@ -26,7 +24,9 @@ import { PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlo
|
|
|
26
24
|
import { statusToneForOptionTone, statusToneForValue, usePrimitives, } from "../primitives.js";
|
|
27
25
|
import { screenFillsHeight } from "../screen-fills-height.js";
|
|
28
26
|
import { actionFormSubmittableFields, synthesizeActionFormEntity, synthesizeActionFormScreen, } from "./action-form-shim.js";
|
|
27
|
+
import { ActionFormLoadStatus, ActionFormSuccessBanner, isSecretFormField, resolveActionFormRecordId, useActionFormLoad, } from "./action-form-support.js";
|
|
29
28
|
import { useAppFeatures } from "./app-features-context.js";
|
|
29
|
+
import { useClientActionRuntime } from "./client-actions.js";
|
|
30
30
|
import { buildConfigEditSchema, synthesizeConfigEditEntity, synthesizeConfigEditScreen, } from "./config-edit-shim.js";
|
|
31
31
|
import { useCustomScreenComponent } from "./custom-screens.js";
|
|
32
32
|
import { useDashboardBody } from "./dashboard-body.js";
|
|
@@ -34,7 +34,7 @@ import { applyEditPrefill } from "./edit-prefill.js";
|
|
|
34
34
|
import { EntityListExpandedRow } from "./entity-list-expanded-row.js";
|
|
35
35
|
import { FacetCountBridges } from "./facet-count-bridge.js";
|
|
36
36
|
import { buildFormSchema } from "./form-schema.js";
|
|
37
|
-
import {
|
|
37
|
+
import { HeaderActionsBar } from "./header-actions-bar.js";
|
|
38
38
|
import { hiddenRequiredFieldsFilled, layoutEditFields, layoutFieldNames } from "./layout-fields.js";
|
|
39
39
|
import { isListNarrowed, useListEmptyContent } from "./list-empty-content.js";
|
|
40
40
|
import { buildDateRangePayload, buildEntityFilterFacets, buildFacetCountRequests, buildFilterFacets, buildFilterPayload, clampDateRange, mergeReferenceFacetOptions, normalizeDefaultFilters, readDateRange, resolveDateRangeFacets, resolveProjectionFacetSpecs, } from "./list-facets.js";
|
|
@@ -46,7 +46,7 @@ import { featureNameFromQualifiedScreenId, qualifyScreenId } from "./qualify-scr
|
|
|
46
46
|
import { QueryErrorBanner, QueryLoadingBanner } from "./query-state-banners.js";
|
|
47
47
|
import { ReferenceFacetBridges } from "./reference-facet-bridge.js";
|
|
48
48
|
import { navigateToReturn, navigateToReturnOr, navigateWithReturnTo, ReturnHostProvider, useReturnHost, useReturnTarget, } from "./return-to.js";
|
|
49
|
-
import { buildDefaultEditRowAction, buildDrawerRowAction, buildProjectionRowActions, buildProjectionToolbarActions, buildRecordActions, evalRowExtractor,
|
|
49
|
+
import { buildClientActionRowAction, buildDefaultEditRowAction, buildDrawerRowAction, buildProjectionRowActions, buildProjectionToolbarActions, buildRecordActions, evalRowExtractor, findRowClickAction, navigateActionSearchParams, refetchAfterWrite, runDrawerRowAction, runProjectionRowNavigate, } from "./row-actions.js";
|
|
50
50
|
import { findEditScreenFor, navigateTargetAllows, screenAccessAllows } from "./screen-access.js";
|
|
51
51
|
import { SecretMintBody } from "./secret-mint-body.js";
|
|
52
52
|
import { SecretsEditBody } from "./secrets-edit-body.js";
|
|
@@ -174,7 +174,7 @@ function renderScreenBody({ schema, screen, translate, entityId, onRowClick, onC
|
|
|
174
174
|
case "dashboard":
|
|
175
175
|
return (_jsx(DashboardScreenBody, { featureName: schema.featureName, screen: screen, translate: translate }));
|
|
176
176
|
case "actionForm":
|
|
177
|
-
return _jsx(ActionFormBody, { schema: schema, screen: screen, translate: translate });
|
|
177
|
+
return (_jsx(ActionFormBody, { schema: schema, screen: screen, translate: translate, ...(entityId !== undefined && { entityId }) }));
|
|
178
178
|
case "secretMint":
|
|
179
179
|
return _jsx(SecretMintBody, { schema: schema, screen: screen, translate: translate });
|
|
180
180
|
case "configEdit":
|
|
@@ -891,6 +891,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
891
891
|
const nav = useNav();
|
|
892
892
|
const appFeatures = useAppFeatures();
|
|
893
893
|
const dispatcher = useDispatcher();
|
|
894
|
+
const clientActions = useClientActionRuntime();
|
|
894
895
|
const t = useTranslation();
|
|
895
896
|
const effectiveTranslate = translate ?? t;
|
|
896
897
|
const navigateToList = useNavigateToListAfter(schema, screen.entity);
|
|
@@ -954,6 +955,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
954
955
|
onWriteSuccess: onReload,
|
|
955
956
|
onRecordLeft: handleRecordLeft,
|
|
956
957
|
sameEntityScreenId,
|
|
958
|
+
clientActions,
|
|
957
959
|
defaultWritePayloadId: entityId,
|
|
958
960
|
...(formDirty && {
|
|
959
961
|
discardChanges: {
|
|
@@ -972,6 +974,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
972
974
|
onReload,
|
|
973
975
|
handleRecordLeft,
|
|
974
976
|
sameEntityScreenId,
|
|
977
|
+
clientActions,
|
|
975
978
|
entityId,
|
|
976
979
|
]);
|
|
977
980
|
const headerActions = useMemo(() => buildSectionActions((screen.actions ?? []).filter((a) => a.placement !== "menu")), [buildSectionActions, screen.actions]);
|
|
@@ -1428,6 +1431,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1428
1431
|
// (sonst rendert die Action-Spalte still nichts und der "warum sind
|
|
1429
1432
|
// meine Buttons weg?"-Debug ist teuer).
|
|
1430
1433
|
const dispatcher = useOptionalDispatcher();
|
|
1434
|
+
const clientActions = useClientActionRuntime();
|
|
1431
1435
|
const hasRowActions = screen.rowActions !== undefined && screen.rowActions.length > 0;
|
|
1432
1436
|
useEffect(() => {
|
|
1433
1437
|
if (hasRowActions && dispatcher === undefined) {
|
|
@@ -1511,10 +1515,11 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1511
1515
|
if (action.kind === "drawer") {
|
|
1512
1516
|
return buildDrawerRowAction(action, effectiveTranslate, openDrawer);
|
|
1513
1517
|
}
|
|
1518
|
+
if (action.kind === "clientAction") {
|
|
1519
|
+
return buildClientActionRowAction(action, effectiveTranslate, clientActions, refreshRowsAfterWrite);
|
|
1520
|
+
}
|
|
1514
1521
|
if (dispatcher === undefined)
|
|
1515
1522
|
return null;
|
|
1516
|
-
if (!isWriteHandlerRowAction(action))
|
|
1517
|
-
return null;
|
|
1518
1523
|
const writeAction = action;
|
|
1519
1524
|
const writeActionVisible = writeAction.visible;
|
|
1520
1525
|
return {
|
|
@@ -1551,6 +1556,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1551
1556
|
defaultEditScreen,
|
|
1552
1557
|
effectiveTranslate,
|
|
1553
1558
|
dispatcher,
|
|
1559
|
+
clientActions,
|
|
1554
1560
|
runNavigate,
|
|
1555
1561
|
refreshRowsAfterWrite,
|
|
1556
1562
|
openDrawer,
|
|
@@ -1625,9 +1631,11 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
1625
1631
|
// gewinnt (deklarierte Author-Absicht "Klick auf die Zeile tut X"). Ohne Flag
|
|
1626
1632
|
// bleibt der Pfad unverändert — create-app's entityEdit-Default bzw. ein vom
|
|
1627
1633
|
// App-Author gesetztes onRowClick.
|
|
1628
|
-
const rowClickAction = screen.rowActions
|
|
1634
|
+
const rowClickAction = findRowClickAction(screen.rowActions);
|
|
1629
1635
|
const wrappedOnRowClick = rowClickAction
|
|
1630
|
-
? (row) =>
|
|
1636
|
+
? (row) => rowClickAction.kind === "drawer"
|
|
1637
|
+
? runDrawerRowAction(rowClickAction, openDrawer, row)
|
|
1638
|
+
: runNavigate(rowClickAction, row)
|
|
1631
1639
|
: onRowClick !== undefined
|
|
1632
1640
|
? (row) => onRowClick(row, screen.entity)
|
|
1633
1641
|
: undefined;
|
|
@@ -1696,6 +1704,7 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1696
1704
|
const nav = useNav();
|
|
1697
1705
|
const host = useReturnHost();
|
|
1698
1706
|
const dispatcher = useOptionalDispatcher();
|
|
1707
|
+
const clientActions = useClientActionRuntime();
|
|
1699
1708
|
const effectiveTranslate = translate ?? t;
|
|
1700
1709
|
const userRoles = useUserRoles();
|
|
1701
1710
|
const appFeatures = useAppFeatures();
|
|
@@ -1808,10 +1817,12 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1808
1817
|
openDrawer,
|
|
1809
1818
|
defaultEditRowAction,
|
|
1810
1819
|
host,
|
|
1820
|
+
clientActions,
|
|
1811
1821
|
}), [
|
|
1812
1822
|
screen.rowActions,
|
|
1813
1823
|
effectiveTranslate,
|
|
1814
1824
|
dispatcher,
|
|
1825
|
+
clientActions,
|
|
1815
1826
|
nav,
|
|
1816
1827
|
rowsQuery.refetch,
|
|
1817
1828
|
openDrawer,
|
|
@@ -1856,9 +1867,11 @@ function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
1856
1867
|
if (rawRowsData !== null && !Array.isArray(rawRowsData.rows)) {
|
|
1857
1868
|
return (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-screen-projection-list-bad-shape", children: ["Screen ", _jsx(Text, { variant: "code", children: screen.id }), " query", " ", _jsx(Text, { variant: "code", children: screen.query }), " did not return a PagedRows envelope; a projectionList query must return ", _jsx(Text, { variant: "code", children: "{ rows, nextCursor }" }), "."] }));
|
|
1858
1869
|
}
|
|
1859
|
-
const rowClickAction = screen.rowActions
|
|
1870
|
+
const rowClickAction = findRowClickAction(screen.rowActions);
|
|
1860
1871
|
const wrappedOnRowClick = rowClickAction
|
|
1861
|
-
? (row) =>
|
|
1872
|
+
? (row) => rowClickAction.kind === "drawer"
|
|
1873
|
+
? runDrawerRowAction(rowClickAction, openDrawer, row)
|
|
1874
|
+
: runNavigate(rowClickAction, row)
|
|
1862
1875
|
: onRowClick !== undefined
|
|
1863
1876
|
? (row) => onRowClick(row, listScreen.entity)
|
|
1864
1877
|
: undefined;
|
|
@@ -2010,31 +2023,6 @@ function resolveSubtitleHref(header, record) {
|
|
|
2010
2023
|
// natives Form-Submit (Enter-Keypress) würde ohne customSubmit gegen
|
|
2011
2024
|
// controller.submit() ohne submit-config throwen — der No-Op macht diesen
|
|
2012
2025
|
// Pfad harmlos statt ihn dem Zufall zu überlassen.
|
|
2013
|
-
// Header actions bar (A7): <=2 actions render as plain buttons; >2 with an
|
|
2014
|
-
// `ActionOverflowMenu` primitive collapse to the primary button plus a menu
|
|
2015
|
-
// for the rest — same rule as RowActionsCell for table rows. Owns confirm
|
|
2016
|
-
// state for menu items: RenderEditActionButton already owns a per-button
|
|
2017
|
-
// confirm dialog for the always-visible primary action, but a menu item has
|
|
2018
|
-
// no button of its own to carry one.
|
|
2019
|
-
function HeaderActionsBar({ actions, Button, Dialog, ActionOverflowMenu, onError, collapseAfterPrimary = false, compact = false, preferredPrimaryId, PageHeader, }) {
|
|
2020
|
-
const t = useTranslation();
|
|
2021
|
-
const { toMenuItem, confirmDialog: pendingActionDialog } = useActionMenuItems({
|
|
2022
|
-
Dialog,
|
|
2023
|
-
onError,
|
|
2024
|
-
});
|
|
2025
|
-
if (compact && PageHeader !== undefined) {
|
|
2026
|
-
const compactPrimary = compactHeaderPrimary(actions, preferredPrimaryId);
|
|
2027
|
-
const overflowActions = actions.filter((a) => a !== compactPrimary?.action);
|
|
2028
|
-
return (_jsxs(_Fragment, { children: [compactPrimary !== undefined && (_jsx(RenderEditActionButton, { action: compactPrimary.action, Button: Button, Dialog: Dialog, iconOnly: compactPrimary.iconOnly, compactLabel: true, onError: onError }, compactPrimary.action.id)), overflowActions.length > 0 && (_jsx(PageHeader, { overflowItems: overflowActions.map(toMenuItem) })), pendingActionDialog] }));
|
|
2029
|
-
}
|
|
2030
|
-
const maxPlainButtons = collapseAfterPrimary ? 1 : 2;
|
|
2031
|
-
if (actions.length <= maxPlainButtons || ActionOverflowMenu === undefined) {
|
|
2032
|
-
return (_jsx(_Fragment, { children: orderPlainHeaderActions(actions, preferredPrimaryId).map((action) => (_jsx(RenderEditActionButton, { action: action, Button: Button, Dialog: Dialog, hideIcon: true, onError: onError }, action.id))) }));
|
|
2033
|
-
}
|
|
2034
|
-
const primary = primaryHeaderAction(actions, preferredPrimaryId);
|
|
2035
|
-
const rest = actions.filter((a) => a.id !== primary?.id);
|
|
2036
|
-
return (_jsxs(_Fragment, { children: [primary !== undefined && (_jsx(RenderEditActionButton, { action: primary, Button: Button, Dialog: Dialog, hideIcon: true, onError: onError }, primary.id)), _jsx(ActionOverflowMenu, { label: t("kumiko.list.row-actions.more"), testId: "kumiko-screen-projection-detail-actions-overflow", items: rest.map(toMenuItem) }), pendingActionDialog] }));
|
|
2037
|
-
}
|
|
2038
2026
|
function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
2039
2027
|
const { Banner, Button, Dialog, Text, Grid, GridCell, Card, Tabs, StatusBadge, Metric, MetricBand, PageHeader, Link, ActionOverflowMenu, } = usePrimitives();
|
|
2040
2028
|
const t = useTranslation();
|
|
@@ -2101,6 +2089,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2101
2089
|
const appFeatures = useAppFeatures();
|
|
2102
2090
|
const userRoles = useUserRoles();
|
|
2103
2091
|
const dispatcher = useOptionalDispatcher();
|
|
2092
|
+
const clientActions = useClientActionRuntime();
|
|
2104
2093
|
const host = useReturnHost();
|
|
2105
2094
|
const returnTargetParam = useReturnTarget(screen.id);
|
|
2106
2095
|
// A singleton has no record route to come back to — same as entityEdit.
|
|
@@ -2194,6 +2183,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2194
2183
|
onWriteSuccess: detailQuery.refetch,
|
|
2195
2184
|
onRecordLeft: handleRecordLeft,
|
|
2196
2185
|
sameEntityScreenId,
|
|
2186
|
+
clientActions,
|
|
2197
2187
|
}), [
|
|
2198
2188
|
detailQuery.data,
|
|
2199
2189
|
detailQuery.refetch,
|
|
@@ -2201,6 +2191,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2201
2191
|
nav,
|
|
2202
2192
|
host,
|
|
2203
2193
|
dispatcher,
|
|
2194
|
+
clientActions,
|
|
2204
2195
|
openDrawer,
|
|
2205
2196
|
handleRecordLeft,
|
|
2206
2197
|
sameEntityScreenId,
|
|
@@ -2218,6 +2209,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2218
2209
|
onWriteSuccess: detailQuery.refetch,
|
|
2219
2210
|
onRecordLeft: handleRecordLeft,
|
|
2220
2211
|
sameEntityScreenId,
|
|
2212
|
+
clientActions,
|
|
2221
2213
|
});
|
|
2222
2214
|
if (defaultEditAction === undefined || declaredHasEdit)
|
|
2223
2215
|
return resolved;
|
|
@@ -2229,6 +2221,7 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2229
2221
|
nav,
|
|
2230
2222
|
host,
|
|
2231
2223
|
dispatcher,
|
|
2224
|
+
clientActions,
|
|
2232
2225
|
detailQuery.data,
|
|
2233
2226
|
detailQuery.refetch,
|
|
2234
2227
|
openDrawer,
|
|
@@ -2354,7 +2347,6 @@ function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
2354
2347
|
void reloadDetail();
|
|
2355
2348
|
} })] }));
|
|
2356
2349
|
}
|
|
2357
|
-
// ---- actionForm (Tier 2.7d) ----
|
|
2358
2350
|
function redirectScreenTarget(redirect) {
|
|
2359
2351
|
return typeof redirect === "string" ? redirect : redirect.screen;
|
|
2360
2352
|
}
|
|
@@ -2414,7 +2406,7 @@ function returnToWinsOverRedirect(returnTarget, redirect, schema, appFeatures) {
|
|
|
2414
2406
|
// write executor's success payload doesn't flatly expose.
|
|
2415
2407
|
function resolveRedirectTarget(redirect, submittedData, schema, appFeatures, fallbackRecord) {
|
|
2416
2408
|
const redirectScreen = redirectScreenTarget(redirect);
|
|
2417
|
-
const idField = typeof redirect === "string" ? "id" : redirect.idFrom;
|
|
2409
|
+
const idField = typeof redirect === "string" ? "id" : (redirect.idFrom ?? "id");
|
|
2418
2410
|
const targetId = lastSegment(redirectScreen);
|
|
2419
2411
|
const target = findRedirectTargetScreen(redirect, schema, appFeatures);
|
|
2420
2412
|
const carriesId = target !== undefined && (target.type === "entityEdit" || target.type === "projectionDetail");
|
|
@@ -2438,6 +2430,30 @@ function navigateToRedirectTarget(nav, target) {
|
|
|
2438
2430
|
else
|
|
2439
2431
|
nav.navigate(navTarget, { searchParams: { tab: target.tab } });
|
|
2440
2432
|
}
|
|
2433
|
+
// Links of the success box. Targets resolve like `redirect`; a link whose target
|
|
2434
|
+
// needs a record id (entityEdit, projectionDetail) but whose payload field holds
|
|
2435
|
+
// no string id is dropped instead of navigating to an id-less record screen.
|
|
2436
|
+
// A link without idFrom to a non-record screen always navigates id-less.
|
|
2437
|
+
function resolveSuccessLinks(links, data, schema, appFeatures, nav, translate) {
|
|
2438
|
+
const resolved = [];
|
|
2439
|
+
(links ?? []).forEach((link, index) => {
|
|
2440
|
+
const target = resolveRedirectTarget(link, data, schema, appFeatures);
|
|
2441
|
+
if (target.entityId === undefined && redirectTargetsRecord(link, schema, appFeatures))
|
|
2442
|
+
return;
|
|
2443
|
+
const navTarget = {
|
|
2444
|
+
screenId: target.screenId,
|
|
2445
|
+
...(target.entityId !== undefined && { entityId: target.entityId }),
|
|
2446
|
+
};
|
|
2447
|
+
const options = target.tab === undefined ? undefined : { searchParams: { tab: target.tab } };
|
|
2448
|
+
resolved.push({
|
|
2449
|
+
key: String(index),
|
|
2450
|
+
label: translate(link.label),
|
|
2451
|
+
href: nav.hrefFor(navTarget, options),
|
|
2452
|
+
onPress: () => nav.navigate(navTarget, options),
|
|
2453
|
+
});
|
|
2454
|
+
});
|
|
2455
|
+
return resolved;
|
|
2456
|
+
}
|
|
2441
2457
|
// Success navigation for a configured redirect — shared by entityEdit's
|
|
2442
2458
|
// save and writeHandler record actions. The string form navigates without an
|
|
2443
2459
|
// entityId; the object form resolves like actionForm's, with `record` as the
|
|
@@ -2471,7 +2487,7 @@ function overlayUpdatedProjection(record, resultData) {
|
|
|
2471
2487
|
// Button) ist identisch zu entityEdit, nur der Submit-Pfad wechselt
|
|
2472
2488
|
// vom CRUD-verb auf den Author-deklarierten handler-QN +
|
|
2473
2489
|
// payloadMode="values" (alle Form-Werte schicken statt nur Changes).
|
|
2474
|
-
function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess, onCancelOverride, onDirtyChange, submitLabelFallback, }) {
|
|
2490
|
+
function ActionFormBody({ schema, screen, translate, entityId, initialOverrides, onSuccess, onCancelOverride, onDirtyChange, submitLabelFallback, }) {
|
|
2475
2491
|
const nav = useNav();
|
|
2476
2492
|
const appFeatures = useAppFeatures();
|
|
2477
2493
|
const insideDrawer = useInsideDrawer();
|
|
@@ -2492,27 +2508,55 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
|
|
|
2492
2508
|
// submitting a guessed currency would mis-book the amount — block instead.
|
|
2493
2509
|
const tenantCurrencyFieldNames = useMemo(() => tenantCurrencyMoneyFieldNames(screen.fields), [screen.fields]);
|
|
2494
2510
|
const { overrides: moneyCurrencyOverrides, loading: currencyLoading, unresolved: currencyUnresolved, } = useMoneyCurrencyOverrides(screen.fields, tenantCurrencyFieldNames, undefined);
|
|
2495
|
-
const
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2511
|
+
const loadableFieldNames = useMemo(() => Object.entries(submittableFields)
|
|
2512
|
+
.filter(([, field]) => !isSecretFormField(field))
|
|
2513
|
+
.map(([name]) => name), [submittableFields]);
|
|
2514
|
+
const loadState = useActionFormLoad(screen.load, screen.load !== undefined
|
|
2515
|
+
? resolveActionFormRecordId({
|
|
2516
|
+
routeEntityId: entityId,
|
|
2517
|
+
drawerOverrides: initialOverrides,
|
|
2518
|
+
searchParams: nav.searchParams,
|
|
2519
|
+
})
|
|
2520
|
+
: undefined, loadableFieldNames);
|
|
2521
|
+
const loadedValues = loadState?.status === "ready" ? loadState.values : undefined;
|
|
2522
|
+
const initial = useMemo(() => {
|
|
2523
|
+
// With `load` the loaded record is the only source of values: a crafted link
|
|
2524
|
+
// or a row's params must not seed fields the record would otherwise own.
|
|
2525
|
+
const prefillSources = screen.load !== undefined
|
|
2526
|
+
? { searchParams: {}, urlPrefillFields: undefined }
|
|
2527
|
+
: {
|
|
2528
|
+
searchParams: nav.searchParams,
|
|
2529
|
+
urlPrefillFields: screen.urlPrefillFields,
|
|
2530
|
+
...(initialOverrides !== undefined && { drawerOverrides: initialOverrides }),
|
|
2531
|
+
...(handoffValues !== undefined && { handoffValues }),
|
|
2532
|
+
};
|
|
2533
|
+
return {
|
|
2534
|
+
...mergeSearchParamsIntoInitial(submittableFields, {
|
|
2535
|
+
...prefillSources,
|
|
2536
|
+
renderableFields: layoutFieldNames(synthScreen),
|
|
2537
|
+
...(moneyCurrencyOverrides !== undefined && { moneyCurrencyOverrides }),
|
|
2538
|
+
}),
|
|
2539
|
+
...loadedValues,
|
|
2540
|
+
};
|
|
2541
|
+
}, [
|
|
2503
2542
|
submittableFields,
|
|
2543
|
+
screen.load,
|
|
2504
2544
|
screen.urlPrefillFields,
|
|
2505
2545
|
nav.searchParams,
|
|
2506
2546
|
synthScreen,
|
|
2507
2547
|
moneyCurrencyOverrides,
|
|
2508
2548
|
initialOverrides,
|
|
2509
2549
|
handoffValues,
|
|
2550
|
+
loadedValues,
|
|
2510
2551
|
]);
|
|
2511
2552
|
const t = useTranslation();
|
|
2512
2553
|
const effectiveTranslate = translate ?? t;
|
|
2513
2554
|
const summary = useMemo(() => resolveActionFormSummary(screen.summary, initialOverrides, effectiveTranslate, locale), [screen.summary, initialOverrides, effectiveTranslate, locale]);
|
|
2514
2555
|
const dispatcher = useDispatcher();
|
|
2515
|
-
const [
|
|
2556
|
+
const [success, setSuccess] = useState(undefined);
|
|
2557
|
+
// Bumped after a successful submit with resetOnSuccess: remounts RenderEdit, which
|
|
2558
|
+
// re-seeds the form from `initial` (prefill kept, typed values cleared).
|
|
2559
|
+
const [formEpoch, setFormEpoch] = useState(0);
|
|
2516
2560
|
// A slow label lookup of an earlier submit must not overwrite the banner of a later one.
|
|
2517
2561
|
const successSequence = useRef(0);
|
|
2518
2562
|
useEffect(() => () => {
|
|
@@ -2521,23 +2565,35 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
|
|
|
2521
2565
|
const handleSubmitted = useCallback((result, values) => {
|
|
2522
2566
|
successSequence.current += 1;
|
|
2523
2567
|
const sequence = successSequence.current;
|
|
2524
|
-
|
|
2568
|
+
setSuccess(undefined);
|
|
2525
2569
|
if (!result.isSuccess)
|
|
2526
2570
|
return;
|
|
2527
2571
|
if (onSuccess !== undefined) {
|
|
2528
2572
|
onSuccess();
|
|
2529
2573
|
return;
|
|
2530
2574
|
}
|
|
2531
|
-
if (screen.
|
|
2532
|
-
const
|
|
2533
|
-
const
|
|
2534
|
-
if (successSequence.current
|
|
2535
|
-
|
|
2536
|
-
|
|
2575
|
+
if (screen.redirect === undefined) {
|
|
2576
|
+
const hasSuccessLinks = (screen.successLinks?.length ?? 0) > 0;
|
|
2577
|
+
const publish = (text) => {
|
|
2578
|
+
if (successSequence.current !== sequence)
|
|
2579
|
+
return;
|
|
2580
|
+
setSuccess({
|
|
2581
|
+
text,
|
|
2582
|
+
links: resolveSuccessLinks(screen.successLinks, result.data, schema, appFeatures, nav, effectiveTranslate),
|
|
2583
|
+
});
|
|
2537
2584
|
};
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2585
|
+
const messageKey = screen.successMessage;
|
|
2586
|
+
if (messageKey !== undefined) {
|
|
2587
|
+
const show = (params) => publish(effectiveTranslate(messageKey, params));
|
|
2588
|
+
void resolveSuccessMessageParams(screen, schema.featureName, values, dispatcher, locale)
|
|
2589
|
+
.then(show)
|
|
2590
|
+
.catch(() => show(formatSummaryParams(values, locale)));
|
|
2591
|
+
}
|
|
2592
|
+
else if (hasSuccessLinks || screen.resetOnSuccess === true) {
|
|
2593
|
+
publish(effectiveTranslate("kumiko.actionForm.success"));
|
|
2594
|
+
}
|
|
2595
|
+
if (screen.resetOnSuccess === true)
|
|
2596
|
+
setFormEpoch((epoch) => epoch + 1);
|
|
2541
2597
|
}
|
|
2542
2598
|
// Without a redirect the form stays put; returnTo only replaces an existing
|
|
2543
2599
|
// navigation, and never one to a record screen (see redirectTargetsRecord).
|
|
@@ -2560,6 +2616,14 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
|
|
|
2560
2616
|
locale,
|
|
2561
2617
|
effectiveTranslate,
|
|
2562
2618
|
]);
|
|
2619
|
+
// The success box disappears with the next edit; a submit clears it itself.
|
|
2620
|
+
const handleDirtyChange = useCallback((dirty) => {
|
|
2621
|
+
if (dirty) {
|
|
2622
|
+
successSequence.current += 1;
|
|
2623
|
+
setSuccess(undefined);
|
|
2624
|
+
}
|
|
2625
|
+
onDirtyChange?.(dirty);
|
|
2626
|
+
}, [onDirtyChange]);
|
|
2563
2627
|
// Cancel ist nur sinnvoll wenn ein Navigations-Ziel existiert —
|
|
2564
2628
|
// sonst hätte der Button nirgendwo hin zu navigieren. cancelTarget
|
|
2565
2629
|
// gewinnt über redirect; `false` schaltet den Button explizit ab
|
|
@@ -2582,6 +2646,9 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
|
|
|
2582
2646
|
if (currencyUnresolved) {
|
|
2583
2647
|
return (_jsx(Banner, { padded: true, variant: "error", testId: "kumiko-screen-currency-unresolved", children: "The tenant currency could not be loaded, so this form cannot be submitted." }));
|
|
2584
2648
|
}
|
|
2649
|
+
if (loadState !== undefined && loadState.status !== "ready") {
|
|
2650
|
+
return (_jsx(ActionFormLoadStatus, { state: loadState, screenId: screen.id, translate: effectiveTranslate }));
|
|
2651
|
+
}
|
|
2585
2652
|
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial,
|
|
2586
2653
|
// Extension-Sections im actionForm sehen die initialen Form-Values
|
|
2587
2654
|
// (inkl. searchParams-Prefill, z.B. ?incidentId=…) als initialValues
|
|
@@ -2589,11 +2656,11 @@ function ActionFormBody({ schema, screen, translate, initialOverrides, onSuccess
|
|
|
2589
2656
|
// sie Kontext ziehen könnten.
|
|
2590
2657
|
extensionInitialValues: initial, ...(screen.submitPrefilled === true && {
|
|
2591
2658
|
submitWithoutChanges: hiddenRequiredFieldsFilled(synthScreen, screen.fields, initial),
|
|
2592
|
-
}), ...(
|
|
2593
|
-
leadContent:
|
|
2594
|
-
}), writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, ...(handleCancel !== undefined && { onCancel: handleCancel }),
|
|
2659
|
+
}), ...(success !== undefined && {
|
|
2660
|
+
leadContent: _jsx(ActionFormSuccessBanner, { success: success }),
|
|
2661
|
+
}), writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, ...(handleCancel !== undefined && { onCancel: handleCancel }), onDirtyChange: handleDirtyChange, ...((insideDrawer || screenFillsHeight(screen)) && { fillScreenHeight: true }), ...((screen.submitLabel ?? submitLabelFallback) !== undefined && {
|
|
2595
2662
|
submitLabel: screen.submitLabel ?? submitLabelFallback,
|
|
2596
|
-
}), ...(screen.submitStyle !== undefined && { submitVariant: screen.submitStyle }), ...(screen.footerActions !== undefined && { footerActions: screen.footerActions }), ...(summary !== undefined && { summary }), ...(translate !== undefined && { translate }) }));
|
|
2663
|
+
}), ...(screen.submitStyle !== undefined && { submitVariant: screen.submitStyle }), ...(screen.footerActions !== undefined && { footerActions: screen.footerActions }), ...(summary !== undefined && { summary }), ...(translate !== undefined && { translate }) }, formEpoch));
|
|
2597
2664
|
}
|
|
2598
2665
|
// A money-typed config-edit field renders through RenderField's entityEdit
|
|
2599
2666
|
// `{amount, currency}` payload shape (render-field.tsx, #1923), but
|
|
@@ -2,7 +2,7 @@ import type { ListEmptyStateSpec } from "@cosmicdrift/kumiko-framework/ui-types"
|
|
|
2
2
|
import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import type { ListEmptyStateContent } from "../components/list-empty-state.tsx";
|
|
4
4
|
import { buildRecordActions } from "./row-actions.ts";
|
|
5
|
-
type ActionBindings = Pick<Parameters<typeof buildRecordActions>[0], "nav" | "host" | "dispatcher" | "openDrawer" | "onWriteSuccess">;
|
|
5
|
+
type ActionBindings = Pick<Parameters<typeof buildRecordActions>[0], "nav" | "host" | "dispatcher" | "openDrawer" | "onWriteSuccess" | "clientActions">;
|
|
6
6
|
export declare function useListEmptyContent(spec: ListEmptyStateSpec | undefined, translate: Translate, bindings: ActionBindings): ListEmptyStateContent | undefined;
|
|
7
7
|
export declare function isListNarrowed(search: string, filters: Readonly<Record<string, readonly string[]>>, defaultFilters?: Readonly<Record<string, readonly string[]>>): boolean;
|
|
8
8
|
export {};
|
|
@@ -3,7 +3,7 @@ import { buildRecordActions } from "./row-actions.js";
|
|
|
3
3
|
const NO_RECORD = {};
|
|
4
4
|
// An empty list has no row, so the action is resolved against an empty record.
|
|
5
5
|
export function useListEmptyContent(spec, translate, bindings) {
|
|
6
|
-
const { nav, host, dispatcher, openDrawer, onWriteSuccess } = bindings;
|
|
6
|
+
const { nav, host, dispatcher, openDrawer, onWriteSuccess, clientActions } = bindings;
|
|
7
7
|
return useMemo(() => {
|
|
8
8
|
if (spec === undefined)
|
|
9
9
|
return undefined;
|
|
@@ -18,13 +18,14 @@ export function useListEmptyContent(spec, translate, bindings) {
|
|
|
18
18
|
dispatcher,
|
|
19
19
|
openDrawer,
|
|
20
20
|
onWriteSuccess,
|
|
21
|
+
clientActions,
|
|
21
22
|
})?.[0];
|
|
22
23
|
return {
|
|
23
24
|
title: translate(spec.title),
|
|
24
25
|
...(spec.description !== undefined && { description: translate(spec.description) }),
|
|
25
26
|
...(action !== undefined && { action }),
|
|
26
27
|
};
|
|
27
|
-
}, [spec, translate, nav, host, dispatcher, openDrawer, onWriteSuccess]);
|
|
28
|
+
}, [spec, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, clientActions]);
|
|
28
29
|
}
|
|
29
30
|
// Facets with a default are only "narrowed" once the user moved off that default.
|
|
30
31
|
export function isListNarrowed(search, filters, defaultFilters = {}) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { EntityEditScreenDefinition, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ToolbarAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
1
|
+
import type { EntityEditScreenDefinition, RelatedListToolbarAction, RowAction, RowActionClientAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ToolbarAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import type { Dispatcher, ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
3
|
import type { RenderEditAction } from "../components/render-edit-types.ts";
|
|
4
4
|
import type { ToolbarActionButton } from "../components/render-list.tsx";
|
|
5
5
|
import type { DataTableRowAction } from "../primitives.tsx";
|
|
6
|
+
import { type ClientActionRuntime } from "./client-actions.tsx";
|
|
6
7
|
import type { NavApi } from "./nav.tsx";
|
|
7
8
|
import { type ReturnHost } from "./return-to.tsx";
|
|
8
9
|
export declare function buildDefaultEditRowAction(editScreen: EntityEditScreenDefinition | undefined, idColumn?: string): RowActionNavigate | undefined;
|
|
@@ -15,7 +16,12 @@ export declare function runProjectionRowNavigate(nav: NavApi, action: RowActionN
|
|
|
15
16
|
type OpenDrawer = (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
|
|
16
17
|
/** @internal test-only */
|
|
17
18
|
export declare function resetDrawerActionWarningsForTests(): void;
|
|
19
|
+
export declare function runDrawerRowAction(action: RowActionDrawer, openDrawer: OpenDrawer, row: ListRowViewModel): void;
|
|
20
|
+
/** The action that claims the row-body click (`rowClick: true`); the boot
|
|
21
|
+
* validator allows at most one per list. */
|
|
22
|
+
export declare function findRowClickAction(rowActions: readonly RowAction[] | undefined): RowActionNavigate | RowActionDrawer | undefined;
|
|
18
23
|
export declare function buildDrawerRowAction(action: RowActionDrawer, translate: Translate, openDrawer: OpenDrawer): DataTableRowAction;
|
|
24
|
+
export declare function buildClientActionRowAction(action: RowActionClientAction, translate: Translate, runtime: ClientActionRuntime | undefined, refetch: () => Promise<unknown>): DataTableRowAction;
|
|
19
25
|
export declare function buildProjectionRowActions(options: {
|
|
20
26
|
readonly rowActions: readonly RowAction[] | undefined;
|
|
21
27
|
readonly translate: Translate;
|
|
@@ -30,6 +36,7 @@ export declare function buildProjectionRowActions(options: {
|
|
|
30
36
|
/** Prepended unless a declared action already has id "edit" — declared wins. */
|
|
31
37
|
readonly defaultEditRowAction?: RowActionNavigate;
|
|
32
38
|
readonly host: ReturnHost | undefined;
|
|
39
|
+
readonly clientActions?: ClientActionRuntime;
|
|
33
40
|
}): readonly DataTableRowAction[] | undefined;
|
|
34
41
|
type OpenToolbarDrawer = (action: ToolbarAction & {
|
|
35
42
|
readonly kind: "drawer";
|
|
@@ -93,5 +100,6 @@ export declare function buildRecordActions(options: {
|
|
|
93
100
|
* action (they reload or remount the surrounding form). Set while that
|
|
94
101
|
* form holds unsaved input so it is not lost silently. */
|
|
95
102
|
readonly discardChanges?: DiscardChanges;
|
|
103
|
+
readonly clientActions?: ClientActionRuntime;
|
|
96
104
|
}): readonly RenderEditAction[] | undefined;
|
|
97
105
|
export {};
|
package/dist/app/row-actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evalFieldCondition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
3
|
+
import { runClientAction } from "./client-actions.js";
|
|
3
4
|
import { lastSegment } from "./qn.js";
|
|
4
5
|
import { navigateWithReturnTo } from "./return-to.js";
|
|
5
6
|
import { dispatcherErrorText, WriteFailedError } from "./write-failed-error.js";
|
|
@@ -119,8 +120,16 @@ function warnDrawerActionDropped(actionKind, actionId) {
|
|
|
119
120
|
// biome-ignore lint/suspicious/noConsole: dev-warning for a setup error
|
|
120
121
|
console.warn(`[kumiko] ${actionKind} "${actionId}" is kind:"drawer", but the host did not wire openDrawer (RelatedListSection: pass onOpenDrawer) — it will not render.`);
|
|
121
122
|
}
|
|
123
|
+
export function runDrawerRowAction(action, openDrawer, row) {
|
|
124
|
+
openDrawer(action, action.params !== undefined ? evalRowExtractor(action.params, row.values) : undefined);
|
|
125
|
+
}
|
|
126
|
+
/** The action that claims the row-body click (`rowClick: true`); the boot
|
|
127
|
+
* validator allows at most one per list. */
|
|
128
|
+
export function findRowClickAction(rowActions) {
|
|
129
|
+
return rowActions?.find((a) => (a.kind === "navigate" || a.kind === "drawer") && a.rowClick === true);
|
|
130
|
+
}
|
|
122
131
|
export function buildDrawerRowAction(action, translate, openDrawer) {
|
|
123
|
-
const { visible
|
|
132
|
+
const { visible } = action;
|
|
124
133
|
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
125
134
|
return {
|
|
126
135
|
id: action.id,
|
|
@@ -128,10 +137,9 @@ export function buildDrawerRowAction(action, translate, openDrawer) {
|
|
|
128
137
|
...(action.style !== undefined && { style: action.style }),
|
|
129
138
|
...(action.display !== undefined && { display: action.display }),
|
|
130
139
|
confirmRequired: false,
|
|
140
|
+
...(action.rowClick === true && { rowClick: true }),
|
|
131
141
|
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
132
|
-
onTrigger: (row) =>
|
|
133
|
-
openDrawer(action, params !== undefined ? evalRowExtractor(params, row.values) : undefined);
|
|
134
|
-
},
|
|
142
|
+
onTrigger: (row) => runDrawerRowAction(action, openDrawer, row),
|
|
135
143
|
...(visible !== undefined && {
|
|
136
144
|
isVisible: (row) => evalFieldCondition(visible, row.values),
|
|
137
145
|
}),
|
|
@@ -172,6 +180,36 @@ function buildWriteHandlerRowAction(action, translate, refetch, dispatcher) {
|
|
|
172
180
|
}),
|
|
173
181
|
};
|
|
174
182
|
}
|
|
183
|
+
function clientActionPayload(action, values, defaultId) {
|
|
184
|
+
return action.payload !== undefined
|
|
185
|
+
? evalRowExtractor(action.payload, values)
|
|
186
|
+
: { id: defaultId };
|
|
187
|
+
}
|
|
188
|
+
export function buildClientActionRowAction(action, translate, runtime, refetch) {
|
|
189
|
+
const { visible } = action;
|
|
190
|
+
return {
|
|
191
|
+
id: action.id,
|
|
192
|
+
label: translate(action.label),
|
|
193
|
+
...(action.style !== undefined && { style: action.style }),
|
|
194
|
+
...(action.display !== undefined && { display: action.display }),
|
|
195
|
+
icon: resolveActionIcon(action.id, action.icon),
|
|
196
|
+
...(action.confirm !== undefined && { confirm: translate(action.confirm) }),
|
|
197
|
+
...(action.confirmLabel !== undefined && { confirmLabel: translate(action.confirmLabel) }),
|
|
198
|
+
onTrigger: async (row) => {
|
|
199
|
+
const succeeded = await runClientAction({
|
|
200
|
+
action,
|
|
201
|
+
payload: clientActionPayload(action, row.values, row.values["id"]),
|
|
202
|
+
runtime,
|
|
203
|
+
translate,
|
|
204
|
+
});
|
|
205
|
+
if (succeeded)
|
|
206
|
+
await refetchAfterWrite(refetch);
|
|
207
|
+
},
|
|
208
|
+
...(visible !== undefined && {
|
|
209
|
+
isVisible: (row) => evalFieldCondition(visible, row.values),
|
|
210
|
+
}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
175
213
|
// Builds the DataTable-ready row-action set for a query-driven row source
|
|
176
214
|
// (projectionList, relatedList) — navigate dispatches through
|
|
177
215
|
// runProjectionRowNavigate, writeHandler dispatches through the shared
|
|
@@ -179,7 +217,7 @@ function buildWriteHandlerRowAction(action, translate, refetch, dispatcher) {
|
|
|
179
217
|
// projectionList's rowActions and a projectionDetail relatedList section's
|
|
180
218
|
// rowActions can't drift apart (fw editable-detail-screens).
|
|
181
219
|
export function buildProjectionRowActions(options) {
|
|
182
|
-
const { rowActions, translate, dispatcher, nav, refetch, openDrawer, defaultEditRowAction, host, } = options;
|
|
220
|
+
const { rowActions, translate, dispatcher, nav, refetch, openDrawer, defaultEditRowAction, host, clientActions, } = options;
|
|
183
221
|
const effectiveActions = mergeDefaultEditAction(rowActions, defaultEditRowAction);
|
|
184
222
|
if (effectiveActions.length === 0)
|
|
185
223
|
return undefined;
|
|
@@ -197,6 +235,10 @@ export function buildProjectionRowActions(options) {
|
|
|
197
235
|
out.push(buildDrawerRowAction(action, translate, openDrawer));
|
|
198
236
|
continue;
|
|
199
237
|
}
|
|
238
|
+
if (action.kind === "clientAction") {
|
|
239
|
+
out.push(buildClientActionRowAction(action, translate, clientActions, refetch));
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
200
242
|
// writeHandler (default-kind) — a swallowed failure result must become a
|
|
201
243
|
// thrown error (fw prod-bug 2026-06-07), same as every other write path.
|
|
202
244
|
if (dispatcher === undefined)
|
|
@@ -369,6 +411,31 @@ function buildDrawerRecordAction(action, options) {
|
|
|
369
411
|
},
|
|
370
412
|
};
|
|
371
413
|
}
|
|
414
|
+
function buildClientActionRecordAction(action, options) {
|
|
415
|
+
const { record, translate, actionIcon, runtime, onWriteSuccess, defaultWritePayloadId, discardChanges, } = options;
|
|
416
|
+
const confirm = withDiscardHint(action.confirm !== undefined ? translate(action.confirm) : undefined, discardChanges);
|
|
417
|
+
const confirmLabel = discardChanges?.confirmLabel ??
|
|
418
|
+
(action.confirmLabel !== undefined ? translate(action.confirmLabel) : undefined);
|
|
419
|
+
return {
|
|
420
|
+
id: action.id,
|
|
421
|
+
label: translate(action.label),
|
|
422
|
+
...(action.style !== undefined && { style: action.style }),
|
|
423
|
+
...(action.display !== undefined && { display: action.display }),
|
|
424
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
425
|
+
...(confirm !== undefined && { confirm }),
|
|
426
|
+
...(confirmLabel !== undefined && { confirmLabel }),
|
|
427
|
+
onPress: async () => {
|
|
428
|
+
const succeeded = await runClientAction({
|
|
429
|
+
action,
|
|
430
|
+
payload: clientActionPayload(action, record, defaultWritePayloadId ?? record["id"]),
|
|
431
|
+
runtime,
|
|
432
|
+
translate,
|
|
433
|
+
});
|
|
434
|
+
if (succeeded)
|
|
435
|
+
await onWriteSuccess();
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
372
439
|
// Entity delete handler QN: `<feature>:write:<entity>:delete` (see
|
|
373
440
|
// entityWriteCommand in kumiko-screen.tsx).
|
|
374
441
|
const ENTITY_DELETE_HANDLER_PATTERN = /:write:[^:]+:delete$/;
|
|
@@ -417,7 +484,7 @@ function buildWriteHandlerRecordAction(action, options) {
|
|
|
417
484
|
// resolved against the record currently being viewed/edited, independent of
|
|
418
485
|
// any table row.
|
|
419
486
|
export function buildRecordActions(options) {
|
|
420
|
-
const { actions, record, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, onRecordLeft, defaultScreenTargetEntityId, sameEntityScreenId, defaultWritePayloadId, discardChanges, } = options;
|
|
487
|
+
const { actions, record, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, onRecordLeft, defaultScreenTargetEntityId, sameEntityScreenId, defaultWritePayloadId, discardChanges, clientActions, } = options;
|
|
421
488
|
const out = [];
|
|
422
489
|
for (const action of actions) {
|
|
423
490
|
if (action.visible !== undefined && !evalFieldCondition(action.visible, record))
|
|
@@ -447,6 +514,18 @@ export function buildRecordActions(options) {
|
|
|
447
514
|
}));
|
|
448
515
|
continue;
|
|
449
516
|
}
|
|
517
|
+
if (action.kind === "clientAction") {
|
|
518
|
+
out.push(buildClientActionRecordAction(action, {
|
|
519
|
+
record,
|
|
520
|
+
translate,
|
|
521
|
+
actionIcon,
|
|
522
|
+
runtime: clientActions,
|
|
523
|
+
onWriteSuccess,
|
|
524
|
+
defaultWritePayloadId,
|
|
525
|
+
discardChanges,
|
|
526
|
+
}));
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
450
529
|
// writeHandler — skip without a dispatcher instead of crashing.
|
|
451
530
|
if (dispatcher === undefined)
|
|
452
531
|
continue;
|