@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,67 +0,0 @@
1
- // Read-only render of a content editor's format with the collection's
2
- // example variableSchema values substituted in for `{{name}}` — reuses the
3
- // same registered editor the collection edits with (readOnly), so "rich"
4
- // renders formatted HTML and "plain"/"markdown" render as text through the
5
- // exact same component, no separate render path per format.
6
-
7
- import type { ContentEditorFormat } from "@cosmicdrift/kumiko-types/nav";
8
- import { type ReactNode, useId } from "react";
9
- import { useContentEditor } from "./content-editors";
10
-
11
- const VARIABLE_PATTERN = /\{\{\s*(\w+)\s*\}\}/g;
12
-
13
- const noop = (): void => {};
14
-
15
- /** A name with no example value (or not in the schema at all) stays as the
16
- * literal `{{name}}` placeholder — there is nothing else to show for it. */
17
- export function substituteVariables(
18
- content: string,
19
- variables: Readonly<Record<string, string>>,
20
- ): string {
21
- return content.replace(VARIABLE_PATTERN, (match, name: string) =>
22
- Object.hasOwn(variables, name) ? (variables[name] ?? match) : match,
23
- );
24
- }
25
-
26
- function escapeHtml(value: string): string {
27
- return value
28
- .replace(/&/g, "&amp;")
29
- .replace(/</g, "&lt;")
30
- .replace(/>/g, "&gt;")
31
- .replace(/"/g, "&quot;")
32
- .replace(/'/g, "&#39;");
33
- }
34
-
35
- export type ContentPreviewProps = {
36
- readonly content: string;
37
- readonly variables: Readonly<Record<string, string>>;
38
- readonly contentFormat?: ContentEditorFormat;
39
- };
40
-
41
- export function ContentPreview({
42
- content,
43
- variables,
44
- contentFormat,
45
- }: ContentPreviewProps): ReactNode {
46
- const Editor = useContentEditor(contentFormat);
47
- const id = useId();
48
- // "rich" content is HTML (see ContentCollectionDefinition.contentFormat) —
49
- // an example value substituted in raw could break the markup (`<`) or
50
- // render as an unescaped entity (`&`). "plain"/"markdown" content is text,
51
- // no escaping wanted there.
52
- const safeVariables =
53
- contentFormat === "rich"
54
- ? Object.fromEntries(
55
- Object.entries(variables).map(([name, value]) => [name, escapeHtml(value)]),
56
- )
57
- : variables;
58
- return (
59
- <Editor
60
- id={id}
61
- value={substituteVariables(content, safeVariables)}
62
- onChange={noop}
63
- variables={[]}
64
- readOnly
65
- />
66
- );
67
- }
@@ -1,29 +0,0 @@
1
- // Custom-Screen-Components-Map: client-side Lookup für Screens vom Typ
2
- // `custom`. KumikoScreen schaut hier nach `screen.id` (oder qn) und
3
- // rendert die passende Component. createKumikoApp im renderer-web sammelt
4
- // alle clientFeatures.components und mountet den Provider; Tests die
5
- // einzelne Custom-Screens prüfen wollen, mounten den Provider direkt.
6
-
7
- import { type ComponentType, createContext, type ReactNode, useContext } from "react";
8
-
9
- export type CustomScreensMap = Readonly<Record<string, ComponentType>>;
10
-
11
- const CustomScreensContext = createContext<CustomScreensMap | undefined>(undefined);
12
-
13
- export type CustomScreensProviderProps = {
14
- readonly children: ReactNode;
15
- readonly value: CustomScreensMap;
16
- };
17
-
18
- export function CustomScreensProvider({ children, value }: CustomScreensProviderProps): ReactNode {
19
- return <CustomScreensContext.Provider value={value}>{children}</CustomScreensContext.Provider>;
20
- }
21
-
22
- /** Schaut die Component für ein Custom-Screen nach. Returnt undefined
23
- * wenn weder Provider gemounted noch screenId in der Map ist — der
24
- * Caller (KumikoScreen) zeigt dann seinen Placeholder-Banner. */
25
- export function useCustomScreenComponent(screenId: string): ComponentType | undefined {
26
- const map = useContext(CustomScreensContext);
27
- if (map === undefined) return undefined;
28
- return map[screenId];
29
- }
@@ -1,33 +0,0 @@
1
- // Dashboard-Body-Injection: der KumikoScreen-Switch ist plattform-agnostisch,
2
- // die Dashboard-Panels (StatCard, Charts) sind es nicht — die Implementierung
3
- // kommt vom Platform-Package (renderer-web registriert seine Web-Variante in
4
- // createKumikoApp). Gleiches Muster wie CustomScreensProvider.
5
-
6
- import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
7
- import type { Translate } from "@cosmicdrift/kumiko-headless";
8
- import { type ComponentType, createContext, type ReactNode, useContext } from "react";
9
-
10
- export type DashboardBodyProps = {
11
- readonly featureName: string;
12
- readonly screen: DashboardScreenDefinition;
13
- readonly translate?: Translate;
14
- };
15
-
16
- const DashboardBodyContext = createContext<ComponentType<DashboardBodyProps> | undefined>(
17
- undefined,
18
- );
19
-
20
- export type DashboardBodyProviderProps = {
21
- readonly children: ReactNode;
22
- readonly value: ComponentType<DashboardBodyProps>;
23
- };
24
-
25
- export function DashboardBodyProvider({ children, value }: DashboardBodyProviderProps): ReactNode {
26
- return <DashboardBodyContext.Provider value={value}>{children}</DashboardBodyContext.Provider>;
27
- }
28
-
29
- /** Undefined wenn kein Platform-Package einen Dashboard-Body registriert
30
- * hat — KumikoScreen zeigt dann seinen Placeholder-Banner. */
31
- export function useDashboardBody(): ComponentType<DashboardBodyProps> | undefined {
32
- return useContext(DashboardBodyContext);
33
- }
@@ -1,123 +0,0 @@
1
- // @runtime client
2
- // Extension-Form-Submit-Registry — erlaubt einer Extension-Section (z.B.
3
- // Custom-Fields), beim Submit des umgebenden entityEdit-Forms mitzuschreiben,
4
- // statt einen eigenen Save-Button zu führen ("composed form, ein Save",
5
- // Bug-Bash 3 #1). Die Section meldet (a) einen Submit-Handler und (b) ihren
6
- // dirty-State an die Form; die Form aktiviert ihren Save-Button auch wenn nur
7
- // eine Section dirty ist und ruft nach dem Entity-Write alle Section-Handler
8
- // mit der entityId.
9
- //
10
- // Ohne umgebende Form (Context === null) fällt die Section auf ihren eigenen
11
- // Save-Button zurück (Standalone-Backward-Compat).
12
-
13
- import { createContext, useContext, useEffect, useId, useMemo, useRef } from "react";
14
-
15
- export type ExtensionSubmitContext = { readonly entityId: string };
16
-
17
- export type ExtensionSubmitResult = {
18
- readonly isSuccess: boolean;
19
- /** i18n-Key für die Fehlermeldung, die die Form als Banner zeigt. */
20
- readonly errorKey?: string;
21
- };
22
-
23
- export type ExtensionFormSubmitHandler = (
24
- ctx: ExtensionSubmitContext,
25
- ) => Promise<ExtensionSubmitResult>;
26
-
27
- type Registration = {
28
- readonly key: string;
29
- readonly dirty: boolean;
30
- readonly handler: ExtensionFormSubmitHandler;
31
- };
32
-
33
- export type ExtensionFormRegistry = {
34
- readonly upsert: (reg: Registration) => void;
35
- readonly remove: (key: string) => void;
36
- };
37
-
38
- const ExtensionFormRegistryContext = createContext<ExtensionFormRegistry | null>(null);
39
-
40
- export const ExtensionFormRegistryProvider = ExtensionFormRegistryContext.Provider;
41
-
42
- // Host-Seite (render-edit): hält die Registrierungen in einem ref, meldet den
43
- // aggregierten dirty-State via onDirtyChange hoch (damit der Save-Button
44
- // re-rendert) und liefert runAll() zum Ausführen aller Handler beim Submit.
45
- export type ExtensionFormHostStatus = {
46
- readonly anyDirty: boolean;
47
- readonly hasRegistrations: boolean;
48
- };
49
-
50
- export function useExtensionFormHost(onStatusChange: (status: ExtensionFormHostStatus) => void): {
51
- readonly registry: ExtensionFormRegistry;
52
- readonly runAll: (ctx: ExtensionSubmitContext) => Promise<readonly ExtensionSubmitResult[]>;
53
- } {
54
- const regsRef = useRef<Map<string, Registration>>(new Map());
55
- const onStatusRef = useRef(onStatusChange);
56
- onStatusRef.current = onStatusChange;
57
-
58
- const registry = useMemo<ExtensionFormRegistry>(() => {
59
- const emitDirty = (): void => {
60
- let any = false;
61
- for (const r of regsRef.current.values()) {
62
- if (r.dirty) {
63
- any = true;
64
- break;
65
- }
66
- }
67
- onStatusRef.current({ anyDirty: any, hasRegistrations: regsRef.current.size > 0 });
68
- };
69
- return {
70
- upsert: (reg) => {
71
- regsRef.current.set(reg.key, reg);
72
- emitDirty();
73
- },
74
- remove: (key) => {
75
- regsRef.current.delete(key);
76
- emitDirty();
77
- },
78
- };
79
- }, []);
80
-
81
- const runAll = useMemo(
82
- () =>
83
- async (ctx: ExtensionSubmitContext): Promise<readonly ExtensionSubmitResult[]> => {
84
- const results: ExtensionSubmitResult[] = [];
85
- // Insertion-Order (Map) = Section-Reihenfolge im Form.
86
- for (const reg of regsRef.current.values()) {
87
- results.push(await reg.handler(ctx));
88
- }
89
- return results;
90
- },
91
- [],
92
- );
93
-
94
- return { registry, runAll };
95
- }
96
-
97
- // Section-Seite: meldet Handler + dirty an die umgebende Form. Returnt true
98
- // wenn eine composed-Form vorhanden ist (Section blendet dann ihren eigenen
99
- // Save-Button aus); false = standalone (eigener Save-Button bleibt).
100
- export function useExtensionFormSubmit(opts: {
101
- readonly dirty: boolean;
102
- readonly onSubmit: ExtensionFormSubmitHandler;
103
- }): boolean {
104
- const registry = useContext(ExtensionFormRegistryContext);
105
- const key = useId();
106
- // Handler über ref, damit das Re-Registrieren NICHT an jeder Handler-
107
- // Identity hängt (nur an dirty); der Handler liest immer den frischen
108
- // Closure-State (pending) zur Aufruf-Zeit.
109
- const handlerRef = useRef(opts.onSubmit);
110
- handlerRef.current = opts.onSubmit;
111
-
112
- useEffect(() => {
113
- if (registry === null) return;
114
- registry.upsert({
115
- key,
116
- dirty: opts.dirty,
117
- handler: (ctx) => handlerRef.current(ctx),
118
- });
119
- return () => registry.remove(key);
120
- }, [registry, key, opts.dirty]);
121
-
122
- return registry !== null;
123
- }
@@ -1,111 +0,0 @@
1
- // Extension-Section-Components-Map: client-side Lookup für entityEdit
2
- // sections vom Type `extension`, List-Header-Slots UND Dashboard-`custom`-
3
- // Panels. Jeder Mount-Ort löst den `__component`-Namen aus einer
4
- // PlatformComponent über dieselbe Registry auf und mountet die passende
5
- // Component — die Bundled-Feature-/App-Component lädt + persistiert dann
6
- // ihre eigenen Daten (z.B. custom-fields, oder ein eigenständiger Chart).
7
- // The entityEdit footer slot (`screen.slots.footer`) mounts through it too.
8
- //
9
- // Mounting analog zu CustomScreensProvider — createKumikoApp im
10
- // renderer-web sammelt alle clientFeatures.extensionSectionComponents und
11
- // mountet den Provider; Tests die einzelne Sections prüfen wollen
12
- // mounten den Provider direkt.
13
-
14
- import type { PlatformComponent } from "@cosmicdrift/kumiko-framework/ui-types";
15
- import { type ComponentType, createContext, type ReactNode, useContext } from "react";
16
-
17
- /** Extrahiert den `__component`-Namen aus einer PlatformComponent. Liest
18
- * react- und native-Branch (die App-Author registriert beide unter dem
19
- * gleichen Namen); returnt den ersten gefundenen string. Pure narrowing
20
- * via `in` — kein cast, kein assertion. */
21
- export function extensionSectionName(component: PlatformComponent): string | undefined {
22
- for (const branch of [component.react, component.native]) {
23
- if (branch !== null && typeof branch === "object" && "__component" in branch) {
24
- const candidate = branch.__component;
25
- if (typeof candidate === "string") return candidate;
26
- }
27
- }
28
- return undefined;
29
- }
30
-
31
- export type ExtensionSectionProps = {
32
- readonly entityName: string;
33
- readonly entityId: string | null;
34
- /** Bereits gespeicherte Extension-Werte der Entity (aus der geladenen
35
- * detail-row durchgereicht). `undefined` im Create-Mode oder wenn der
36
- * Host-Screen keine Werte liefert. Erlaubt der Section, den Bestand
37
- * beim Edit anzuzeigen statt write-only zu sein. */
38
- readonly initialValues?: Readonly<Record<string, unknown>>;
39
- /** Nur im List-Header-Slot gesetzt (entityId ist dort null): die screen.id
40
- * der Liste, damit ein Header-Control den per-Screen URL-Filter-State
41
- * (useListUrlState) ansprechen kann — z.B. ein Tag-Filter der die Liste auf
42
- * eine id-Menge narrowed. In entityEdit-Sections undefined. */
43
- readonly screenId?: string;
44
- /** Nur im Dashboard-`custom`-Panel gesetzt: der aktuell gewählte Wert des
45
- * Screen-Filters (siehe DashboardFilterDefinition), gemerged wie bei jeder
46
- * anderen Panel-Query. In allen anderen Mount-Orten undefined. Dashboard-
47
- * Panels haben keine Entity — entityName/entityId tragen dort die
48
- * screen.id bzw. null, siehe CustomPanelBody in dashboard-body.tsx. */
49
- readonly filterParams?: Readonly<Record<string, unknown>>;
50
- /** Current form values of the host RenderEdit (controlled mode,
51
- * issue #1887/#1888) — same snapshot reference as
52
- * `RenderEditChangeState.values`. Lets the section read the host
53
- * form's live state (e.g. for a review step). Only set in
54
- * entityEdit sections; undefined in list-header and dashboard
55
- * mounts, where no host form exists. */
56
- readonly values?: Readonly<Record<string, unknown>>;
57
- /** Sets host RenderEdit form values from outside (e.g. a VIN-decode
58
- * roundtrip that fills other fields) — same function as
59
- * `RenderEditControls.patch`, merges only the given keys. `patch` must
60
- * converge: `setValues` is a no-op only when the merged result is
61
- * reference-equal to the current values, so an unconditional
62
- * `patch(...)` from a `values`-dependent effect loops. Undefined outside
63
- * entityEdit sections. */
64
- readonly patch?: (partial: Readonly<Record<string, unknown>>) => void;
65
- /** Validates the host form without writing — same function as
66
- * `RenderEditControls.validate`; field errors land in
67
- * `snapshot.errors` on the field instead of a collective message.
68
- * Undefined outside entityEdit sections. */
69
- readonly validate?: () => boolean;
70
- /** Whether the host form has unsaved changes. Only set in the entityEdit
71
- * footer slot (`screen.slots.footer`); undefined in all other mounts. */
72
- readonly hasUnsavedChanges?: boolean;
73
- /** Current wizard step, only set for `layout.mode === "wizard"` forms.
74
- * Only set in the entityEdit footer slot (`screen.slots.footer`);
75
- * undefined in all other mounts, and undefined there too for a
76
- * non-wizard form. */
77
- readonly wizardStep?: { readonly index: number; readonly isLast: boolean };
78
- };
79
-
80
- export type ExtensionSectionComponent = ComponentType<ExtensionSectionProps>;
81
-
82
- export type ExtensionSectionsMap = Readonly<Record<string, ExtensionSectionComponent>>;
83
-
84
- const ExtensionSectionsContext = createContext<ExtensionSectionsMap | undefined>(undefined);
85
-
86
- export type ExtensionSectionsProviderProps = {
87
- readonly children: ReactNode;
88
- readonly value: ExtensionSectionsMap;
89
- };
90
-
91
- export function ExtensionSectionsProvider({
92
- children,
93
- value,
94
- }: ExtensionSectionsProviderProps): ReactNode {
95
- return (
96
- <ExtensionSectionsContext.Provider value={value}>{children}</ExtensionSectionsContext.Provider>
97
- );
98
- }
99
-
100
- /** Schaut die Component für einen extension-section-Namen nach. Returnt
101
- * undefined wenn weder Provider gemounted noch der Name in der Map
102
- * registriert ist — der Caller (RenderEdit) zeigt dann seinen
103
- * Placeholder-Banner. `name` ist optional damit Caller den Hook
104
- * unkonditional aufrufen können (Rules-of-Hooks), ohne einen Stub-Key
105
- * wie `""` reichen zu müssen — analog `useColumnRenderer`. */
106
- export function useExtensionSectionComponent(name?: string): ExtensionSectionComponent | undefined {
107
- const map = useContext(ExtensionSectionsContext);
108
- if (name === undefined || name === "") return undefined;
109
- if (map === undefined) return undefined;
110
- return map[name];
111
- }
@@ -1,91 +0,0 @@
1
- import type {
2
- EntityDefinition,
3
- EntityEditScreenDefinition,
4
- FieldDefinition,
5
- } from "@cosmicdrift/kumiko-framework/ui-types";
6
- import { evalFieldCondition, NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-framework/ui-types";
7
- import { I18N_KEY_PARAM } from "@cosmicdrift/kumiko-headless";
8
- import * as z from "zod";
9
- import { layoutEditFields } from "./layout-fields";
10
-
11
- // `required` means "has a value", not "is truthy" — `false` and `0` count
12
- // as present, only the actually-empty representations don't.
13
- function isPresent(value: unknown): boolean {
14
- if (value === undefined || value === null || value === "") return false;
15
- if (Array.isArray(value) && value.length === 0) return false;
16
- return true;
17
- }
18
-
19
- function isEmbeddedListField(field: FieldDefinition): boolean {
20
- return field.type === "embedded" && field.multiple === true;
21
- }
22
-
23
- // Renders raw to the user without a de+en default in i18n-defaults.ts.
24
- export const REQUIRED_FIELD_I18N_KEY = "kumiko.validation.required";
25
-
26
- // Client-side presence validation for the auto-wired entityEdit path —
27
- // checks that every rendered required field HAS a value, not that the
28
- // value has the right shape. Format/range/type validation stays server-
29
- // authoritative (buildInsertSchema/buildUpdateSchema): the form-state
30
- // representation of a value can diverge from the server-payload shape (e.g.
31
- // money is `{amount,currency}`, not a bare number), so a format check here
32
- // would either reject valid values or need per-representation branches that
33
- // rot the moment either side changes.
34
- //
35
- // One `superRefine` instead of a per-field shape: a field-level `.refine()`
36
- // wouldn't run at all for a key that's simply absent from `values` —
37
- // `superRefine` sees the whole object and catches that case too.
38
- // `.passthrough()` is load-bearing: the default `z.object({})` STRIPS every
39
- // key (there's no declared shape), so `superRefine` would see an empty
40
- // object regardless of what was actually submitted — every required field
41
- // would misreport as missing.
42
- //
43
- // Iterates the screen's layout field specs, not `entity.fields` — `required`
44
- // and `readOnly` are per-spec, evaluated against the current values the
45
- // same way `view-model/edit.ts` resolves them for the rendered form. Not
46
- // checked here: `visible`. `runValidate` already filters issues on hidden
47
- // fields via `computeFieldStates(options.fields, …)` (form-controller.ts:163,181),
48
- // fed by `deriveFormFields(screen)` in render-edit.tsx.
49
- export function buildFormSchema(
50
- entity: EntityDefinition,
51
- screen: EntityEditScreenDefinition,
52
- ): z.ZodType {
53
- const fields = layoutEditFields(screen);
54
- return z
55
- .object({})
56
- .passthrough()
57
- .superRefine((values, ctx) => {
58
- // `.passthrough()` types `values` as a plain object but doesn't declare
59
- // its keys — the runtime object always carries every form field.
60
- // @cast-boundary form-values
61
- const record = values as Record<string, unknown>;
62
- for (const spec of fields) {
63
- const field = entity.fields[spec.field];
64
- if (!field) continue;
65
- // Not operable by the user — a presence error would be unresolvable,
66
- // same reason as the NO_WIDGET_FIELD_TYPES check below.
67
- if (spec.readOnly !== undefined && evalFieldCondition(spec.readOnly, record)) continue;
68
- // Screen-spec `required` overrides the entity default, mirroring
69
- // `view-model/edit.ts` — the rendered form is the reference, and a
70
- // presence check stricter than the form blocks the user for nothing.
71
- const entityRequired = "required" in field && field.required === true;
72
- const isRequired =
73
- spec.required === undefined ? entityRequired : evalFieldCondition(spec.required, record);
74
- if (!isRequired) continue;
75
- // Embedded LIST fields get their own EmbeddedListField grid widget
76
- // (#1838) — they're fillable, so NO_WIDGET_FIELD_TYPES's "embedded"
77
- // entry must not exempt them. A statically-`required: true` field
78
- // with no bound widget is also caught at boot
79
- // (validateNoWidgetRequiredField in boot-validator/screens.ts).
80
- if (NO_WIDGET_FIELD_TYPES.includes(field.type) && !isEmbeddedListField(field)) continue;
81
- if (isPresent(record[spec.field])) continue;
82
- ctx.addIssue({
83
- code: "custom",
84
- path: [spec.field],
85
- message: `"${spec.field}" is required.`,
86
- // I18N_KEY_PARAM override, see packages/headless/src/form/zod-bridge.ts.
87
- params: { [I18N_KEY_PARAM]: REQUIRED_FIELD_I18N_KEY },
88
- });
89
- }
90
- });
91
- }