@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,561 +0,0 @@
1
- import type { EagerloadedRow } from "@cosmicdrift/kumiko-framework/db";
2
- import type {
3
- EntityDefinition,
4
- EntityListScreenDefinition,
5
- IconKey,
6
- } from "@cosmicdrift/kumiko-framework/ui-types";
7
- import type {
8
- ListColumnViewModel,
9
- ListRowViewModel,
10
- ListViewModel,
11
- RuntimeRenderer,
12
- Translate,
13
- } from "@cosmicdrift/kumiko-headless";
14
- import { computeListViewModel } from "@cosmicdrift/kumiko-headless";
15
- import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
16
- import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections";
17
- import type { ListSort } from "../hooks/use-list-url-state";
18
- import { type ReferenceLookupMap, useReferenceLookup } from "../hooks/use-reference-lookup";
19
- import { useTranslation } from "../i18n";
20
- import {
21
- type DataTableDateRangeFacet,
22
- type DataTableFacet,
23
- type DataTableRowAction,
24
- type DataTableRowActionMode,
25
- shouldRenderActionsIconOnly,
26
- usePrimitives,
27
- } from "../primitives";
28
-
29
- // RenderList — präsentationaler View für entityList-Screens.
30
- //
31
- // Daten + State sind komplett controlled vom Caller (KumikoScreen):
32
- // - rows kommen aus dem Server (durch useQuery + URL-State-Payload)
33
- // - sort/q sind Props, RenderList ruft onSortChange/onQChange wenn
34
- // der User UI-Aktionen macht
35
- // - Search ist serverseitig (via SearchAdapter / Meilisearch). Lokaler
36
- // State im Search-Input ist nur Debounce-Buffer, keine Filterung.
37
- //
38
- // Apps die RenderList ohne Server-State nutzen wollen (z.B. ein
39
- // statischer Lookup) liefern stabile rows + lassen sort/q weg —
40
- // DataTable fällt dann auf "kein Sort-Wiring + kein Search" zurück.
41
-
42
- export type RenderListProps = {
43
- readonly screen: EntityListScreenDefinition;
44
- readonly entity: EntityDefinition;
45
- readonly rows: readonly Readonly<Record<string, unknown>>[];
46
- readonly featureName: string;
47
- readonly translate?: Translate;
48
- readonly onRowClick?: (row: ListRowViewModel) => void;
49
- /** Override der Default-Empty-Box. Wenn gesetzt, wird der Auto-CTA
50
- * via `onCreate` ignoriert — Caller-Inhalt gewinnt. */
51
- readonly emptyState?: ReactNode;
52
- /** Setzt einen "+ Neu" Button in die Toolbar UND dient als CTA in
53
- * der Default-Empty-Box. Caller liefert die Action selber (z. B.
54
- * navigate auf den Edit-Screen). */
55
- readonly onCreate?: () => void;
56
- /** Label für den + Neu Button. Default kommt aus dem i18n-Bundle
57
- * (`kumiko.actions.create`). Caller kann durch eigenen String
58
- * überschreiben. */
59
- readonly createLabel?: string;
60
- /** Aktiviert ein Search-Input in der Toolbar. Server-side Filter
61
- * via Caller's onSearchChange — RenderList filtert NICHT lokal. */
62
- readonly searchable?: boolean;
63
- /** Placeholder für das Search-Input. Default kommt aus dem i18n-
64
- * Bundle (`kumiko.list.search-placeholder`). */
65
- readonly searchPlaceholder?: string;
66
- /** Aktueller Search-Term (vom URL-State / Parent). RenderList puffert
67
- * Tipps lokal mit 300ms Debounce, bevor onSearchChange gefeuert
68
- * wird — sonst macht jeder Tastendruck einen Server-Roundtrip. */
69
- readonly searchValue?: string;
70
- /** Wird gerufen wenn der debounced Search-Term sich ändert. Caller
71
- * setzt damit URL-State (?<id>.q=…) und triggert ein refetch. */
72
- readonly onSearchChange?: (next: string) => void;
73
- /** Aktuelle Sortierung (oder null = Server-Default-Order). */
74
- readonly sort?: ListSort | null;
75
- /** Wird gerufen mit dem nächsten Sort-State nach einem Header-Klick. */
76
- readonly onSortChange?: (next: ListSort | null) => void;
77
- /** Pager-Props für pagination="pages". Wenn undefined, kein Pager-
78
- * UI — Default oder infinite-Mode. KumikoScreen liefert das je nach
79
- * screen.pagination. */
80
- readonly pager?: {
81
- readonly page: number;
82
- readonly limit: number;
83
- readonly total: number;
84
- readonly onPageChange: (next: number) => void;
85
- };
86
- /** Infinite-Scroll-Wiring für pagination="infinite". KumikoScreen
87
- * hält accumulated rows + cursor, RenderList reicht die Callbacks
88
- * einfach durch an DataTable. */
89
- readonly onReachEnd?: () => void;
90
- readonly loadingMore?: boolean;
91
- readonly hasMore?: boolean;
92
- /** Pro-Row-Aktionen — Resolved-Form (KumikoScreen baut das aus
93
- * EntityListScreenDefinition.rowActions: handler-QN → dispatcher-Call,
94
- * i18n-Keys → translated Strings). */
95
- readonly rowActions?: readonly DataTableRowAction[];
96
- /** How the row-action column renders (see `DataTableProps.rowActionMode`).
97
- * KumikoScreen derives it from the actions themselves; without it the
98
- * DataTable falls back to its adaptive default. */
99
- readonly rowActionMode?: DataTableRowActionMode;
100
- /** Toolbar-Aktionen im List-Header — Resolved-Form (KumikoScreen baut
101
- * das aus EntityListScreenDefinition.toolbarActions: navigate-target
102
- * → useNav, handler-QN → dispatcher-Call). RenderList rendert die
103
- * Buttons rechts in der Toolbar, vor "+ Neu". */
104
- readonly toolbarActions?: readonly ToolbarActionButton[];
105
- /** Faceted-Filter-Dropdowns — KumikoScreen baut sie aus den filterable
106
- * select/boolean-Feldern; RenderList reicht sie an die DataTable durch. */
107
- readonly filterFacets?: readonly DataTableFacet[];
108
- /** Aktuell gewählte Facet-Werte je Feld (vom URL-State). */
109
- readonly filterValues?: Readonly<Record<string, readonly string[]>>;
110
- /** Toggle eines Facet-Werts (URL-State setzen + refetch). */
111
- readonly onFilterChange?: (field: string, values: readonly string[]) => void;
112
- /** Reset aller aktiven Facets. */
113
- readonly onFilterReset?: () => void;
114
- /** Time-range filters (fw#3104) — forwarded to the DataTable. */
115
- readonly dateRangeFacets?: readonly DataTableDateRangeFacet[];
116
- /** One bound changed (set URL state + refetch). */
117
- readonly onDateRangeChange?: (field: string, bound: "from" | "to", value: string) => void;
118
- /** Forwarded to `DataTableProps.chromeless` — drops the table's own card
119
- * frame for a host that already provides one (relatedList in a tabs-mode
120
- * section, fw#2722). Default false. */
121
- readonly chromeless?: boolean;
122
- /** Forwarded to `DataTableProps.scrollBody` — bounds the table height and
123
- * scrolls rows internally instead of growing the page (relatedList in a
124
- * tabs-mode section, fw#2722). Default false. */
125
- readonly scrollBody?: boolean;
126
- /** Forwarded to `DataTableProps.screenPadding` — `true`: the table carries
127
- * the shared screen padding because it is the screen body (entityList/
128
- * projectionList, fw#2640). Explicit `false`: no padding at all — the
129
- * host already pads its own children (relatedList in a tabs-mode Card,
130
- * fw#3234). Omitted: legacy `p-6` fallback for a standalone table with no
131
- * padded host. */
132
- readonly screenPadding?: boolean;
133
- };
134
-
135
- // Resolved-Form einer Toolbar-Action: KumikoScreen baut das aus dem
136
- // Schema (entweder navigate- oder writeHandler-kind), RenderList sieht
137
- // nur einen onTrigger-Callback + Label/Style — keine kind-Discrimination
138
- // mehr.
139
- export type ToolbarActionButton = {
140
- readonly id: string;
141
- readonly label: string;
142
- readonly style?: "primary" | "secondary" | "danger";
143
- readonly confirm?: string;
144
- readonly confirmLabel?: string;
145
- /** Overrides the default "danger implies a confirm dialog" rule. Schema-driven
146
- * navigate/drawer actions set it to false: the colour marks the action as
147
- * destructive, but the target form is the confirmation. */
148
- readonly confirmRequired?: boolean;
149
- readonly onTrigger: () => Promise<void> | void;
150
- /** Id-derived default icon (ACTION_ICON_BY_ID in kumiko-screen.tsx) —
151
- * ToolbarAction has no author-declared icon field, unlike RowAction. */
152
- readonly icon?: IconKey;
153
- };
154
-
155
- const SEARCH_DEBOUNCE_MS = 300;
156
-
157
- export function RenderList(props: RenderListProps): ReactNode {
158
- const {
159
- screen,
160
- entity,
161
- rows,
162
- featureName,
163
- translate: translateProp,
164
- onRowClick,
165
- emptyState,
166
- onCreate,
167
- createLabel,
168
- searchable = false,
169
- searchPlaceholder,
170
- searchValue,
171
- onSearchChange,
172
- sort,
173
- onSortChange,
174
- pager,
175
- onReachEnd,
176
- loadingMore,
177
- hasMore,
178
- rowActions,
179
- rowActionMode,
180
- toolbarActions,
181
- filterFacets,
182
- filterValues,
183
- onFilterChange,
184
- onFilterReset,
185
- dateRangeFacets,
186
- onDateRangeChange,
187
- chromeless,
188
- scrollBody,
189
- screenPadding,
190
- } = props;
191
- // Wie RenderEdit: Translate-Fallback aus dem i18next-Context, sonst
192
- // wären Column-Header raw i18n-Keys.
193
- const t = useTranslation();
194
- const translate: Translate = translateProp ?? t;
195
- const { DataTable, Button, Dialog, Input, Text, Banner } = usePrimitives();
196
-
197
- // Local search buffer + debounce. External changes (browser back,
198
- // cross-component reset) are mirrored back; typing only fires
199
- // onSearchChange after 300ms without further keystrokes.
200
- const [localQ, setLocalQ] = useState(searchValue ?? "");
201
- // Sync during render, not in a useEffect: the passive effect also fires on
202
- // mount and can flush after a keystroke landing in the same commit,
203
- // resetting localQ and swallowing the input.
204
- const [syncedSearchValue, setSyncedSearchValue] = useState(searchValue);
205
- if (searchValue !== syncedSearchValue) {
206
- setSyncedSearchValue(searchValue);
207
- setLocalQ(searchValue ?? "");
208
- }
209
- useEffect(() => {
210
- if (onSearchChange === undefined) return;
211
- if (localQ === (searchValue ?? "")) return;
212
- const timer = setTimeout(() => onSearchChange(localQ), SEARCH_DEBOUNCE_MS);
213
- return () => clearTimeout(timer);
214
- }, [localQ, searchValue, onSearchChange]);
215
-
216
- const vm = useMemo(
217
- () => computeListViewModel({ screen, entity, rows, translate, featureName }),
218
- [screen, entity, rows, translate, featureName],
219
- );
220
-
221
- // Tier 2.7e-4: Reference-Field-Eagerload via Bridge-Component-Pattern.
222
- // Hooks-Rule-Konflikt: pro reference-Spalte muss useReferenceLookup
223
- // gerufen werden, aber die Anzahl varriiert per Schema. Lösung —
224
- // ReferenceLookupBridge-Komponenten pro Spalte gemountet, die ihre
225
- // Map über onMap-Callback in einen lokalen State veröffentlichen.
226
- // Anzahl der Hook-Calls pro Bridge-Component ist konstant (1), und
227
- // React verwaltet die mounted/unmounted Lifecycles beim Schema-
228
- // Wechsel über die key-Property automatisch.
229
- const referenceColumns = useMemo(
230
- () =>
231
- vm.columns
232
- .filter((c: ListColumnViewModel) => c.type === "reference" && c.refEntity !== undefined)
233
- .map((c: ListColumnViewModel) => ({
234
- field: c.field,
235
- refEntity: c.refEntity ?? "",
236
- // Tier 2.7e Cross-Feature: refFeature kommt aus parseRefTarget
237
- // im ViewModel (default = current featureName).
238
- refFeature: c.refFeature ?? featureName,
239
- labelField: c.refLabelField ?? "id",
240
- })),
241
- [vm.columns, featureName],
242
- );
243
- const [referenceLookups, setReferenceLookups] = useState<Record<string, ReferenceLookupMap>>({});
244
- const handleLookupMap = useCallback((field: string, map: ReferenceLookupMap) => {
245
- setReferenceLookups((prev) => {
246
- // skip-update wenn die Map identisch ist (Render-Loop-Schutz —
247
- // Bridge-Component re-rendert sonst und schickt die gleiche Map
248
- // wieder rein bis React in eine Endlosschleife geht).
249
- if (prev[field] === map) return prev;
250
- return { ...prev, [field]: map };
251
- });
252
- }, []);
253
- const enrichedColumns = useMemo((): readonly ListColumnViewModel[] => {
254
- if (referenceColumns.length === 0) return vm.columns;
255
- return vm.columns.map((col: ListColumnViewModel) => {
256
- if (col.type !== "reference") return col;
257
- // Author-deklarierter Renderer übersteuert immer — Default greift
258
- // nur wenn keiner gesetzt ist.
259
- if (col.renderer !== undefined) return col;
260
- const map = referenceLookups[col.field];
261
- const labelField = col.refLabelField ?? "id";
262
- const renderer: RuntimeRenderer = (
263
- value: unknown,
264
- row?: Readonly<Record<string, unknown>>,
265
- ): string => {
266
- // Tier 2.7e Server-Eagerload: wenn der Server _refs mit-
267
- // schickt, lesen wir den Display-Wert direkt aus der
268
- // resolved Row — kein Roundtrip durch die Bridge-Map nötig
269
- // und keine limit:200-Constraint. EagerloadedRow-Type aus
270
- // @cosmicdrift/kumiko-framework/db pinnt die Form von _refs.
271
- const eagerloadedRow = row as EagerloadedRow | undefined;
272
- const resolved = eagerloadedRow?._refs?.[col.field];
273
- if (Array.isArray(resolved) && resolved.length > 0) {
274
- return resolved.map((r) => String(r[labelField] ?? r["id"] ?? "")).join(", ");
275
- }
276
- if (resolved !== undefined && !Array.isArray(resolved)) {
277
- const single = resolved as Record<string, unknown>; // @cast-boundary render-helper
278
- return String(single[labelField] ?? single["id"] ?? "");
279
- }
280
- // Renderer-Side-Fallback (kein Server-Eagerload aktiv).
281
- if (Array.isArray(value)) {
282
- if (value.length === 0) return "—";
283
- return value.map((v) => map?.get(String(v)) ?? String(v)).join(", ");
284
- }
285
- if (value === null || value === undefined || value === "") return "—";
286
- const idStr = String(value);
287
- return map?.get(idStr) ?? idStr;
288
- };
289
- return { ...col, renderer };
290
- });
291
- }, [vm.columns, referenceColumns, referenceLookups]);
292
- const enrichedVm = useMemo(
293
- (): Omit<ListViewModel, "columns"> & { columns: readonly ListColumnViewModel[] } => ({
294
- ...vm,
295
- columns: enrichedColumns,
296
- }),
297
- [vm, enrichedColumns],
298
- );
299
-
300
- // i18n-Defaults für Toolbar/Empty-State Strings — Caller kann jeden
301
- // einzeln per Prop überschreiben, sonst kommen die Framework-Bundles
302
- // (kumiko.actions.create, kumiko.list.search-placeholder, …).
303
- const effectiveCreateLabel = createLabel ?? translate("kumiko.actions.create");
304
- const effectiveSearchPlaceholder =
305
- searchPlaceholder ?? translate("kumiko.list.search-placeholder");
306
-
307
- const toolbarStart = searchable ? (
308
- <Input
309
- kind="text"
310
- id="render-list-search"
311
- name="search"
312
- value={localQ}
313
- onChange={setLocalQ}
314
- placeholder={effectiveSearchPlaceholder}
315
- />
316
- ) : undefined;
317
-
318
- // Toolbar-End-Slot: optionaler Header-Slot (z.B. Cap-Counter, links im
319
- // rechten Cluster) + Toolbar-Actions (List-Header-Buttons aus dem Schema)
320
- // + optional "+ Neu" am rechten Edge. Der Header-Slot lebt IN der Toolbar-
321
- // Zeile (gleicher Card-Rahmen), nicht als loser Text über dem Screen-Titel
322
- // (Bug-Bash 3 #12). Reihenfolge = Array-Reihenfolge (Schema-deklariert),
323
- // "+ Neu" zuletzt weil das die häufigste/auffälligste CTA ist.
324
- const hasToolbarActions = toolbarActions !== undefined && toolbarActions.length > 0;
325
- const hasHeaderSlot = screen.slots?.header !== undefined;
326
- const toolbarIconOnly = hasToolbarActions && shouldRenderActionsIconOnly(toolbarActions);
327
- const toolbarEnd =
328
- hasHeaderSlot || hasToolbarActions || onCreate !== undefined ? (
329
- <>
330
- {hasHeaderSlot && <ListHeaderSlotMount screen={screen} />}
331
- {hasToolbarActions &&
332
- toolbarActions.map((a) => (
333
- <ToolbarActionView
334
- key={a.id}
335
- action={a}
336
- iconOnly={toolbarIconOnly}
337
- Button={Button}
338
- Dialog={Dialog}
339
- Banner={Banner}
340
- />
341
- ))}
342
- {onCreate !== undefined && (
343
- <Button variant="primary" onClick={onCreate} testId="render-list-create">
344
- {`+ ${effectiveCreateLabel}`}
345
- </Button>
346
- )}
347
- </>
348
- ) : undefined;
349
-
350
- // Empty-State: Default zeigt Heading + Description + optional CTA-
351
- // Button. Caller kann via emptyState-Prop komplett überschreiben.
352
- // Wenn weder onCreate noch ein Override gegeben ist, fällt die
353
- // DataTable auf den DataTable-Default zurück (`kumiko.list.no-entries`).
354
- const composedEmptyState =
355
- emptyState ??
356
- (onCreate !== undefined ? (
357
- <>
358
- <Text>{translate("kumiko.list.empty.title")}</Text>
359
- <Text variant="small">{translate("kumiko.list.empty.hint")}</Text>
360
- <Button variant="primary" onClick={onCreate} testId="render-list-empty-create">
361
- {`+ ${effectiveCreateLabel}`}
362
- </Button>
363
- </>
364
- ) : undefined);
365
-
366
- // Title-Resolution: ein konventioneller i18n-Key `screen:<id>.title`.
367
- // Wenn das Bundle den Key kennt, schöner Titel; sonst kommt der
368
- // screenId selber raus (kein "Untitled"-Fallback — der App-Dev sieht
369
- // dann dass die Übersetzung fehlt).
370
- const titleKey = `screen:${screen.id}.title`;
371
- const resolvedTitle = translate(titleKey);
372
- const toolbarTitle = resolvedTitle === titleKey ? screen.id : resolvedTitle;
373
-
374
- // ListSort = DataTableSort (use-list-url-state aliased) — kein Cast nötig.
375
- return (
376
- <>
377
- {referenceColumns.map(
378
- (rc: { field: string; refEntity: string; refFeature: string; labelField: string }) => (
379
- <ReferenceLookupBridge
380
- key={rc.field}
381
- field={rc.field}
382
- refEntity={rc.refEntity}
383
- labelField={rc.labelField}
384
- featureName={rc.refFeature}
385
- onMap={handleLookupMap}
386
- />
387
- ),
388
- )}
389
- <DataTable
390
- columns={enrichedVm.columns}
391
- rows={enrichedVm.rows}
392
- toolbarTitle={toolbarTitle}
393
- {...(onRowClick !== undefined && { onRowClick })}
394
- {...(composedEmptyState !== undefined && { emptyState: composedEmptyState })}
395
- {...(toolbarStart !== undefined && { toolbarStart })}
396
- {...(toolbarEnd !== undefined && { toolbarEnd })}
397
- {...(sort !== undefined && { sort })}
398
- {...(onSortChange !== undefined && { onSortChange })}
399
- {...(pager !== undefined && { pager })}
400
- {...(onReachEnd !== undefined && { onReachEnd })}
401
- {...(loadingMore !== undefined && { loadingMore })}
402
- {...(hasMore !== undefined && { hasMore })}
403
- {...(rowActions !== undefined && { rowActions })}
404
- {...(rowActionMode !== undefined && { rowActionMode })}
405
- {...(filterFacets !== undefined && { filterFacets })}
406
- {...(filterValues !== undefined && { filterValues })}
407
- {...(onFilterChange !== undefined && { onFilterChange })}
408
- {...(onFilterReset !== undefined && { onFilterReset })}
409
- {...(dateRangeFacets !== undefined && { dateRangeFacets })}
410
- {...(onDateRangeChange !== undefined && { onDateRangeChange })}
411
- {...(chromeless !== undefined && { chromeless })}
412
- {...(scrollBody !== undefined && { scrollBody })}
413
- {...(screenPadding !== undefined && { screenPadding })}
414
- testId="render-list-table"
415
- />
416
- </>
417
- );
418
- }
419
-
420
- // Header-Slot in der Listen-Toolbar (toolbarEnd): `screen.slots.header` ist
421
- // eine PlatformComponent (`{ react: { __component: "X" } }`), aufgelöst über
422
- // dieselbe ExtensionSectionsProvider-Registry wie entityEdit-Sections.
423
- // entityId ist null (Listen-Kontext, keine Row); die Component lädt ihre
424
- // Daten selbst (z.B. ein Cap-Counter aus einer usage-Query). Eigene
425
- // Component wegen Rules-of-Hooks — useExtensionSectionComponent darf nicht
426
- // konditional im RenderList-Body laufen.
427
- function ListHeaderSlotMount({
428
- screen,
429
- }: {
430
- readonly screen: EntityListScreenDefinition;
431
- }): ReactNode {
432
- const header = screen.slots?.header;
433
- const name = header !== undefined ? extensionSectionName(header) : undefined;
434
- const Component = useExtensionSectionComponent(name);
435
- // A declared-but-unregistered header slot renders nothing; warn so the author
436
- // finds the typo / missing clientFeatures.extensionSectionComponents entry,
437
- // mirroring the diagnostic banner in render-edit's ExtensionSectionMount
438
- // (the toolbar has no room for a banner, so a dev-warn it is).
439
- useEffect(() => {
440
- if (name !== undefined && Component === undefined) {
441
- // biome-ignore lint/suspicious/noConsole: dev-warning für Setup-Fehler
442
- console.warn(
443
- `[kumiko] List header slot component "${name}" on screen "${screen.id}" is not registered in clientFeatures.extensionSectionComponents — the header slot renders nothing.`,
444
- );
445
- }
446
- }, [name, Component, screen.id]);
447
- if (Component === undefined) return null;
448
- // screenId lets a header control drive this list's URL-filter state
449
- // (useListUrlState) — e.g. a tag filter narrowing the list to an id-set.
450
- return <Component entityName={screen.entity} entityId={null} screenId={screen.id} />;
451
- }
452
-
453
- // Tier 2.7e-4: Bridge-Component pro reference-Spalte. Mounted für jede
454
- // Spalte einmal, ruft useReferenceLookup unconditional (Hook-Rules
455
- // happy), und published die Map über onMap an den Parent. React
456
- // verwaltet das Mounting/Unmounting beim Schema-Wechsel über die
457
- // key-Prop im map-Loop des Parents.
458
- function ReferenceLookupBridge({
459
- field,
460
- refEntity,
461
- labelField,
462
- featureName,
463
- onMap,
464
- }: {
465
- readonly field: string;
466
- readonly refEntity: string;
467
- readonly labelField: string;
468
- readonly featureName: string;
469
- readonly onMap: (field: string, map: ReferenceLookupMap) => void;
470
- }): null {
471
- const lookup = useReferenceLookup(featureName, refEntity, labelField);
472
- // useEffect statt direktem call damit setState außerhalb des Render-
473
- // Pfads passiert (sonst React-Warning "Cannot update a component
474
- // while rendering a different component").
475
- useEffect(() => {
476
- onMap(field, lookup.map);
477
- }, [field, lookup.map, onMap]);
478
- return null;
479
- }
480
-
481
- // ToolbarActionView — pro Toolbar-Action ein Button (+ Confirm-Dialog
482
- // wenn confirm gesetzt). Same Pattern wie RowAction (Inline-Variante)
483
- // aber ohne row-Context. busy-State während async onTrigger; Dialog
484
- // öffnet sich vor dem Trigger wenn confirm/danger gesetzt.
485
- function ToolbarActionView({
486
- action,
487
- iconOnly = false,
488
- Button,
489
- Dialog,
490
- Banner,
491
- }: {
492
- readonly action: ToolbarActionButton;
493
- /** Group-level collapse (see `shouldRenderActionsIconOnly`) — only takes
494
- * effect when this action actually resolved an icon. */
495
- readonly iconOnly?: boolean;
496
- readonly Button: ReturnType<typeof usePrimitives>["Button"];
497
- readonly Dialog: ReturnType<typeof usePrimitives>["Dialog"];
498
- readonly Banner: ReturnType<typeof usePrimitives>["Banner"];
499
- }): ReactNode {
500
- const [busy, setBusy] = useState(false);
501
- const [confirmOpen, setConfirmOpen] = useState(false);
502
- // Toolbar-Actions haben (anders als rowActions, die über die DataTable-
503
- // Primitive laufen) keine Toast-Primitive zur Verfügung — ein inline
504
- // Error-Banner ist der surfacing-Pfad hier. Gleicher Grund wie bei
505
- // rowActions (Prod-Bug 2026-06-07): ein verschlucktes Failure-Result
506
- // sieht für den User wie "nichts passiert" aus.
507
- const [errorText, setErrorText] = useState<string | null>(null);
508
-
509
- const trigger = async (): Promise<void> => {
510
- setBusy(true);
511
- setErrorText(null);
512
- try {
513
- await action.onTrigger();
514
- } catch (e) {
515
- setErrorText(e instanceof Error ? e.message : String(e));
516
- } finally {
517
- setBusy(false);
518
- }
519
- };
520
-
521
- const variant: "primary" | "secondary" | "danger" = action.style ?? "secondary";
522
- const needsConfirm =
523
- action.confirm !== undefined || (action.confirmRequired ?? action.style === "danger");
524
- const showIconOnly = iconOnly && action.icon !== undefined;
525
-
526
- return (
527
- <>
528
- <Button
529
- variant={variant}
530
- loading={busy}
531
- {...(action.icon !== undefined && { icon: action.icon })}
532
- {...(showIconOnly && { size: "icon" as const, ariaLabel: action.label })}
533
- onClick={() => {
534
- if (needsConfirm) {
535
- setConfirmOpen(true);
536
- } else {
537
- void trigger();
538
- }
539
- }}
540
- testId={`render-list-toolbar-action-${action.id}`}
541
- >
542
- {showIconOnly ? null : action.label}
543
- </Button>
544
- <Dialog
545
- open={confirmOpen}
546
- onOpenChange={setConfirmOpen}
547
- title={action.label}
548
- {...(action.confirm !== undefined && { description: action.confirm })}
549
- confirmLabel={action.confirmLabel ?? action.label}
550
- {...(action.style === "danger" && { variant: "danger" as const })}
551
- onConfirm={trigger}
552
- testId={`render-list-toolbar-action-${action.id}-dialog`}
553
- />
554
- {errorText !== null && (
555
- <Banner variant="error" testId={`render-list-toolbar-action-${action.id}-error`}>
556
- {errorText}
557
- </Banner>
558
- )}
559
- </>
560
- );
561
- }