@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,40 +0,0 @@
1
- // toAppSchema normalisiert FeatureSchema → AppSchema (idempotent),
2
- // isAppSchema diskriminiert die beiden Formen. Zero source-change.
3
-
4
- import { describe, expect, test } from "bun:test";
5
- import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-framework/ui-types";
6
- import { isAppSchema, toAppSchema } from "../feature-schema";
7
-
8
- const feature: FeatureSchema = { featureName: "tasks", entities: {}, screens: [] };
9
-
10
- describe("toAppSchema", () => {
11
- test("wraps a FeatureSchema into the AppSchema envelope", () => {
12
- const app = toAppSchema(feature);
13
- expect(app.features).toHaveLength(1);
14
- expect(app.features[0]?.featureName).toBe("tasks");
15
- expect(app.workspaces).toBeUndefined();
16
- });
17
-
18
- test("idempotent for AppSchema input (returns the same reference)", () => {
19
- const app: AppSchema = { features: [feature] };
20
- expect(toAppSchema(app)).toBe(app);
21
- });
22
- });
23
-
24
- describe("isAppSchema", () => {
25
- test("true for AppSchema (has 'features'), false for FeatureSchema", () => {
26
- expect(isAppSchema({ features: [feature] })).toBe(true);
27
- expect(isAppSchema(feature)).toBe(false);
28
- });
29
- });
30
-
31
- describe("toAppSchema — workspaces-Hoist (Legacy-Form)", () => {
32
- test("hebt feature-lokale workspaces auf App-Ebene und entfernt sie vom Feature", () => {
33
- const ws = [{ definition: { id: "admin", label: "Admin", navs: [] }, navMembers: [] }];
34
- const withWs: FeatureSchema = { ...feature, workspaces: ws };
35
- const app = toAppSchema(withWs);
36
- expect(app.workspaces).toEqual(ws);
37
- expect(app.features[0]).not.toHaveProperty("workspaces");
38
- expect(app.features[0]?.featureName).toBe("tasks");
39
- });
40
- });
@@ -1,247 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type {
3
- EditFieldSpec,
4
- EntityDefinition,
5
- EntityEditScreenDefinition,
6
- } from "@cosmicdrift/kumiko-framework/ui-types";
7
- import { createFormController } from "@cosmicdrift/kumiko-headless";
8
- import { buildFormSchema, REQUIRED_FIELD_I18N_KEY } from "../form-schema";
9
-
10
- function screenWith(fields: readonly EditFieldSpec[]): EntityEditScreenDefinition {
11
- return {
12
- id: "s",
13
- type: "entityEdit",
14
- entity: "e",
15
- layout: { sections: [{ fields }] },
16
- };
17
- }
18
-
19
- function entityWith(fields: EntityDefinition["fields"]): EntityDefinition {
20
- return { fields } as EntityDefinition;
21
- }
22
-
23
- describe("buildFormSchema", () => {
24
- describe("required field missing → issue on that field", () => {
25
- const entity = entityWith({ name: { type: "text", required: true } });
26
- const screen = screenWith(["name"]);
27
-
28
- for (const [label, value] of [
29
- ['""', ""],
30
- ["null", null],
31
- ["undefined", undefined],
32
- ["[]", [] as unknown[]],
33
- ] as const) {
34
- test(label, () => {
35
- const result = buildFormSchema(entity, screen).safeParse({ name: value });
36
- expect(result.success).toBe(false);
37
- if (result.success) return;
38
- expect(result.error.issues).toHaveLength(1);
39
- expect(result.error.issues[0]?.path).toEqual(["name"]);
40
- });
41
- }
42
- });
43
-
44
- test("required field inside a groups-only section → presence issue", () => {
45
- const entity = entityWith({ name: { type: "text", required: true } });
46
- const screen: EntityEditScreenDefinition = {
47
- id: "s",
48
- type: "entityEdit",
49
- entity: "e",
50
- layout: { sections: [{ fields: [], groups: [{ title: "g", fields: ["name"] }] }] },
51
- };
52
-
53
- const result = buildFormSchema(entity, screen).safeParse({ name: "" });
54
- expect(result.success).toBe(false);
55
- if (result.success) return;
56
- expect(result.error.issues[0]?.path).toEqual(["name"]);
57
- });
58
-
59
- test("required field missing → issue carries the required-field i18nKey override", () => {
60
- const entity = entityWith({ name: { type: "text", required: true } });
61
- const screen = screenWith(["name"]);
62
-
63
- const result = buildFormSchema(entity, screen).safeParse({ name: "" });
64
- expect(result.success).toBe(false);
65
- if (result.success) return;
66
- const issue = result.error.issues[0];
67
- if (issue?.code !== "custom") throw new Error("expected a custom issue");
68
- expect(issue.params).toMatchObject({ i18nKey: REQUIRED_FIELD_I18N_KEY });
69
- });
70
-
71
- // The seam the bug actually lived in — a schema built here only round-trips
72
- // through createFormController's validate(), which is what feeds
73
- // FormSnapshot.errors that render-edit.tsx passes to RenderField. A unit
74
- // test on buildFormSchema() alone can't catch a break in that hand-off
75
- // (e.g. zodErrorToFieldIssues not honoring the override).
76
- test("end-to-end via createFormController: required field left empty → snapshot error carries kumiko.validation.required", () => {
77
- const entity = entityWith({ name: { type: "text", required: true } });
78
- const screen = screenWith(["name"]);
79
-
80
- const form = createFormController({
81
- initial: { name: "" },
82
- schema: buildFormSchema(entity, screen),
83
- });
84
-
85
- expect(form.validate()).toBe(false);
86
- const fieldErrors = form.getSnapshot().errors["name"];
87
- expect(fieldErrors?.[0]?.i18nKey).toBe(REQUIRED_FIELD_I18N_KEY);
88
- });
89
-
90
- describe("required field present → no issue", () => {
91
- const entity = entityWith({ name: { type: "number", required: true } });
92
- const screen = screenWith(["name"]);
93
-
94
- for (const [label, value] of [
95
- ["0", 0],
96
- ["false", false],
97
- ['"Ada"', "Ada"],
98
- ] as const) {
99
- test(label, () => {
100
- const result = buildFormSchema(entity, screen).safeParse({ name: value });
101
- expect(result.success).toBe(true);
102
- });
103
- }
104
- });
105
-
106
- test("optional field left empty → no issue", () => {
107
- const entity = entityWith({ name: { type: "text", required: false } });
108
- const screen = screenWith(["name"]);
109
- expect(buildFormSchema(entity, screen).safeParse({ name: "" }).success).toBe(true);
110
- });
111
-
112
- test("required field not rendered by the layout → no issue", () => {
113
- const entity = entityWith({
114
- name: { type: "text", required: true },
115
- hidden: { type: "text", required: true },
116
- });
117
- const screen = screenWith(["name"]);
118
- const result = buildFormSchema(entity, screen).safeParse({ name: "Ada", hidden: "" });
119
- expect(result.success).toBe(true);
120
- });
121
-
122
- test("required multiSelect, empty array → issue on that field (#1925: has a combobox widget now)", () => {
123
- const entity = entityWith({
124
- tags: { type: "multiSelect", required: true, options: ["a", "b"] },
125
- });
126
- const screen = screenWith(["tags"]);
127
- const result = buildFormSchema(entity, screen).safeParse({ tags: [] });
128
- expect(result.success).toBe(false);
129
- if (result.success) return;
130
- expect(result.error.issues).toHaveLength(1);
131
- expect(result.error.issues[0]?.path).toEqual(["tags"]);
132
- });
133
-
134
- test("required multiSelect, non-empty array → no issue", () => {
135
- const entity = entityWith({
136
- tags: { type: "multiSelect", required: true, options: ["a", "b"] },
137
- });
138
- const screen = screenWith(["tags"]);
139
- expect(buildFormSchema(entity, screen).safeParse({ tags: ["a"] }).success).toBe(true);
140
- });
141
-
142
- test("jsonb field → no issue (no editable widget on the auto-wired path)", () => {
143
- const entity = entityWith({ data: { type: "jsonb" } });
144
- const screen = screenWith(["data"]);
145
- expect(buildFormSchema(entity, screen).safeParse({ data: undefined }).success).toBe(true);
146
- });
147
-
148
- test("required embedded field → no issue (no editable widget on the auto-wired path)", () => {
149
- const entity = entityWith({
150
- lines: { type: "embedded", required: true, schema: {} },
151
- });
152
- const screen = screenWith(["lines"]);
153
- expect(buildFormSchema(entity, screen).safeParse({ lines: undefined }).success).toBe(true);
154
- });
155
-
156
- test("required embedded list field → issue when missing (has EmbeddedListField widget)", () => {
157
- const entity = entityWith({
158
- lines: { type: "embedded", multiple: true, required: true, schema: {} },
159
- });
160
- const screen = screenWith(["lines"]);
161
- const result = buildFormSchema(entity, screen).safeParse({ lines: undefined });
162
- expect(result.success).toBe(false);
163
- });
164
-
165
- test("required files field → no issue (#1925: no multi-upload widget yet, deliberately deferred)", () => {
166
- const entity = entityWith({ attachments: { type: "files" } });
167
- const screen = screenWith([{ field: "attachments", required: true }]);
168
- expect(buildFormSchema(entity, screen).safeParse({ attachments: undefined }).success).toBe(
169
- true,
170
- );
171
- });
172
-
173
- test("required images field → no issue (#1925: no multi-upload widget yet, deliberately deferred)", () => {
174
- const entity = entityWith({ gallery: { type: "images" } });
175
- const screen = screenWith([{ field: "gallery", required: true }]);
176
- expect(buildFormSchema(entity, screen).safeParse({ gallery: undefined }).success).toBe(true);
177
- });
178
-
179
- test("required money — bare number (create-form representation) → no issue", () => {
180
- const entity = entityWith({ price: { type: "money", required: true } });
181
- const screen = screenWith(["price"]);
182
- expect(buildFormSchema(entity, screen).safeParse({ price: 1000 }).success).toBe(true);
183
- });
184
-
185
- test("required money — {amount,currency} (update-form representation) → no issue", () => {
186
- const entity = entityWith({ price: { type: "money", required: true } });
187
- const screen = screenWith(["price"]);
188
- const result = buildFormSchema(entity, screen).safeParse({
189
- price: { amount: 10, currency: "EUR" },
190
- });
191
- expect(result.success).toBe(true);
192
- });
193
-
194
- test("required money — undefined → issue on that field (has a widget)", () => {
195
- const entity = entityWith({ price: { type: "money", required: true } });
196
- const screen = screenWith(["price"]);
197
- const result = buildFormSchema(entity, screen).safeParse({ price: undefined });
198
- expect(result.success).toBe(false);
199
- if (result.success) return;
200
- expect(result.error.issues).toHaveLength(1);
201
- expect(result.error.issues[0]?.path).toEqual(["price"]);
202
- });
203
-
204
- describe("screen-spec required/readOnly override the entity default", () => {
205
- test("spec required:false on an entity-required field, left empty → no issue", () => {
206
- const entity = entityWith({ x: { type: "text", required: true } });
207
- const screen = screenWith([{ field: "x", required: false }]);
208
- expect(buildFormSchema(entity, screen).safeParse({ x: "" }).success).toBe(true);
209
- });
210
-
211
- test("spec required:true on an entity-optional field, left empty → issue on that field", () => {
212
- const entity = entityWith({ x: { type: "text", required: false } });
213
- const screen = screenWith([{ field: "x", required: true }]);
214
- const result = buildFormSchema(entity, screen).safeParse({ x: "" });
215
- expect(result.success).toBe(false);
216
- if (result.success) return;
217
- expect(result.error.issues).toHaveLength(1);
218
- expect(result.error.issues[0]?.path).toEqual(["x"]);
219
- });
220
-
221
- test("readOnly field, entity-required and left empty → no issue (unresolvable by the user)", () => {
222
- const entity = entityWith({ x: { type: "text", required: true } });
223
- const screen = screenWith([{ field: "x", readOnly: true }]);
224
- expect(buildFormSchema(entity, screen).safeParse({ x: "" }).success).toBe(true);
225
- });
226
-
227
- test("conditional required, entity-optional field", () => {
228
- const entity = entityWith({
229
- kind: { type: "text", required: false },
230
- x: { type: "text", required: false },
231
- });
232
- const screen = screenWith([
233
- "kind",
234
- { field: "x", required: { field: "kind", eq: "business" } },
235
- ]);
236
-
237
- const notTriggered = buildFormSchema(entity, screen).safeParse({ kind: "private", x: "" });
238
- expect(notTriggered.success).toBe(true);
239
-
240
- const triggered = buildFormSchema(entity, screen).safeParse({ kind: "business", x: "" });
241
- expect(triggered.success).toBe(false);
242
- if (triggered.success) return;
243
- expect(triggered.error.issues).toHaveLength(1);
244
- expect(triggered.error.issues[0]?.path).toEqual(["x"]);
245
- });
246
- });
247
- });
@@ -1,165 +0,0 @@
1
- // fw#3104: a dateRange facet turns two calendar dates into the two query
2
- // params it declares. The conversion is the whole risk surface — a calendar
3
- // date has no time, `createdAt` is an instant, and "to the 14th" has to mean
4
- // "through the end of the 14th" in the viewer's zone, not "at 00:00".
5
-
6
- import { describe, expect, test } from "bun:test";
7
- import type { ListFacetSpec } from "@cosmicdrift/kumiko-framework/ui-types";
8
- import { Temporal } from "temporal-polyfill";
9
- import {
10
- buildDateRangePayload,
11
- clampDateRange,
12
- readDateRange,
13
- resolveDateRangeFacets,
14
- resolveProjectionFacetSpecs,
15
- } from "../list-facets";
16
-
17
- const VIENNA = "Europe/Vienna";
18
-
19
- const auditFacets: readonly ListFacetSpec[] = [
20
- {
21
- type: "dateRange",
22
- field: "createdAt",
23
- label: "audit.log.col.when",
24
- params: { from: "from", to: "to" },
25
- },
26
- ];
27
-
28
- const specs = resolveDateRangeFacets(auditFacets, (key) => key);
29
-
30
- function payload(
31
- filters: Record<string, readonly string[]>,
32
- timeZone = VIENNA,
33
- ): Record<string, string> {
34
- return buildDateRangePayload(specs, filters, timeZone);
35
- }
36
-
37
- describe("dateRange facet resolution", () => {
38
- test("resolves the declared param names, not a from/to convention", () => {
39
- const resolved = resolveDateRangeFacets(
40
- [
41
- {
42
- type: "dateRange",
43
- field: "occurredAt",
44
- label: "when",
45
- params: { from: "since", to: "until" },
46
- },
47
- ],
48
- (key) => key,
49
- );
50
- expect(resolved).toEqual([
51
- { field: "occurredAt", label: "when", fromParam: "since", toParam: "until" },
52
- ]);
53
- });
54
-
55
- test("stays out of the option-dropdown facets (no `filters` entry)", () => {
56
- expect(resolveProjectionFacetSpecs(auditFacets, (key) => key, "audit")).toEqual([]);
57
- });
58
-
59
- test("reads both bounds out of the shared f.<field> URL namespace", () => {
60
- expect(
61
- readDateRange(
62
- { "createdAt.from": ["2020-06-10"], "createdAt.to": ["2020-06-14"] },
63
- "createdAt",
64
- ),
65
- ).toEqual({ from: "2020-06-10", to: "2020-06-14" });
66
- expect(readDateRange({}, "createdAt")).toEqual({ from: "", to: "" });
67
- });
68
- });
69
-
70
- describe("dateRange facet → query params", () => {
71
- test("both bounds travel under the declared param names", () => {
72
- expect(payload({ "createdAt.from": ["2020-06-10"], "createdAt.to": ["2020-06-14"] })).toEqual({
73
- from: "2020-06-09T22:00:00Z",
74
- to: "2020-06-14T21:59:59.999999999Z",
75
- });
76
- });
77
-
78
- test("open interval: only `from` set sends only that param", () => {
79
- expect(payload({ "createdAt.from": ["2020-06-10"] })).toEqual({ from: "2020-06-09T22:00:00Z" });
80
- });
81
-
82
- test("open interval: only `to` set sends only that param", () => {
83
- expect(payload({ "createdAt.to": ["2020-06-14"] })).toEqual({
84
- to: "2020-06-14T21:59:59.999999999Z",
85
- });
86
- });
87
-
88
- test("no bounds set sends nothing", () => {
89
- expect(payload({})).toEqual({});
90
- });
91
-
92
- test("a half-typed or hand-crafted URL value is dropped, not sent", () => {
93
- expect(payload({ "createdAt.from": ["2020-06"], "createdAt.to": ["nonsense"] })).toEqual({});
94
- });
95
-
96
- test("bounds follow the viewer's zone", () => {
97
- expect(payload({ "createdAt.from": ["2020-06-10"] }, "UTC")).toEqual({
98
- from: "2020-06-10T00:00:00Z",
99
- });
100
- });
101
-
102
- test("DST: the day the clock jumps forward is still bounded by its own edges", () => {
103
- // 2020-03-29 is 23h long in Vienna — a naive +24h end-of-day would spill
104
- // an hour into the 30th.
105
- expect(payload({ "createdAt.from": ["2020-03-29"], "createdAt.to": ["2020-03-29"] })).toEqual({
106
- from: "2020-03-28T23:00:00Z",
107
- to: "2020-03-29T21:59:59.999999999Z",
108
- });
109
- });
110
- });
111
-
112
- describe("dateRange facet timezone edge (issue #3104)", () => {
113
- test("an event at 23:59 of the `to` day falls inside the range", () => {
114
- const { from, to } = payload({
115
- "createdAt.from": ["2020-06-14"],
116
- "createdAt.to": ["2020-06-14"],
117
- });
118
- if (from === undefined || to === undefined) throw new Error("expected both bounds");
119
- const lateEvent = Temporal.ZonedDateTime.from(`2020-06-14T23:59:00[${VIENNA}]`).toInstant();
120
- expect(Temporal.Instant.compare(lateEvent, Temporal.Instant.from(from))).toBeGreaterThan(0);
121
- // The handler filters with `lte`, so the last instant of the day counts.
122
- expect(Temporal.Instant.compare(lateEvent, Temporal.Instant.from(to))).toBeLessThanOrEqual(0);
123
- });
124
-
125
- test("the next day's first moment falls outside", () => {
126
- const { to } = payload({ "createdAt.to": ["2020-06-14"] });
127
- if (to === undefined) throw new Error("expected a `to` bound");
128
- const nextDay = Temporal.ZonedDateTime.from(`2020-06-15T00:00:00[${VIENNA}]`).toInstant();
129
- expect(Temporal.Instant.compare(nextDay, Temporal.Instant.from(to))).toBeGreaterThan(0);
130
- });
131
- });
132
-
133
- describe("clampDateRange keeps from <= to", () => {
134
- test("a `from` past the current `to` pushes `to` along", () => {
135
- expect(clampDateRange({ from: "2020-06-01", to: "2020-06-10" }, "from", "2020-06-20")).toEqual({
136
- from: "2020-06-20",
137
- to: "2020-06-20",
138
- });
139
- });
140
-
141
- test("a `to` before the current `from` pulls `from` along", () => {
142
- expect(clampDateRange({ from: "2020-06-10", to: "2020-06-20" }, "to", "2020-06-01")).toEqual({
143
- from: "2020-06-01",
144
- to: "2020-06-01",
145
- });
146
- });
147
-
148
- test("a valid bound leaves the other alone", () => {
149
- expect(clampDateRange({ from: "2020-06-01", to: "2020-06-20" }, "from", "2020-06-05")).toEqual({
150
- from: "2020-06-05",
151
- to: "2020-06-20",
152
- });
153
- });
154
-
155
- test("clearing a bound never drags the other one with it", () => {
156
- expect(clampDateRange({ from: "2020-06-10", to: "2020-06-20" }, "from", "")).toEqual({
157
- from: "",
158
- to: "2020-06-20",
159
- });
160
- expect(clampDateRange({ from: "2020-06-10", to: "2020-06-20" }, "to", "")).toEqual({
161
- from: "2020-06-10",
162
- to: "",
163
- });
164
- });
165
- });