@cosmicdrift/kumiko-renderer 0.327.0 → 0.328.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) 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 -44
  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 -38
  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/dist/inside-drawer.js +9 -0
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/dist/page-header-slot.js +9 -0
  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/changes.json +52 -0
  135. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  136. package/src/__tests__/format-when.test.ts +0 -12
  137. package/src/__tests__/i18n.test.tsx +0 -283
  138. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  139. package/src/__tests__/qn.test.ts +0 -83
  140. package/src/__tests__/share-links.test.ts +0 -26
  141. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  142. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  143. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  144. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  145. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  146. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  147. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  148. package/src/app/__tests__/content-editors.test.tsx +0 -128
  149. package/src/app/__tests__/content-preview.test.tsx +0 -184
  150. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  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 -111
  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 -3614
  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 -85
  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 -639
  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 -1669
  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 -60
  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 -467
  228. package/src/components/render-edit-action-button.tsx +0 -106
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -216
  231. package/src/components/render-edit.tsx +0 -1700
  232. package/src/components/render-field.tsx +0 -942
  233. package/src/components/render-list.tsx +0 -561
  234. package/src/components/write-form-section.tsx +0 -199
  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-stream-handler.test.tsx +0 -107
  241. package/src/hooks/use-ai-text.ts +0 -172
  242. package/src/hooks/use-disclosure.ts +0 -20
  243. package/src/hooks/use-form.ts +0 -78
  244. package/src/hooks/use-list-url-state.ts +0 -171
  245. package/src/hooks/use-mutation.ts +0 -61
  246. package/src/hooks/use-query.ts +0 -132
  247. package/src/hooks/use-reference-lookup.ts +0 -76
  248. package/src/hooks/use-store.ts +0 -48
  249. package/src/hooks/use-stream-handler.ts +0 -134
  250. package/src/i18n-defaults.ts +0 -220
  251. package/src/i18n.tsx +0 -286
  252. package/src/index.ts +0 -272
  253. package/src/primitives.tsx +0 -1446
  254. package/src/share-links.ts +0 -12
  255. package/src/sort-by-accessor.ts +0 -20
  256. package/src/sse/live-events.tsx +0 -56
  257. package/src/tokens.tsx +0 -170
