@cosmicdrift/kumiko-renderer 0.327.0 → 0.328.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -44
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -38
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/dist/inside-drawer.js +9 -0
- package/dist/page-header-slot.d.ts +2 -0
- package/dist/page-header-slot.js +9 -0
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/changes.json +52 -0
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -111
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3614
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -85
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -639
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1669
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -60
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -467
- package/src/components/render-edit-action-button.tsx +0 -106
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -216
- package/src/components/render-edit.tsx +0 -1700
- package/src/components/render-field.tsx +0 -942
- package/src/components/render-list.tsx +0 -561
- package/src/components/write-form-section.tsx +0 -199
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -171
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -132
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -220
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -272
- package/src/primitives.tsx +0 -1446
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- 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
|
-
});
|