@btst/plugin-form-builder 1.0.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.
Files changed (84) hide show
  1. package/dist/api/index.cjs +1 -0
  2. package/dist/api/index.d.cts +1 -1
  3. package/dist/api/index.d.mts +1 -1
  4. package/dist/api/index.d.ts +1 -1
  5. package/dist/api/index.mjs +1 -1
  6. package/dist/api/operations.cjs +64 -4
  7. package/dist/api/operations.mjs +64 -5
  8. package/dist/api/plugin.cjs +8 -0
  9. package/dist/api/plugin.mjs +8 -0
  10. package/dist/api/submissions-csv.cjs +69 -0
  11. package/dist/api/submissions-csv.mjs +64 -0
  12. package/dist/client/components/forms/form-renderer.cjs +31 -2
  13. package/dist/client/components/forms/form-renderer.mjs +31 -2
  14. package/dist/client/components/forms/media-file-field.cjs +48 -0
  15. package/dist/client/components/forms/media-file-field.mjs +46 -0
  16. package/dist/client/components/index.d.cts +4 -2
  17. package/dist/client/components/index.d.mts +4 -2
  18. package/dist/client/components/index.d.ts +4 -2
  19. package/dist/client/components/pages/ai-schema.cjs +120 -0
  20. package/dist/client/components/pages/ai-schema.mjs +118 -0
  21. package/dist/client/components/pages/form-builder-page.internal.cjs +40 -1
  22. package/dist/client/components/pages/form-builder-page.internal.mjs +41 -2
  23. package/dist/client/components/pages/form-list-page.internal.cjs +1 -0
  24. package/dist/client/components/pages/form-list-page.internal.mjs +1 -0
  25. package/dist/client/components/pages/submissions-page.internal.cjs +40 -0
  26. package/dist/client/components/pages/submissions-page.internal.mjs +41 -1
  27. package/dist/client/components/shared/pagination.cjs +19 -0
  28. package/dist/client/components/shared/pagination.mjs +20 -1
  29. package/dist/client/hooks/form-builder-hooks.cjs +4 -0
  30. package/dist/client/hooks/form-builder-hooks.mjs +4 -1
  31. package/dist/client/hooks/index.cjs +1 -0
  32. package/dist/client/hooks/index.d.cts +7 -2
  33. package/dist/client/hooks/index.d.mts +7 -2
  34. package/dist/client/hooks/index.d.ts +7 -2
  35. package/dist/client/hooks/index.mjs +1 -1
  36. package/dist/client/index.d.cts +42 -3
  37. package/dist/client/index.d.mts +42 -3
  38. package/dist/client/index.d.ts +42 -3
  39. package/dist/permissions.cjs +8 -0
  40. package/dist/permissions.d.cts +6 -0
  41. package/dist/permissions.d.mts +6 -0
  42. package/dist/permissions.d.ts +6 -0
  43. package/dist/permissions.mjs +8 -0
  44. package/dist/query-keys.cjs +7 -0
  45. package/dist/query-keys.d.cts +29 -1
  46. package/dist/query-keys.d.mts +29 -1
  47. package/dist/query-keys.d.ts +29 -1
  48. package/dist/query-keys.mjs +7 -0
  49. package/dist/shared/{plugin-form-builder.CSP5HR2O.d.ts → plugin-form-builder.65tLgfcd.d.ts} +46 -29
  50. package/dist/shared/{plugin-form-builder.Bbl9NufB.d.cts → plugin-form-builder.DXdOj9vM.d.cts} +46 -29
  51. package/dist/shared/{plugin-form-builder.lp98rFNl.d.mts → plugin-form-builder.PWs25HN1.d.mts} +46 -29
  52. package/dist/shared/{plugin-form-builder.DWrQ3lRX.d.ts → plugin-form-builder.sdjcoJcH.d.cts} +15 -1
  53. package/dist/shared/{plugin-form-builder.DWrQ3lRX.d.cts → plugin-form-builder.sdjcoJcH.d.mts} +15 -1
  54. package/dist/shared/{plugin-form-builder.DWrQ3lRX.d.mts → plugin-form-builder.sdjcoJcH.d.ts} +15 -1
  55. package/dist/ui/src/components/auto-form/config.cjs +2 -0
  56. package/dist/ui/src/components/auto-form/config.mjs +2 -0
  57. package/dist/ui/src/components/auto-form/fields/file.cjs +68 -0
  58. package/dist/ui/src/components/auto-form/fields/file.mjs +63 -0
  59. package/dist/ui/src/components/auto-form/fields/object.cjs +9 -2
  60. package/dist/ui/src/components/auto-form/fields/object.mjs +9 -2
  61. package/dist/ui/src/components/auto-form/helpers.cjs +9 -0
  62. package/dist/ui/src/components/auto-form/helpers.mjs +9 -0
  63. package/dist/ui/src/components/auto-form/index.cjs +29 -4
  64. package/dist/ui/src/components/auto-form/index.mjs +31 -6
  65. package/dist/ui/src/components/auto-form/stepped-auto-form.cjs +34 -29
  66. package/dist/ui/src/components/auto-form/stepped-auto-form.mjs +35 -30
  67. package/dist/ui/src/components/form-builder/canvas.cjs +9 -0
  68. package/dist/ui/src/components/form-builder/canvas.mjs +9 -0
  69. package/dist/ui/src/components/form-builder/components/index.cjs +12 -0
  70. package/dist/ui/src/components/form-builder/components/index.mjs +12 -1
  71. package/dist/ui/src/components/form-builder/condition-editor.cjs +73 -0
  72. package/dist/ui/src/components/form-builder/condition-editor.mjs +71 -0
  73. package/dist/ui/src/components/form-builder/edit-field-dialog.cjs +6 -2
  74. package/dist/ui/src/components/form-builder/edit-field-dialog.mjs +6 -2
  75. package/dist/ui/src/components/form-builder/index.cjs +6 -1
  76. package/dist/ui/src/components/form-builder/index.mjs +6 -1
  77. package/dist/ui/src/components/form-builder/schema-utils.cjs +2 -0
  78. package/dist/ui/src/components/form-builder/schema-utils.mjs +2 -0
  79. package/dist/ui/src/components/form.mjs +1 -1
  80. package/dist/ui/src/lib/form-visibility.cjs +20 -0
  81. package/dist/ui/src/lib/form-visibility.mjs +18 -0
  82. package/dist/ui/src/lib/schema-converter.cjs +101 -0
  83. package/dist/ui/src/lib/schema-converter.mjs +100 -1
  84. 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.DWrQ3lRX.cjs';
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.DWrQ3lRX.mjs';
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.DWrQ3lRX.js';
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
  }
@@ -238,6 +238,7 @@ function FormListPage() {
238
238
  /* @__PURE__ */ jsxRuntime.jsx(
239
239
  pagination.Pagination,
240
240
  {
241
+ page: "formList",
241
242
  total,
242
243
  showing: forms.length,
243
244
  hasMore,
@@ -236,6 +236,7 @@ function FormListPage() {
236
236
  /* @__PURE__ */ jsx(
237
237
  Pagination,
238
238
  {
239
+ page: "formList",
239
240
  total,
240
241
  showing: forms.length,
241
242
  hasMore,
@@ -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,