@btst/plugin-form-builder 1.0.0-beta.0 → 1.2.0-beta.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/api/index.cjs +1 -0
- package/dist/api/index.d.cts +1 -1
- package/dist/api/index.d.mts +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.mjs +1 -1
- package/dist/api/operations.cjs +64 -4
- package/dist/api/operations.mjs +64 -5
- package/dist/api/plugin.cjs +8 -0
- package/dist/api/plugin.mjs +8 -0
- package/dist/api/submissions-csv.cjs +69 -0
- package/dist/api/submissions-csv.mjs +64 -0
- package/dist/client/components/forms/form-renderer.cjs +31 -2
- package/dist/client/components/forms/form-renderer.mjs +31 -2
- package/dist/client/components/forms/media-file-field.cjs +48 -0
- package/dist/client/components/forms/media-file-field.mjs +46 -0
- package/dist/client/components/index.d.cts +4 -2
- package/dist/client/components/index.d.mts +4 -2
- package/dist/client/components/index.d.ts +4 -2
- package/dist/client/components/pages/ai-schema.cjs +120 -0
- package/dist/client/components/pages/ai-schema.mjs +118 -0
- package/dist/client/components/pages/form-builder-page.internal.cjs +40 -1
- package/dist/client/components/pages/form-builder-page.internal.mjs +41 -2
- package/dist/client/components/pages/form-list-page.internal.cjs +1 -0
- package/dist/client/components/pages/form-list-page.internal.mjs +1 -0
- package/dist/client/components/pages/submissions-page.internal.cjs +40 -0
- package/dist/client/components/pages/submissions-page.internal.mjs +41 -1
- package/dist/client/components/shared/pagination.cjs +19 -0
- package/dist/client/components/shared/pagination.mjs +20 -1
- package/dist/client/hooks/form-builder-hooks.cjs +4 -0
- package/dist/client/hooks/form-builder-hooks.mjs +4 -1
- package/dist/client/hooks/index.cjs +1 -0
- package/dist/client/hooks/index.d.cts +7 -2
- package/dist/client/hooks/index.d.mts +7 -2
- package/dist/client/hooks/index.d.ts +7 -2
- package/dist/client/hooks/index.mjs +1 -1
- package/dist/client/index.d.cts +42 -3
- package/dist/client/index.d.mts +42 -3
- package/dist/client/index.d.ts +42 -3
- package/dist/permissions.cjs +8 -0
- package/dist/permissions.d.cts +6 -0
- package/dist/permissions.d.mts +6 -0
- package/dist/permissions.d.ts +6 -0
- package/dist/permissions.mjs +8 -0
- package/dist/query-keys.cjs +7 -0
- package/dist/query-keys.d.cts +29 -1
- package/dist/query-keys.d.mts +29 -1
- package/dist/query-keys.d.ts +29 -1
- package/dist/query-keys.mjs +7 -0
- package/dist/shared/{plugin-form-builder.CSP5HR2O.d.ts → plugin-form-builder.65tLgfcd.d.ts} +46 -29
- package/dist/shared/{plugin-form-builder.Bbl9NufB.d.cts → plugin-form-builder.DXdOj9vM.d.cts} +46 -29
- package/dist/shared/{plugin-form-builder.lp98rFNl.d.mts → plugin-form-builder.PWs25HN1.d.mts} +46 -29
- package/dist/shared/{plugin-form-builder.DWrQ3lRX.d.ts → plugin-form-builder.sdjcoJcH.d.cts} +15 -1
- package/dist/shared/{plugin-form-builder.DWrQ3lRX.d.cts → plugin-form-builder.sdjcoJcH.d.mts} +15 -1
- package/dist/shared/{plugin-form-builder.DWrQ3lRX.d.mts → plugin-form-builder.sdjcoJcH.d.ts} +15 -1
- package/dist/ui/src/components/auto-form/config.cjs +2 -0
- package/dist/ui/src/components/auto-form/config.mjs +2 -0
- package/dist/ui/src/components/auto-form/fields/file.cjs +68 -0
- package/dist/ui/src/components/auto-form/fields/file.mjs +63 -0
- package/dist/ui/src/components/auto-form/fields/object.cjs +9 -2
- package/dist/ui/src/components/auto-form/fields/object.mjs +9 -2
- package/dist/ui/src/components/auto-form/helpers.cjs +9 -0
- package/dist/ui/src/components/auto-form/helpers.mjs +9 -0
- package/dist/ui/src/components/auto-form/index.cjs +29 -4
- package/dist/ui/src/components/auto-form/index.mjs +31 -6
- package/dist/ui/src/components/auto-form/stepped-auto-form.cjs +34 -29
- package/dist/ui/src/components/auto-form/stepped-auto-form.mjs +35 -30
- package/dist/ui/src/components/form-builder/canvas.cjs +9 -0
- package/dist/ui/src/components/form-builder/canvas.mjs +9 -0
- package/dist/ui/src/components/form-builder/components/index.cjs +12 -0
- package/dist/ui/src/components/form-builder/components/index.mjs +12 -1
- package/dist/ui/src/components/form-builder/condition-editor.cjs +73 -0
- package/dist/ui/src/components/form-builder/condition-editor.mjs +71 -0
- package/dist/ui/src/components/form-builder/edit-field-dialog.cjs +6 -2
- package/dist/ui/src/components/form-builder/edit-field-dialog.mjs +6 -2
- package/dist/ui/src/components/form-builder/index.cjs +6 -1
- package/dist/ui/src/components/form-builder/index.mjs +6 -1
- package/dist/ui/src/components/form-builder/schema-utils.cjs +2 -0
- package/dist/ui/src/components/form-builder/schema-utils.mjs +2 -0
- package/dist/ui/src/components/form.mjs +1 -1
- package/dist/ui/src/lib/form-visibility.cjs +20 -0
- package/dist/ui/src/lib/form-visibility.mjs +18 -0
- package/dist/ui/src/lib/schema-converter.cjs +101 -0
- package/dist/ui/src/lib/schema-converter.mjs +100 -1
- package/package.json +4 -4
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { useFormContext } from 'react-hook-form';
|
|
4
|
+
import { usePluginOverrides } from '@btst/stack/context';
|
|
5
|
+
import { FormItem, FormControl, FormMessage } from '../../../ui/src/components/form.mjs';
|
|
6
|
+
import AutoFormLabel from '../../../ui/src/components/auto-form/common/label.mjs';
|
|
7
|
+
import AutoFormTooltip from '../../../ui/src/components/auto-form/common/tooltip.mjs';
|
|
8
|
+
import { FORM_BUILDER_PLUGIN_ID } from '../../constants.mjs';
|
|
9
|
+
|
|
10
|
+
function MediaFileField({
|
|
11
|
+
field,
|
|
12
|
+
fieldProps,
|
|
13
|
+
fieldConfigItem,
|
|
14
|
+
label,
|
|
15
|
+
isRequired
|
|
16
|
+
}) {
|
|
17
|
+
const form = useFormContext();
|
|
18
|
+
const { imageInputField: ImageInput, imagePicker: ImagePicker } = usePluginOverrides(FORM_BUILDER_PLUGIN_ID);
|
|
19
|
+
const select = (url) => {
|
|
20
|
+
if (fieldProps.disabled || form.formState.isSubmitting) return;
|
|
21
|
+
field.onChange(url);
|
|
22
|
+
void form.trigger(field.name);
|
|
23
|
+
};
|
|
24
|
+
return /* @__PURE__ */ jsxs(FormItem, { children: [
|
|
25
|
+
/* @__PURE__ */ jsx(
|
|
26
|
+
AutoFormLabel,
|
|
27
|
+
{
|
|
28
|
+
label: fieldConfigItem.label || label,
|
|
29
|
+
isRequired
|
|
30
|
+
}
|
|
31
|
+
),
|
|
32
|
+
/* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(
|
|
33
|
+
"div",
|
|
34
|
+
{
|
|
35
|
+
role: "group",
|
|
36
|
+
"aria-label": fieldConfigItem.label || label,
|
|
37
|
+
inert: fieldProps.disabled || form.formState.isSubmitting,
|
|
38
|
+
children: ImageInput ? /* @__PURE__ */ jsx(ImageInput, { value: field.value ?? "", onChange: select }) : ImagePicker ? /* @__PURE__ */ jsx(ImagePicker, { onSelect: select }) : null
|
|
39
|
+
}
|
|
40
|
+
) }),
|
|
41
|
+
/* @__PURE__ */ jsx(AutoFormTooltip, { fieldConfigItem }),
|
|
42
|
+
/* @__PURE__ */ jsx(FormMessage, {})
|
|
43
|
+
] });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { MediaFileField };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { F as FormListPageComponent } from '../../shared/plugin-form-builder.
|
|
2
|
+
import { F as FormListPageComponent } from '../../shared/plugin-form-builder.sdjcoJcH.cjs';
|
|
3
3
|
import { AutoFormInputComponentProps } from '../index.cjs';
|
|
4
4
|
import * as react from 'react';
|
|
5
5
|
import { ComponentType } from 'react';
|
|
@@ -22,6 +22,8 @@ interface SubmissionsPageProps {
|
|
|
22
22
|
declare function SubmissionsPageComponent({ formId }: SubmissionsPageProps): react.JSX.Element;
|
|
23
23
|
|
|
24
24
|
interface FormRendererProps {
|
|
25
|
+
/** Upload a standalone file and return its URL string. */
|
|
26
|
+
onUpload?: (file: File) => Promise<string>;
|
|
25
27
|
/** Form slug to render */
|
|
26
28
|
slug: string;
|
|
27
29
|
/** Callback when form submission succeeds */
|
|
@@ -66,7 +68,7 @@ interface FormRendererProps {
|
|
|
66
68
|
* />
|
|
67
69
|
* ```
|
|
68
70
|
*/
|
|
69
|
-
declare function FormRenderer$1({ slug, onSuccess, onError, fieldComponents: propFieldComponents, successMessage: propSuccessMessage, submitButtonText, LoadingComponent, ErrorComponent, className, }: FormRendererProps): react.JSX.Element;
|
|
71
|
+
declare function FormRenderer$1({ slug, onUpload: propOnUpload, onSuccess, onError, fieldComponents: propFieldComponents, successMessage: propSuccessMessage, submitButtonText, LoadingComponent, ErrorComponent, className, }: FormRendererProps): react.JSX.Element;
|
|
70
72
|
|
|
71
73
|
declare function FormListSkeleton(): react.JSX.Element;
|
|
72
74
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { F as FormListPageComponent } from '../../shared/plugin-form-builder.
|
|
2
|
+
import { F as FormListPageComponent } from '../../shared/plugin-form-builder.sdjcoJcH.mjs';
|
|
3
3
|
import { AutoFormInputComponentProps } from '../index.mjs';
|
|
4
4
|
import * as react from 'react';
|
|
5
5
|
import { ComponentType } from 'react';
|
|
@@ -22,6 +22,8 @@ interface SubmissionsPageProps {
|
|
|
22
22
|
declare function SubmissionsPageComponent({ formId }: SubmissionsPageProps): react.JSX.Element;
|
|
23
23
|
|
|
24
24
|
interface FormRendererProps {
|
|
25
|
+
/** Upload a standalone file and return its URL string. */
|
|
26
|
+
onUpload?: (file: File) => Promise<string>;
|
|
25
27
|
/** Form slug to render */
|
|
26
28
|
slug: string;
|
|
27
29
|
/** Callback when form submission succeeds */
|
|
@@ -66,7 +68,7 @@ interface FormRendererProps {
|
|
|
66
68
|
* />
|
|
67
69
|
* ```
|
|
68
70
|
*/
|
|
69
|
-
declare function FormRenderer$1({ slug, onSuccess, onError, fieldComponents: propFieldComponents, successMessage: propSuccessMessage, submitButtonText, LoadingComponent, ErrorComponent, className, }: FormRendererProps): react.JSX.Element;
|
|
71
|
+
declare function FormRenderer$1({ slug, onUpload: propOnUpload, onSuccess, onError, fieldComponents: propFieldComponents, successMessage: propSuccessMessage, submitButtonText, LoadingComponent, ErrorComponent, className, }: FormRendererProps): react.JSX.Element;
|
|
70
72
|
|
|
71
73
|
declare function FormListSkeleton(): react.JSX.Element;
|
|
72
74
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { F as FormListPageComponent } from '../../shared/plugin-form-builder.
|
|
2
|
+
import { F as FormListPageComponent } from '../../shared/plugin-form-builder.sdjcoJcH.js';
|
|
3
3
|
import { AutoFormInputComponentProps } from '../index.js';
|
|
4
4
|
import * as react from 'react';
|
|
5
5
|
import { ComponentType } from 'react';
|
|
@@ -22,6 +22,8 @@ interface SubmissionsPageProps {
|
|
|
22
22
|
declare function SubmissionsPageComponent({ formId }: SubmissionsPageProps): react.JSX.Element;
|
|
23
23
|
|
|
24
24
|
interface FormRendererProps {
|
|
25
|
+
/** Upload a standalone file and return its URL string. */
|
|
26
|
+
onUpload?: (file: File) => Promise<string>;
|
|
25
27
|
/** Form slug to render */
|
|
26
28
|
slug: string;
|
|
27
29
|
/** Callback when form submission succeeds */
|
|
@@ -66,7 +68,7 @@ interface FormRendererProps {
|
|
|
66
68
|
* />
|
|
67
69
|
* ```
|
|
68
70
|
*/
|
|
69
|
-
declare function FormRenderer$1({ slug, onSuccess, onError, fieldComponents: propFieldComponents, successMessage: propSuccessMessage, submitButtonText, LoadingComponent, ErrorComponent, className, }: FormRendererProps): react.JSX.Element;
|
|
71
|
+
declare function FormRenderer$1({ slug, onUpload: propOnUpload, onSuccess, onError, fieldComponents: propFieldComponents, successMessage: propSuccessMessage, submitButtonText, LoadingComponent, ErrorComponent, className, }: FormRendererProps): react.JSX.Element;
|
|
70
72
|
|
|
71
73
|
declare function FormListSkeleton(): react.JSX.Element;
|
|
72
74
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const z = require('zod');
|
|
4
|
+
const schemaConverter = require('../../../ui/src/lib/schema-converter.cjs');
|
|
5
|
+
|
|
6
|
+
const condition = z.z.object({
|
|
7
|
+
field: z.z.string().min(1),
|
|
8
|
+
operator: z.z.enum(["equals", "notEquals", "in", "isTruthy"]),
|
|
9
|
+
value: z.z.unknown().optional()
|
|
10
|
+
}).refine(
|
|
11
|
+
(rule) => rule.operator !== "in" || Array.isArray(rule.value),
|
|
12
|
+
"The in operator needs an array"
|
|
13
|
+
);
|
|
14
|
+
const property = z.z.lazy(
|
|
15
|
+
() => z.z.object({
|
|
16
|
+
type: z.z.enum([
|
|
17
|
+
"string",
|
|
18
|
+
"number",
|
|
19
|
+
"integer",
|
|
20
|
+
"boolean",
|
|
21
|
+
"object",
|
|
22
|
+
"array"
|
|
23
|
+
]),
|
|
24
|
+
showIf: condition.optional(),
|
|
25
|
+
properties: z.z.record(z.z.string(), property).optional(),
|
|
26
|
+
items: property.optional(),
|
|
27
|
+
stepGroup: z.z.number().int().nonnegative().optional()
|
|
28
|
+
}).passthrough()
|
|
29
|
+
);
|
|
30
|
+
const schemaPatch = z.z.object({
|
|
31
|
+
type: z.z.literal("object"),
|
|
32
|
+
properties: z.z.record(z.z.string(), property),
|
|
33
|
+
required: z.z.array(z.z.string()).optional(),
|
|
34
|
+
steps: z.z.array(
|
|
35
|
+
z.z.object({
|
|
36
|
+
id: z.z.string().min(1),
|
|
37
|
+
title: z.z.string().min(1),
|
|
38
|
+
showIf: condition.optional()
|
|
39
|
+
})
|
|
40
|
+
).optional(),
|
|
41
|
+
stepGroupMap: z.z.record(z.z.string(), z.z.number().int().nonnegative()).optional()
|
|
42
|
+
}).passthrough();
|
|
43
|
+
function mergeProperty(current, patch) {
|
|
44
|
+
const next = { ...current, ...patch };
|
|
45
|
+
if (patch.properties)
|
|
46
|
+
next.properties = mergeProperties(current?.properties, patch.properties);
|
|
47
|
+
if (patch.items && !Array.isArray(patch.items))
|
|
48
|
+
next.items = mergeProperty(
|
|
49
|
+
Array.isArray(current?.items) ? void 0 : current?.items,
|
|
50
|
+
patch.items
|
|
51
|
+
);
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
function mergeProperties(current, patch) {
|
|
55
|
+
return {
|
|
56
|
+
...current,
|
|
57
|
+
...Object.fromEntries(
|
|
58
|
+
Object.entries(patch).map(([key, value]) => [
|
|
59
|
+
key,
|
|
60
|
+
mergeProperty(
|
|
61
|
+
Object.hasOwn(current ?? {}, key) ? current?.[key] : void 0,
|
|
62
|
+
value
|
|
63
|
+
)
|
|
64
|
+
])
|
|
65
|
+
)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function applyAISchema(current, input) {
|
|
69
|
+
const args = z.z.object({ schema: z.z.string().min(2).max(2e5) }).strict().parse(input);
|
|
70
|
+
const patch = schemaPatch.parse(JSON.parse(args.schema));
|
|
71
|
+
const properties = mergeProperties(current?.properties, patch.properties);
|
|
72
|
+
const next = { ...current, ...patch, properties };
|
|
73
|
+
const validate = (fields) => {
|
|
74
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
75
|
+
if (["__proto__", "prototype", "constructor"].includes(key))
|
|
76
|
+
throw new Error("Invalid field name");
|
|
77
|
+
if (field.showIf && !Object.hasOwn(fields, field.showIf.field))
|
|
78
|
+
throw new Error("Visibility condition references an unknown field");
|
|
79
|
+
if (field.properties) validate(field.properties);
|
|
80
|
+
if (field.items?.properties) validate(field.items.properties);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
validate(properties);
|
|
84
|
+
if (next.required?.some((key) => !Object.hasOwn(properties, key)))
|
|
85
|
+
throw new Error("Required references an unknown field");
|
|
86
|
+
if (next.steps?.some(
|
|
87
|
+
(step) => step.showIf && !Object.hasOwn(properties, step.showIf.field)
|
|
88
|
+
))
|
|
89
|
+
throw new Error("Step condition references an unknown field");
|
|
90
|
+
if (new Set(next.steps?.map((step) => step.id)).size !== (next.steps?.length ?? 0))
|
|
91
|
+
throw new Error("Step IDs must be unique");
|
|
92
|
+
const stepCount = Math.max(1, next.steps?.length ?? 0);
|
|
93
|
+
if (Object.keys(next.stepGroupMap ?? {}).some(
|
|
94
|
+
(field) => !Object.hasOwn(properties, field)
|
|
95
|
+
))
|
|
96
|
+
throw new Error("Step map references an unknown field");
|
|
97
|
+
if (next.steps?.length || next.stepGroupMap) {
|
|
98
|
+
const requestedSteps = new Map(Object.entries(patch.stepGroupMap ?? {}));
|
|
99
|
+
const previousSteps = new Map(Object.entries(current?.stepGroupMap ?? {}));
|
|
100
|
+
for (const [key, field] of Object.entries(properties)) {
|
|
101
|
+
const propertyStep = patch.properties[key]?.stepGroup;
|
|
102
|
+
const mapStep = requestedSteps.get(key);
|
|
103
|
+
if (propertyStep !== void 0 && mapStep !== void 0 && propertyStep !== mapStep)
|
|
104
|
+
throw new Error("Conflicting step assignments");
|
|
105
|
+
properties[key] = {
|
|
106
|
+
...field,
|
|
107
|
+
stepGroup: propertyStep ?? mapStep ?? current?.properties[key]?.stepGroup ?? previousSteps.get(key) ?? 0
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
next.stepGroupMap = void 0;
|
|
111
|
+
}
|
|
112
|
+
if (Object.values(properties).some(
|
|
113
|
+
({ stepGroup }) => stepGroup !== void 0 && (!Number.isInteger(stepGroup) || stepGroup < 0 || stepGroup >= stepCount)
|
|
114
|
+
))
|
|
115
|
+
throw new Error("Field references an unknown step");
|
|
116
|
+
schemaConverter.formSchemaToZod(next);
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
exports.applyAISchema = applyAISchema;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formSchemaToZod } from '../../../ui/src/lib/schema-converter.mjs';
|
|
3
|
+
|
|
4
|
+
const condition = z.object({
|
|
5
|
+
field: z.string().min(1),
|
|
6
|
+
operator: z.enum(["equals", "notEquals", "in", "isTruthy"]),
|
|
7
|
+
value: z.unknown().optional()
|
|
8
|
+
}).refine(
|
|
9
|
+
(rule) => rule.operator !== "in" || Array.isArray(rule.value),
|
|
10
|
+
"The in operator needs an array"
|
|
11
|
+
);
|
|
12
|
+
const property = z.lazy(
|
|
13
|
+
() => z.object({
|
|
14
|
+
type: z.enum([
|
|
15
|
+
"string",
|
|
16
|
+
"number",
|
|
17
|
+
"integer",
|
|
18
|
+
"boolean",
|
|
19
|
+
"object",
|
|
20
|
+
"array"
|
|
21
|
+
]),
|
|
22
|
+
showIf: condition.optional(),
|
|
23
|
+
properties: z.record(z.string(), property).optional(),
|
|
24
|
+
items: property.optional(),
|
|
25
|
+
stepGroup: z.number().int().nonnegative().optional()
|
|
26
|
+
}).passthrough()
|
|
27
|
+
);
|
|
28
|
+
const schemaPatch = z.object({
|
|
29
|
+
type: z.literal("object"),
|
|
30
|
+
properties: z.record(z.string(), property),
|
|
31
|
+
required: z.array(z.string()).optional(),
|
|
32
|
+
steps: z.array(
|
|
33
|
+
z.object({
|
|
34
|
+
id: z.string().min(1),
|
|
35
|
+
title: z.string().min(1),
|
|
36
|
+
showIf: condition.optional()
|
|
37
|
+
})
|
|
38
|
+
).optional(),
|
|
39
|
+
stepGroupMap: z.record(z.string(), z.number().int().nonnegative()).optional()
|
|
40
|
+
}).passthrough();
|
|
41
|
+
function mergeProperty(current, patch) {
|
|
42
|
+
const next = { ...current, ...patch };
|
|
43
|
+
if (patch.properties)
|
|
44
|
+
next.properties = mergeProperties(current?.properties, patch.properties);
|
|
45
|
+
if (patch.items && !Array.isArray(patch.items))
|
|
46
|
+
next.items = mergeProperty(
|
|
47
|
+
Array.isArray(current?.items) ? void 0 : current?.items,
|
|
48
|
+
patch.items
|
|
49
|
+
);
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
function mergeProperties(current, patch) {
|
|
53
|
+
return {
|
|
54
|
+
...current,
|
|
55
|
+
...Object.fromEntries(
|
|
56
|
+
Object.entries(patch).map(([key, value]) => [
|
|
57
|
+
key,
|
|
58
|
+
mergeProperty(
|
|
59
|
+
Object.hasOwn(current ?? {}, key) ? current?.[key] : void 0,
|
|
60
|
+
value
|
|
61
|
+
)
|
|
62
|
+
])
|
|
63
|
+
)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function applyAISchema(current, input) {
|
|
67
|
+
const args = z.object({ schema: z.string().min(2).max(2e5) }).strict().parse(input);
|
|
68
|
+
const patch = schemaPatch.parse(JSON.parse(args.schema));
|
|
69
|
+
const properties = mergeProperties(current?.properties, patch.properties);
|
|
70
|
+
const next = { ...current, ...patch, properties };
|
|
71
|
+
const validate = (fields) => {
|
|
72
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
73
|
+
if (["__proto__", "prototype", "constructor"].includes(key))
|
|
74
|
+
throw new Error("Invalid field name");
|
|
75
|
+
if (field.showIf && !Object.hasOwn(fields, field.showIf.field))
|
|
76
|
+
throw new Error("Visibility condition references an unknown field");
|
|
77
|
+
if (field.properties) validate(field.properties);
|
|
78
|
+
if (field.items?.properties) validate(field.items.properties);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
validate(properties);
|
|
82
|
+
if (next.required?.some((key) => !Object.hasOwn(properties, key)))
|
|
83
|
+
throw new Error("Required references an unknown field");
|
|
84
|
+
if (next.steps?.some(
|
|
85
|
+
(step) => step.showIf && !Object.hasOwn(properties, step.showIf.field)
|
|
86
|
+
))
|
|
87
|
+
throw new Error("Step condition references an unknown field");
|
|
88
|
+
if (new Set(next.steps?.map((step) => step.id)).size !== (next.steps?.length ?? 0))
|
|
89
|
+
throw new Error("Step IDs must be unique");
|
|
90
|
+
const stepCount = Math.max(1, next.steps?.length ?? 0);
|
|
91
|
+
if (Object.keys(next.stepGroupMap ?? {}).some(
|
|
92
|
+
(field) => !Object.hasOwn(properties, field)
|
|
93
|
+
))
|
|
94
|
+
throw new Error("Step map references an unknown field");
|
|
95
|
+
if (next.steps?.length || next.stepGroupMap) {
|
|
96
|
+
const requestedSteps = new Map(Object.entries(patch.stepGroupMap ?? {}));
|
|
97
|
+
const previousSteps = new Map(Object.entries(current?.stepGroupMap ?? {}));
|
|
98
|
+
for (const [key, field] of Object.entries(properties)) {
|
|
99
|
+
const propertyStep = patch.properties[key]?.stepGroup;
|
|
100
|
+
const mapStep = requestedSteps.get(key);
|
|
101
|
+
if (propertyStep !== void 0 && mapStep !== void 0 && propertyStep !== mapStep)
|
|
102
|
+
throw new Error("Conflicting step assignments");
|
|
103
|
+
properties[key] = {
|
|
104
|
+
...field,
|
|
105
|
+
stepGroup: propertyStep ?? mapStep ?? current?.properties[key]?.stepGroup ?? previousSteps.get(key) ?? 0
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
next.stepGroupMap = void 0;
|
|
109
|
+
}
|
|
110
|
+
if (Object.values(properties).some(
|
|
111
|
+
({ stepGroup }) => stepGroup !== void 0 && (!Number.isInteger(stepGroup) || stepGroup < 0 || stepGroup >= stepCount)
|
|
112
|
+
))
|
|
113
|
+
throw new Error("Field references an unknown step");
|
|
114
|
+
formSchemaToZod(next);
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { applyAISchema };
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const jsxRuntime = require('react/jsx-runtime');
|
|
5
5
|
const React = require('react');
|
|
6
|
+
const pageContext = require('@btst/stack/page-context');
|
|
7
|
+
const aiSchema = require('./ai-schema.cjs');
|
|
6
8
|
const context = require('@btst/stack/context');
|
|
7
9
|
const button = require('../../../ui/src/components/button.cjs');
|
|
8
10
|
const input = require('../../../ui/src/components/input.cjs');
|
|
@@ -60,12 +62,16 @@ function FormBuilderPageContent({
|
|
|
60
62
|
return void 0;
|
|
61
63
|
});
|
|
62
64
|
const [autoSlug, setAutoSlug] = React.useState(!id);
|
|
65
|
+
const [schemaVersion, setSchemaVersion] = React.useState(0);
|
|
66
|
+
const schemaRef = React.useRef(schema);
|
|
67
|
+
schemaRef.current = schema;
|
|
63
68
|
React.useEffect(() => {
|
|
64
69
|
if (autoSlug && name) {
|
|
65
70
|
setSlug(utils.slugify(name));
|
|
66
71
|
}
|
|
67
72
|
}, [name, autoSlug]);
|
|
68
73
|
const handleSchemaChange = React.useCallback((newSchema) => {
|
|
74
|
+
schemaRef.current = newSchema;
|
|
69
75
|
setSchema(newSchema);
|
|
70
76
|
}, []);
|
|
71
77
|
const resourceForm = formBuilderHooks.useFormBuilderForm({
|
|
@@ -122,6 +128,38 @@ function FormBuilderPageContent({
|
|
|
122
128
|
});
|
|
123
129
|
};
|
|
124
130
|
const isSaving = resourceForm.isSubmitting;
|
|
131
|
+
pageContext.useRegisterPageAIContext({
|
|
132
|
+
routeName: "form-builder-editor",
|
|
133
|
+
pageDescription: JSON.stringify({
|
|
134
|
+
page: "Visual form draft. Add/edit only requested fields, steps and showIf rules. Do not discard unmentioned fields. The user saves the form.",
|
|
135
|
+
name,
|
|
136
|
+
schema
|
|
137
|
+
}).slice(0, 16e3),
|
|
138
|
+
clientTools: {
|
|
139
|
+
updateFormSchema: async (input) => {
|
|
140
|
+
if (isSaving)
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
message: "Wait for the form save to finish."
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
const next = aiSchema.applyAISchema(schemaRef.current, input);
|
|
147
|
+
schemaRef.current = next;
|
|
148
|
+
setSchema(next);
|
|
149
|
+
setSchemaVersion((value) => value + 1);
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
message: "Form draft updated. Review its fields and conditions before saving."
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
message: "Invalid form schema or visibility rules."
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
125
163
|
const fieldError = (field) => {
|
|
126
164
|
const error = resourceForm.fieldErrors[field];
|
|
127
165
|
if (!error) return void 0;
|
|
@@ -209,7 +247,8 @@ function FormBuilderPageContent({
|
|
|
209
247
|
value: schema,
|
|
210
248
|
onChange: handleSchemaChange,
|
|
211
249
|
className: "flex-1"
|
|
212
|
-
}
|
|
250
|
+
},
|
|
251
|
+
schemaVersion
|
|
213
252
|
)
|
|
214
253
|
] });
|
|
215
254
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useRegisterPageAIContext } from '@btst/stack/page-context';
|
|
5
|
+
import { applyAISchema } from './ai-schema.mjs';
|
|
4
6
|
import { useTranslate, useNotify, usePluginOverrides, usePluginSiteNavigation, PermissionAccess } from '@btst/stack/context';
|
|
5
7
|
import { Button } from '../../../ui/src/components/button.mjs';
|
|
6
8
|
import { Input } from '../../../ui/src/components/input.mjs';
|
|
@@ -58,12 +60,16 @@ function FormBuilderPageContent({
|
|
|
58
60
|
return void 0;
|
|
59
61
|
});
|
|
60
62
|
const [autoSlug, setAutoSlug] = useState(!id);
|
|
63
|
+
const [schemaVersion, setSchemaVersion] = useState(0);
|
|
64
|
+
const schemaRef = useRef(schema);
|
|
65
|
+
schemaRef.current = schema;
|
|
61
66
|
useEffect(() => {
|
|
62
67
|
if (autoSlug && name) {
|
|
63
68
|
setSlug(slugify(name));
|
|
64
69
|
}
|
|
65
70
|
}, [name, autoSlug]);
|
|
66
71
|
const handleSchemaChange = useCallback((newSchema) => {
|
|
72
|
+
schemaRef.current = newSchema;
|
|
67
73
|
setSchema(newSchema);
|
|
68
74
|
}, []);
|
|
69
75
|
const resourceForm = useFormBuilderForm({
|
|
@@ -120,6 +126,38 @@ function FormBuilderPageContent({
|
|
|
120
126
|
});
|
|
121
127
|
};
|
|
122
128
|
const isSaving = resourceForm.isSubmitting;
|
|
129
|
+
useRegisterPageAIContext({
|
|
130
|
+
routeName: "form-builder-editor",
|
|
131
|
+
pageDescription: JSON.stringify({
|
|
132
|
+
page: "Visual form draft. Add/edit only requested fields, steps and showIf rules. Do not discard unmentioned fields. The user saves the form.",
|
|
133
|
+
name,
|
|
134
|
+
schema
|
|
135
|
+
}).slice(0, 16e3),
|
|
136
|
+
clientTools: {
|
|
137
|
+
updateFormSchema: async (input) => {
|
|
138
|
+
if (isSaving)
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
message: "Wait for the form save to finish."
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
const next = applyAISchema(schemaRef.current, input);
|
|
145
|
+
schemaRef.current = next;
|
|
146
|
+
setSchema(next);
|
|
147
|
+
setSchemaVersion((value) => value + 1);
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
message: "Form draft updated. Review its fields and conditions before saving."
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
message: "Invalid form schema or visibility rules."
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
123
161
|
const fieldError = (field) => {
|
|
124
162
|
const error = resourceForm.fieldErrors[field];
|
|
125
163
|
if (!error) return void 0;
|
|
@@ -207,7 +245,8 @@ function FormBuilderPageContent({
|
|
|
207
245
|
value: schema,
|
|
208
246
|
onChange: handleSchemaChange,
|
|
209
247
|
className: "flex-1"
|
|
210
|
-
}
|
|
248
|
+
},
|
|
249
|
+
schemaVersion
|
|
211
250
|
)
|
|
212
251
|
] });
|
|
213
252
|
}
|
|
@@ -26,6 +26,7 @@ function SubmissionsPage({ formId }) {
|
|
|
26
26
|
constants.FORM_BUILDER_PLUGIN_ID
|
|
27
27
|
);
|
|
28
28
|
const { form, submissions, total, hasMore, isLoadingMore, loadMore } = formBuilderHooks.useSuspenseSubmissions(formId);
|
|
29
|
+
const exportMutation = formBuilderHooks.useExportSubmissions();
|
|
29
30
|
const deleteMutation = formBuilderHooks.useDeleteSubmission(formId);
|
|
30
31
|
const [deleteId, setDeleteId] = React.useState(null);
|
|
31
32
|
const [viewSubmissionId, setViewSubmissionId] = React.useState(null);
|
|
@@ -64,6 +65,44 @@ function SubmissionsPage({ formId }) {
|
|
|
64
65
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: localization?.FORM_BUILDER_SUBMISSIONS_SUBTITLE ?? t("formBuilder.submissions.subtitle", "View form submissions") })
|
|
65
66
|
] })
|
|
66
67
|
] }),
|
|
68
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
69
|
+
context.PermissionAccess,
|
|
70
|
+
{
|
|
71
|
+
permission: permissions.formBuilderPermissions.submission.export({
|
|
72
|
+
formId,
|
|
73
|
+
formExists: true,
|
|
74
|
+
...form?.createdBy ? { ownerId: form.createdBy } : {}
|
|
75
|
+
}),
|
|
76
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
77
|
+
button.Button,
|
|
78
|
+
{
|
|
79
|
+
variant: "outline",
|
|
80
|
+
disabled: exportMutation.isPending,
|
|
81
|
+
onClick: async () => {
|
|
82
|
+
try {
|
|
83
|
+
const { csv, filename } = await exportMutation.mutateAsync(formId);
|
|
84
|
+
const url = URL.createObjectURL(
|
|
85
|
+
new Blob(["\uFEFF", csv], { type: "text/csv;charset=utf-8" })
|
|
86
|
+
);
|
|
87
|
+
const anchor = document.createElement("a");
|
|
88
|
+
anchor.href = url;
|
|
89
|
+
anchor.download = filename;
|
|
90
|
+
anchor.click();
|
|
91
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
notify.error(
|
|
94
|
+
error instanceof Error && "code" in error && error.code === "FORM_EXPORT_TOO_LARGE" ? error.message : t(
|
|
95
|
+
"formBuilder.submissions.exportFailed",
|
|
96
|
+
"Export failed. Please try again."
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
children: exportMutation.isPending ? t("formBuilder.submissions.exporting", "Exporting\u2026") : t("formBuilder.submissions.exportCsv", "Export CSV")
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
),
|
|
67
106
|
submissions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
68
107
|
emptyState.EmptyState,
|
|
69
108
|
{
|
|
@@ -161,6 +200,7 @@ function SubmissionsPage({ formId }) {
|
|
|
161
200
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
162
201
|
pagination.Pagination,
|
|
163
202
|
{
|
|
203
|
+
page: "submissions",
|
|
164
204
|
total,
|
|
165
205
|
showing: submissions.length,
|
|
166
206
|
hasMore,
|
|
@@ -13,7 +13,7 @@ import { Pagination } from '../shared/pagination.mjs';
|
|
|
13
13
|
import { formBuilderPermissions } from '../../../permissions.mjs';
|
|
14
14
|
import { FORM_BUILDER_PLUGIN_ID } from '../../constants.mjs';
|
|
15
15
|
import { cn } from '../../../utils.mjs';
|
|
16
|
-
import { useSuspenseSubmissions, useDeleteSubmission, useSubmission } from '../../hooks/form-builder-hooks.mjs';
|
|
16
|
+
import { useSuspenseSubmissions, useExportSubmissions, useDeleteSubmission, useSubmission } from '../../hooks/form-builder-hooks.mjs';
|
|
17
17
|
|
|
18
18
|
function SubmissionsPage({ formId }) {
|
|
19
19
|
const t = useTranslate();
|
|
@@ -24,6 +24,7 @@ function SubmissionsPage({ formId }) {
|
|
|
24
24
|
FORM_BUILDER_PLUGIN_ID
|
|
25
25
|
);
|
|
26
26
|
const { form, submissions, total, hasMore, isLoadingMore, loadMore } = useSuspenseSubmissions(formId);
|
|
27
|
+
const exportMutation = useExportSubmissions();
|
|
27
28
|
const deleteMutation = useDeleteSubmission(formId);
|
|
28
29
|
const [deleteId, setDeleteId] = useState(null);
|
|
29
30
|
const [viewSubmissionId, setViewSubmissionId] = useState(null);
|
|
@@ -62,6 +63,44 @@ function SubmissionsPage({ formId }) {
|
|
|
62
63
|
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: localization?.FORM_BUILDER_SUBMISSIONS_SUBTITLE ?? t("formBuilder.submissions.subtitle", "View form submissions") })
|
|
63
64
|
] })
|
|
64
65
|
] }),
|
|
66
|
+
/* @__PURE__ */ jsx(
|
|
67
|
+
PermissionAccess,
|
|
68
|
+
{
|
|
69
|
+
permission: formBuilderPermissions.submission.export({
|
|
70
|
+
formId,
|
|
71
|
+
formExists: true,
|
|
72
|
+
...form?.createdBy ? { ownerId: form.createdBy } : {}
|
|
73
|
+
}),
|
|
74
|
+
children: /* @__PURE__ */ jsx(
|
|
75
|
+
Button,
|
|
76
|
+
{
|
|
77
|
+
variant: "outline",
|
|
78
|
+
disabled: exportMutation.isPending,
|
|
79
|
+
onClick: async () => {
|
|
80
|
+
try {
|
|
81
|
+
const { csv, filename } = await exportMutation.mutateAsync(formId);
|
|
82
|
+
const url = URL.createObjectURL(
|
|
83
|
+
new Blob(["\uFEFF", csv], { type: "text/csv;charset=utf-8" })
|
|
84
|
+
);
|
|
85
|
+
const anchor = document.createElement("a");
|
|
86
|
+
anchor.href = url;
|
|
87
|
+
anchor.download = filename;
|
|
88
|
+
anchor.click();
|
|
89
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
notify.error(
|
|
92
|
+
error instanceof Error && "code" in error && error.code === "FORM_EXPORT_TOO_LARGE" ? error.message : t(
|
|
93
|
+
"formBuilder.submissions.exportFailed",
|
|
94
|
+
"Export failed. Please try again."
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
children: exportMutation.isPending ? t("formBuilder.submissions.exporting", "Exporting\u2026") : t("formBuilder.submissions.exportCsv", "Export CSV")
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
),
|
|
65
104
|
submissions.length === 0 ? /* @__PURE__ */ jsx(
|
|
66
105
|
EmptyState,
|
|
67
106
|
{
|
|
@@ -159,6 +198,7 @@ function SubmissionsPage({ formId }) {
|
|
|
159
198
|
/* @__PURE__ */ jsx(
|
|
160
199
|
Pagination,
|
|
161
200
|
{
|
|
201
|
+
page: "submissions",
|
|
162
202
|
total,
|
|
163
203
|
showing: submissions.length,
|
|
164
204
|
hasMore,
|