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