@cosmicdrift/kumiko-renderer 0.327.0 → 0.328.1
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-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -44
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -38
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/dist/inside-drawer.js +9 -0
- package/dist/page-header-slot.d.ts +2 -0
- package/dist/page-header-slot.js +9 -0
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/changes.json +52 -0
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -111
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3614
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -85
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -639
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1669
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -60
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -467
- package/src/components/render-edit-action-button.tsx +0 -106
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -216
- package/src/components/render-edit.tsx +0 -1700
- package/src/components/render-field.tsx +0 -942
- package/src/components/render-list.tsx +0 -561
- package/src/components/write-form-section.tsx +0 -199
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -171
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -132
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -220
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -272
- package/src/primitives.tsx +0 -1446
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
package/src/app/row-actions.ts
DELETED
|
@@ -1,639 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EntityEditScreenDefinition,
|
|
3
|
-
RelatedListToolbarAction,
|
|
4
|
-
RowAction,
|
|
5
|
-
RowActionDrawer,
|
|
6
|
-
RowActionNavigate,
|
|
7
|
-
RowActionWriteHandler,
|
|
8
|
-
RowFieldExtractor,
|
|
9
|
-
ToolbarAction,
|
|
10
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
11
|
-
import { evalFieldCondition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
12
|
-
import type { Dispatcher, ListRowViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
13
|
-
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
14
|
-
import type { IconKey } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
15
|
-
import type { RenderEditAction } from "../components/render-edit-types";
|
|
16
|
-
import type { ToolbarActionButton } from "../components/render-list";
|
|
17
|
-
import type { DataTableRowAction } from "../primitives";
|
|
18
|
-
import type { NavApi, ScreenTarget } from "./nav";
|
|
19
|
-
import { lastSegment } from "./qn";
|
|
20
|
-
import { navigateWithReturnTo, type ReturnHost } from "./return-to";
|
|
21
|
-
import { dispatcherErrorText, WriteFailedError } from "./write-failed-error";
|
|
22
|
-
|
|
23
|
-
// entityId is explicit: the edit screen may live in another feature than
|
|
24
|
-
// the row source, where the same-feature fallback would miss it.
|
|
25
|
-
export function buildDefaultEditRowAction(
|
|
26
|
-
editScreen: EntityEditScreenDefinition | undefined,
|
|
27
|
-
idColumn = "id",
|
|
28
|
-
): RowActionNavigate | undefined {
|
|
29
|
-
if (editScreen === undefined) return undefined;
|
|
30
|
-
return {
|
|
31
|
-
kind: "navigate",
|
|
32
|
-
id: "edit",
|
|
33
|
-
label: "kumiko.actions.edit",
|
|
34
|
-
screen: lastSegment(editScreen.id),
|
|
35
|
-
entityId: idColumn,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function evalRowExtractor(
|
|
40
|
-
extractor: RowFieldExtractor,
|
|
41
|
-
row: Record<string, unknown>,
|
|
42
|
-
): Record<string, unknown> {
|
|
43
|
-
if ("pick" in extractor) {
|
|
44
|
-
return Object.fromEntries(extractor.pick.map((f) => [f, row[f]]));
|
|
45
|
-
}
|
|
46
|
-
return Object.fromEntries(Object.entries(extractor.map).map(([to, from]) => [to, row[from]]));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function isWriteHandlerRowAction(action: RowAction): action is RowActionWriteHandler {
|
|
50
|
-
return action.kind === "writeHandler" || action.kind === undefined;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
54
|
-
if (typeof value !== "object" || value === null) return false;
|
|
55
|
-
const proto: object | null = Object.getPrototypeOf(value);
|
|
56
|
-
return proto === Object.prototype || proto === null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function stringifyNavParams(params: Record<string, unknown>): Record<string, string | null> {
|
|
60
|
-
const out: Record<string, string | null> = {};
|
|
61
|
-
for (const [k, v] of Object.entries(params)) {
|
|
62
|
-
// Structured field values such as money `{amount, currency}` must survive
|
|
63
|
-
// the URL round-trip as JSON; Date & other class instances stay String().
|
|
64
|
-
out[k] =
|
|
65
|
-
v === null || v === undefined
|
|
66
|
-
? null
|
|
67
|
-
: Array.isArray(v) || isPlainObject(v)
|
|
68
|
-
? JSON.stringify(v)
|
|
69
|
-
: String(v);
|
|
70
|
-
}
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function navigateActionSearchParams(
|
|
75
|
-
action: RowActionNavigate,
|
|
76
|
-
values: Readonly<Record<string, unknown>>,
|
|
77
|
-
): Record<string, string | null> | undefined {
|
|
78
|
-
const params =
|
|
79
|
-
action.params !== undefined
|
|
80
|
-
? stringifyNavParams(evalRowExtractor(action.params, values))
|
|
81
|
-
: undefined;
|
|
82
|
-
if (action.tab === undefined) return params;
|
|
83
|
-
return { ...params, tab: action.tab };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export async function refetchAfterWrite(refetch: () => Promise<unknown>): Promise<void> {
|
|
87
|
-
await refetch().catch((err: unknown) => {
|
|
88
|
-
// biome-ignore lint/suspicious/noConsole: refetch must not poison the write-action error path
|
|
89
|
-
console.error("kumiko-screen: refetch after write action failed", err);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Navigate execution for query-driven rows (projectionList, and relatedList
|
|
94
|
-
// — both have no guaranteed "id" field, unlike entityList's rows which back
|
|
95
|
-
// a real entity). No same-entity row["id"] fallback (see EntityListBody's
|
|
96
|
-
// own runNavigate for that variant, which stays separate — entityList's
|
|
97
|
-
// fallback needs `screen.entity`, which neither projectionList nor
|
|
98
|
-
// relatedList has).
|
|
99
|
-
export function runProjectionRowNavigate(
|
|
100
|
-
nav: NavApi,
|
|
101
|
-
action: RowActionNavigate,
|
|
102
|
-
row: ListRowViewModel,
|
|
103
|
-
host: ReturnHost | undefined,
|
|
104
|
-
): void {
|
|
105
|
-
if (action.entity !== undefined) {
|
|
106
|
-
const id = action.entityId !== undefined ? String(row.values[action.entityId] ?? "") : "";
|
|
107
|
-
// skip: no entityId column on this row — nothing to navigate to.
|
|
108
|
-
if (id === "") return;
|
|
109
|
-
const params = navigateActionSearchParams(action, row.values);
|
|
110
|
-
navigateWithReturnTo(nav, { entity: action.entity, id }, host, params);
|
|
111
|
-
} else if (action.screen !== undefined) {
|
|
112
|
-
const entityId =
|
|
113
|
-
action.entityId !== undefined ? String(row.values[action.entityId] ?? "") : undefined;
|
|
114
|
-
const target: ScreenTarget = {
|
|
115
|
-
screenId: action.screen,
|
|
116
|
-
...(entityId !== undefined && entityId !== "" && { entityId }),
|
|
117
|
-
};
|
|
118
|
-
const params = navigateActionSearchParams(action, row.values);
|
|
119
|
-
navigateWithReturnTo(nav, target, host, params);
|
|
120
|
-
}
|
|
121
|
-
// skip: neither entity nor screen set — the boot-validator rejects this
|
|
122
|
-
// shape (resolveRowActionNavigateTarget), so this only guards types.
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function buildNavigateRowAction(
|
|
126
|
-
action: RowActionNavigate,
|
|
127
|
-
translate: Translate,
|
|
128
|
-
nav: NavApi,
|
|
129
|
-
host: ReturnHost | undefined,
|
|
130
|
-
): DataTableRowAction {
|
|
131
|
-
const { visible } = action;
|
|
132
|
-
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
133
|
-
return {
|
|
134
|
-
id: action.id,
|
|
135
|
-
label: translate(action.label),
|
|
136
|
-
...(action.style !== undefined && { style: action.style }),
|
|
137
|
-
confirmRequired: false,
|
|
138
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
139
|
-
onTrigger: (row: ListRowViewModel) => runProjectionRowNavigate(nav, action, row, host),
|
|
140
|
-
...(visible !== undefined && {
|
|
141
|
-
isVisible: (row: ListRowViewModel) => evalFieldCondition(visible, row.values),
|
|
142
|
-
}),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
type OpenDrawer = (
|
|
147
|
-
action: RowActionDrawer,
|
|
148
|
-
initialValues: Readonly<Record<string, unknown>> | undefined,
|
|
149
|
-
) => void;
|
|
150
|
-
|
|
151
|
-
// Dedupes the "openDrawer not wired" warning per action id, since the
|
|
152
|
-
// builders re-run inside a useMemo on every dep change.
|
|
153
|
-
const warnedDrawerActionIds = new Set<string>();
|
|
154
|
-
|
|
155
|
-
/** @internal test-only */
|
|
156
|
-
export function resetDrawerActionWarningsForTests(): void {
|
|
157
|
-
warnedDrawerActionIds.clear();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function warnDrawerActionDropped(
|
|
161
|
-
actionKind: "rowAction" | "toolbarAction",
|
|
162
|
-
actionId: string,
|
|
163
|
-
): void {
|
|
164
|
-
const key = `${actionKind}:${actionId}`;
|
|
165
|
-
// skip: already warned for this id — suppresses the repeat, not the warning itself.
|
|
166
|
-
if (warnedDrawerActionIds.has(key)) return;
|
|
167
|
-
warnedDrawerActionIds.add(key);
|
|
168
|
-
// biome-ignore lint/suspicious/noConsole: dev-warning for a setup error
|
|
169
|
-
console.warn(
|
|
170
|
-
`[kumiko] ${actionKind} "${actionId}" is kind:"drawer", but the host did not wire openDrawer (RelatedListSection: pass onOpenDrawer) — it will not render.`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function buildDrawerRowAction(
|
|
175
|
-
action: RowActionDrawer,
|
|
176
|
-
translate: Translate,
|
|
177
|
-
openDrawer: OpenDrawer,
|
|
178
|
-
): DataTableRowAction {
|
|
179
|
-
const { visible, params } = action;
|
|
180
|
-
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
181
|
-
return {
|
|
182
|
-
id: action.id,
|
|
183
|
-
label: translate(action.label),
|
|
184
|
-
...(action.style !== undefined && { style: action.style }),
|
|
185
|
-
confirmRequired: false,
|
|
186
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
187
|
-
onTrigger: (row: ListRowViewModel) => {
|
|
188
|
-
openDrawer(action, params !== undefined ? evalRowExtractor(params, row.values) : undefined);
|
|
189
|
-
},
|
|
190
|
-
...(visible !== undefined && {
|
|
191
|
-
isVisible: (row: ListRowViewModel) => evalFieldCondition(visible, row.values),
|
|
192
|
-
}),
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Prepends defaultEditRowAction unless a declared action already claims id
|
|
197
|
-
// "edit" — declared wins.
|
|
198
|
-
function mergeDefaultEditAction(
|
|
199
|
-
rowActions: readonly RowAction[] | undefined,
|
|
200
|
-
defaultEditRowAction: RowActionNavigate | undefined,
|
|
201
|
-
): readonly RowAction[] {
|
|
202
|
-
const declaredHasEdit = rowActions?.some((a) => a.id === "edit") === true;
|
|
203
|
-
return defaultEditRowAction !== undefined && !declaredHasEdit
|
|
204
|
-
? [defaultEditRowAction, ...(rowActions ?? [])]
|
|
205
|
-
: (rowActions ?? []);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function buildWriteHandlerRowAction(
|
|
209
|
-
action: RowActionWriteHandler,
|
|
210
|
-
translate: Translate,
|
|
211
|
-
refetch: () => Promise<unknown>,
|
|
212
|
-
dispatcher: Dispatcher,
|
|
213
|
-
): DataTableRowAction {
|
|
214
|
-
const writeVisible = action.visible;
|
|
215
|
-
return {
|
|
216
|
-
id: action.id,
|
|
217
|
-
label: translate(action.label),
|
|
218
|
-
...(action.style !== undefined && { style: action.style }),
|
|
219
|
-
icon: resolveActionIcon(action.id, action.icon),
|
|
220
|
-
...(action.confirm !== undefined && { confirm: translate(action.confirm) }),
|
|
221
|
-
...(action.confirmLabel !== undefined && {
|
|
222
|
-
confirmLabel: translate(action.confirmLabel),
|
|
223
|
-
}),
|
|
224
|
-
onTrigger: async (row: ListRowViewModel) => {
|
|
225
|
-
const payload =
|
|
226
|
-
action.payload !== undefined
|
|
227
|
-
? evalRowExtractor(action.payload, row.values)
|
|
228
|
-
: { id: row.values["id"] };
|
|
229
|
-
const result = await dispatcher.write(action.handler, payload);
|
|
230
|
-
if (!result.isSuccess) {
|
|
231
|
-
throw new WriteFailedError(result.error, dispatcherErrorText(result.error, translate));
|
|
232
|
-
}
|
|
233
|
-
await refetchAfterWrite(refetch);
|
|
234
|
-
},
|
|
235
|
-
...(writeVisible !== undefined && {
|
|
236
|
-
isVisible: (row: ListRowViewModel) => evalFieldCondition(writeVisible, row.values),
|
|
237
|
-
}),
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Builds the DataTable-ready row-action set for a query-driven row source
|
|
242
|
-
// (projectionList, relatedList) — navigate dispatches through
|
|
243
|
-
// runProjectionRowNavigate, writeHandler dispatches through the shared
|
|
244
|
-
// Dispatcher and refetches `refetch` on success. Single implementation so
|
|
245
|
-
// projectionList's rowActions and a projectionDetail relatedList section's
|
|
246
|
-
// rowActions can't drift apart (fw editable-detail-screens).
|
|
247
|
-
export function buildProjectionRowActions(options: {
|
|
248
|
-
readonly rowActions: readonly RowAction[] | undefined;
|
|
249
|
-
readonly translate: Translate;
|
|
250
|
-
readonly dispatcher: Dispatcher | undefined;
|
|
251
|
-
readonly nav: NavApi;
|
|
252
|
-
readonly refetch: () => Promise<unknown>;
|
|
253
|
-
/** Opens the drawer-kind action's target actionForm, prefilled from the
|
|
254
|
-
* clicked row. Omitted callers (e.g. RelatedListSection embedded directly
|
|
255
|
-
* without onOpenDrawer) drop drawer actions and get a dev warning —
|
|
256
|
-
* mirrors the `dispatcher === undefined` skip below for writeHandler. */
|
|
257
|
-
readonly openDrawer?: OpenDrawer;
|
|
258
|
-
/** Prepended unless a declared action already has id "edit" — declared wins. */
|
|
259
|
-
readonly defaultEditRowAction?: RowActionNavigate;
|
|
260
|
-
readonly host: ReturnHost | undefined;
|
|
261
|
-
}): readonly DataTableRowAction[] | undefined {
|
|
262
|
-
const {
|
|
263
|
-
rowActions,
|
|
264
|
-
translate,
|
|
265
|
-
dispatcher,
|
|
266
|
-
nav,
|
|
267
|
-
refetch,
|
|
268
|
-
openDrawer,
|
|
269
|
-
defaultEditRowAction,
|
|
270
|
-
host,
|
|
271
|
-
} = options;
|
|
272
|
-
const effectiveActions = mergeDefaultEditAction(rowActions, defaultEditRowAction);
|
|
273
|
-
if (effectiveActions.length === 0) return undefined;
|
|
274
|
-
const out: DataTableRowAction[] = [];
|
|
275
|
-
for (const action of effectiveActions) {
|
|
276
|
-
if (action.kind === "navigate") {
|
|
277
|
-
out.push(buildNavigateRowAction(action, translate, nav, host));
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
if (action.kind === "drawer") {
|
|
281
|
-
if (openDrawer === undefined) {
|
|
282
|
-
warnDrawerActionDropped("rowAction", action.id);
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
out.push(buildDrawerRowAction(action, translate, openDrawer));
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
// writeHandler (default-kind) — a swallowed failure result must become a
|
|
289
|
-
// thrown error (fw prod-bug 2026-06-07), same as every other write path.
|
|
290
|
-
if (dispatcher === undefined) continue;
|
|
291
|
-
out.push(buildWriteHandlerRowAction(action, translate, refetch, dispatcher));
|
|
292
|
-
}
|
|
293
|
-
return out.length > 0 ? out : undefined;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
type OpenToolbarDrawer = (action: ToolbarAction & { readonly kind: "drawer" }) => void;
|
|
297
|
-
|
|
298
|
-
function buildNavigateToolbarAction(
|
|
299
|
-
action: RelatedListToolbarAction & { readonly kind: "navigate" },
|
|
300
|
-
translate: Translate,
|
|
301
|
-
nav: NavApi,
|
|
302
|
-
host: ReturnHost | undefined,
|
|
303
|
-
prefill: Readonly<Record<string, unknown>> | undefined,
|
|
304
|
-
record: Readonly<Record<string, unknown>> | undefined,
|
|
305
|
-
): ToolbarActionButton {
|
|
306
|
-
const actionIcon = resolveActionIcon(action.id);
|
|
307
|
-
const target: ScreenTarget = { screenId: action.screen };
|
|
308
|
-
return {
|
|
309
|
-
id: action.id,
|
|
310
|
-
label: translate(action.label),
|
|
311
|
-
...(action.style !== undefined && { style: action.style }),
|
|
312
|
-
confirmRequired: false,
|
|
313
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
314
|
-
onTrigger: () => {
|
|
315
|
-
// A declared `params` extractor (evaluated against the relatedList's
|
|
316
|
-
// parent record) replaces the caller's implicit prefill (e.g. a
|
|
317
|
-
// relatedList's `{ [parentParam]: parentId }` or, with `parentFilter`
|
|
318
|
-
// set, `{ [parentFilter.field]: parentId }`) rather than merging
|
|
319
|
-
// with it — same "params present → drop the default" rule as
|
|
320
|
-
// rowActions.
|
|
321
|
-
const resolvedParams =
|
|
322
|
-
action.params !== undefined && record !== undefined
|
|
323
|
-
? evalRowExtractor(action.params, record)
|
|
324
|
-
: prefill;
|
|
325
|
-
const stringifiedParams =
|
|
326
|
-
resolvedParams !== undefined ? stringifyNavParams(resolvedParams) : undefined;
|
|
327
|
-
const params =
|
|
328
|
-
action.tab === undefined ? stringifiedParams : { ...stringifiedParams, tab: action.tab };
|
|
329
|
-
navigateWithReturnTo(nav, target, host, params);
|
|
330
|
-
},
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function buildDrawerToolbarAction(
|
|
335
|
-
action: ToolbarAction & { readonly kind: "drawer" },
|
|
336
|
-
translate: Translate,
|
|
337
|
-
openDrawer: OpenToolbarDrawer,
|
|
338
|
-
): ToolbarActionButton {
|
|
339
|
-
const actionIcon = resolveActionIcon(action.id);
|
|
340
|
-
return {
|
|
341
|
-
id: action.id,
|
|
342
|
-
label: translate(action.label),
|
|
343
|
-
...(action.style !== undefined && { style: action.style }),
|
|
344
|
-
confirmRequired: false,
|
|
345
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
346
|
-
onTrigger: () => openDrawer(action),
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function buildWriteHandlerToolbarAction(
|
|
351
|
-
action: ToolbarAction & { readonly kind: "writeHandler" },
|
|
352
|
-
translate: Translate,
|
|
353
|
-
refetch: () => Promise<unknown>,
|
|
354
|
-
dispatcher: Dispatcher,
|
|
355
|
-
): ToolbarActionButton {
|
|
356
|
-
const actionIcon = resolveActionIcon(action.id);
|
|
357
|
-
return {
|
|
358
|
-
id: action.id,
|
|
359
|
-
label: translate(action.label),
|
|
360
|
-
...(action.style !== undefined && { style: action.style }),
|
|
361
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
362
|
-
...(action.confirm !== undefined && { confirm: translate(action.confirm) }),
|
|
363
|
-
...(action.confirmLabel !== undefined && {
|
|
364
|
-
confirmLabel: translate(action.confirmLabel),
|
|
365
|
-
}),
|
|
366
|
-
onTrigger: async () => {
|
|
367
|
-
const result = await dispatcher.write(action.handler, action.payload ?? {});
|
|
368
|
-
if (!result.isSuccess) {
|
|
369
|
-
throw new WriteFailedError(result.error, dispatcherErrorText(result.error, translate));
|
|
370
|
-
}
|
|
371
|
-
await refetchAfterWrite(refetch);
|
|
372
|
-
},
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Shared by entityList, projectionList, and relatedList toolbars so the
|
|
377
|
-
// three call sites can't drift apart.
|
|
378
|
-
export function buildProjectionToolbarActions(options: {
|
|
379
|
-
readonly toolbarActions: readonly RelatedListToolbarAction[] | undefined;
|
|
380
|
-
readonly translate: Translate;
|
|
381
|
-
readonly dispatcher: Dispatcher | undefined;
|
|
382
|
-
readonly nav: NavApi;
|
|
383
|
-
readonly refetch: () => Promise<unknown>;
|
|
384
|
-
/** Omitted callers drop drawer-kind toolbar actions and get a dev warning,
|
|
385
|
-
* mirroring the rowActions behavior above. */
|
|
386
|
-
readonly openDrawer?: OpenToolbarDrawer;
|
|
387
|
-
/** Search params set on the target after a navigate-kind action, e.g. a
|
|
388
|
-
* relatedList's parent id for create-form prefill. */
|
|
389
|
-
readonly navigatePrefill?: Readonly<Record<string, unknown>>;
|
|
390
|
-
/** The relatedList's parent record — only a relatedList caller has one.
|
|
391
|
-
* Enables `visible`/`params` (RelatedListToolbarAction), evaluated
|
|
392
|
-
* against it exactly like header actions/RowAction.visible. Plain
|
|
393
|
-
* entityList/projectionList toolbars have no record and omit this. */
|
|
394
|
-
readonly record?: Readonly<Record<string, unknown>>;
|
|
395
|
-
readonly host: ReturnHost | undefined;
|
|
396
|
-
}): readonly ToolbarActionButton[] | undefined {
|
|
397
|
-
const {
|
|
398
|
-
toolbarActions,
|
|
399
|
-
translate,
|
|
400
|
-
dispatcher,
|
|
401
|
-
nav,
|
|
402
|
-
refetch,
|
|
403
|
-
openDrawer,
|
|
404
|
-
navigatePrefill,
|
|
405
|
-
record,
|
|
406
|
-
host,
|
|
407
|
-
} = options;
|
|
408
|
-
if (toolbarActions === undefined) return undefined;
|
|
409
|
-
const out: ToolbarActionButton[] = [];
|
|
410
|
-
for (const action of toolbarActions) {
|
|
411
|
-
if (
|
|
412
|
-
action.visible !== undefined &&
|
|
413
|
-
record !== undefined &&
|
|
414
|
-
!evalFieldCondition(action.visible, record)
|
|
415
|
-
) {
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
if (action.kind === "navigate") {
|
|
419
|
-
out.push(buildNavigateToolbarAction(action, translate, nav, host, navigatePrefill, record));
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
if (action.kind === "drawer") {
|
|
423
|
-
if (openDrawer === undefined) {
|
|
424
|
-
warnDrawerActionDropped("toolbarAction", action.id);
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
out.push(buildDrawerToolbarAction(action, translate, openDrawer));
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
// writeHandler — skip without a dispatcher instead of crashing (same as rowActions).
|
|
431
|
-
if (dispatcher === undefined) continue;
|
|
432
|
-
out.push(buildWriteHandlerToolbarAction(action, translate, refetch, dispatcher));
|
|
433
|
-
}
|
|
434
|
-
return out.length > 0 ? out : undefined;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function buildNavigateRecordAction(
|
|
438
|
-
action: RowActionNavigate,
|
|
439
|
-
options: {
|
|
440
|
-
readonly record: Readonly<Record<string, unknown>>;
|
|
441
|
-
readonly translate: Translate;
|
|
442
|
-
readonly nav: NavApi;
|
|
443
|
-
readonly host: ReturnHost | undefined;
|
|
444
|
-
readonly actionIcon: IconKey | undefined;
|
|
445
|
-
readonly defaultScreenTargetEntityId: string | undefined;
|
|
446
|
-
readonly sameEntityScreenId: ((targetScreen: string) => string | undefined) | undefined;
|
|
447
|
-
},
|
|
448
|
-
): RenderEditAction | undefined {
|
|
449
|
-
const {
|
|
450
|
-
record,
|
|
451
|
-
translate,
|
|
452
|
-
nav,
|
|
453
|
-
host,
|
|
454
|
-
actionIcon,
|
|
455
|
-
defaultScreenTargetEntityId,
|
|
456
|
-
sameEntityScreenId,
|
|
457
|
-
} = options;
|
|
458
|
-
if (action.entity !== undefined) {
|
|
459
|
-
const targetEntity = action.entity;
|
|
460
|
-
const id = action.entityId !== undefined ? String(record[action.entityId] ?? "") : "";
|
|
461
|
-
return {
|
|
462
|
-
id: action.id,
|
|
463
|
-
label: translate(action.label),
|
|
464
|
-
...(action.style !== undefined && { style: action.style }),
|
|
465
|
-
confirmRequired: false,
|
|
466
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
467
|
-
onPress: () => {
|
|
468
|
-
// No entityId on record (id === "") → nothing to navigate to.
|
|
469
|
-
if (id !== "") {
|
|
470
|
-
const params = navigateActionSearchParams(action, record);
|
|
471
|
-
navigateWithReturnTo(nav, { entity: targetEntity, id }, host, params);
|
|
472
|
-
}
|
|
473
|
-
},
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
if (action.screen !== undefined) {
|
|
477
|
-
const explicit =
|
|
478
|
-
action.entityId !== undefined ? String(record[action.entityId] ?? "") : undefined;
|
|
479
|
-
const fallback = sameEntityScreenId?.(action.screen) ?? defaultScreenTargetEntityId;
|
|
480
|
-
const navEntityId = explicit ?? fallback;
|
|
481
|
-
const targetScreen = action.screen;
|
|
482
|
-
return {
|
|
483
|
-
id: action.id,
|
|
484
|
-
label: translate(action.label),
|
|
485
|
-
...(action.style !== undefined && { style: action.style }),
|
|
486
|
-
confirmRequired: false,
|
|
487
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
488
|
-
onPress: () => {
|
|
489
|
-
const target: ScreenTarget = {
|
|
490
|
-
screenId: targetScreen,
|
|
491
|
-
...(navEntityId !== undefined && navEntityId !== "" && { entityId: navEntityId }),
|
|
492
|
-
};
|
|
493
|
-
const params = navigateActionSearchParams(action, record);
|
|
494
|
-
navigateWithReturnTo(nav, target, host, params);
|
|
495
|
-
},
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
return undefined;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function buildDrawerRecordAction(
|
|
502
|
-
action: RowActionDrawer,
|
|
503
|
-
options: {
|
|
504
|
-
readonly record: Readonly<Record<string, unknown>>;
|
|
505
|
-
readonly translate: Translate;
|
|
506
|
-
readonly actionIcon: IconKey | undefined;
|
|
507
|
-
readonly openDrawer: (
|
|
508
|
-
action: RowActionDrawer,
|
|
509
|
-
initialValues: Readonly<Record<string, unknown>> | undefined,
|
|
510
|
-
) => void;
|
|
511
|
-
},
|
|
512
|
-
): RenderEditAction {
|
|
513
|
-
const { record, translate, actionIcon, openDrawer } = options;
|
|
514
|
-
return {
|
|
515
|
-
id: action.id,
|
|
516
|
-
label: translate(action.label),
|
|
517
|
-
...(action.style !== undefined && { style: action.style }),
|
|
518
|
-
confirmRequired: false,
|
|
519
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
520
|
-
onPress: () => {
|
|
521
|
-
const initialValues =
|
|
522
|
-
action.params !== undefined ? evalRowExtractor(action.params, record) : undefined;
|
|
523
|
-
openDrawer(action, initialValues);
|
|
524
|
-
},
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function buildWriteHandlerRecordAction(
|
|
529
|
-
action: RowActionWriteHandler,
|
|
530
|
-
options: {
|
|
531
|
-
readonly record: Readonly<Record<string, unknown>>;
|
|
532
|
-
readonly translate: Translate;
|
|
533
|
-
readonly actionIcon: IconKey | undefined;
|
|
534
|
-
readonly dispatcher: Dispatcher;
|
|
535
|
-
readonly onWriteSuccess: () => void | Promise<void>;
|
|
536
|
-
},
|
|
537
|
-
): RenderEditAction {
|
|
538
|
-
const { record, translate, actionIcon, dispatcher, onWriteSuccess } = options;
|
|
539
|
-
return {
|
|
540
|
-
id: action.id,
|
|
541
|
-
label: translate(action.label),
|
|
542
|
-
...(action.style !== undefined && { style: action.style }),
|
|
543
|
-
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
544
|
-
...(action.confirm !== undefined && { confirm: translate(action.confirm) }),
|
|
545
|
-
...(action.confirmLabel !== undefined && {
|
|
546
|
-
confirmLabel: translate(action.confirmLabel),
|
|
547
|
-
}),
|
|
548
|
-
onPress: async () => {
|
|
549
|
-
const payload =
|
|
550
|
-
action.payload !== undefined
|
|
551
|
-
? evalRowExtractor(action.payload, record)
|
|
552
|
-
: { id: record["id"] };
|
|
553
|
-
const result = await dispatcher.write(action.handler, payload);
|
|
554
|
-
if (!result.isSuccess) {
|
|
555
|
-
throw new WriteFailedError(result.error, dispatcherErrorText(result.error, translate));
|
|
556
|
-
}
|
|
557
|
-
await onWriteSuccess();
|
|
558
|
-
},
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Shared builder for a RowAction[] evaluated against one "record"
|
|
563
|
-
// context (not a specific list row) — a head card's `screen.actions`, an
|
|
564
|
-
// entityEdit's own `screen.actions`, and any section-level
|
|
565
|
-
// `actions`/`emptyState.action` are all this same shape: a RowAction
|
|
566
|
-
// resolved against the record currently being viewed/edited, independent of
|
|
567
|
-
// any table row.
|
|
568
|
-
export function buildRecordActions(options: {
|
|
569
|
-
readonly actions: readonly RowAction[];
|
|
570
|
-
readonly record: Readonly<Record<string, unknown>>;
|
|
571
|
-
readonly translate: Translate;
|
|
572
|
-
readonly nav: NavApi;
|
|
573
|
-
readonly host: ReturnHost | undefined;
|
|
574
|
-
readonly dispatcher: Dispatcher | undefined;
|
|
575
|
-
readonly openDrawer: (
|
|
576
|
-
action: RowActionDrawer,
|
|
577
|
-
initialValues: Readonly<Record<string, unknown>> | undefined,
|
|
578
|
-
) => void;
|
|
579
|
-
/** Called after a writeHandler action succeeds (refetch/reload). */
|
|
580
|
-
readonly onWriteSuccess: () => void | Promise<void>;
|
|
581
|
-
/** Screen-target navigate default entityId when the action declares no
|
|
582
|
-
* explicit `entityId` field-source. A caller editing a fixed entity can
|
|
583
|
-
* pass its own `entityId` here directly (the record it already is). A
|
|
584
|
-
* caller whose shown record may differ from the navigate target's entity
|
|
585
|
-
* resolves this per-target via `sameEntityScreenId` instead. */
|
|
586
|
-
readonly defaultScreenTargetEntityId?: string;
|
|
587
|
-
/** Given a navigate action's target screen id, returns the record id to
|
|
588
|
-
* default to IF that screen edits the same entity the current record
|
|
589
|
-
* belongs to (cross-feature lookup) — undefined otherwise. Omitted by
|
|
590
|
-
* callers with no such cross-screen lookup. */
|
|
591
|
-
readonly sameEntityScreenId?: (targetScreen: string) => string | undefined;
|
|
592
|
-
}): readonly RenderEditAction[] | undefined {
|
|
593
|
-
const {
|
|
594
|
-
actions,
|
|
595
|
-
record,
|
|
596
|
-
translate,
|
|
597
|
-
nav,
|
|
598
|
-
host,
|
|
599
|
-
dispatcher,
|
|
600
|
-
openDrawer,
|
|
601
|
-
onWriteSuccess,
|
|
602
|
-
defaultScreenTargetEntityId,
|
|
603
|
-
sameEntityScreenId,
|
|
604
|
-
} = options;
|
|
605
|
-
const out: RenderEditAction[] = [];
|
|
606
|
-
for (const action of actions) {
|
|
607
|
-
if (action.visible !== undefined && !evalFieldCondition(action.visible, record)) continue;
|
|
608
|
-
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
609
|
-
if (action.kind === "navigate") {
|
|
610
|
-
const built = buildNavigateRecordAction(action, {
|
|
611
|
-
record,
|
|
612
|
-
translate,
|
|
613
|
-
nav,
|
|
614
|
-
host,
|
|
615
|
-
actionIcon,
|
|
616
|
-
defaultScreenTargetEntityId,
|
|
617
|
-
sameEntityScreenId,
|
|
618
|
-
});
|
|
619
|
-
if (built !== undefined) out.push(built);
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
if (action.kind === "drawer") {
|
|
623
|
-
out.push(buildDrawerRecordAction(action, { record, translate, actionIcon, openDrawer }));
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
// writeHandler — skip without a dispatcher instead of crashing.
|
|
627
|
-
if (dispatcher === undefined) continue;
|
|
628
|
-
out.push(
|
|
629
|
-
buildWriteHandlerRecordAction(action, {
|
|
630
|
-
record,
|
|
631
|
-
translate,
|
|
632
|
-
actionIcon,
|
|
633
|
-
dispatcher,
|
|
634
|
-
onWriteSuccess,
|
|
635
|
-
}),
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
return out.length > 0 ? out : undefined;
|
|
639
|
-
}
|
package/src/app/screen-access.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EntityEditScreenDefinition,
|
|
3
|
-
FeatureSchema,
|
|
4
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
-
import { isUiAccessGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
|
-
import { lastSegment } from "./qn";
|
|
7
|
-
|
|
8
|
-
// Shared UI-visibility predicate for the screen-render path (#1203 — nav
|
|
9
|
-
// filtering via filterByAccess in workspace-shell.tsx hid role-gated
|
|
10
|
-
// screens from the menu, but a direct URL/screenQn hit reached
|
|
11
|
-
// KumikoScreen unchecked). Public name kept as an alias of the shared
|
|
12
|
-
// predicate that headless/nav and workspace-shell use as well.
|
|
13
|
-
export { isUiAccessGranted as screenAccessAllows };
|
|
14
|
-
|
|
15
|
-
// A navigate target (metric click) declares no access rule of its own — the
|
|
16
|
-
// destination screen already does, so the jump is offered exactly when the
|
|
17
|
-
// user may open where it lands (solon#424). No second copy of the rule to
|
|
18
|
-
// drift from the screen's own.
|
|
19
|
-
export function navigateTargetAllows(
|
|
20
|
-
navigate: { readonly screen?: string; readonly entity?: string },
|
|
21
|
-
appFeatures: readonly FeatureSchema[],
|
|
22
|
-
userRoles: readonly string[] | undefined,
|
|
23
|
-
): boolean {
|
|
24
|
-
const { screen, entity } = navigate;
|
|
25
|
-
// Neither set — stays on the current screen (tab switch), already allowed.
|
|
26
|
-
if (screen === undefined && entity === undefined) return true;
|
|
27
|
-
// KumikoScreen rendered outside createKumikoApp has no screen registry to
|
|
28
|
-
// resolve against; gating on an empty one would hide every jump, not the
|
|
29
|
-
// forbidden ones.
|
|
30
|
-
if (appFeatures.length === 0) return true;
|
|
31
|
-
for (const feature of appFeatures) {
|
|
32
|
-
for (const candidate of feature.screens) {
|
|
33
|
-
const isTarget =
|
|
34
|
-
entity !== undefined
|
|
35
|
-
? candidate.detailFor === entity
|
|
36
|
-
: lastSegment(candidate.id) === screen;
|
|
37
|
-
// First match IS the destination: short ids are globally unique
|
|
38
|
-
// (validateScreenShortIdCollisions) and both the router and
|
|
39
|
-
// resolveTarget take the first hit.
|
|
40
|
-
if (isTarget) return isUiAccessGranted(candidate.access, userRoles);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// Unresolvable target — the boot-validator doesn't check metric navigate
|
|
44
|
-
// targets, so this is a typo'd dead jump. Don't offer it.
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Searches all mounted features, and access is part of the predicate so a
|
|
49
|
-
// role-gated first match can't hide an accessible second one.
|
|
50
|
-
export function findEditScreenFor(
|
|
51
|
-
entity: string,
|
|
52
|
-
appFeatures: readonly FeatureSchema[],
|
|
53
|
-
userRoles: readonly string[] | undefined,
|
|
54
|
-
): EntityEditScreenDefinition | undefined {
|
|
55
|
-
for (const feature of appFeatures) {
|
|
56
|
-
const match = feature.screens.find(
|
|
57
|
-
(s): s is EntityEditScreenDefinition =>
|
|
58
|
-
s.type === "entityEdit" && s.entity === entity && isUiAccessGranted(s.access, userRoles),
|
|
59
|
-
);
|
|
60
|
-
if (match !== undefined) return match;
|
|
61
|
-
}
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|