@btst/stack 2.11.5 → 2.11.6
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/components/form-builder/index.d.cts +8 -0
- package/dist/components/form-builder/index.d.mts +8 -0
- package/dist/components/form-builder/index.d.ts +8 -0
- package/dist/packages/ui/src/components/form-builder/schema-utils.cjs +11 -5
- package/dist/packages/ui/src/components/form-builder/schema-utils.mjs +11 -5
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/package.json +1 -1
- package/src/plugins/form-builder/__tests__/schema-utils.test.ts +362 -0
- package/dist/shared/{stack.D4Cea8II.d.ts → stack.9dFHtvXR.d.ts} +6 -6
- package/dist/shared/{stack.HE_IvqV5.d.mts → stack.BzKW7ToW.d.mts} +6 -6
- package/dist/shared/{stack.Rtcvl8sS.d.cts → stack.f6MrG7ys.d.cts} +6 -6
|
@@ -23,6 +23,14 @@ interface JSONSchema {
|
|
|
23
23
|
$schema?: string;
|
|
24
24
|
/** Step definitions for multi-step forms (stored in schema meta) */
|
|
25
25
|
steps?: FormStep[];
|
|
26
|
+
/**
|
|
27
|
+
* Optional root-level field-name → step-index map. This is the format produced
|
|
28
|
+
* by `zodToFormSchema(schema, { steps, stepGroupMap })` and consumed by
|
|
29
|
+
* `SteppedAutoForm`. The visual FormBuilder writes per-property `stepGroup`
|
|
30
|
+
* instead, but we accept both on read so a Zod-seeded form opens with its
|
|
31
|
+
* fields correctly assigned to their steps.
|
|
32
|
+
*/
|
|
33
|
+
stepGroupMap?: Record<string, number>;
|
|
26
34
|
additionalProperties?: boolean;
|
|
27
35
|
}
|
|
28
36
|
/**
|
|
@@ -23,6 +23,14 @@ interface JSONSchema {
|
|
|
23
23
|
$schema?: string;
|
|
24
24
|
/** Step definitions for multi-step forms (stored in schema meta) */
|
|
25
25
|
steps?: FormStep[];
|
|
26
|
+
/**
|
|
27
|
+
* Optional root-level field-name → step-index map. This is the format produced
|
|
28
|
+
* by `zodToFormSchema(schema, { steps, stepGroupMap })` and consumed by
|
|
29
|
+
* `SteppedAutoForm`. The visual FormBuilder writes per-property `stepGroup`
|
|
30
|
+
* instead, but we accept both on read so a Zod-seeded form opens with its
|
|
31
|
+
* fields correctly assigned to their steps.
|
|
32
|
+
*/
|
|
33
|
+
stepGroupMap?: Record<string, number>;
|
|
26
34
|
additionalProperties?: boolean;
|
|
27
35
|
}
|
|
28
36
|
/**
|
|
@@ -23,6 +23,14 @@ interface JSONSchema {
|
|
|
23
23
|
$schema?: string;
|
|
24
24
|
/** Step definitions for multi-step forms (stored in schema meta) */
|
|
25
25
|
steps?: FormStep[];
|
|
26
|
+
/**
|
|
27
|
+
* Optional root-level field-name → step-index map. This is the format produced
|
|
28
|
+
* by `zodToFormSchema(schema, { steps, stepGroupMap })` and consumed by
|
|
29
|
+
* `SteppedAutoForm`. The visual FormBuilder writes per-property `stepGroup`
|
|
30
|
+
* instead, but we accept both on read so a Zod-seeded form opens with its
|
|
31
|
+
* fields correctly assigned to their steps.
|
|
32
|
+
*/
|
|
33
|
+
stepGroupMap?: Record<string, number>;
|
|
26
34
|
additionalProperties?: boolean;
|
|
27
35
|
}
|
|
28
36
|
/**
|
|
@@ -49,7 +49,7 @@ function fieldsToJSONSchema(fields, components, steps) {
|
|
|
49
49
|
...hasMultipleSteps ? { steps } : {}
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
function propertiesToFields(properties, requiredSet, components) {
|
|
52
|
+
function propertiesToFields(properties, requiredSet, components, stepGroupMap) {
|
|
53
53
|
const fields = [];
|
|
54
54
|
for (const [key, prop] of Object.entries(properties)) {
|
|
55
55
|
const isRequired = requiredSet.has(key);
|
|
@@ -60,9 +60,10 @@ function propertiesToFields(properties, requiredSet, components) {
|
|
|
60
60
|
break;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
const resolvedStepGroup = prop.stepGroup !== void 0 ? prop.stepGroup : stepGroupMap?.[key];
|
|
63
64
|
if (field) {
|
|
64
|
-
if (
|
|
65
|
-
field.stepGroup =
|
|
65
|
+
if (resolvedStepGroup !== void 0) {
|
|
66
|
+
field.stepGroup = resolvedStepGroup;
|
|
66
67
|
}
|
|
67
68
|
if (field.type === "object" && prop.properties) {
|
|
68
69
|
const childRequiredSet = new Set(prop.required || []);
|
|
@@ -85,7 +86,7 @@ function propertiesToFields(properties, requiredSet, components) {
|
|
|
85
86
|
required: isRequired
|
|
86
87
|
},
|
|
87
88
|
// Include stepGroup even for fallback fields
|
|
88
|
-
...
|
|
89
|
+
...resolvedStepGroup !== void 0 ? { stepGroup: resolvedStepGroup } : {}
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
92
|
}
|
|
@@ -96,7 +97,12 @@ function jsonSchemaToFieldsAndSteps(schema, components) {
|
|
|
96
97
|
return { fields: [], steps: [] };
|
|
97
98
|
}
|
|
98
99
|
const requiredSet = new Set(schema.required || []);
|
|
99
|
-
const fields = propertiesToFields(
|
|
100
|
+
const fields = propertiesToFields(
|
|
101
|
+
schema.properties,
|
|
102
|
+
requiredSet,
|
|
103
|
+
components,
|
|
104
|
+
schema.stepGroupMap
|
|
105
|
+
);
|
|
100
106
|
const steps = schema.steps || [];
|
|
101
107
|
return { fields, steps };
|
|
102
108
|
}
|
|
@@ -47,7 +47,7 @@ function fieldsToJSONSchema(fields, components, steps) {
|
|
|
47
47
|
...hasMultipleSteps ? { steps } : {}
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
function propertiesToFields(properties, requiredSet, components) {
|
|
50
|
+
function propertiesToFields(properties, requiredSet, components, stepGroupMap) {
|
|
51
51
|
const fields = [];
|
|
52
52
|
for (const [key, prop] of Object.entries(properties)) {
|
|
53
53
|
const isRequired = requiredSet.has(key);
|
|
@@ -58,9 +58,10 @@ function propertiesToFields(properties, requiredSet, components) {
|
|
|
58
58
|
break;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
const resolvedStepGroup = prop.stepGroup !== void 0 ? prop.stepGroup : stepGroupMap?.[key];
|
|
61
62
|
if (field) {
|
|
62
|
-
if (
|
|
63
|
-
field.stepGroup =
|
|
63
|
+
if (resolvedStepGroup !== void 0) {
|
|
64
|
+
field.stepGroup = resolvedStepGroup;
|
|
64
65
|
}
|
|
65
66
|
if (field.type === "object" && prop.properties) {
|
|
66
67
|
const childRequiredSet = new Set(prop.required || []);
|
|
@@ -83,7 +84,7 @@ function propertiesToFields(properties, requiredSet, components) {
|
|
|
83
84
|
required: isRequired
|
|
84
85
|
},
|
|
85
86
|
// Include stepGroup even for fallback fields
|
|
86
|
-
...
|
|
87
|
+
...resolvedStepGroup !== void 0 ? { stepGroup: resolvedStepGroup } : {}
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
}
|
|
@@ -94,7 +95,12 @@ function jsonSchemaToFieldsAndSteps(schema, components) {
|
|
|
94
95
|
return { fields: [], steps: [] };
|
|
95
96
|
}
|
|
96
97
|
const requiredSet = new Set(schema.required || []);
|
|
97
|
-
const fields = propertiesToFields(
|
|
98
|
+
const fields = propertiesToFields(
|
|
99
|
+
schema.properties,
|
|
100
|
+
requiredSet,
|
|
101
|
+
components,
|
|
102
|
+
schema.stepGroupMap
|
|
103
|
+
);
|
|
98
104
|
const steps = schema.steps || [];
|
|
99
105
|
return { fields, steps };
|
|
100
106
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.
|
|
1
|
+
export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.f6MrG7ys.cjs';
|
|
2
2
|
import { B as BoardWithColumns, S as SerializedBoardWithColumns, C as ColumnWithTasks, a as SerializedColumn, T as Task, b as SerializedTask } from '../../../shared/stack.DJaKVY7v.cjs';
|
|
3
3
|
import '@btst/stack/plugins/api';
|
|
4
4
|
import '@btst/db';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.
|
|
1
|
+
export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.BzKW7ToW.mjs';
|
|
2
2
|
import { B as BoardWithColumns, S as SerializedBoardWithColumns, C as ColumnWithTasks, a as SerializedColumn, T as Task, b as SerializedTask } from '../../../shared/stack.DJaKVY7v.mjs';
|
|
3
3
|
import '@btst/stack/plugins/api';
|
|
4
4
|
import '@btst/db';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.
|
|
1
|
+
export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.9dFHtvXR.js';
|
|
2
2
|
import { B as BoardWithColumns, S as SerializedBoardWithColumns, C as ColumnWithTasks, a as SerializedColumn, T as Task, b as SerializedTask } from '../../../shared/stack.DJaKVY7v.js';
|
|
3
3
|
import '@btst/stack/plugins/api';
|
|
4
4
|
import '@btst/db';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.
|
|
2
|
+
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.f6MrG7ys.cjs';
|
|
3
3
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
4
4
|
import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.cjs';
|
|
5
5
|
import '@btst/stack/plugins/api';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.
|
|
2
|
+
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.BzKW7ToW.mjs';
|
|
3
3
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
4
4
|
import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.mjs';
|
|
5
5
|
import '@btst/stack/plugins/api';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.
|
|
2
|
+
import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.9dFHtvXR.js';
|
|
3
3
|
import { createApiClient } from '@btst/stack/plugins/client';
|
|
4
4
|
import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.js';
|
|
5
5
|
import '@btst/stack/plugins/api';
|
package/package.json
CHANGED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the JSON Schema ↔ FormBuilderField conversion utilities.
|
|
3
|
+
*
|
|
4
|
+
* The form-builder ecosystem accepts two flavours of multi-step encoding:
|
|
5
|
+
* - **Path A**: per-property `stepGroup` on each field (what the visual
|
|
6
|
+
* FormBuilder writes on save — canonical wire format).
|
|
7
|
+
* - **Path B**: root-level `stepGroupMap` (what `zodToFormSchema(schema, {
|
|
8
|
+
* steps, stepGroupMap })` produces and what `SteppedAutoForm` reads).
|
|
9
|
+
*
|
|
10
|
+
* Both must be accepted on read so a Zod-seeded form opens with all fields
|
|
11
|
+
* correctly assigned to their step tabs in the admin canvas. Without Path B
|
|
12
|
+
* support, every field in a Zod-seeded multi-step form collapses onto step 0.
|
|
13
|
+
*
|
|
14
|
+
* @see packages/ui/src/components/form-builder/schema-utils.ts
|
|
15
|
+
* @see packages/ui/src/lib/schema-converter.ts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from "vitest";
|
|
19
|
+
import {
|
|
20
|
+
fieldsToJSONSchema,
|
|
21
|
+
jsonSchemaToFieldsAndSteps,
|
|
22
|
+
} from "@workspace/ui/components/form-builder/schema-utils";
|
|
23
|
+
import { defaultComponents } from "@workspace/ui/components/form-builder";
|
|
24
|
+
import type {
|
|
25
|
+
JSONSchema,
|
|
26
|
+
FormStep,
|
|
27
|
+
} from "@workspace/ui/components/form-builder/types";
|
|
28
|
+
|
|
29
|
+
const STEPS: FormStep[] = [
|
|
30
|
+
{ id: "goals", title: "Your goals" },
|
|
31
|
+
{ id: "experience", title: "Experience" },
|
|
32
|
+
{ id: "constraints", title: "Constraints" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
describe("jsonSchemaToFieldsAndSteps", () => {
|
|
36
|
+
describe("core usage", () => {
|
|
37
|
+
it("returns empty result for null/undefined schema", () => {
|
|
38
|
+
expect(jsonSchemaToFieldsAndSteps(null, defaultComponents)).toEqual({
|
|
39
|
+
fields: [],
|
|
40
|
+
steps: [],
|
|
41
|
+
});
|
|
42
|
+
expect(jsonSchemaToFieldsAndSteps(undefined, defaultComponents)).toEqual({
|
|
43
|
+
fields: [],
|
|
44
|
+
steps: [],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns empty result when schema has no properties", () => {
|
|
49
|
+
const schema = { type: "object", properties: {} } as JSONSchema;
|
|
50
|
+
expect(jsonSchemaToFieldsAndSteps(schema, defaultComponents)).toEqual({
|
|
51
|
+
fields: [],
|
|
52
|
+
steps: [],
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("parses a flat single-step schema with mixed field types", () => {
|
|
57
|
+
const schema: JSONSchema = {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
name: { type: "string", label: "Full Name" },
|
|
61
|
+
email: { type: "string", format: "email", label: "Email" },
|
|
62
|
+
age: { type: "number", label: "Age", minimum: 0 },
|
|
63
|
+
},
|
|
64
|
+
required: ["name", "email"],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const { fields, steps } = jsonSchemaToFieldsAndSteps(
|
|
68
|
+
schema,
|
|
69
|
+
defaultComponents,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(steps).toEqual([]);
|
|
73
|
+
expect(fields).toHaveLength(3);
|
|
74
|
+
|
|
75
|
+
const [name, email, age] = fields;
|
|
76
|
+
expect(name).toMatchObject({
|
|
77
|
+
id: "name",
|
|
78
|
+
type: "text",
|
|
79
|
+
props: { label: "Full Name", required: true },
|
|
80
|
+
});
|
|
81
|
+
expect(email).toMatchObject({
|
|
82
|
+
id: "email",
|
|
83
|
+
type: "email",
|
|
84
|
+
props: { label: "Email", required: true },
|
|
85
|
+
});
|
|
86
|
+
expect(age).toMatchObject({
|
|
87
|
+
id: "age",
|
|
88
|
+
type: "number",
|
|
89
|
+
props: { label: "Age", required: false },
|
|
90
|
+
});
|
|
91
|
+
expect(name?.stepGroup).toBeUndefined();
|
|
92
|
+
expect(email?.stepGroup).toBeUndefined();
|
|
93
|
+
expect(age?.stepGroup).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("preserves the steps array on the output", () => {
|
|
97
|
+
const schema: JSONSchema = {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
name: { type: "string", label: "Name", stepGroup: 0 },
|
|
101
|
+
},
|
|
102
|
+
steps: STEPS,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const { steps } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
|
|
106
|
+
expect(steps).toEqual(STEPS);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("multi-step: Path A — per-property stepGroup", () => {
|
|
111
|
+
// Canonical format written by the visual FormBuilder when saving.
|
|
112
|
+
const schema: JSONSchema = {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
name: { type: "string", label: "Name", stepGroup: 0 },
|
|
116
|
+
email: {
|
|
117
|
+
type: "string",
|
|
118
|
+
format: "email",
|
|
119
|
+
label: "Email",
|
|
120
|
+
stepGroup: 0,
|
|
121
|
+
},
|
|
122
|
+
experience: {
|
|
123
|
+
type: "string",
|
|
124
|
+
enum: ["beginner", "advanced"],
|
|
125
|
+
label: "Experience",
|
|
126
|
+
stepGroup: 1,
|
|
127
|
+
},
|
|
128
|
+
agree: {
|
|
129
|
+
type: "boolean",
|
|
130
|
+
fieldType: "switch",
|
|
131
|
+
label: "I agree",
|
|
132
|
+
stepGroup: 2,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
steps: STEPS,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
it("assigns each field to the step encoded on its property", () => {
|
|
139
|
+
const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
|
|
140
|
+
|
|
141
|
+
expect(fields).toHaveLength(4);
|
|
142
|
+
const byId = new Map(fields.map((f) => [f.id, f]));
|
|
143
|
+
expect(byId.get("name")?.stepGroup).toBe(0);
|
|
144
|
+
expect(byId.get("email")?.stepGroup).toBe(0);
|
|
145
|
+
expect(byId.get("experience")?.stepGroup).toBe(1);
|
|
146
|
+
expect(byId.get("agree")?.stepGroup).toBe(2);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("multi-step: Path B — root-level stepGroupMap (regression)", () => {
|
|
151
|
+
// What `zodToFormSchema(LooksmaxQuizSchema, { steps, stepGroupMap })` writes.
|
|
152
|
+
// Before the fix, every field collapsed onto step 0 because the parser
|
|
153
|
+
// only consulted per-property `stepGroup`.
|
|
154
|
+
const schema: JSONSchema = {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
primaryGoal: {
|
|
158
|
+
type: "string",
|
|
159
|
+
enum: ["skin", "hair", "jaw", "lean", "all"],
|
|
160
|
+
label: "Primary goal",
|
|
161
|
+
fieldType: "radio",
|
|
162
|
+
},
|
|
163
|
+
experience: {
|
|
164
|
+
type: "string",
|
|
165
|
+
enum: ["none", "some-peptides", "full-cycle-history"],
|
|
166
|
+
label: "Experience",
|
|
167
|
+
fieldType: "radio",
|
|
168
|
+
},
|
|
169
|
+
riskTolerance: {
|
|
170
|
+
type: "string",
|
|
171
|
+
enum: ["low", "moderate", "high"],
|
|
172
|
+
label: "Risk tolerance",
|
|
173
|
+
fieldType: "radio",
|
|
174
|
+
},
|
|
175
|
+
noInjections: {
|
|
176
|
+
type: "boolean",
|
|
177
|
+
fieldType: "switch",
|
|
178
|
+
label: "No injections",
|
|
179
|
+
},
|
|
180
|
+
email: {
|
|
181
|
+
type: "string",
|
|
182
|
+
format: "email",
|
|
183
|
+
label: "Email",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
steps: STEPS,
|
|
187
|
+
stepGroupMap: {
|
|
188
|
+
primaryGoal: 0,
|
|
189
|
+
experience: 1,
|
|
190
|
+
riskTolerance: 1,
|
|
191
|
+
noInjections: 2,
|
|
192
|
+
email: 2,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
it("assigns step from root-level stepGroupMap when properties omit stepGroup", () => {
|
|
197
|
+
const { fields, steps } = jsonSchemaToFieldsAndSteps(
|
|
198
|
+
schema,
|
|
199
|
+
defaultComponents,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(steps).toEqual(STEPS);
|
|
203
|
+
expect(fields).toHaveLength(5);
|
|
204
|
+
|
|
205
|
+
const byId = new Map(fields.map((f) => [f.id, f]));
|
|
206
|
+
expect(byId.get("primaryGoal")?.stepGroup).toBe(0);
|
|
207
|
+
expect(byId.get("experience")?.stepGroup).toBe(1);
|
|
208
|
+
expect(byId.get("riskTolerance")?.stepGroup).toBe(1);
|
|
209
|
+
expect(byId.get("noInjections")?.stepGroup).toBe(2);
|
|
210
|
+
expect(byId.get("email")?.stepGroup).toBe(2);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("works for fields that fall back to the generic text component", () => {
|
|
214
|
+
// Fields with unknown shapes fall through to the fallback `text` field
|
|
215
|
+
// in propertiesToFields. The fix must apply there too.
|
|
216
|
+
const schemaWithUnknown: JSONSchema = {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
mystery: {
|
|
220
|
+
// no `type` → no component matches → fallback to text
|
|
221
|
+
label: "Mystery field",
|
|
222
|
+
} as JSONSchema["properties"][string],
|
|
223
|
+
},
|
|
224
|
+
steps: STEPS,
|
|
225
|
+
stepGroupMap: { mystery: 2 },
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const { fields } = jsonSchemaToFieldsAndSteps(
|
|
229
|
+
schemaWithUnknown,
|
|
230
|
+
defaultComponents,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
expect(fields).toHaveLength(1);
|
|
234
|
+
expect(fields[0]?.id).toBe("mystery");
|
|
235
|
+
expect(fields[0]?.stepGroup).toBe(2);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("multi-step: precedence when both encodings are present", () => {
|
|
240
|
+
it("per-property stepGroup wins over stepGroupMap", () => {
|
|
241
|
+
const schema: JSONSchema = {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
primary: {
|
|
245
|
+
type: "string",
|
|
246
|
+
label: "Primary",
|
|
247
|
+
stepGroup: 0, // per-property says step 0
|
|
248
|
+
},
|
|
249
|
+
secondary: {
|
|
250
|
+
type: "string",
|
|
251
|
+
label: "Secondary",
|
|
252
|
+
stepGroup: 1, // per-property says step 1
|
|
253
|
+
},
|
|
254
|
+
tertiary: {
|
|
255
|
+
type: "string",
|
|
256
|
+
label: "Tertiary",
|
|
257
|
+
// no per-property — should use map
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
steps: STEPS,
|
|
261
|
+
stepGroupMap: {
|
|
262
|
+
primary: 99, // map disagrees, but per-property wins
|
|
263
|
+
secondary: 99,
|
|
264
|
+
tertiary: 2, // map applies
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
|
|
269
|
+
|
|
270
|
+
const byId = new Map(fields.map((f) => [f.id, f]));
|
|
271
|
+
expect(byId.get("primary")?.stepGroup).toBe(0);
|
|
272
|
+
expect(byId.get("secondary")?.stepGroup).toBe(1);
|
|
273
|
+
expect(byId.get("tertiary")?.stepGroup).toBe(2);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("missing entries in stepGroupMap leave stepGroup undefined", () => {
|
|
277
|
+
const schema: JSONSchema = {
|
|
278
|
+
type: "object",
|
|
279
|
+
properties: {
|
|
280
|
+
mapped: { type: "string", label: "Mapped" },
|
|
281
|
+
unmapped: { type: "string", label: "Unmapped" },
|
|
282
|
+
},
|
|
283
|
+
steps: STEPS,
|
|
284
|
+
stepGroupMap: { mapped: 1 },
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
|
|
288
|
+
const byId = new Map(fields.map((f) => [f.id, f]));
|
|
289
|
+
expect(byId.get("mapped")?.stepGroup).toBe(1);
|
|
290
|
+
expect(byId.get("unmapped")?.stepGroup).toBeUndefined();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("fieldsToJSONSchema → jsonSchemaToFieldsAndSteps round-trip", () => {
|
|
296
|
+
it("normalises Path B (stepGroupMap) input to Path A (per-property) output", () => {
|
|
297
|
+
// This is the workflow that breaks the original bug: open a Zod-seeded
|
|
298
|
+
// form (Path B), save without changes — the FormBuilder writes Path A.
|
|
299
|
+
const inputSchema: JSONSchema = {
|
|
300
|
+
type: "object",
|
|
301
|
+
properties: {
|
|
302
|
+
goal: { type: "string", label: "Goal" },
|
|
303
|
+
risk: { type: "string", label: "Risk" },
|
|
304
|
+
},
|
|
305
|
+
steps: STEPS.slice(0, 2),
|
|
306
|
+
stepGroupMap: { goal: 0, risk: 1 },
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const { fields, steps } = jsonSchemaToFieldsAndSteps(
|
|
310
|
+
inputSchema,
|
|
311
|
+
defaultComponents,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Re-serialise with the same steps array
|
|
315
|
+
const roundTripped = fieldsToJSONSchema(fields, defaultComponents, steps);
|
|
316
|
+
|
|
317
|
+
// Output uses Path A (per-property stepGroup) and drops the root-level map
|
|
318
|
+
expect(roundTripped.properties.goal?.stepGroup).toBe(0);
|
|
319
|
+
expect(roundTripped.properties.risk?.stepGroup).toBe(1);
|
|
320
|
+
expect(roundTripped.steps).toEqual(STEPS.slice(0, 2));
|
|
321
|
+
expect(
|
|
322
|
+
(roundTripped as JSONSchema & { stepGroupMap?: unknown }).stepGroupMap,
|
|
323
|
+
).toBeUndefined();
|
|
324
|
+
|
|
325
|
+
// And re-parsing the normalised output produces the same field shape
|
|
326
|
+
const reparsed = jsonSchemaToFieldsAndSteps(
|
|
327
|
+
roundTripped,
|
|
328
|
+
defaultComponents,
|
|
329
|
+
);
|
|
330
|
+
const byIdOriginal = new Map(fields.map((f) => [f.id, f.stepGroup]));
|
|
331
|
+
const byIdRoundTrip = new Map(
|
|
332
|
+
reparsed.fields.map((f) => [f.id, f.stepGroup]),
|
|
333
|
+
);
|
|
334
|
+
expect(byIdRoundTrip).toEqual(byIdOriginal);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("preserves Path A through a full round-trip", () => {
|
|
338
|
+
const inputSchema: JSONSchema = {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: {
|
|
341
|
+
name: { type: "string", label: "Name", stepGroup: 0 },
|
|
342
|
+
email: {
|
|
343
|
+
type: "string",
|
|
344
|
+
format: "email",
|
|
345
|
+
label: "Email",
|
|
346
|
+
stepGroup: 1,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
steps: STEPS.slice(0, 2),
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const { fields, steps } = jsonSchemaToFieldsAndSteps(
|
|
353
|
+
inputSchema,
|
|
354
|
+
defaultComponents,
|
|
355
|
+
);
|
|
356
|
+
const roundTripped = fieldsToJSONSchema(fields, defaultComponents, steps);
|
|
357
|
+
|
|
358
|
+
expect(roundTripped.properties.name?.stepGroup).toBe(0);
|
|
359
|
+
expect(roundTripped.properties.email?.stepGroup).toBe(1);
|
|
360
|
+
expect(roundTripped.steps).toEqual(STEPS.slice(0, 2));
|
|
361
|
+
});
|
|
362
|
+
});
|
|
@@ -7,9 +7,9 @@ import { QueryClient } from '@tanstack/react-query';
|
|
|
7
7
|
|
|
8
8
|
declare const createBoardSchema: z.ZodObject<{
|
|
9
9
|
description: z.ZodOptional<z.ZodString>;
|
|
10
|
-
name: z.ZodString;
|
|
11
10
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
12
11
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
12
|
+
name: z.ZodString;
|
|
13
13
|
slug: z.ZodOptional<z.ZodString>;
|
|
14
14
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
15
15
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
|
|
|
28
28
|
title: z.ZodString;
|
|
29
29
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
30
30
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
31
|
-
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
32
31
|
boardId: z.ZodString;
|
|
32
|
+
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
33
33
|
}, z.core.$strip>;
|
|
34
34
|
declare const updateColumnSchema: z.ZodObject<{
|
|
35
35
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
@@ -360,17 +360,17 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
360
360
|
method: "PUT";
|
|
361
361
|
body: better_call.StandardSchemaV1<{
|
|
362
362
|
description?: string | undefined;
|
|
363
|
-
name?: string | undefined;
|
|
364
363
|
createdAt?: unknown;
|
|
365
364
|
updatedAt?: unknown;
|
|
365
|
+
name?: string | undefined;
|
|
366
366
|
slug?: string | undefined;
|
|
367
367
|
ownerId?: string | undefined;
|
|
368
368
|
organizationId?: string | undefined;
|
|
369
369
|
}, {
|
|
370
370
|
description?: string | undefined;
|
|
371
|
-
name?: string | undefined;
|
|
372
371
|
createdAt?: unknown;
|
|
373
372
|
updatedAt?: unknown;
|
|
373
|
+
name?: string | undefined;
|
|
374
374
|
slug?: string | undefined;
|
|
375
375
|
ownerId?: string | undefined;
|
|
376
376
|
organizationId?: string | undefined;
|
|
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
404
404
|
title?: string | undefined;
|
|
405
405
|
createdAt?: unknown;
|
|
406
406
|
updatedAt?: unknown;
|
|
407
|
-
order?: number | undefined;
|
|
408
407
|
boardId?: string | undefined;
|
|
408
|
+
order?: number | undefined;
|
|
409
409
|
}, {
|
|
410
410
|
title?: string | undefined;
|
|
411
411
|
createdAt?: unknown;
|
|
412
412
|
updatedAt?: unknown;
|
|
413
|
-
order?: number | undefined;
|
|
414
413
|
boardId?: string | undefined;
|
|
414
|
+
order?: number | undefined;
|
|
415
415
|
}>;
|
|
416
416
|
}, Column>;
|
|
417
417
|
readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {
|
|
@@ -7,9 +7,9 @@ import { QueryClient } from '@tanstack/react-query';
|
|
|
7
7
|
|
|
8
8
|
declare const createBoardSchema: z.ZodObject<{
|
|
9
9
|
description: z.ZodOptional<z.ZodString>;
|
|
10
|
-
name: z.ZodString;
|
|
11
10
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
12
11
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
12
|
+
name: z.ZodString;
|
|
13
13
|
slug: z.ZodOptional<z.ZodString>;
|
|
14
14
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
15
15
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
|
|
|
28
28
|
title: z.ZodString;
|
|
29
29
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
30
30
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
31
|
-
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
32
31
|
boardId: z.ZodString;
|
|
32
|
+
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
33
33
|
}, z.core.$strip>;
|
|
34
34
|
declare const updateColumnSchema: z.ZodObject<{
|
|
35
35
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
@@ -360,17 +360,17 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
360
360
|
method: "PUT";
|
|
361
361
|
body: better_call.StandardSchemaV1<{
|
|
362
362
|
description?: string | undefined;
|
|
363
|
-
name?: string | undefined;
|
|
364
363
|
createdAt?: unknown;
|
|
365
364
|
updatedAt?: unknown;
|
|
365
|
+
name?: string | undefined;
|
|
366
366
|
slug?: string | undefined;
|
|
367
367
|
ownerId?: string | undefined;
|
|
368
368
|
organizationId?: string | undefined;
|
|
369
369
|
}, {
|
|
370
370
|
description?: string | undefined;
|
|
371
|
-
name?: string | undefined;
|
|
372
371
|
createdAt?: unknown;
|
|
373
372
|
updatedAt?: unknown;
|
|
373
|
+
name?: string | undefined;
|
|
374
374
|
slug?: string | undefined;
|
|
375
375
|
ownerId?: string | undefined;
|
|
376
376
|
organizationId?: string | undefined;
|
|
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
404
404
|
title?: string | undefined;
|
|
405
405
|
createdAt?: unknown;
|
|
406
406
|
updatedAt?: unknown;
|
|
407
|
-
order?: number | undefined;
|
|
408
407
|
boardId?: string | undefined;
|
|
408
|
+
order?: number | undefined;
|
|
409
409
|
}, {
|
|
410
410
|
title?: string | undefined;
|
|
411
411
|
createdAt?: unknown;
|
|
412
412
|
updatedAt?: unknown;
|
|
413
|
-
order?: number | undefined;
|
|
414
413
|
boardId?: string | undefined;
|
|
414
|
+
order?: number | undefined;
|
|
415
415
|
}>;
|
|
416
416
|
}, Column>;
|
|
417
417
|
readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {
|
|
@@ -7,9 +7,9 @@ import { QueryClient } from '@tanstack/react-query';
|
|
|
7
7
|
|
|
8
8
|
declare const createBoardSchema: z.ZodObject<{
|
|
9
9
|
description: z.ZodOptional<z.ZodString>;
|
|
10
|
-
name: z.ZodString;
|
|
11
10
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
12
11
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
12
|
+
name: z.ZodString;
|
|
13
13
|
slug: z.ZodOptional<z.ZodString>;
|
|
14
14
|
ownerId: z.ZodOptional<z.ZodString>;
|
|
15
15
|
organizationId: z.ZodOptional<z.ZodString>;
|
|
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
|
|
|
28
28
|
title: z.ZodString;
|
|
29
29
|
createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
30
30
|
updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
|
|
31
|
-
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
32
31
|
boardId: z.ZodString;
|
|
32
|
+
order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
33
33
|
}, z.core.$strip>;
|
|
34
34
|
declare const updateColumnSchema: z.ZodObject<{
|
|
35
35
|
createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
|
|
@@ -360,17 +360,17 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
360
360
|
method: "PUT";
|
|
361
361
|
body: better_call.StandardSchemaV1<{
|
|
362
362
|
description?: string | undefined;
|
|
363
|
-
name?: string | undefined;
|
|
364
363
|
createdAt?: unknown;
|
|
365
364
|
updatedAt?: unknown;
|
|
365
|
+
name?: string | undefined;
|
|
366
366
|
slug?: string | undefined;
|
|
367
367
|
ownerId?: string | undefined;
|
|
368
368
|
organizationId?: string | undefined;
|
|
369
369
|
}, {
|
|
370
370
|
description?: string | undefined;
|
|
371
|
-
name?: string | undefined;
|
|
372
371
|
createdAt?: unknown;
|
|
373
372
|
updatedAt?: unknown;
|
|
373
|
+
name?: string | undefined;
|
|
374
374
|
slug?: string | undefined;
|
|
375
375
|
ownerId?: string | undefined;
|
|
376
376
|
organizationId?: string | undefined;
|
|
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
|
|
|
404
404
|
title?: string | undefined;
|
|
405
405
|
createdAt?: unknown;
|
|
406
406
|
updatedAt?: unknown;
|
|
407
|
-
order?: number | undefined;
|
|
408
407
|
boardId?: string | undefined;
|
|
408
|
+
order?: number | undefined;
|
|
409
409
|
}, {
|
|
410
410
|
title?: string | undefined;
|
|
411
411
|
createdAt?: unknown;
|
|
412
412
|
updatedAt?: unknown;
|
|
413
|
-
order?: number | undefined;
|
|
414
413
|
boardId?: string | undefined;
|
|
414
|
+
order?: number | undefined;
|
|
415
415
|
}>;
|
|
416
416
|
}, Column>;
|
|
417
417
|
readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {
|