@cosmicdrift/kumiko-renderer 0.328.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 -46
- 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 -41
- 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/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- 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/__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-fill-height.test.tsx +0 -118
- 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 -128
- 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 -3801
- 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 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- 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 -1733
- 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 -88
- 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 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- 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-query.test.tsx +0 -149
- 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 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- 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 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- 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
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { evalFieldCondition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
3
|
+
import { lastSegment } from "./qn.js";
|
|
4
|
+
import { navigateWithReturnTo } from "./return-to.js";
|
|
5
|
+
import { dispatcherErrorText, WriteFailedError } from "./write-failed-error.js";
|
|
6
|
+
// entityId is explicit: the edit screen may live in another feature than
|
|
7
|
+
// the row source, where the same-feature fallback would miss it.
|
|
8
|
+
export function buildDefaultEditRowAction(editScreen, idColumn = "id") {
|
|
9
|
+
if (editScreen === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
kind: "navigate",
|
|
13
|
+
id: "edit",
|
|
14
|
+
label: "kumiko.actions.edit",
|
|
15
|
+
screen: lastSegment(editScreen.id),
|
|
16
|
+
entityId: idColumn,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function evalRowExtractor(extractor, row) {
|
|
20
|
+
if ("pick" in extractor) {
|
|
21
|
+
return Object.fromEntries(extractor.pick.map((f) => [f, row[f]]));
|
|
22
|
+
}
|
|
23
|
+
return Object.fromEntries(Object.entries(extractor.map).map(([to, from]) => [to, row[from]]));
|
|
24
|
+
}
|
|
25
|
+
export function isWriteHandlerRowAction(action) {
|
|
26
|
+
return action.kind === "writeHandler" || action.kind === undefined;
|
|
27
|
+
}
|
|
28
|
+
function isPlainObject(value) {
|
|
29
|
+
if (typeof value !== "object" || value === null)
|
|
30
|
+
return false;
|
|
31
|
+
const proto = Object.getPrototypeOf(value);
|
|
32
|
+
return proto === Object.prototype || proto === null;
|
|
33
|
+
}
|
|
34
|
+
export function stringifyNavParams(params) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [k, v] of Object.entries(params)) {
|
|
37
|
+
// Structured field values such as money `{amount, currency}` must survive
|
|
38
|
+
// the URL round-trip as JSON; Date & other class instances stay String().
|
|
39
|
+
out[k] =
|
|
40
|
+
v === null || v === undefined
|
|
41
|
+
? null
|
|
42
|
+
: Array.isArray(v) || isPlainObject(v)
|
|
43
|
+
? JSON.stringify(v)
|
|
44
|
+
: String(v);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function navigateActionSearchParams(action, values) {
|
|
49
|
+
const params = action.params !== undefined
|
|
50
|
+
? stringifyNavParams(evalRowExtractor(action.params, values))
|
|
51
|
+
: undefined;
|
|
52
|
+
if (action.tab === undefined)
|
|
53
|
+
return params;
|
|
54
|
+
return { ...params, tab: action.tab };
|
|
55
|
+
}
|
|
56
|
+
export async function refetchAfterWrite(refetch) {
|
|
57
|
+
await refetch().catch((err) => {
|
|
58
|
+
// biome-ignore lint/suspicious/noConsole: refetch must not poison the write-action error path
|
|
59
|
+
console.error("kumiko-screen: refetch after write action failed", err);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Navigate execution for query-driven rows (projectionList, and relatedList
|
|
63
|
+
// — both have no guaranteed "id" field, unlike entityList's rows which back
|
|
64
|
+
// a real entity). No same-entity row["id"] fallback (see EntityListBody's
|
|
65
|
+
// own runNavigate for that variant, which stays separate — entityList's
|
|
66
|
+
// fallback needs `screen.entity`, which neither projectionList nor
|
|
67
|
+
// relatedList has).
|
|
68
|
+
export function runProjectionRowNavigate(nav, action, row, host) {
|
|
69
|
+
if (action.entity !== undefined) {
|
|
70
|
+
const id = action.entityId !== undefined ? String(row.values[action.entityId] ?? "") : "";
|
|
71
|
+
// skip: no entityId column on this row — nothing to navigate to.
|
|
72
|
+
if (id === "")
|
|
73
|
+
return;
|
|
74
|
+
const params = navigateActionSearchParams(action, row.values);
|
|
75
|
+
navigateWithReturnTo(nav, { entity: action.entity, id }, host, params);
|
|
76
|
+
}
|
|
77
|
+
else if (action.screen !== undefined) {
|
|
78
|
+
const entityId = action.entityId !== undefined ? String(row.values[action.entityId] ?? "") : undefined;
|
|
79
|
+
const target = {
|
|
80
|
+
screenId: action.screen,
|
|
81
|
+
...(entityId !== undefined && entityId !== "" && { entityId }),
|
|
82
|
+
};
|
|
83
|
+
const params = navigateActionSearchParams(action, row.values);
|
|
84
|
+
navigateWithReturnTo(nav, target, host, params);
|
|
85
|
+
}
|
|
86
|
+
// skip: neither entity nor screen set — the boot-validator rejects this
|
|
87
|
+
// shape (resolveRowActionNavigateTarget), so this only guards types.
|
|
88
|
+
}
|
|
89
|
+
function buildNavigateRowAction(action, translate, nav, host) {
|
|
90
|
+
const { visible } = action;
|
|
91
|
+
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
92
|
+
return {
|
|
93
|
+
id: action.id,
|
|
94
|
+
label: translate(action.label),
|
|
95
|
+
...(action.style !== undefined && { style: action.style }),
|
|
96
|
+
confirmRequired: false,
|
|
97
|
+
...(action.rowClick === true && { rowClick: true }),
|
|
98
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
99
|
+
onTrigger: (row) => runProjectionRowNavigate(nav, action, row, host),
|
|
100
|
+
...(visible !== undefined && {
|
|
101
|
+
isVisible: (row) => evalFieldCondition(visible, row.values),
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Dedupes the "openDrawer not wired" warning per action id, since the
|
|
106
|
+
// builders re-run inside a useMemo on every dep change.
|
|
107
|
+
const warnedDrawerActionIds = new Set();
|
|
108
|
+
/** @internal test-only */
|
|
109
|
+
export function resetDrawerActionWarningsForTests() {
|
|
110
|
+
warnedDrawerActionIds.clear();
|
|
111
|
+
}
|
|
112
|
+
function warnDrawerActionDropped(actionKind, actionId) {
|
|
113
|
+
const key = `${actionKind}:${actionId}`;
|
|
114
|
+
// skip: already warned for this id — suppresses the repeat, not the warning itself.
|
|
115
|
+
if (warnedDrawerActionIds.has(key))
|
|
116
|
+
return;
|
|
117
|
+
warnedDrawerActionIds.add(key);
|
|
118
|
+
// biome-ignore lint/suspicious/noConsole: dev-warning for a setup error
|
|
119
|
+
console.warn(`[kumiko] ${actionKind} "${actionId}" is kind:"drawer", but the host did not wire openDrawer (RelatedListSection: pass onOpenDrawer) — it will not render.`);
|
|
120
|
+
}
|
|
121
|
+
function buildDrawerRowAction(action, translate, openDrawer) {
|
|
122
|
+
const { visible, params } = action;
|
|
123
|
+
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
124
|
+
return {
|
|
125
|
+
id: action.id,
|
|
126
|
+
label: translate(action.label),
|
|
127
|
+
...(action.style !== undefined && { style: action.style }),
|
|
128
|
+
confirmRequired: false,
|
|
129
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
130
|
+
onTrigger: (row) => {
|
|
131
|
+
openDrawer(action, params !== undefined ? evalRowExtractor(params, row.values) : undefined);
|
|
132
|
+
},
|
|
133
|
+
...(visible !== undefined && {
|
|
134
|
+
isVisible: (row) => evalFieldCondition(visible, row.values),
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// Prepends defaultEditRowAction unless a declared action already claims id
|
|
139
|
+
// "edit" — declared wins.
|
|
140
|
+
function mergeDefaultEditAction(rowActions, defaultEditRowAction) {
|
|
141
|
+
const declaredHasEdit = rowActions?.some((a) => a.id === "edit") === true;
|
|
142
|
+
return defaultEditRowAction !== undefined && !declaredHasEdit
|
|
143
|
+
? [defaultEditRowAction, ...(rowActions ?? [])]
|
|
144
|
+
: (rowActions ?? []);
|
|
145
|
+
}
|
|
146
|
+
function buildWriteHandlerRowAction(action, translate, refetch, dispatcher) {
|
|
147
|
+
const writeVisible = action.visible;
|
|
148
|
+
return {
|
|
149
|
+
id: action.id,
|
|
150
|
+
label: translate(action.label),
|
|
151
|
+
...(action.style !== undefined && { style: action.style }),
|
|
152
|
+
icon: resolveActionIcon(action.id, action.icon),
|
|
153
|
+
...(action.confirm !== undefined && { confirm: translate(action.confirm) }),
|
|
154
|
+
...(action.confirmLabel !== undefined && {
|
|
155
|
+
confirmLabel: translate(action.confirmLabel),
|
|
156
|
+
}),
|
|
157
|
+
onTrigger: async (row) => {
|
|
158
|
+
const payload = action.payload !== undefined
|
|
159
|
+
? evalRowExtractor(action.payload, row.values)
|
|
160
|
+
: { id: row.values["id"] };
|
|
161
|
+
const result = await dispatcher.write(action.handler, payload);
|
|
162
|
+
if (!result.isSuccess) {
|
|
163
|
+
throw new WriteFailedError(result.error, dispatcherErrorText(result.error, translate));
|
|
164
|
+
}
|
|
165
|
+
await refetchAfterWrite(refetch);
|
|
166
|
+
},
|
|
167
|
+
...(writeVisible !== undefined && {
|
|
168
|
+
isVisible: (row) => evalFieldCondition(writeVisible, row.values),
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Builds the DataTable-ready row-action set for a query-driven row source
|
|
173
|
+
// (projectionList, relatedList) — navigate dispatches through
|
|
174
|
+
// runProjectionRowNavigate, writeHandler dispatches through the shared
|
|
175
|
+
// Dispatcher and refetches `refetch` on success. Single implementation so
|
|
176
|
+
// projectionList's rowActions and a projectionDetail relatedList section's
|
|
177
|
+
// rowActions can't drift apart (fw editable-detail-screens).
|
|
178
|
+
export function buildProjectionRowActions(options) {
|
|
179
|
+
const { rowActions, translate, dispatcher, nav, refetch, openDrawer, defaultEditRowAction, host, } = options;
|
|
180
|
+
const effectiveActions = mergeDefaultEditAction(rowActions, defaultEditRowAction);
|
|
181
|
+
if (effectiveActions.length === 0)
|
|
182
|
+
return undefined;
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const action of effectiveActions) {
|
|
185
|
+
if (action.kind === "navigate") {
|
|
186
|
+
out.push(buildNavigateRowAction(action, translate, nav, host));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (action.kind === "drawer") {
|
|
190
|
+
if (openDrawer === undefined) {
|
|
191
|
+
warnDrawerActionDropped("rowAction", action.id);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
out.push(buildDrawerRowAction(action, translate, openDrawer));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// writeHandler (default-kind) — a swallowed failure result must become a
|
|
198
|
+
// thrown error (fw prod-bug 2026-06-07), same as every other write path.
|
|
199
|
+
if (dispatcher === undefined)
|
|
200
|
+
continue;
|
|
201
|
+
out.push(buildWriteHandlerRowAction(action, translate, refetch, dispatcher));
|
|
202
|
+
}
|
|
203
|
+
return out.length > 0 ? out : undefined;
|
|
204
|
+
}
|
|
205
|
+
function buildNavigateToolbarAction(action, translate, nav, host, prefill, record) {
|
|
206
|
+
const actionIcon = resolveActionIcon(action.id);
|
|
207
|
+
const target = { screenId: action.screen };
|
|
208
|
+
return {
|
|
209
|
+
id: action.id,
|
|
210
|
+
label: translate(action.label),
|
|
211
|
+
...(action.style !== undefined && { style: action.style }),
|
|
212
|
+
confirmRequired: false,
|
|
213
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
214
|
+
onTrigger: () => {
|
|
215
|
+
// A declared `params` extractor (evaluated against the relatedList's
|
|
216
|
+
// parent record) replaces the caller's implicit prefill (e.g. a
|
|
217
|
+
// relatedList's `{ [parentParam]: parentId }` or, with `parentFilter`
|
|
218
|
+
// set, `{ [parentFilter.field]: parentId }`) rather than merging
|
|
219
|
+
// with it — same "params present → drop the default" rule as
|
|
220
|
+
// rowActions.
|
|
221
|
+
const resolvedParams = action.params !== undefined && record !== undefined
|
|
222
|
+
? evalRowExtractor(action.params, record)
|
|
223
|
+
: prefill;
|
|
224
|
+
const stringifiedParams = resolvedParams !== undefined ? stringifyNavParams(resolvedParams) : undefined;
|
|
225
|
+
const params = action.tab === undefined ? stringifiedParams : { ...stringifiedParams, tab: action.tab };
|
|
226
|
+
navigateWithReturnTo(nav, target, host, params);
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function buildDrawerToolbarAction(action, translate, openDrawer) {
|
|
231
|
+
const actionIcon = resolveActionIcon(action.id);
|
|
232
|
+
return {
|
|
233
|
+
id: action.id,
|
|
234
|
+
label: translate(action.label),
|
|
235
|
+
...(action.style !== undefined && { style: action.style }),
|
|
236
|
+
confirmRequired: false,
|
|
237
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
238
|
+
onTrigger: () => openDrawer(action),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function buildWriteHandlerToolbarAction(action, translate, refetch, dispatcher) {
|
|
242
|
+
const actionIcon = resolveActionIcon(action.id);
|
|
243
|
+
return {
|
|
244
|
+
id: action.id,
|
|
245
|
+
label: translate(action.label),
|
|
246
|
+
...(action.style !== undefined && { style: action.style }),
|
|
247
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
248
|
+
...(action.confirm !== undefined && { confirm: translate(action.confirm) }),
|
|
249
|
+
...(action.confirmLabel !== undefined && {
|
|
250
|
+
confirmLabel: translate(action.confirmLabel),
|
|
251
|
+
}),
|
|
252
|
+
onTrigger: async () => {
|
|
253
|
+
const result = await dispatcher.write(action.handler, action.payload ?? {});
|
|
254
|
+
if (!result.isSuccess) {
|
|
255
|
+
throw new WriteFailedError(result.error, dispatcherErrorText(result.error, translate));
|
|
256
|
+
}
|
|
257
|
+
await refetchAfterWrite(refetch);
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// Shared by entityList, projectionList, and relatedList toolbars so the
|
|
262
|
+
// three call sites can't drift apart.
|
|
263
|
+
export function buildProjectionToolbarActions(options) {
|
|
264
|
+
const { toolbarActions, translate, dispatcher, nav, refetch, openDrawer, navigatePrefill, record, host, } = options;
|
|
265
|
+
if (toolbarActions === undefined)
|
|
266
|
+
return undefined;
|
|
267
|
+
const out = [];
|
|
268
|
+
for (const action of toolbarActions) {
|
|
269
|
+
if (action.visible !== undefined &&
|
|
270
|
+
record !== undefined &&
|
|
271
|
+
!evalFieldCondition(action.visible, record)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (action.kind === "navigate") {
|
|
275
|
+
out.push(buildNavigateToolbarAction(action, translate, nav, host, navigatePrefill, record));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (action.kind === "drawer") {
|
|
279
|
+
if (openDrawer === undefined) {
|
|
280
|
+
warnDrawerActionDropped("toolbarAction", action.id);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
out.push(buildDrawerToolbarAction(action, translate, openDrawer));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// writeHandler — skip without a dispatcher instead of crashing (same as rowActions).
|
|
287
|
+
if (dispatcher === undefined)
|
|
288
|
+
continue;
|
|
289
|
+
out.push(buildWriteHandlerToolbarAction(action, translate, refetch, dispatcher));
|
|
290
|
+
}
|
|
291
|
+
return out.length > 0 ? out : undefined;
|
|
292
|
+
}
|
|
293
|
+
function buildNavigateRecordAction(action, options) {
|
|
294
|
+
const { record, translate, nav, host, actionIcon, defaultScreenTargetEntityId, sameEntityScreenId, } = options;
|
|
295
|
+
if (action.entity !== undefined) {
|
|
296
|
+
const targetEntity = action.entity;
|
|
297
|
+
const id = action.entityId !== undefined ? String(record[action.entityId] ?? "") : "";
|
|
298
|
+
return {
|
|
299
|
+
id: action.id,
|
|
300
|
+
label: translate(action.label),
|
|
301
|
+
...(action.style !== undefined && { style: action.style }),
|
|
302
|
+
confirmRequired: false,
|
|
303
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
304
|
+
onPress: () => {
|
|
305
|
+
// No entityId on record (id === "") → nothing to navigate to.
|
|
306
|
+
if (id !== "") {
|
|
307
|
+
const params = navigateActionSearchParams(action, record);
|
|
308
|
+
navigateWithReturnTo(nav, { entity: targetEntity, id }, host, params);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (action.screen !== undefined) {
|
|
314
|
+
const explicit = action.entityId !== undefined ? String(record[action.entityId] ?? "") : undefined;
|
|
315
|
+
const fallback = sameEntityScreenId?.(action.screen) ?? defaultScreenTargetEntityId;
|
|
316
|
+
const navEntityId = explicit ?? fallback;
|
|
317
|
+
const targetScreen = action.screen;
|
|
318
|
+
return {
|
|
319
|
+
id: action.id,
|
|
320
|
+
label: translate(action.label),
|
|
321
|
+
...(action.style !== undefined && { style: action.style }),
|
|
322
|
+
confirmRequired: false,
|
|
323
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
324
|
+
onPress: () => {
|
|
325
|
+
const target = {
|
|
326
|
+
screenId: targetScreen,
|
|
327
|
+
...(navEntityId !== undefined && navEntityId !== "" && { entityId: navEntityId }),
|
|
328
|
+
};
|
|
329
|
+
const params = navigateActionSearchParams(action, record);
|
|
330
|
+
navigateWithReturnTo(nav, target, host, params);
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
function buildDrawerRecordAction(action, options) {
|
|
337
|
+
const { record, translate, actionIcon, openDrawer } = options;
|
|
338
|
+
return {
|
|
339
|
+
id: action.id,
|
|
340
|
+
label: translate(action.label),
|
|
341
|
+
...(action.style !== undefined && { style: action.style }),
|
|
342
|
+
confirmRequired: false,
|
|
343
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
344
|
+
onPress: () => {
|
|
345
|
+
const initialValues = action.params !== undefined ? evalRowExtractor(action.params, record) : undefined;
|
|
346
|
+
openDrawer(action, initialValues);
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function buildWriteHandlerRecordAction(action, options) {
|
|
351
|
+
const { record, translate, actionIcon, dispatcher, onWriteSuccess } = options;
|
|
352
|
+
return {
|
|
353
|
+
id: action.id,
|
|
354
|
+
label: translate(action.label),
|
|
355
|
+
...(action.style !== undefined && { style: action.style }),
|
|
356
|
+
...(actionIcon !== undefined && { icon: actionIcon }),
|
|
357
|
+
...(action.confirm !== undefined && { confirm: translate(action.confirm) }),
|
|
358
|
+
...(action.confirmLabel !== undefined && {
|
|
359
|
+
confirmLabel: translate(action.confirmLabel),
|
|
360
|
+
}),
|
|
361
|
+
onPress: async () => {
|
|
362
|
+
const payload = action.payload !== undefined
|
|
363
|
+
? evalRowExtractor(action.payload, record)
|
|
364
|
+
: { id: record["id"] };
|
|
365
|
+
const result = await dispatcher.write(action.handler, payload);
|
|
366
|
+
if (!result.isSuccess) {
|
|
367
|
+
throw new WriteFailedError(result.error, dispatcherErrorText(result.error, translate));
|
|
368
|
+
}
|
|
369
|
+
await onWriteSuccess();
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// Shared builder for a RowAction[] evaluated against one "record"
|
|
374
|
+
// context (not a specific list row) — a head card's `screen.actions`, an
|
|
375
|
+
// entityEdit's own `screen.actions`, and any section-level
|
|
376
|
+
// `actions`/`emptyState.action` are all this same shape: a RowAction
|
|
377
|
+
// resolved against the record currently being viewed/edited, independent of
|
|
378
|
+
// any table row.
|
|
379
|
+
export function buildRecordActions(options) {
|
|
380
|
+
const { actions, record, translate, nav, host, dispatcher, openDrawer, onWriteSuccess, defaultScreenTargetEntityId, sameEntityScreenId, } = options;
|
|
381
|
+
const out = [];
|
|
382
|
+
for (const action of actions) {
|
|
383
|
+
if (action.visible !== undefined && !evalFieldCondition(action.visible, record))
|
|
384
|
+
continue;
|
|
385
|
+
const actionIcon = resolveActionIcon(action.id, action.icon);
|
|
386
|
+
if (action.kind === "navigate") {
|
|
387
|
+
const built = buildNavigateRecordAction(action, {
|
|
388
|
+
record,
|
|
389
|
+
translate,
|
|
390
|
+
nav,
|
|
391
|
+
host,
|
|
392
|
+
actionIcon,
|
|
393
|
+
defaultScreenTargetEntityId,
|
|
394
|
+
sameEntityScreenId,
|
|
395
|
+
});
|
|
396
|
+
if (built !== undefined)
|
|
397
|
+
out.push(built);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (action.kind === "drawer") {
|
|
401
|
+
out.push(buildDrawerRecordAction(action, { record, translate, actionIcon, openDrawer }));
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// writeHandler — skip without a dispatcher instead of crashing.
|
|
405
|
+
if (dispatcher === undefined)
|
|
406
|
+
continue;
|
|
407
|
+
out.push(buildWriteHandlerRecordAction(action, {
|
|
408
|
+
record,
|
|
409
|
+
translate,
|
|
410
|
+
actionIcon,
|
|
411
|
+
dispatcher,
|
|
412
|
+
onWriteSuccess,
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
return out.length > 0 ? out : undefined;
|
|
416
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EntityEditScreenDefinition, FeatureSchema } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { isUiAccessGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
+
export { isUiAccessGranted as screenAccessAllows };
|
|
4
|
+
export declare function navigateTargetAllows(navigate: {
|
|
5
|
+
readonly screen?: string;
|
|
6
|
+
readonly entity?: string;
|
|
7
|
+
}, appFeatures: readonly FeatureSchema[], userRoles: readonly string[] | undefined): boolean;
|
|
8
|
+
export declare function findEditScreenFor(entity: string, appFeatures: readonly FeatureSchema[], userRoles: readonly string[] | undefined): EntityEditScreenDefinition | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isUiAccessGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { lastSegment } from "./qn.js";
|
|
3
|
+
// Shared UI-visibility predicate for the screen-render path (#1203 — nav
|
|
4
|
+
// filtering via filterByAccess in workspace-shell.tsx hid role-gated
|
|
5
|
+
// screens from the menu, but a direct URL/screenQn hit reached
|
|
6
|
+
// KumikoScreen unchecked). Public name kept as an alias of the shared
|
|
7
|
+
// predicate that headless/nav and workspace-shell use as well.
|
|
8
|
+
export { isUiAccessGranted as screenAccessAllows };
|
|
9
|
+
// A navigate target (metric click) declares no access rule of its own — the
|
|
10
|
+
// destination screen already does, so the jump is offered exactly when the
|
|
11
|
+
// user may open where it lands (solon#424). No second copy of the rule to
|
|
12
|
+
// drift from the screen's own.
|
|
13
|
+
export function navigateTargetAllows(navigate, appFeatures, userRoles) {
|
|
14
|
+
const { screen, entity } = navigate;
|
|
15
|
+
// Neither set — stays on the current screen (tab switch), already allowed.
|
|
16
|
+
if (screen === undefined && entity === undefined)
|
|
17
|
+
return true;
|
|
18
|
+
// KumikoScreen rendered outside createKumikoApp has no screen registry to
|
|
19
|
+
// resolve against; gating on an empty one would hide every jump, not the
|
|
20
|
+
// forbidden ones.
|
|
21
|
+
if (appFeatures.length === 0)
|
|
22
|
+
return true;
|
|
23
|
+
for (const feature of appFeatures) {
|
|
24
|
+
for (const candidate of feature.screens) {
|
|
25
|
+
const isTarget = entity !== undefined
|
|
26
|
+
? candidate.detailFor === entity
|
|
27
|
+
: lastSegment(candidate.id) === screen;
|
|
28
|
+
// First match IS the destination: short ids are globally unique
|
|
29
|
+
// (validateScreenShortIdCollisions) and both the router and
|
|
30
|
+
// resolveTarget take the first hit.
|
|
31
|
+
if (isTarget)
|
|
32
|
+
return isUiAccessGranted(candidate.access, userRoles);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Unresolvable target — the boot-validator doesn't check metric navigate
|
|
36
|
+
// targets, so this is a typo'd dead jump. Don't offer it.
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// Searches all mounted features, and access is part of the predicate so a
|
|
40
|
+
// role-gated first match can't hide an accessible second one.
|
|
41
|
+
export function findEditScreenFor(entity, appFeatures, userRoles) {
|
|
42
|
+
for (const feature of appFeatures) {
|
|
43
|
+
const match = feature.screens.find((s) => s.type === "entityEdit" && s.entity === entity && isUiAccessGranted(s.access, userRoles));
|
|
44
|
+
if (match !== undefined)
|
|
45
|
+
return match;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SecretMintScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
import type { FeatureSchema } from "./feature-schema.js";
|
|
5
|
+
export type SecretMintBodyProps = {
|
|
6
|
+
readonly schema: FeatureSchema;
|
|
7
|
+
readonly screen: SecretMintScreenDefinition;
|
|
8
|
+
readonly translate?: Translate;
|
|
9
|
+
};
|
|
10
|
+
export declare function SecretMintBody({ schema, screen, translate }: SecretMintBodyProps): ReactNode;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { RenderEdit } from "../components/render-edit.js";
|
|
4
|
+
import { useTranslation } from "../i18n.js";
|
|
5
|
+
import { usePrimitives } from "../primitives.js";
|
|
6
|
+
import { synthesizeActionFormEntity, synthesizeActionFormScreen, synthesizeSecretMintConfirmScreen, } from "./action-form-shim.js";
|
|
7
|
+
import { buildInitialValues, literalCurrencyOverrides, mergeSearchParamsIntoInitial, } from "./kumiko-screen.js";
|
|
8
|
+
import { layoutFieldNames } from "./layout-fields.js";
|
|
9
|
+
import { useInitialValuesHandoff, useNav } from "./nav.js";
|
|
10
|
+
import { lastSegment } from "./qn.js";
|
|
11
|
+
import { navigateToReturnOr, useReturnTarget } from "./return-to.js";
|
|
12
|
+
function extractRevealValue(data, field) {
|
|
13
|
+
if (typeof data !== "object" || data === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (!Object.hasOwn(data, field))
|
|
16
|
+
return undefined;
|
|
17
|
+
return data[field];
|
|
18
|
+
}
|
|
19
|
+
function extractCarriedValues(data, carry) {
|
|
20
|
+
const carried = {};
|
|
21
|
+
for (const field of carry) {
|
|
22
|
+
const value = extractRevealValue(data, field);
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
continue;
|
|
25
|
+
carried[field] = value;
|
|
26
|
+
}
|
|
27
|
+
return carried;
|
|
28
|
+
}
|
|
29
|
+
function isBlank(value) {
|
|
30
|
+
if (value === undefined || value === null || value === "")
|
|
31
|
+
return true;
|
|
32
|
+
return Array.isArray(value) && value.length === 0;
|
|
33
|
+
}
|
|
34
|
+
// Mint form → one-time reveal → optional confirm → done. The revealed values
|
|
35
|
+
// live ONLY in this component's `revealed` state — never in the URL, a query
|
|
36
|
+
// cache, or nav — the write-handler's success payload is the only place the
|
|
37
|
+
// secret ever exists (fw#2548). onSubmit copies exclusively the fields
|
|
38
|
+
// declared in `screen.reveal.fields` out of that payload (a whitelist, never
|
|
39
|
+
// the payload as a whole) so an unrelated field (e.g. an internal "id") can
|
|
40
|
+
// never leak into the reveal. `screen.confirm` (fw#2838, e.g. TOTP enroll:
|
|
41
|
+
// scan the code, then enter one) works the same way for its own carried
|
|
42
|
+
// mint-payload fields — those live only in `carriedRef`, never in React state
|
|
43
|
+
// that gets rendered, never in the confirm form's own values.
|
|
44
|
+
export function SecretMintBody({ schema, screen, translate }) {
|
|
45
|
+
const nav = useNav();
|
|
46
|
+
const returnTarget = useReturnTarget(screen.id);
|
|
47
|
+
const { Card, Heading, Banner, Button, Text, Grid, GridCell, SecretReveal } = usePrimitives();
|
|
48
|
+
const t = useTranslation();
|
|
49
|
+
const effectiveTranslate = translate ?? t;
|
|
50
|
+
const synthEntity = useMemo(() => synthesizeActionFormEntity(screen.fields), [screen.fields]);
|
|
51
|
+
const synthScreen = useMemo(() => synthesizeActionFormScreen(screen), [screen]);
|
|
52
|
+
const handoffValues = useInitialValuesHandoff(screen.id);
|
|
53
|
+
const initial = useMemo(() => mergeSearchParamsIntoInitial(screen.fields, {
|
|
54
|
+
searchParams: nav.searchParams,
|
|
55
|
+
urlPrefillFields: screen.urlPrefillFields,
|
|
56
|
+
renderableFields: layoutFieldNames(synthScreen),
|
|
57
|
+
// A mint form has no entity either, so its money fields name their own
|
|
58
|
+
// currency source (fw#2839). Only the literal form is resolved here:
|
|
59
|
+
// `{ kind: "tenant" }` would need the config query this screen never
|
|
60
|
+
// runs, and a one-time secret mint has no money use case to justify it.
|
|
61
|
+
moneyCurrencyOverrides: literalCurrencyOverrides(screen.fields),
|
|
62
|
+
...(handoffValues !== undefined && { handoffValues }),
|
|
63
|
+
}), [screen.fields, screen.urlPrefillFields, nav.searchParams, synthScreen, handoffValues]);
|
|
64
|
+
const [revealed, setRevealed] = useState(null);
|
|
65
|
+
const [done, setDone] = useState(false);
|
|
66
|
+
// Never rendered, never merged into `revealed` or the confirm form's
|
|
67
|
+
// initial values — read only from `buildPayload` at confirm-submit time.
|
|
68
|
+
const carriedRef = useRef({});
|
|
69
|
+
const confirm = screen.confirm;
|
|
70
|
+
const confirmEntity = useMemo(() => (confirm !== undefined ? synthesizeActionFormEntity(confirm.fields) : undefined), [confirm]);
|
|
71
|
+
const confirmScreen = useMemo(() => (confirm !== undefined ? synthesizeSecretMintConfirmScreen(screen, confirm) : undefined), [screen, confirm]);
|
|
72
|
+
const confirmInitial = useMemo(() => confirm !== undefined
|
|
73
|
+
? buildInitialValues(confirm.fields, undefined, literalCurrencyOverrides(confirm.fields))
|
|
74
|
+
: undefined, [confirm]);
|
|
75
|
+
const handleSubmitted = useCallback((result) => {
|
|
76
|
+
if (!result.isSuccess)
|
|
77
|
+
return;
|
|
78
|
+
const values = {};
|
|
79
|
+
for (const revealField of screen.reveal.fields) {
|
|
80
|
+
const value = extractRevealValue(result.data, revealField.field);
|
|
81
|
+
if (value === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
values[revealField.field] = value;
|
|
84
|
+
}
|
|
85
|
+
setRevealed(values);
|
|
86
|
+
carriedRef.current =
|
|
87
|
+
confirm?.carry !== undefined ? extractCarriedValues(result.data, confirm.carry) : {};
|
|
88
|
+
}, [screen.reveal.fields, confirm?.carry]);
|
|
89
|
+
const handleCancel = useMemo(() => {
|
|
90
|
+
const target = screen.cancelTarget ?? screen.redirect;
|
|
91
|
+
if (target === undefined || target === false)
|
|
92
|
+
return undefined;
|
|
93
|
+
return () => navigateToReturnOr(nav, returnTarget, () => nav.navigate({ screenId: lastSegment(target) }));
|
|
94
|
+
}, [nav, screen.redirect, screen.cancelTarget, returnTarget]);
|
|
95
|
+
// Ends the reveal phase for both paths (the bare acknowledge button, and a
|
|
96
|
+
// successful confirm submit): clears the secret and the carried values, then
|
|
97
|
+
// either navigates (screen.redirect) or shows a done-state — never falls
|
|
98
|
+
// back to re-rendering the mint form, which would let a stray click mint
|
|
99
|
+
// (and invalidate) the secret again.
|
|
100
|
+
const finishMint = useCallback(() => {
|
|
101
|
+
setRevealed(null);
|
|
102
|
+
carriedRef.current = {};
|
|
103
|
+
const redirect = screen.redirect;
|
|
104
|
+
if (redirect !== undefined) {
|
|
105
|
+
navigateToReturnOr(nav, returnTarget, () => nav.navigate({ screenId: lastSegment(redirect) }));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
setDone(true);
|
|
109
|
+
}
|
|
110
|
+
}, [nav, screen.redirect, returnTarget]);
|
|
111
|
+
const handleConfirmSubmitted = useCallback((result) => {
|
|
112
|
+
if (result.isSuccess)
|
|
113
|
+
finishMint();
|
|
114
|
+
}, [finishMint]);
|
|
115
|
+
if (done) {
|
|
116
|
+
return (_jsx(Card, { children: _jsx(Banner, { variant: "info", testId: "kumiko-screen-secret-mint-done", children: effectiveTranslate(confirm?.doneMessage ?? "kumiko.secretMint.done") }) }));
|
|
117
|
+
}
|
|
118
|
+
if (revealed !== null) {
|
|
119
|
+
const values = screen.reveal.fields.flatMap((revealField) => {
|
|
120
|
+
const raw = revealed[revealField.field];
|
|
121
|
+
if (isBlank(raw))
|
|
122
|
+
return [];
|
|
123
|
+
const display = revealField.display ?? "code";
|
|
124
|
+
const value = display === "list" && Array.isArray(raw)
|
|
125
|
+
? raw.map((v) => String(v)).join("\n")
|
|
126
|
+
: String(raw);
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
label: effectiveTranslate(revealField.label),
|
|
130
|
+
value,
|
|
131
|
+
copyable: revealField.copyable ?? true,
|
|
132
|
+
multiline: display === "list",
|
|
133
|
+
...(display === "qr" && { qr: true }),
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
});
|
|
137
|
+
return (_jsxs(Card, { testId: "kumiko-screen-secret-mint-card", children: [_jsx(Heading, { variant: "page", children: effectiveTranslate(screen.reveal.title ?? "kumiko.secretMint.title") }), _jsx(Banner, { variant: "warning", testId: "kumiko-screen-secret-mint-warning", children: effectiveTranslate(screen.reveal.warning ?? "kumiko.secretMint.warning") }), SecretReveal !== undefined ? (_jsx(SecretReveal, { values: values, copyLabel: effectiveTranslate("kumiko.secretMint.copy"), copiedLabel: effectiveTranslate("kumiko.secretMint.copied"), testId: "kumiko-screen-secret-mint-reveal" })) : (_jsx(Grid, { columns: 1, testId: "kumiko-screen-secret-mint-reveal", children: values.map((v) => (_jsxs(GridCell, { children: [_jsx(Text, { children: v.label }), _jsx(Text, { variant: "code", children: v.value })] }, v.label))) })), confirm !== undefined && confirmEntity !== undefined && confirmScreen !== undefined ? (_jsx(RenderEdit, { screen: confirmScreen, entity: confirmEntity, featureName: schema.featureName, initial: confirmInitial ?? {}, writeCommand: confirm.handler, payloadMode: "values", buildPayload: (snapshot) => ({ ...snapshot.values, ...carriedRef.current }), onSubmit: handleConfirmSubmitted, ...(handleCancel !== undefined && { onCancel: handleCancel }), ...(translate !== undefined && { translate }), ...(confirm.submitLabel !== undefined && { submitLabel: confirm.submitLabel }) })) : (_jsx(Button, { type: "button", variant: "primary", onClick: finishMint, testId: "kumiko-screen-secret-mint-confirm", children: effectiveTranslate(screen.reveal.confirmLabel ?? "kumiko.secretMint.confirm") }))] }));
|
|
138
|
+
}
|
|
139
|
+
return (_jsx(RenderEdit, { screen: synthScreen, entity: synthEntity, featureName: schema.featureName, initial: initial, extensionInitialValues: initial, writeCommand: screen.handler, payloadMode: "values", onSubmit: handleSubmitted, ...(handleCancel !== undefined && { onCancel: handleCancel }), ...(translate !== undefined && { translate }), ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) }));
|
|
140
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SecretsEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
export type SecretsEditBodyProps = {
|
|
5
|
+
readonly screen: SecretsEditScreenDefinition;
|
|
6
|
+
readonly translate?: Translate;
|
|
7
|
+
};
|
|
8
|
+
export declare function SecretsEditBody({ screen, translate }: SecretsEditBodyProps): ReactNode;
|