@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,31 +0,0 @@
1
- import type {
2
- EditFieldSpec,
3
- EntityEditScreenDefinition,
4
- } from "@cosmicdrift/kumiko-framework/ui-types";
5
- import {
6
- isFieldsEditSection,
7
- normalizeEditField,
8
- sectionFieldSpecs,
9
- } from "@cosmicdrift/kumiko-framework/ui-types";
10
-
11
- // Normalized field specs actually rendered by the screen's layout, extension
12
- // (and relatedList) sections skipped. Both this and `layoutFieldNames` key
13
- // off "rendered by the layout" for the same reason: a field the user never
14
- // sees gets no chance to review/correct a value nor to fix a presence error
15
- // (search-param merge, #1708; presence schema in form-schema.ts).
16
- export function layoutEditFields(
17
- screen: EntityEditScreenDefinition,
18
- ): readonly Exclude<EditFieldSpec, string>[] {
19
- const specs: Exclude<EditFieldSpec, string>[] = [];
20
- for (const section of screen.layout.sections) {
21
- if (!isFieldsEditSection(section)) continue;
22
- for (const spec of sectionFieldSpecs(section)) {
23
- specs.push(normalizeEditField(spec));
24
- }
25
- }
26
- return specs;
27
- }
28
-
29
- export function layoutFieldNames(screen: EntityEditScreenDefinition): ReadonlySet<string> {
30
- return new Set(layoutEditFields(screen).map((spec) => spec.field));
31
- }
@@ -1,259 +0,0 @@
1
- // Split from kumiko-screen.tsx: related-list-section.tsx renders through
2
- // kumiko-screen.tsx, so importing this back from there would be a require cycle.
3
-
4
- import type { ListFacetSpec } from "@cosmicdrift/kumiko-framework/ui-types";
5
- import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
6
- import type { Translate } from "@cosmicdrift/kumiko-headless";
7
- import { Temporal } from "temporal-polyfill";
8
- import type { DataTableFacet } from "../primitives";
9
-
10
- // One resolved facet, independent of where the type info came from — an
11
- // entity field (entityList) or an explicit ListFacetSpec (projectionList,
12
- // relatedList). Shared by buildFilterFacets/buildFilterPayload so all list
13
- // screens build filters and facet UI through one code path.
14
- export type ResolvedFacetSpec = {
15
- readonly field: string;
16
- readonly type: "select" | "boolean" | "reference";
17
- readonly label: string;
18
- /** Empty for a "reference" facet until `ReferenceFacetBridges` resolves it
19
- * at render time, unlike select/boolean's author-declared options. */
20
- readonly options: readonly { readonly value: string; readonly label: string }[];
21
- /** Present only for a "reference" facet — the resolved lookup target
22
- * `ReferenceFacetBridges` needs to fetch its options. */
23
- readonly reference?: {
24
- readonly refEntity: string;
25
- readonly refFeature: string;
26
- readonly labelField: string;
27
- };
28
- };
29
-
30
- // Fills in a "reference" spec's initially-empty `options` from the
31
- // field-keyed options ReferenceFacetBridges resolved, shared by every caller.
32
- export function mergeReferenceFacetOptions(
33
- specs: readonly ResolvedFacetSpec[],
34
- optionsByField: Readonly<
35
- Record<string, readonly { readonly value: string; readonly label: string }[]>
36
- >,
37
- ): ResolvedFacetSpec[] {
38
- return specs.map((spec) =>
39
- spec.reference !== undefined
40
- ? { ...spec, options: optionsByField[spec.field] ?? spec.options }
41
- : spec,
42
- );
43
- }
44
-
45
- export function buildFilterFacets(specs: readonly ResolvedFacetSpec[]): DataTableFacet[] {
46
- return specs.map((spec) => ({ field: spec.field, label: spec.label, options: spec.options }));
47
- }
48
-
49
- // User-selected faceted filters from URL-state → payload.filters. Boolean
50
- // fields coerce "true"/"false" strings to real booleans (DB column is
51
- // boolean); everything else stays string[] under op:"in" (multi-select
52
- // semantics). `typeOf` resolves a field to its known type string —
53
- // undefined means "unknown field", so it's dropped (typo-safe: a stale/
54
- // hand-crafted URL param for an undeclared field never reaches the
55
- // server). Deliberately NOT gated on the field being a *facet* — entityList
56
- // passes through any field present in entity.fields, matching its
57
- // pre-fw#2224 behavior; only the boolean-coercion branch cares about type.
58
-
59
- function isFacetI18nKey(label: string): boolean {
60
- return !/\s/.test(label) && (label.includes(".") || label.includes(":"));
61
- }
62
-
63
- export function buildFilterPayload(
64
- urlFilters: Readonly<Record<string, readonly string[]>>,
65
- typeOf: (field: string) => string | undefined,
66
- ): { field: string; op: "in"; value: unknown }[] {
67
- const out: { field: string; op: "in"; value: unknown }[] = [];
68
- for (const [field, values] of Object.entries(urlFilters)) {
69
- if (values.length === 0) continue;
70
- // `id` is a base column (not a declared facet), allowed as an id-set
71
- // filter so a header-slot control — e.g. the tags TagFilter — can narrow
72
- // ANY list to a resolved set of row ids without the host declaring a facet.
73
- if (field === "id") {
74
- out.push({ field, op: "in", value: values });
75
- continue;
76
- }
77
- const type = typeOf(field);
78
- if (type === undefined) continue;
79
- const value = type === "boolean" ? values.map((v) => v === "true") : values;
80
- out.push({ field, op: "in", value });
81
- }
82
- return out;
83
- }
84
-
85
- // projectionList/relatedList adapter — neither has an entity/i18n convention
86
- // to derive labels from, so ListFacetSpec carries every label explicitly
87
- // (fw#2224). Labels may be raw display strings or i18n keys; run them
88
- // through translate like entity facets (passthrough when the key is missing).
89
- // `featureName` resolves a reference facet's `entity` target the same way
90
- // ListColumnSpec.refEntity does (same-feature short name, or "feature:entity").
91
- export function resolveProjectionFacetSpecs(
92
- facets: readonly ListFacetSpec[] | undefined,
93
- translate: Translate,
94
- featureName: string,
95
- ): ResolvedFacetSpec[] {
96
- if (facets === undefined) return [];
97
- const tr = (label: string): string => (isFacetI18nKey(label) ? translate(label) : label);
98
- // dateRange carries no option list and no `filters` entry — it resolves
99
- // through resolveDateRangeFacets into its own toolbar control instead.
100
- return facets.flatMap((facet): ResolvedFacetSpec[] => {
101
- if (facet.type === "dateRange") return [];
102
- if (facet.type === "select") {
103
- return [
104
- {
105
- field: facet.field,
106
- type: "select",
107
- label: tr(facet.label),
108
- options: facet.options.map((opt) => ({
109
- value: opt.value,
110
- label: tr(opt.label),
111
- })),
112
- },
113
- ];
114
- }
115
- if (facet.type === "boolean") {
116
- return [
117
- {
118
- field: facet.field,
119
- type: "boolean",
120
- label: tr(facet.label),
121
- options: [
122
- { value: "true", label: tr(facet.trueLabel) },
123
- { value: "false", label: tr(facet.falseLabel) },
124
- ],
125
- },
126
- ];
127
- }
128
- const target = parseRefTarget(facet.entity, featureName);
129
- return [
130
- {
131
- field: facet.field,
132
- type: "reference",
133
- label: tr(facet.label),
134
- options: [],
135
- reference: {
136
- refEntity: target.entityName,
137
- refFeature: target.featureName,
138
- labelField: facet.labelField ?? "id",
139
- },
140
- },
141
- ];
142
- });
143
- }
144
-
145
- // --- dateRange facet (fw#3104) ---
146
-
147
- // A resolved dateRange facet. Kept out of ResolvedFacetSpec because it shares
148
- // nothing with the option-dropdown facets: no options, no `filters` entry —
149
- // the two bounds travel as named top-level query params.
150
- export type ResolvedDateRangeFacet = {
151
- readonly field: string;
152
- readonly label: string;
153
- readonly fromParam: string;
154
- readonly toParam: string;
155
- };
156
-
157
- export type DateRangeBound = "from" | "to";
158
-
159
- export type DateRangeValue = { readonly from: string; readonly to: string };
160
-
161
- export function resolveDateRangeFacets(
162
- facets: readonly ListFacetSpec[] | undefined,
163
- translate: Translate,
164
- ): ResolvedDateRangeFacet[] {
165
- if (facets === undefined) return [];
166
- return facets.flatMap((facet): ResolvedDateRangeFacet[] =>
167
- facet.type === "dateRange"
168
- ? [
169
- {
170
- field: facet.field,
171
- label: isFacetI18nKey(facet.label) ? translate(facet.label) : facet.label,
172
- fromParam: facet.params.from,
173
- toParam: facet.params.to,
174
- },
175
- ]
176
- : [],
177
- );
178
- }
179
-
180
- // The bounds live in the same `<screenId>.f.<key>` URL namespace the option
181
- // facets use, so setFilter's page-reset and clearFilters cover them for free.
182
- // buildFilterPayload drops them again: a dotted key resolves to no facet
183
- // type, which is its "unknown field" case.
184
- export function dateRangeUrlField(field: string, bound: DateRangeBound): string {
185
- return `${field}.${bound}`;
186
- }
187
-
188
- export function readDateRange(
189
- urlFilters: Readonly<Record<string, readonly string[]>>,
190
- field: string,
191
- ): DateRangeValue {
192
- return {
193
- from: urlFilters[dateRangeUrlField(field, "from")]?.[0] ?? "",
194
- to: urlFilters[dateRangeUrlField(field, "to")]?.[0] ?? "",
195
- };
196
- }
197
-
198
- // Keeps from <= to by pushing the opposite bound along, so an inverted range
199
- // can't reach the server and trip a handler's `from <= to` refine (a 422 the
200
- // user would see as a broken list, not as a correction).
201
- export function clampDateRange(
202
- current: DateRangeValue,
203
- bound: DateRangeBound,
204
- value: string,
205
- ): DateRangeValue {
206
- if (bound === "from") {
207
- const to = current.to !== "" && value !== "" && value > current.to ? value : current.to;
208
- return { from: value, to };
209
- }
210
- const from = current.from !== "" && value !== "" && value < current.from ? value : current.from;
211
- return { from, to: value };
212
- }
213
-
214
- // A calendar date covers a whole day in the viewer's zone: "from the 14th"
215
- // starts at that day's first instant, "to the 14th" includes everything up to
216
- // its last. Going through the next day's start keeps both ends right across a
217
- // DST boundary, where the day is not 24h long.
218
- function dayBoundInstant(date: string, bound: DateRangeBound, timeZone: string): string | null {
219
- let plainDate: Temporal.PlainDate;
220
- try {
221
- plainDate = Temporal.PlainDate.from(date);
222
- } catch {
223
- // Half-typed or hand-crafted URL value — drop the bound rather than send
224
- // something the handler's ISO-datetime schema would reject.
225
- return null;
226
- }
227
- const start = plainDate.toZonedDateTime(timeZone).toInstant();
228
- return bound === "from"
229
- ? start.toString()
230
- : plainDate
231
- .add({ days: 1 })
232
- .toZonedDateTime(timeZone)
233
- .toInstant()
234
- .subtract({
235
- nanoseconds: 1,
236
- })
237
- .toString();
238
- }
239
-
240
- export function buildDateRangePayload(
241
- specs: readonly ResolvedDateRangeFacet[],
242
- urlFilters: Readonly<Record<string, readonly string[]>>,
243
- timeZone: string,
244
- ): Record<string, string> {
245
- const out: Record<string, string> = {};
246
- for (const spec of specs) {
247
- const range = readDateRange(urlFilters, spec.field);
248
- for (const [bound, param] of [
249
- ["from", spec.fromParam],
250
- ["to", spec.toParam],
251
- ] as const) {
252
- const value = range[bound];
253
- if (value === "") continue;
254
- const instant = dayBoundInstant(value, bound, timeZone);
255
- if (instant !== null) out[param] = instant;
256
- }
257
- }
258
- return out;
259
- }
package/src/app/nav.tsx DELETED
@@ -1,258 +0,0 @@
1
- import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
2
- import type { FeatureSchema } from "./feature-schema";
3
- import { lastSegment } from "./qn";
4
-
5
- // Navigation-Contract, plattform-neutral. Types + Context + Hook leben
6
- // hier; die konkrete Implementation (window.history im Web,
7
- // react-navigation im Native) kommt via `<NavProvider value={...}>`
8
- // vom Plattform-Package rein.
9
- //
10
- // Pfad-Format hat zwei Modi je nach App-Schema:
11
- // * ohne Workspaces: `/<screenId>[/<entityId>]`
12
- // * mit Workspaces: `/<workspaceId>/<screenId>[/<entityId>]`
13
- // Der Mode wird beim parsePath() per `hasWorkspaces` Flag durchgereicht
14
- // — Schema-Layer entscheidet, parsePath bleibt dumm/pure. formatPath()
15
- // braucht keinen Hint: target.workspaceId vorhanden → Prefix; sonst flach.
16
-
17
- export type NavRoute = {
18
- // Active workspace short id, present iff the app runs in workspace
19
- // mode (schema.workspaces non-empty). undefined for non-workspace apps.
20
- readonly workspaceId?: string;
21
- readonly screenId: string;
22
- readonly entityId?: string;
23
- };
24
-
25
- export type ScreenTarget = {
26
- // Optional in workspace-aware navigate calls. Omit for cross-workspace
27
- // navigation (current workspace stays); set to switch workspaces in the
28
- // same call as picking a screen — e.g. WorkspaceSwitcher does this.
29
- readonly workspaceId?: string;
30
- readonly screenId: string;
31
- readonly entityId?: string;
32
- };
33
-
34
- // Object-based nav target — callers name WHAT they want to see (an entity's
35
- // row) instead of WHICH screen shows it. resolveTarget() below turns this
36
- // into a ScreenTarget by looking up the screen with matching `detailFor`.
37
- // No edit-form resolution here: an entity can have several entityEdit
38
- // screens (e.g. solon's "property" has three), so "the" edit screen isn't
39
- // well-defined — callers that want a form still name its screenId directly.
40
- export type ObjectTarget = {
41
- readonly workspaceId?: string;
42
- readonly entity: string;
43
- readonly id: string;
44
- };
45
-
46
- // No `kind` tag: `"screenId" in target` narrows cleanly, and a tag would
47
- // force every existing NavTarget literal (every navigate/hrefFor call in
48
- // every app) to add one for an additive ticket. See resolveTarget().
49
- export type NavTarget = ScreenTarget | ObjectTarget;
50
-
51
- export type NavApi = {
52
- /** Current route — `undefined` when the URL is at the root / there's
53
- * no route selected. Caller's initial fallback kicks in then. */
54
- readonly route: NavRoute | undefined;
55
- /** Push a new route. Platform-specific Impl writes to
56
- * history/stack and notifies subscribers. */
57
- readonly navigate: (target: NavTarget) => void;
58
- /** Replace the current route in place. Same effect as navigate from
59
- * the user's perspective, but doesn't add a history entry — used for
60
- * mount-time URL fills (e.g. WorkspaceShell defaulting to `/admin/x`
61
- * when the user typed `/`). Browser Back must take the user out of
62
- * the app, not back to the original empty path. */
63
- readonly replace: (target: NavTarget) => void;
64
- /** Build the href a click on {target} would produce. Used by
65
- * platform-specific Link-Komponenten (Web: `<a href>`; Native
66
- * typically doesn't need this). */
67
- readonly hrefFor: (target: NavTarget) => string;
68
- /** Lese-Snapshot der aktuellen Search-Params (Browser: ?key=value-
69
- * Pairs nach dem Pfad). Native-Impls die kein URL-Konzept haben
70
- * liefern ein leeres Object. Wert ist ein Plain-Record (kein Map)
71
- * damit React-Subscribers shallow-compare können. */
72
- readonly searchParams: Readonly<Record<string, string>>;
73
- /** Mergt Updates in die aktuellen Search-Params. Wert `null` löscht
74
- * den Key. Ändert NICHT den Pfad. Plattform-Impls nutzen
75
- * history.replaceState (kein Push) — Sort/Filter-Toggles sollen
76
- * nicht die Back-Navigation fluten. Native-Impls können no-op'en
77
- * wenn das Konzept nicht existiert. */
78
- readonly setSearchParams: (updates: Readonly<Record<string, string | null>>) => void;
79
- };
80
-
81
- // Pfad-Format:
82
- // ohne workspaces: `/task-list`, `/task-edit/abc-123`, `/` → undefined
83
- // mit workspaces: `/admin/task-list`, `/admin/task-edit/abc`,
84
- // `/admin` → workspace-only (no screen yet),
85
- // `/` → undefined.
86
- // Alles nach den ersten 2/3 Segmenten wird ignoriert — kumikos
87
- // Navigation-Grammatik nested nicht weiter. Nested-Routes wäre eine
88
- // Spec-Änderung, nicht ein URL-Shape-Unfall.
89
- export function parsePath(pathname: string, hasWorkspaces?: boolean): NavRoute | undefined {
90
- const parts = pathname.split("/").filter((p) => p !== "");
91
- if (hasWorkspaces === true) {
92
- const [workspaceId, screenId, entityId] = parts;
93
- if (workspaceId === undefined || workspaceId === "") return undefined;
94
- if (screenId === undefined || screenId === "") {
95
- // Workspace-only URL ("/admin") — caller resolves the default screen
96
- // for that workspace. We carry workspaceId WITHOUT a screen so the
97
- // shell can branch instead of making something up.
98
- return { workspaceId, screenId: "" };
99
- }
100
- return {
101
- workspaceId,
102
- screenId,
103
- ...(entityId !== undefined && { entityId }),
104
- };
105
- }
106
- const [screenId, entityId] = parts;
107
- if (screenId === undefined || screenId === "") return undefined;
108
- return { screenId, ...(entityId !== undefined && { entityId }) };
109
- }
110
-
111
- export function formatPath(target: ScreenTarget): string {
112
- // Workspace-Mode: prefix the workspace short id. Order matters —
113
- // workspace before screen mirrors parsePath's segment order.
114
- const segments: string[] = [];
115
- if (target.workspaceId !== undefined) segments.push(target.workspaceId);
116
- segments.push(target.screenId);
117
- if (target.entityId !== undefined) segments.push(target.entityId);
118
- return `/${segments.join("/")}`;
119
- }
120
-
121
- // Shared entity→detailFor-screen lookup — resolveTarget's ObjectTarget
122
- // branch and hasDetailScreen (create-app's row-click default, fw#2164) both
123
- // need "is there a detail screen for this entity", one to resolve it, the
124
- // other to just check before falling back to a different default.
125
- function findDetailForScreen(
126
- features: readonly FeatureSchema[],
127
- entity: string,
128
- ): { readonly featureName: string; readonly screenId: string } | undefined {
129
- for (const feature of features) {
130
- for (const screen of feature.screens) {
131
- if (screen.detailFor === entity)
132
- return { featureName: feature.featureName, screenId: screen.id };
133
- }
134
- }
135
- return undefined;
136
- }
137
-
138
- /** Non-throwing existence check for findDetailForScreen — lets a caller pick
139
- * a different default (fw#2164's row-click fallback) instead of resolving. */
140
- export function hasDetailScreen(features: readonly FeatureSchema[], entity: string): boolean {
141
- return findDetailForScreen(features, entity) !== undefined;
142
- }
143
-
144
- // Resolves a NavTarget to the ScreenTarget form navigate/replace/hrefFor
145
- // operate on. Pure — no context, no I/O — so callers can use it outside
146
- // React too (see resolve-at-NavApi-build alternative in renderer-web).
147
- export function resolveTarget(features: readonly FeatureSchema[], target: NavTarget): ScreenTarget {
148
- if ("screenId" in target) return target;
149
-
150
- const found = findDetailForScreen(features, target.entity);
151
- if (found !== undefined) {
152
- // Short form (globally unique per validateScreenShortIdCollisions) —
153
- // ScreenTarget.screenId round-trips through formatPath/parsePath as a
154
- // short id; qualifying it here double-qualifies at RoutedScreen.
155
- return {
156
- ...(target.workspaceId !== undefined && { workspaceId: target.workspaceId }),
157
- screenId: found.screenId,
158
- entityId: target.id,
159
- };
160
- }
161
-
162
- throw new Error(
163
- `resolveTarget: no detail screen for entity "${target.entity}". Add detailFor: "${target.entity}" to the screen that shows it.`,
164
- );
165
- }
166
-
167
- // Context + Hook. Default ist `undefined` damit fehlender Provider
168
- // laut kracht statt ein silent-no-op NavApi mit toten navigate()
169
- // Aufrufen anzubieten.
170
- const NavContext = createContext<NavApi | undefined>(undefined);
171
-
172
- export type NavProviderProps = {
173
- readonly children: ReactNode;
174
- readonly value: NavApi;
175
- };
176
-
177
- type InitialValuesHandoff = {
178
- readonly screenId: string;
179
- readonly values: Readonly<Record<string, unknown>>;
180
- };
181
-
182
- // A mutable slot instead of state: offering must not re-render the app, and
183
- // the target form reads it once on mount.
184
- type InitialValuesHandoffSlot = { current: InitialValuesHandoff | undefined };
185
-
186
- const InitialValuesHandoffContext = createContext<InitialValuesHandoffSlot | undefined>(undefined);
187
-
188
- export function NavProvider({ children, value }: NavProviderProps): ReactNode {
189
- const [handoffSlot] = useState<InitialValuesHandoffSlot>(() => ({ current: undefined }));
190
- const routeScreenId = value.route?.screenId;
191
- // An offer whose form never mounted (e.g. already on that screen) must not
192
- // resurface on a later, unrelated visit — drop it once the route moves elsewhere.
193
- useEffect(() => {
194
- const pending = handoffSlot.current;
195
- if (pending === undefined || routeScreenId === undefined) return;
196
- if (lastSegment(routeScreenId) !== pending.screenId) handoffSlot.current = undefined;
197
- }, [handoffSlot, routeScreenId]);
198
- return (
199
- <NavContext.Provider value={value}>
200
- <InitialValuesHandoffContext.Provider value={handoffSlot}>
201
- {children}
202
- </InitialValuesHandoffContext.Provider>
203
- </NavContext.Provider>
204
- );
205
- }
206
-
207
- // Navigates to a form screen (actionForm, secretMint, entityEdit-create) with
208
- // initial values carried in memory — never in the URL, so a link cannot
209
- // forge them and they are not limited to the screen's urlPrefillFields.
210
- // sensitive and password fields are still never prefilled.
211
- export function useNavigateWithInitialValues(): (
212
- target: ScreenTarget,
213
- initialValues: Readonly<Record<string, unknown>>,
214
- ) => void {
215
- const nav = useNav();
216
- const handoffSlot = useContext(InitialValuesHandoffContext);
217
- return useCallback(
218
- (target, initialValues) => {
219
- if (handoffSlot !== undefined) {
220
- handoffSlot.current = { screenId: lastSegment(target.screenId), values: initialValues };
221
- }
222
- nav.navigate(target);
223
- },
224
- [nav, handoffSlot],
225
- );
226
- }
227
-
228
- export function useInitialValuesHandoff(
229
- screenId: string,
230
- ): Readonly<Record<string, unknown>> | undefined {
231
- const handoffSlot = useContext(InitialValuesHandoffContext);
232
- const [values] = useState(() => {
233
- const pending = handoffSlot?.current;
234
- return pending !== undefined && pending.screenId === lastSegment(screenId)
235
- ? pending.values
236
- : undefined;
237
- });
238
- useEffect(() => {
239
- if (
240
- handoffSlot !== undefined &&
241
- values !== undefined &&
242
- handoffSlot.current?.values === values
243
- ) {
244
- handoffSlot.current = undefined;
245
- }
246
- }, [handoffSlot, values]);
247
- return values;
248
- }
249
-
250
- export function useNav(): NavApi {
251
- const api = useContext(NavContext);
252
- if (api === undefined) {
253
- throw new Error(
254
- "useNav: no <NavProvider> mounted above this component. Plattform-Packages (z.B. @cosmicdrift/kumiko-renderer-web) liefern eine Default-Impl über createKumikoApp.",
255
- );
256
- }
257
- return api;
258
- }
@@ -1,86 +0,0 @@
1
- // Shim für ProjectionDetail-Renderer (analog zu projection-list-shim.ts /
2
- // config-edit-shim.ts).
3
- //
4
- // RenderEdit verlangt ein `entity: EntityDefinition` + ein
5
- // `screen: EntityEditScreenDefinition`-Pair, nutzt davon aber nur
6
- // `entity.fields` (Feld-Typ für den Input-Renderer) und `screen.layout`
7
- // (welche Felder in welchen Sections). Ein projectionDetail-Screen ist
8
- // query-getrieben und hat KEINE Entity — die zwei Helper hier shapen den
9
- // Input ad-hoc um, damit RenderEdit reused werden kann.
10
- //
11
- // Der strukturelle Read-Only-Beweis liegt in synthesizeProjectionDetailScreen:
12
- // JEDES Feld wird hart auf readOnly:true gesetzt (nicht nur was der Author im
13
- // Layout gesetzt hat) — hasEditableSection() liest genau dieses Flag und
14
- // blendet den Save-Button aus, wenn kein Feld editierbar ist. Der Author kann
15
- // diese Garantie nicht versehentlich umgehen.
16
- //
17
- // Selbe Schulden-Reservation wie bei den anderen Shims: greift RenderEdit
18
- // künftig auf weitere EntityDefinition-Felder (transitions, idType) zu, oder
19
- // cross-referenziert ein Boot-Validator die schema.entities-Map, brechen die
20
- // Type-Lies hier silent — dann ist Zeit für eine echte RenderProjectionDetail-
21
- // Komponente.
22
-
23
- import type {
24
- EditFieldSpec,
25
- EditLayout,
26
- EntityDefinition,
27
- EntityEditScreenDefinition,
28
- ProjectionDetailScreenDefinition,
29
- } from "@cosmicdrift/kumiko-framework/ui-types";
30
- import {
31
- isFieldsEditSection,
32
- normalizeEditField,
33
- PROJECTION_DETAIL_ENTITY as PROJECTION_DETAIL_PSEUDO_ENTITY,
34
- sectionFieldSpecs,
35
- } from "@cosmicdrift/kumiko-framework/ui-types";
36
-
37
- /** Minimale EntityDefinition aus den Layout-Feldern: jedes Feld ein Text-
38
- * Feld — computeEditViewModel liest nur `fields[<f>].type`, Text reicht für
39
- * eine reine Anzeige (kein Select/Number-spezifisches Rendering nötig). */
40
- export function synthesizeProjectionDetailEntity(layout: EditLayout): EntityDefinition {
41
- const fields: Record<string, { type: "text" }> = {};
42
- for (const section of layout.sections) {
43
- // relatedList and extension sections carry no `fields` to synthesize.
44
- if (!isFieldsEditSection(section)) continue;
45
- for (const spec of sectionFieldSpecs(section)) {
46
- fields[normalizeEditField(spec).field] = { type: "text" };
47
- }
48
- }
49
- return { fields } as unknown as EntityDefinition;
50
- }
51
-
52
- /** Wandelt ein ProjectionDetailScreenDefinition in die EntityEditScreen-Shape
53
- * die RenderEdit erwartet — mit jedem Feld hart auf readOnly:true erzwungen. */
54
- export function synthesizeProjectionDetailScreen(
55
- screen: ProjectionDetailScreenDefinition,
56
- ): EntityEditScreenDefinition {
57
- const forceReadOnly = (spec: EditFieldSpec) => ({ ...normalizeEditField(spec), readOnly: true });
58
- const sections = screen.layout.sections.map((section) => {
59
- // relatedList and extension sections pass through unchanged.
60
- if (!isFieldsEditSection(section)) return section;
61
- return {
62
- ...section,
63
- // kumiko-lint-ignore section-fields-raw rewrites both sources in place — the groups branch below does the same for groups[].fields
64
- fields: section.fields.map(forceReadOnly),
65
- ...(section.groups !== undefined && {
66
- groups: section.groups.map((group) => ({
67
- ...group,
68
- fields: group.fields.map(forceReadOnly),
69
- })),
70
- }),
71
- };
72
- });
73
- return {
74
- id: screen.id,
75
- type: "entityEdit",
76
- entity: PROJECTION_DETAIL_PSEUDO_ENTITY,
77
- layout: { sections, ...(screen.layout.width !== undefined && { width: screen.layout.width }) },
78
- allowCreate: false,
79
- allowDelete: false,
80
- ...(screen.description !== undefined && { description: screen.description }),
81
- ...(screen.fieldLabels !== undefined && { fieldLabels: screen.fieldLabels }),
82
- ...(screen.slots !== undefined && { slots: screen.slots }),
83
- ...(screen.access !== undefined && { access: screen.access }),
84
- ...(screen.fillHeight !== undefined && { fillHeight: screen.fillHeight }),
85
- };
86
- }