@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,207 +0,0 @@
1
- // useListUrlState — bündelt sort/dir/page/q einer entityList in einen
2
- // per-screen-id namespaced URL-State. Mit Screen-ID-Prefix damit zwei
3
- // Listen auf derselben Route (z.B. Dashboard mit Orders + Incidents)
4
- // nicht über dieselben Query-Keys streiten.
5
- //
6
- // Param-Schema pro Liste:
7
- // <screenId>.sort — field name (string)
8
- // <screenId>.dir — "asc" | "desc"
9
- // <screenId>.q — search term (URL-encoded)
10
- // <screenId>.page — 1-based page number (nur bei pagination="pages")
11
- // <screenId>.f.<field> — Faceted-Filter: comma-joined selected values
12
- //
13
- // Schreibt mit setSearchParams (replaceState — kein push), damit
14
- // Sort/Filter-Toggles nicht die Browser-History fluten.
15
-
16
- import { useCallback, useMemo } from "react";
17
- import { useNav } from "../app/nav";
18
- import type { DataTableSort, DataTableSortDir } from "../primitives";
19
-
20
- // ListSort + DataTableSort hatten dieselbe Shape und drohten zu driften
21
- // — aliased auf den primitives-Type (eine Quelle, kein Cast in RenderList).
22
- export type ListSortDir = DataTableSortDir;
23
- export type ListSort = DataTableSort;
24
-
25
- export type ListUrlState = {
26
- /** Aktive Sortierung (oder null = unsorted, Server liefert Default-Order). */
27
- readonly sort: ListSort | null;
28
- /** Search-Term (leer wenn nicht gesetzt). */
29
- readonly q: string;
30
- /** 1-basierte Page-Nummer. Bei pagination="infinite" oder false ist
31
- * der Wert ignoriert; Caller liest ihn nur wenn relevant. */
32
- readonly page: number;
33
- /** User-chosen page size from `PAGE_SIZE_OPTIONS`; undefined = screen default. */
34
- readonly pageSize: number | undefined;
35
- /** Aktive Faceted-Filter: field → ausgewählte Werte (Strings; Caller
36
- * coerced pro Field-Type). Leer wenn kein Filter gesetzt. */
37
- readonly filters: Readonly<Record<string, readonly string[]>>;
38
- };
39
-
40
- export type ListUrlStateApi = ListUrlState & {
41
- /** Setzt sort + dir atomar. null = unsorted (löscht beide Keys). */
42
- readonly setSort: (next: ListSort | null) => void;
43
- /** Setzt den Search-Term. Empty-String löscht den Key. Caller debounced
44
- * selber (z.B. useDebouncedCallback in der Search-Input-Komponente). */
45
- readonly setQ: (next: string) => void;
46
- /** Setzt die Page. 1 oder kleiner löscht den Key (Default-Page). */
47
- readonly setPage: (next: number) => void;
48
- /** Sets the page size and resets the page; undefined clears the key. */
49
- readonly setPageSize: (next: number | undefined) => void;
50
- /** Setzt die ausgewählten Werte eines Facet-Felds. Leeres Array löscht
51
- * den Key. Resettet die Page (wie sort/search). */
52
- readonly setFilter: (field: string, values: readonly string[]) => void;
53
- /** Löscht alle aktiven Faceted-Filter (Reset-Button). */
54
- readonly clearFilters: () => void;
55
- /** Sets both bounds of a dateRange facet in ONE params update — two
56
- * setFilter calls would overwrite each other, since the second still sees
57
- * the old searchParams. An empty string clears that bound. Resets the page
58
- * like setFilter. */
59
- readonly setDateRange: (
60
- field: string,
61
- next: { readonly from: string; readonly to: string },
62
- ) => void;
63
- };
64
-
65
- // `.` als Trenner: lesbar (`?orders.sort=name`), kollidiert nicht mit
66
- // üblichen Field-Namen (kebab- oder camelCase ohne Punkt). Boot-Validator
67
- // pinnt screen.id ohne Punkt — siehe boot-validator entityList Section.
68
- export const PAGE_SIZE_OPTIONS: readonly number[] = [25, 50, 100];
69
-
70
- function parsePageSize(value: string | undefined): number | undefined {
71
- if (value === undefined) return undefined;
72
- const n = Number.parseInt(value, 10);
73
- return PAGE_SIZE_OPTIONS.includes(n) ? n : undefined;
74
- }
75
-
76
- function key(screenId: string, suffix: string): string {
77
- return `${screenId}.${suffix}`;
78
- }
79
-
80
- function parseDir(value: string | undefined): ListSortDir | undefined {
81
- return value === "asc" || value === "desc" ? value : undefined;
82
- }
83
-
84
- function parsePage(value: string | undefined): number {
85
- if (value === undefined) return 1;
86
- const n = Number.parseInt(value, 10);
87
- return Number.isFinite(n) && n > 0 ? n : 1;
88
- }
89
-
90
- export function useListUrlState(screenId: string): ListUrlStateApi {
91
- const nav = useNav();
92
- const params = nav.searchParams;
93
-
94
- const sort = useMemo<ListSort | null>(() => {
95
- const field = params[key(screenId, "sort")];
96
- const dir = parseDir(params[key(screenId, "dir")]);
97
- if (field === undefined || field === "" || dir === undefined) return null;
98
- return { field, dir };
99
- }, [params, screenId]);
100
-
101
- const q = params[key(screenId, "q")] ?? "";
102
- const page = parsePage(params[key(screenId, "page")]);
103
- const pageSize = parsePageSize(params[key(screenId, "size")]);
104
-
105
- const filterPrefix = `${screenId}.f.`;
106
- const filters = useMemo<Readonly<Record<string, readonly string[]>>>(() => {
107
- const out: Record<string, readonly string[]> = {};
108
- for (const [k, v] of Object.entries(params)) {
109
- if (!k.startsWith(filterPrefix) || v === "") continue;
110
- const field = k.slice(filterPrefix.length);
111
- if (field === "") continue;
112
- // ponytail: comma-join — Facet-Werte sind select-Options/booleans
113
- // ohne Komma; upgrade auf repeated-keys falls je Komma-Werte nötig.
114
- out[field] = v.split(",").filter((s) => s !== "");
115
- }
116
- return out;
117
- }, [params, filterPrefix]);
118
-
119
- const setSort = useCallback(
120
- (next: ListSort | null) => {
121
- // Atomares Update: bei jedem Sort-Wechsel resetten wir auch die
122
- // Page (sonst wäre der User auf "Seite 5 von alter Sortierung"
123
- // hängen, was visuell verwirrend ist). Page-Reset gilt auch bei
124
- // null — gleiche Logik.
125
- nav.setSearchParams({
126
- [key(screenId, "sort")]: next === null ? null : next.field,
127
- [key(screenId, "dir")]: next === null ? null : next.dir,
128
- [key(screenId, "page")]: null,
129
- });
130
- },
131
- [nav, screenId],
132
- );
133
-
134
- const setQ = useCallback(
135
- (next: string) => {
136
- // Search-Change resettet Page (gleicher Grund wie Sort) UND
137
- // Sort? Nein — User kann mit aktivem Sort suchen wollen, das
138
- // soll die Sortierung nicht zerlegen. Nur Page wird gereset.
139
- nav.setSearchParams({
140
- [key(screenId, "q")]: next === "" ? null : next,
141
- [key(screenId, "page")]: null,
142
- });
143
- },
144
- [nav, screenId],
145
- );
146
-
147
- const setPage = useCallback(
148
- (next: number) => {
149
- nav.setSearchParams({
150
- [key(screenId, "page")]: next <= 1 ? null : String(next),
151
- });
152
- },
153
- [nav, screenId],
154
- );
155
-
156
- const setPageSize = useCallback(
157
- (next: number | undefined) => {
158
- nav.setSearchParams({
159
- [key(screenId, "size")]: next === undefined ? null : String(next),
160
- [key(screenId, "page")]: null,
161
- });
162
- },
163
- [nav, screenId],
164
- );
165
-
166
- const setFilter = useCallback(
167
- (field: string, values: readonly string[]) => {
168
- nav.setSearchParams({
169
- [key(screenId, `f.${field}`)]: values.length === 0 ? null : values.join(","),
170
- [key(screenId, "page")]: null,
171
- });
172
- },
173
- [nav, screenId],
174
- );
175
-
176
- const setDateRange = useCallback(
177
- (field: string, next: { readonly from: string; readonly to: string }) => {
178
- nav.setSearchParams({
179
- [key(screenId, `f.${field}.from`)]: next.from === "" ? null : next.from,
180
- [key(screenId, `f.${field}.to`)]: next.to === "" ? null : next.to,
181
- [key(screenId, "page")]: null,
182
- });
183
- },
184
- [nav, screenId],
185
- );
186
-
187
- const clearFilters = useCallback(() => {
188
- const updates: Record<string, string | null> = { [key(screenId, "page")]: null };
189
- for (const field of Object.keys(filters)) updates[key(screenId, `f.${field}`)] = null;
190
- nav.setSearchParams(updates);
191
- }, [nav, screenId, filters]);
192
-
193
- return {
194
- sort,
195
- q,
196
- page,
197
- pageSize,
198
- filters,
199
- setSort,
200
- setQ,
201
- setPage,
202
- setPageSize,
203
- setFilter,
204
- clearFilters,
205
- setDateRange,
206
- };
207
- }
@@ -1,61 +0,0 @@
1
- import type { DispatcherError, WriteResult } from "@cosmicdrift/kumiko-headless";
2
- import { useCallback, useRef, useState } from "react";
3
- import { useDispatcher } from "../context/dispatcher-context";
4
-
5
- // React wrapper around dispatcher.write — the write-side sibling of
6
- // useQuery. One hook instance per handler-type; `mutate` carries the
7
- // payload so a single instance serves list-row actions with varying
8
- // payloads.
9
- //
10
- // `mutate` resolves with the raw WriteResult so callers can branch
11
- // (navigate on success, keep the form open on failure) without waiting
12
- // for a re-render of `error`/`data`.
13
-
14
- export type UseMutationResult<TData> = {
15
- readonly mutate: (payload: unknown) => Promise<WriteResult<TData>>;
16
- readonly pending: boolean;
17
- readonly error: DispatcherError | null;
18
- readonly data: TData | null;
19
- readonly reset: () => void;
20
- };
21
-
22
- export function useMutation<TData = unknown>(type: string): UseMutationResult<TData> {
23
- const dispatcher = useDispatcher();
24
- const [pending, setPending] = useState(false);
25
- const [error, setError] = useState<DispatcherError | null>(null);
26
- const [data, setData] = useState<TData | null>(null);
27
- // Sequence guard: `mutate` has no abort (unlike useQuery's GETs, a write
28
- // already landed server-side and can't be cancelled). Two overlapping
29
- // calls on one instance (e.g. two list-row actions) must not let the
30
- // first-to-resolve clobber pending/error/data set by the second — only
31
- // the most recently STARTED call's outcome may update shared state.
32
- const sequence = useRef(0);
33
-
34
- const mutate = useCallback(
35
- async (payload: unknown): Promise<WriteResult<TData>> => {
36
- const callSeq = ++sequence.current;
37
- setPending(true);
38
- setError(null);
39
- const result = await dispatcher.write<TData>(type, payload);
40
- if (callSeq === sequence.current) {
41
- if (result.isSuccess) {
42
- setData(result.data);
43
- } else {
44
- setError(result.error);
45
- }
46
- setPending(false);
47
- }
48
- return result;
49
- },
50
- [dispatcher, type],
51
- );
52
-
53
- const reset = useCallback(() => {
54
- sequence.current++; // invalidate any in-flight mutate's late update
55
- setPending(false);
56
- setError(null);
57
- setData(null);
58
- }, []);
59
-
60
- return { mutate, pending, error, data, reset };
61
- }
@@ -1,166 +0,0 @@
1
- import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { useDispatcher } from "../context/dispatcher-context";
4
- import { useLiveEvents } from "../sse/live-events";
5
-
6
- // React wrapper around dispatcher.query. Fires on mount, re-fires
7
- // whenever `type` or the serialized payload change, and exposes a
8
- // `refetch` imperative hook for after-mutation reloads.
9
- //
10
- // Cancellation: each fetch owns an AbortController; a newer fetch
11
- // (change in deps or manual refetch) aborts the older one's network
12
- // call before firing, so a slow request can't overwrite a fresh one.
13
- // Unmount aborts any in-flight call too — React will throw the
14
- // "state on unmounted" warning if we set state after unmount, and
15
- // aborting pre-emptively makes that path a no-op.
16
- //
17
- // Result shape modeled on the dispatcher's own QueryResult: a
18
- // discriminated state so callers can branch on `loading | data | error`
19
- // without undefined-juggling. The first render is `loading: true,
20
- // data: null, error: null` (no optimistic "online" cache — add that
21
- // later if needed).
22
-
23
- export type UseQueryResult<TData> = {
24
- readonly data: TData | null;
25
- readonly error: DispatcherError | null;
26
- readonly loading: boolean;
27
- readonly refetch: () => Promise<void>;
28
- };
29
-
30
- export type UseQueryOptions = {
31
- // When false, skips the automatic fetch-on-mount and re-fetch on
32
- // dep-change. Useful for lazy queries that only run after a user
33
- // action. `refetch()` still works as an imperative trigger.
34
- readonly enabled?: boolean;
35
- // When true, subscribe to SSE events for the entity this query
36
- // targets and refetch on any create/update/delete/restore event.
37
- // The entity-name is parsed from the query type using Kumiko's
38
- // `<feature>:query:<entity>:<verb>` convention — if the type
39
- // doesn't follow that shape, live-mode is a no-op.
40
- readonly live?: boolean;
41
- // Re-runs the query every N ms while enabled. Ticks are silent (no
42
- // `loading` flip) and skipped while a fetch is still in flight. Only
43
- // timers are used, so it behaves the same on web and React Native.
44
- readonly refetchIntervalMs?: number;
45
- };
46
-
47
- // Extract the entity-name from a standard Kumiko query type. Returns
48
- // undefined for non-conforming types so the live-mode silently skips
49
- // them instead of subscribing to a channel no event will ever match.
50
- export function entityFromQueryType(type: string): string | undefined {
51
- // Expected shape: "<feature>:query:<entity>:<verb>"
52
- const parts = type.split(":");
53
- if (parts.length !== 4) return undefined;
54
- if (parts[1] !== "query") return undefined;
55
- return parts[2];
56
- }
57
-
58
- export function useQuery<TData = unknown>(
59
- type: string,
60
- payload: unknown,
61
- options: UseQueryOptions = {},
62
- ): UseQueryResult<TData> {
63
- const dispatcher = useDispatcher();
64
- const { enabled = true, live = false, refetchIntervalMs } = options;
65
-
66
- const [data, setData] = useState<TData | null>(null);
67
- const [error, setError] = useState<DispatcherError | null>(null);
68
- const [loading, setLoading] = useState<boolean>(enabled);
69
-
70
- // Track the AbortController for the most recent fetch so a newer
71
- // call can cancel the older one. Ref, not state, because the
72
- // controller identity shouldn't trigger a re-render.
73
- const activeCtrl = useRef<AbortController | null>(null);
74
- // A tick that aborted a running fetch would starve slow queries, so
75
- // ticks consult this flag instead.
76
- const fetchInFlight = useRef(false);
77
-
78
- // Serialize payload so object-identity-changes across renders (the
79
- // caller building a fresh `{}` every render) don't loop. Strings are
80
- // stable by reference in React's dep-array check.
81
- const payloadKey = JSON.stringify(payload);
82
-
83
- // `dispatcher` is stable (context identity); `type` + `payloadKey`
84
- // are the meaningful re-run triggers. `payload` itself is intentionally
85
- // not in deps — it's serialized into payloadKey above.
86
- // biome-ignore lint/correctness/useExhaustiveDependencies: payload goes through payloadKey
87
- const runFetch = useCallback(
88
- async (background: boolean): Promise<void> => {
89
- // Abort whatever's in flight. observer on Safari 17+ handles this
90
- // without raising a fetch-throw in the previous caller — they
91
- // already set the error path.
92
- activeCtrl.current?.abort();
93
- const ctrl = new AbortController();
94
- activeCtrl.current = ctrl;
95
-
96
- fetchInFlight.current = true;
97
- if (!background) setLoading(true);
98
- let result: Awaited<ReturnType<typeof dispatcher.query<TData>>>;
99
- try {
100
- result = await dispatcher.query<TData>(type, payload, { signal: ctrl.signal });
101
- } finally {
102
- // A rejected fetch must not leave polling stuck; a superseded one must
103
- // not clear the newer fetch's flag.
104
- if (activeCtrl.current === ctrl) fetchInFlight.current = false;
105
- }
106
- // skip: a newer fetch already superseded this one, don't clobber its state
107
- if (ctrl.signal.aborted) return;
108
- if (result.isSuccess) {
109
- setData(result.data);
110
- setError(null);
111
- } else {
112
- // A cancelled request comes back with code "aborted" from the
113
- // skip: aborted request, a newer run already replaces this result
114
- if (result.error.code === "aborted") return;
115
- setError(result.error);
116
- }
117
- setLoading(false);
118
- },
119
- [dispatcher, type, payloadKey],
120
- );
121
-
122
- const run = useCallback((): Promise<void> => runFetch(false), [runFetch]);
123
-
124
- useEffect(() => {
125
- if (!enabled) {
126
- setLoading(false);
127
- // skip: query disabled, loading state already cleared above
128
- return;
129
- }
130
- void run();
131
- return () => {
132
- activeCtrl.current?.abort();
133
- fetchInFlight.current = false;
134
- };
135
- }, [enabled, run]);
136
-
137
- useEffect(() => {
138
- // skip: polling off or disabled query
139
- if (!enabled || refetchIntervalMs === undefined) return;
140
- // skip: non-positive or non-finite interval disables polling
141
- if (!Number.isFinite(refetchIntervalMs) || refetchIntervalMs <= 0) return;
142
- const timer = setInterval(() => {
143
- // skip: a fetch is still running, the next tick retries
144
- if (fetchInFlight.current) return;
145
- void runFetch(true);
146
- }, refetchIntervalMs);
147
- return () => clearInterval(timer);
148
- }, [enabled, refetchIntervalMs, runFetch]);
149
-
150
- // Live-mode: auf SSE-Events für die Query-Entity hören und refetchen.
151
- // Separater Effect, damit eine Änderung an `live` oder `type` das
152
- // Subscription-Lifecycle genau einmal durchwalzt.
153
- const subscribeLive = useLiveEvents();
154
- useEffect(() => {
155
- // skip: live mode or query disabled, no SSE subscription needed
156
- if (!live || !enabled) return;
157
- const entity = entityFromQueryType(type);
158
- // skip: query type has no mapped entity, nothing to subscribe to
159
- if (entity === undefined) return;
160
- return subscribeLive(entity, () => {
161
- void run();
162
- });
163
- }, [live, enabled, type, run, subscribeLive]);
164
-
165
- return { data, error, loading, refetch: run };
166
- }
@@ -1,76 +0,0 @@
1
- // Tier 2.7e-4: Renderer-Side Eagerload für Reference-Felder.
2
- //
3
- // Pro Reference-Spalte einer Liste wird einmal `<feature>:query:
4
- // <refEntity>:list` (limit:200) gerufen, eine Map<uuid, displayValue>
5
- // gebaut, und der Renderer nutzt sie als Cell-Display-Renderer.
6
- //
7
- // Strategy-Trade-offs:
8
- // - Server-side Drizzle-Joins wären effizienter (eine Query statt
9
- // N+1), würden aber die executor-API um lookupTables erweitern
10
- // müssen. Für das MVP geht der Renderer-Side-Pfad vor.
11
- // - Limit:200 ist eine harte UX-Grenze: bei mehr Entries in der
12
- // referenced Entity zeigen die letzten Rows nur noch UUIDs.
13
- // Searchable-Combobox (Tier 2.1c) + Server-Eagerload mit
14
- // ID-Whitelist heben das später auf.
15
- //
16
- // Dieser Hook ist call-stable: Lookup-Map wird durch useQuery's
17
- // internen Cache shared zwischen List + Edit-Form für die gleiche
18
- // Entity, Live-Updates kommen via SSE (use-query-live).
19
-
20
- import {
21
- REFERENCE_LOOKUP_SOURCES,
22
- SYSTEM_REFERENCE_LABELS,
23
- } from "@cosmicdrift/kumiko-framework/ui-types";
24
- import { useMemo } from "react";
25
- import { toKebab } from "../app/qn";
26
- import { useTranslation } from "../i18n";
27
- import { REFERENCE_LIST_LOOKUP_LIMIT } from "./reference-limits";
28
- import { useQuery } from "./use-query";
29
-
30
- export type ReferenceLookupMap = ReadonlyMap<string, string>;
31
-
32
- /** Where a reference target's label rows come from: the central override for
33
- * that `feature:entity` if one exists, else the entity-convention list
34
- * handler plus the caller's own label field. */
35
- export function referenceLookupSource(
36
- refFeature: string,
37
- refEntity: string,
38
- labelField: string,
39
- ): { readonly queryQn: string; readonly labelKey: string } {
40
- const override = REFERENCE_LOOKUP_SOURCES[`${refFeature}:${refEntity}`];
41
- if (override !== undefined) return override;
42
- return {
43
- queryQn: `${toKebab(refFeature)}:query:${toKebab(refEntity)}:list`,
44
- labelKey: labelField,
45
- };
46
- }
47
-
48
- /** Empty while the query loads (or fails), so callers fall back to the raw id.
49
- * `featureName` is the reference's target feature, not the screen's. */
50
- export function useReferenceLookup(
51
- featureName: string,
52
- refEntity: string,
53
- labelField: string,
54
- ): { readonly map: ReferenceLookupMap; readonly loading: boolean } {
55
- const { queryQn, labelKey } = referenceLookupSource(featureName, refEntity, labelField);
56
- const result = useQuery<{ rows: ReadonlyArray<Record<string, unknown>> }>(queryQn, {
57
- limit: REFERENCE_LIST_LOOKUP_LIMIT,
58
- });
59
- const translate = useTranslation();
60
- const systemLabel = SYSTEM_REFERENCE_LABELS[`${featureName}:${refEntity}`];
61
- const map = useMemo(() => {
62
- const out = new Map<string, string>();
63
- for (const row of result.data?.rows ?? []) {
64
- const id = row["id"];
65
- if (id === undefined || id === null) continue;
66
- const idStr = String(id);
67
- const label = row[labelKey] ?? id;
68
- out.set(idStr, String(label));
69
- }
70
- // System-scope ids (e.g. SYSTEM_TENANT_ID) never have a backing row, so
71
- // the bulk lookup above never covers them — inject the label directly.
72
- if (systemLabel !== undefined) out.set(systemLabel.id, translate(systemLabel.labelKey));
73
- return out;
74
- }, [result.data, labelKey, systemLabel, translate]);
75
- return { map, loading: result.loading };
76
- }
@@ -1,48 +0,0 @@
1
- import type { Store } from "@cosmicdrift/kumiko-headless";
2
- import { useRef, useSyncExternalStore } from "react";
3
-
4
- // React-Bindings für Kumikos Store-Primitive (`createStore` aus
5
- // @cosmicdrift/kumiko-headless). Beide Hooks sind dünne Wrapper um React's
6
- // useSyncExternalStore — die ganze Subscribe/Notify-Mechanik lebt im
7
- // Store selbst, hier wird nur React verdrahtet.
8
- //
9
- // useStore: gibt den ganzen Snapshot zurück. Re-rendert wenn der
10
- // Store seine Identität wechselt. Object.is-Gate im Store selbst
11
- // verhindert no-op-Updates.
12
- //
13
- // useStoreSelector: gibt eine abgeleitete Sicht zurück. Selectors die
14
- // Object-/Array-Literale zurückgeben (z.B. `s => ({ a, b })`) erzeugen
15
- // pro Render eine neue Identität — useSyncExternalStore würde das als
16
- // "Change" lesen und in eine Re-Render-Schleife laufen. Der dritte
17
- // Arg `equals` dient genau dafür: gleiche Auswahl → cached
18
- // Identität zurück, kein Re-Render.
19
-
20
- // @wrapper-known semantic-alias
21
- export function useStore<T>(store: Store<T>): T {
22
- return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
23
- }
24
-
25
- export function useStoreSelector<T, S>(
26
- store: Store<T>,
27
- select: (snapshot: T) => S,
28
- equals: (a: S, b: S) => boolean = Object.is,
29
- ): S {
30
- // Cache der letzten Auswahl. Beim nächsten getSnapshot-Call vergleicht
31
- // der Wrapper die neu berechnete Auswahl mit dem Cache und gibt — falls
32
- // gleich — die cached Identität zurück. Das ist der Trick, der
33
- // Selector-Returns wie `s => ({ a, b })` stabil hält.
34
- const lastSelected = useRef<S>(undefined as S);
35
- const initialized = useRef(false);
36
-
37
- const getSelectedSnapshot = (): S => {
38
- const next = select(store.getSnapshot());
39
- if (initialized.current && equals(lastSelected.current, next)) {
40
- return lastSelected.current;
41
- }
42
- lastSelected.current = next;
43
- initialized.current = true;
44
- return next;
45
- };
46
-
47
- return useSyncExternalStore(store.subscribe, getSelectedSnapshot, getSelectedSnapshot);
48
- }