@@ -1,467 +0,0 @@
1
- import type {
2
- EntityDefinition,
3
- EntityListScreenDefinition,
4
- RowActionDrawer,
5
- RowActionNavigate,
6
- } from "@cosmicdrift/kumiko-framework/ui-types";
7
- import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
8
- import type {
9
- EditRelatedListSectionViewModel,
10
- ListRowViewModel,
11
- Translate,
12
- } from "@cosmicdrift/kumiko-headless";
13
- import { type ReactNode, useCallback, useMemo, useState } from "react";
14
- import { useAppFeatures } from "../app/app-features-context";
15
- import {
16
- buildFilterFacets,
17
- buildFilterPayload,
18
- mergeReferenceFacetOptions,
19
- resolveProjectionFacetSpecs,
20
- } from "../app/list-facets";
21
- import { useNav } from "../app/nav";
22
- import { ReferenceFacetBridges, type ReferenceFacetOption } from "../app/reference-facet-bridge";
23
- import { navigateWithReturnTo, useReturnHost } from "../app/return-to";
24
- import {
25
- buildDefaultEditRowAction,
26
- buildProjectionRowActions,
27
- buildProjectionToolbarActions,
28
- buildRecordActions,
29
- runProjectionRowNavigate,
30
- } from "../app/row-actions";
31
- import { findEditScreenFor } from "../app/screen-access";
32
- import { dispatcherErrorText } from "../app/write-failed-error";
33
- import { useOptionalDispatcher } from "../context/dispatcher-context";
34
- import { useUserRoles } from "../context/user-roles-context";
35
- import type { ListSort } from "../hooks/use-list-url-state";
36
- import { useQuery } from "../hooks/use-query";
37
- import { useTranslation } from "../i18n";
38
- import { type DataTableFacet, usePrimitives } from "../primitives";
39
- import { sortByAccessor } from "../sort-by-accessor";
40
- import { RenderEditActionButton } from "./render-edit-action-button";
41
- import { RenderList } from "./render-list";
42
-
43
- const RELATED_LIST_PSEUDO_ENTITY = "__related-list__";
44
-
45
- // Same paged envelope as ProjectionListBody's PagedRows (kumiko-screen.tsx) —
46
- // duplicated locally because that type isn't exported (projectionList and
47
- // relatedList are independent query call-sites, not a shared abstraction).
48
- type PagedRows = {
49
- readonly rows: Readonly<Record<string, unknown>>[];
50
- readonly nextCursor: string | null;
51
- readonly total?: number;
52
- };
53
-
54
- // Minimal EntityDefinition from the section's own columns — same shape as
55
- // projection-list-shim's synthesizeProjectionEntity, but sortable is read
56
- // per-column (ListColumnSpec.sortable) instead of screen-wide, since a
57
- // relatedList section has no single Zod schema to derive it from.
58
- function synthesizeRelatedListEntity(
59
- columns: EditRelatedListSectionViewModel["columns"],
60
- ): EntityDefinition {
61
- const fields: Record<string, { type: "text"; sortable: boolean }> = {};
62
- for (const col of columns) {
63
- const normalized = normalizeListColumn(col);
64
- fields[normalized.field] = { type: "text", sortable: normalized.sortable === true };
65
- }
66
- return { fields } as unknown as EntityDefinition;
67
- }
68
-
69
- // Mirrors row-actions.ts's own warnDrawerActionDropped (module-private
70
- // there, so not reusable directly) — a drawer-kind emptyState.action with no
71
- // onOpenDrawer wired is dropped (not rendered) with a dev warning, same as a
72
- // drawer-kind rowAction/toolbarAction without one.
73
- function warnRelatedListEmptyStateDrawerDropped(actionId: string): void {
74
- // biome-ignore lint/suspicious/noConsole: dev-time warning, same pattern as row-actions.ts
75
- console.warn(
76
- `[kumiko] relatedList emptyState.action "${actionId}" is kind: "drawer" but no onOpenDrawer was supplied — dropped.`,
77
- );
78
- }
79
-
80
- export function RelatedListSection({
81
- section,
82
- parentId,
83
- record,
84
- featureName,
85
- translate,
86
- hideTitle,
87
- onOpenDrawer,
88
- actions,
89
- }: {
90
- readonly section: EditRelatedListSectionViewModel;
91
- readonly parentId: string;
92
- /** The enclosing projectionDetail's own record (the "Akte") — evaluates
93
- * toolbarActions' `visible`/`params` (RelatedListToolbarAction), the same
94
- * record header actions already use. */
95
- readonly record: Readonly<Record<string, unknown>>;
96
- readonly featureName: string;
97
- readonly translate?: Translate;
98
- readonly hideTitle?: boolean;
99
- /** Opens a drawer-kind rowAction (fw#2710). Supplied by the parent
100
- * (ProjectionDetailBody), which owns schema + the actual Drawer render —
101
- * this component only ever invokes the callback. */
102
- readonly onOpenDrawer?: (
103
- action: RowActionDrawer,
104
- initialValues: Readonly<Record<string, unknown>> | undefined,
105
- ) => void;
106
- /** section.actions, already resolved into buttons by the caller —
107
- * rendered in the Section's title row. Only reachable in the standalone
108
- * (non-`hideTitle`) branch below — `hideTitle` uses `FillContainer`, which
109
- * has no title row to render actions into (see the comment on that branch). */
110
- readonly actions?: ReactNode;
111
- }): ReactNode {
112
- const { Banner, Section, FillContainer, Text, Button, Dialog } = usePrimitives();
113
- const [emptyStateActionError, setEmptyStateActionError] = useState<string | null>(null);
114
- const t = useTranslation();
115
- const effectiveTranslate = translate ?? t;
116
- const nav = useNav();
117
- const host = useReturnHost();
118
- const dispatcher = useOptionalDispatcher();
119
- const appFeatures = useAppFeatures();
120
- const userRoles = useUserRoles();
121
- const defaultEditScreen = useMemo(() => {
122
- const targetEntity = section.rowClick?.entity;
123
- if (targetEntity === undefined) return undefined;
124
- return findEditScreenFor(targetEntity, appFeatures, userRoles);
125
- }, [appFeatures, section.rowClick, userRoles]);
126
- const defaultEditRowAction = useMemo(
127
- () => buildDefaultEditRowAction(defaultEditScreen, section.rowClick?.idColumn ?? "id"),
128
- [defaultEditScreen, section.rowClick],
129
- );
130
-
131
- const entity = useMemo(() => synthesizeRelatedListEntity(section.columns), [section.columns]);
132
- const listScreen = useMemo(
133
- (): EntityListScreenDefinition => ({
134
- // Empty id → RenderList's own toolbarTitle resolves to "" (its
135
- // `screen:${id}.title` lookup misses and falls back to `id`) — this
136
- // component renders the visible heading itself via `Section` below,
137
- // so RenderList's toolbar carries none.
138
- id: "",
139
- type: "entityList",
140
- entity: RELATED_LIST_PSEUDO_ENTITY,
141
- columns: section.columns,
142
- }),
143
- [section.columns],
144
- );
145
-
146
- // Local state, not URL state: a section `id` is optional, so there is no
147
- // stable URL key to namespace against — the section's sort is local for the
148
- // same reason. Consequence: search/filters reset on reload.
149
- const [search, setSearch] = useState("");
150
- const [filters, setFilters] = useState<Readonly<Record<string, readonly string[]>>>({});
151
-
152
- const facetSpecs = useMemo(
153
- () => resolveProjectionFacetSpecs(section.facets, effectiveTranslate, featureName),
154
- [section.facets, effectiveTranslate, featureName],
155
- );
156
- const [referenceFacetOptions, setReferenceFacetOptions] = useState<
157
- Record<string, readonly ReferenceFacetOption[]>
158
- >({});
159
- const handleFacetOptions = useCallback(
160
- (field: string, options: readonly ReferenceFacetOption[]) =>
161
- setReferenceFacetOptions((prev) =>
162
- prev[field] === options ? prev : { ...prev, [field]: options },
163
- ),
164
- [],
165
- );
166
- const resolvedFacetSpecs = useMemo(
167
- () => mergeReferenceFacetOptions(facetSpecs, referenceFacetOptions),
168
- [facetSpecs, referenceFacetOptions],
169
- );
170
- const filterPayload = useMemo(
171
- () =>
172
- buildFilterPayload(
173
- filters,
174
- (field) => resolvedFacetSpecs.find((spec) => spec.field === field)?.type,
175
- ),
176
- [filters, resolvedFacetSpecs],
177
- );
178
- const filterFacets = useMemo<DataTableFacet[]>(
179
- () => buildFilterFacets(resolvedFacetSpecs),
180
- [resolvedFacetSpecs],
181
- );
182
-
183
- const payload = useMemo(
184
- () => ({
185
- // `parentFilter` sends the parent id as a server-side `filter` clause
186
- // instead of a bespoke top-level key — the generic `<entity>:list`
187
- // query understands `filter`, not an arbitrary `parentParam` key. Kept
188
- // separate from `filterPayload` (user facets) below so a facet
189
- // selection can never clear or overwrite it.
190
- ...(section.parentFilter !== undefined
191
- ? { filter: { field: section.parentFilter.field, op: "eq" as const, value: parentId } }
192
- : { [section.parentParam ?? "id"]: parentId }),
193
- ...(section.pageSize !== undefined && { limit: section.pageSize }),
194
- // Gated on the declared capability, not just on state carrying a value —
195
- // same rule as ProjectionListBody: a param the bound query's Zod schema
196
- // doesn't accept would 422 the whole section.
197
- ...(section.searchable === true && search !== "" && { search }),
198
- ...(section.facets !== undefined && filterPayload.length > 0 && { filters: filterPayload }),
199
- }),
200
- [
201
- section.parentFilter,
202
- section.parentParam,
203
- section.pageSize,
204
- section.searchable,
205
- section.facets,
206
- parentId,
207
- search,
208
- filterPayload,
209
- ],
210
- );
211
-
212
- const rowsQuery = useQuery<PagedRows>(section.query, payload);
213
-
214
- const onFilterChange = useCallback(
215
- (field: string, values: readonly string[]) =>
216
- setFilters((prev) => ({ ...prev, [field]: values })),
217
- [],
218
- );
219
- const onFilterReset = useCallback(() => setFilters({}), []);
220
-
221
- // Sorted client-side over the already-loaded rows — this section has no
222
- // pager (see `payload` above: a one-shot fetch, no cursor/offset), so the
223
- // loaded set already IS the full display set and there is no "other page"
224
- // a client-side sort could misleadingly hide (fw#2722).
225
- const [sort, setSort] = useState<ListSort | null>(section.defaultSort ?? null);
226
- const sortAccessors = useMemo(() => {
227
- const accessors: Record<string, (row: Readonly<Record<string, unknown>>) => string | number> =
228
- {};
229
- for (const col of section.columns) {
230
- const field = normalizeListColumn(col).field;
231
- accessors[field] = (row) => {
232
- const value = row[field];
233
- return typeof value === "number" ? value : String(value ?? "");
234
- };
235
- }
236
- return accessors;
237
- }, [section.columns]);
238
- const sortedRows = useMemo(
239
- () => sortByAccessor(rowsQuery.data?.rows ?? [], sort, sortAccessors),
240
- [rowsQuery.data, sort, sortAccessors],
241
- );
242
-
243
- const rowClick = section.rowClick;
244
- // A row-body click target comes from EITHER the legacy `rowClick` field OR
245
- // a `rowActions` entry marked rowClick:true — the boot-validator rejects
246
- // both being set, so this order is just a fallback, not a precedence rule.
247
- const rowClickAction = section.rowActions?.find(
248
- (a): a is RowActionNavigate => a.kind === "navigate" && a.rowClick === true,
249
- );
250
- const onRowClick =
251
- rowClick !== undefined
252
- ? (row: ListRowViewModel) => {
253
- const id = String(row.values[rowClick.idColumn ?? "id"] ?? "");
254
- if (id === "") return;
255
- navigateWithReturnTo(nav, { entity: rowClick.entity, id }, host);
256
- }
257
- : rowClickAction !== undefined
258
- ? (row: ListRowViewModel) => runProjectionRowNavigate(nav, rowClickAction, row, host)
259
- : undefined;
260
-
261
- // Same execution path as projectionList's rowActions (kumiko-screen.tsx) —
262
- // a relatedList row has the identical "no guaranteed id field" shape a
263
- // query-projection row has, so navigate/writeHandler dispatch is shared
264
- // rather than a second implementation (fw editable-detail-screens).
265
- const rowActions = useMemo(
266
- () =>
267
- buildProjectionRowActions({
268
- rowActions: section.rowActions,
269
- translate: effectiveTranslate,
270
- dispatcher,
271
- nav,
272
- refetch: rowsQuery.refetch,
273
- openDrawer: onOpenDrawer,
274
- defaultEditRowAction,
275
- host,
276
- }),
277
- [
278
- section.rowActions,
279
- effectiveTranslate,
280
- dispatcher,
281
- nav,
282
- rowsQuery.refetch,
283
- onOpenDrawer,
284
- defaultEditRowAction,
285
- host,
286
- ],
287
- );
288
-
289
- // Toolbar actions (e.g. "+ Create" above the tab table) — same schema
290
- // and dispatch semantics as entityList/projectionList.
291
- const toolbarActionButtons = useMemo(
292
- () =>
293
- buildProjectionToolbarActions({
294
- toolbarActions: section.toolbarActions,
295
- translate: effectiveTranslate,
296
- dispatcher,
297
- nav,
298
- refetch: rowsQuery.refetch,
299
- navigatePrefill:
300
- section.parentFilter !== undefined
301
- ? { [section.parentFilter.field]: parentId }
302
- : { [section.parentParam ?? "id"]: parentId },
303
- record,
304
- host,
305
- ...(onOpenDrawer !== undefined && {
306
- openDrawer: (action) => onOpenDrawer(action, undefined),
307
- }),
308
- }),
309
- [
310
- section.toolbarActions,
311
- section.parentParam,
312
- section.parentFilter,
313
- parentId,
314
- record,
315
- effectiveTranslate,
316
- dispatcher,
317
- nav,
318
- host,
319
- rowsQuery.refetch,
320
- onOpenDrawer,
321
- ],
322
- );
323
-
324
- // emptyState.action is a RowAction — the same record-bound action shape
325
- // (and evalRowExtractor payload/params evaluation) screen-level and
326
- // section-level actions already use via buildRecordActions, NOT the
327
- // toolbar-action builder (whose `payload` is a literal Record, not a
328
- // RowFieldExtractor — casting a RowAction into it would silently skip the
329
- // declared `pick`/`map` evaluation). "Row" here is the parent record: a
330
- // relatedList's emptyState has no row of its own.
331
- const emptyStateActionButton = useMemo(() => {
332
- const action = section.emptyState?.action;
333
- if (action === undefined) return undefined;
334
- if (action.kind === "drawer" && onOpenDrawer === undefined) {
335
- warnRelatedListEmptyStateDrawerDropped(action.id);
336
- return undefined;
337
- }
338
- return buildRecordActions({
339
- actions: [action],
340
- record,
341
- translate: effectiveTranslate,
342
- nav,
343
- host,
344
- dispatcher,
345
- openDrawer: onOpenDrawer ?? (() => {}),
346
- onWriteSuccess: rowsQuery.refetch,
347
- })?.[0];
348
- }, [
349
- section.emptyState,
350
- record,
351
- effectiveTranslate,
352
- nav,
353
- host,
354
- dispatcher,
355
- onOpenDrawer,
356
- rowsQuery.refetch,
357
- ]);
358
- const emptyStateContent =
359
- section.emptyState !== undefined ? (
360
- <>
361
- <Text>{section.emptyState.title}</Text>
362
- {section.emptyState.description !== undefined && (
363
- <Text variant="small">{section.emptyState.description}</Text>
364
- )}
365
- {emptyStateActionButton !== undefined && (
366
- <RenderEditActionButton
367
- action={emptyStateActionButton}
368
- Button={Button}
369
- Dialog={Dialog}
370
- onError={setEmptyStateActionError}
371
- />
372
- )}
373
- </>
374
- ) : undefined;
375
-
376
- // A truncated fetch means `sortedRows` is a sort of a partial set, not of
377
- // the full related-row set — the client-side sort above (or even plain
378
- // unsorted display) would silently claim "these are the top N" when they
379
- // are really just "the first N in the server's own order" (fw#2722 review).
380
- // `nextCursor` is exactly how the paged envelope marks that: non-null means
381
- // more rows exist server-side beyond what was fetched.
382
- const truncated = rowsQuery.data !== null && rowsQuery.data.nextCursor !== null;
383
-
384
- const content =
385
- rowsQuery.loading && rowsQuery.data === null ? (
386
- <Banner padded variant="loading" testId="related-list-loading">
387
- Loading…
388
- </Banner>
389
- ) : rowsQuery.error ? (
390
- <Banner padded variant="error" testId="related-list-error">
391
- {dispatcherErrorText(rowsQuery.error, effectiveTranslate)}
392
- </Banner>
393
- ) : (
394
- <>
395
- {truncated && (
396
- <Banner variant="info" testId="related-list-truncated">
397
- {effectiveTranslate("kumiko.list.related-list-truncated", {
398
- count: sortedRows.length,
399
- })}
400
- </Banner>
401
- )}
402
- {emptyStateActionError !== null && (
403
- <Banner variant="error" testId="related-list-empty-state-action-error">
404
- {emptyStateActionError}
405
- </Banner>
406
- )}
407
- <RenderList
408
- screen={listScreen}
409
- entity={entity}
410
- rows={sortedRows}
411
- featureName={featureName}
412
- translate={effectiveTranslate}
413
- sort={sort}
414
- onSortChange={setSort}
415
- {...(section.searchable === true && {
416
- searchable: true,
417
- searchValue: search,
418
- onSearchChange: setSearch,
419
- })}
420
- {...(filterFacets.length > 0 && {
421
- filterFacets,
422
- filterValues: filters,
423
- onFilterChange,
424
- onFilterReset,
425
- })}
426
- {...(onRowClick !== undefined && { onRowClick })}
427
- {...(rowActions !== undefined && { rowActions })}
428
- {...(toolbarActionButtons !== undefined && { toolbarActions: toolbarActionButtons })}
429
- {...(emptyStateContent !== undefined && { emptyState: emptyStateContent })}
430
- {...(hideTitle === true && { scrollBody: true, screenPadding: false, chromeless: true })}
431
- />
432
- </>
433
- );
434
-
435
- // hideTitle (tabs mode) → the caller (render-edit.tsx) already wraps this
436
- // component's own return value in the same Card frame every other tab
437
- // kind gets — no card here, or the two would nest. `scrollBody` caps the
438
- // table to the panel height; `FillContainer` is this section's link in the
439
- // fillHeight flex chain that Card's `options.fillHeight` continues (see
440
- // render-edit.tsx). A platform primitive (not a raw `<div>`) because
441
- // `renderer` stays DOM-free. Stacked (non-tabs) sections keep their own
442
- // Section card and document-flow height since they render a visible title
443
- // and aren't confined to a tab panel.
444
- const bridges = <ReferenceFacetBridges specs={facetSpecs} onOptions={handleFacetOptions} />;
445
-
446
- if (hideTitle) {
447
- return (
448
- <>
449
- {bridges}
450
- {FillContainer !== undefined ? <FillContainer>{content}</FillContainer> : content}
451
- </>
452
- );
453
- }
454
-
455
- return (
456
- <>
457
- {bridges}
458
- <Section
459
- title={section.title}
460
- {...(actions !== undefined && { actions })}
461
- testId={`related-list-${section.title}`}
462
- >
463
- {content}
464
- </Section>
465
- </>
466
- );
467
- }
@@ -1,106 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { useState } from "react";
3
- import type { usePrimitives } from "../primitives";
4
- import type { RenderEditAction } from "./render-edit-types";
5
-
6
- // Same rule as RowActionWriteHandler: "danger" forces a confirm even
7
- // without an explicit confirm key — unless `confirmRequired` overrides it
8
- // (schema-driven navigate/drawer actions, where the target form is itself
9
- // the confirmation).
10
- export function needsActionConfirm(action: RenderEditAction): boolean {
11
- return action.confirm !== undefined || (action.confirmRequired ?? action.style === "danger");
12
- }
13
-
14
- export function RenderEditActionConfirmDialog({
15
- action,
16
- open,
17
- onOpenChange,
18
- onConfirm,
19
- Dialog,
20
- }: {
21
- readonly action: RenderEditAction;
22
- readonly open: boolean;
23
- readonly onOpenChange: (open: boolean) => void;
24
- readonly onConfirm: () => void | Promise<void>;
25
- readonly Dialog: ReturnType<typeof usePrimitives>["Dialog"];
26
- }): ReactNode {
27
- return (
28
- <Dialog
29
- open={open}
30
- onOpenChange={onOpenChange}
31
- title={action.label}
32
- {...(action.confirm !== undefined && { description: action.confirm })}
33
- confirmLabel={action.confirmLabel ?? action.label}
34
- {...(action.style === "danger" && { variant: "danger" as const })}
35
- onConfirm={onConfirm}
36
- testId={`render-edit-action-${action.id}-dialog`}
37
- />
38
- );
39
- }
40
-
41
- // One header action + its own busy/confirm state — same pattern as
42
- // render-list.tsx's ToolbarActionView (each RenderEditAction is
43
- // independently bound by the caller, there is no shared trigger pipeline
44
- // to hook into like the built-in onDelete/onSubmit paths have).
45
- export function RenderEditActionButton({
46
- action,
47
- iconOnly = false,
48
- Button,
49
- Dialog,
50
- onError,
51
- }: {
52
- readonly action: RenderEditAction;
53
- /** Group-level collapse (see `shouldRenderActionsIconOnly`) — only takes
54
- * effect when this action actually resolved an icon. */
55
- readonly iconOnly?: boolean;
56
- readonly Button: ReturnType<typeof usePrimitives>["Button"];
57
- readonly Dialog: ReturnType<typeof usePrimitives>["Dialog"];
58
- readonly onError: (text: string | null) => void;
59
- }): ReactNode {
60
- const [busy, setBusy] = useState(false);
61
- const [confirmOpen, setConfirmOpen] = useState(false);
62
-
63
- const trigger = async (): Promise<void> => {
64
- setBusy(true);
65
- onError(null);
66
- try {
67
- await action.onPress();
68
- } catch (e) {
69
- onError(e instanceof Error ? e.message : String(e));
70
- } finally {
71
- setBusy(false);
72
- }
73
- };
74
-
75
- const variant = action.style ?? "secondary";
76
- const showIconOnly = iconOnly && action.icon !== undefined;
77
-
78
- return (
79
- <>
80
- <Button
81
- type="button"
82
- variant={variant}
83
- loading={busy}
84
- {...(action.icon !== undefined && { icon: action.icon })}
85
- {...(showIconOnly && { size: "icon" as const, ariaLabel: action.label })}
86
- onClick={() => {
87
- if (needsActionConfirm(action)) {
88
- setConfirmOpen(true);
89
- } else {
90
- void trigger();
91
- }
92
- }}
93
- testId={`render-edit-action-${action.id}`}
94
- >
95
- {showIconOnly ? null : action.label}
96
- </Button>
97
- <RenderEditActionConfirmDialog
98
- action={action}
99
- open={confirmOpen}
100
- onOpenChange={setConfirmOpen}
101
- onConfirm={trigger}
102
- Dialog={Dialog}
103
- />
104
- </>
105
- );
106
- }
@@ -1,85 +0,0 @@
1
- import type { EditSectionViewModel, SubmitResult } from "@cosmicdrift/kumiko-headless";
2
-
3
- // Hides the Save button when no field is editable and no extension section
4
- // contributes to the composed form submit. Extensions that persist via their
5
- // own dispatcher writes must not opt in (fw#2359).
6
- // Explicit-positive per kind (653/1), not `s.kind !== "fields"` — a future
7
- // third EditSectionViewModel member would otherwise default to "editable"
8
- // without anyone deciding that on purpose.
9
- export function hasEditableSection(sections: readonly EditSectionViewModel[]): boolean {
10
- return sections.some(
11
- (s) =>
12
- (s.kind === "extension" && s.contributesToFormSubmit) ||
13
- (s.kind === "fields" && s.visible && s.fields.some((f) => !f.readOnly && f.visible)),
14
- );
15
- }
16
-
17
- // Single source of truth for the extension-section entity-id. The section mount
18
- // and persistExtensions MUST resolve the same id — otherwise a section mounts
19
- // editable against one id (vm.id) while the persist step writes to (or skips)
20
- // another (null), silently dropping the user's input. An explicit `null` prop
21
- // forces "no entity" (no extension persistence); an omitted prop (undefined)
22
- // falls back to vm.id (= values["id"]), which the update form carries for the
23
- // existing row, so editing custom fields on that row actually persists.
24
- export function resolveExtensionEntityId(
25
- entityIdProp: string | null | undefined,
26
- vmId: string | null,
27
- ): string | null {
28
- return entityIdProp !== undefined ? entityIdProp : vmId;
29
- }
30
-
31
- // After a submit, decide whether to invoke the caller's onSubmit. The success
32
- // callback typically navigates away, which unmounts the extension-error banner.
33
- // Suppress the callback ONLY when the entity write succeeded but an extension-
34
- // section persist failed: the user must stay on the form to see the banner and
35
- // retry. Every other case still notifies the caller — entity failures and
36
- // validation blocks carry information the caller needs.
37
- export function shouldNotifyCaller(
38
- result: SubmitResult<unknown>,
39
- extensionsPersisted: boolean,
40
- ): boolean {
41
- return !(result.isSuccess && !extensionsPersisted);
42
- }
43
-
44
- // Matched on the first path segment (embedded-list rows are dotted, e.g. `tasks.2.title`).
45
- // No issues at all counts as unmatched, so the caller keeps the banner.
46
- export function hasIssueWithoutRenderedField(
47
- issuePaths: readonly string[],
48
- sections: readonly EditSectionViewModel[],
49
- ): boolean {
50
- if (issuePaths.length === 0) return true;
51
- const renderedFields = new Set(
52
- sections
53
- .filter((s) => s.kind === "fields" && (s.visible || s.fields.length === 0))
54
- .flatMap((s) => (s.kind === "fields" ? s.fields.filter((f) => f.visible) : []))
55
- .map((f) => f.field),
56
- );
57
- return issuePaths.some((path) => !renderedFields.has(path.split(".")[0] ?? path));
58
- }
59
-
60
- // Extension, relatedList and writeForm sections skip the `fields` filter
61
- // (writeForm's fields belong to its own independent form, not the host's);
62
- // a `fields` section left with zero fields after filtering is dropped, not
63
- // rendered empty.
64
- export function filterEditSections(
65
- sections: readonly EditSectionViewModel[],
66
- fieldsFilter: readonly string[] | undefined,
67
- ): readonly EditSectionViewModel[] {
68
- if (fieldsFilter === undefined) return sections;
69
- const filterSet = new Set(fieldsFilter);
70
- const result: EditSectionViewModel[] = [];
71
- for (const section of sections) {
72
- if (
73
- section.kind === "extension" ||
74
- section.kind === "relatedList" ||
75
- section.kind === "writeForm"
76
- ) {
77
- result.push(section);
78
- continue;
79
- }
80
- const fields = section.fields.filter((f) => filterSet.has(f.field));
81
- if (fields.length === 0) continue;
82
- result.push({ ...section, fields });
83
- }
84
- return result;
85
- }