@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.
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
@@ -11,6 +11,7 @@ const queryKeyDefs = require('./query-key-defs.cjs');
11
11
 
12
12
  exports.formBuilderBackendPlugin = plugin.formBuilderBackendPlugin;
13
13
  exports.FORM_BUILDER_LIFECYCLE_HOOK_MIGRATIONS = lifecycleMigrations.FORM_BUILDER_LIFECYCLE_HOOK_MIGRATIONS;
14
+ exports.ExportSubmissionsOperationInputSchema = operations.ExportSubmissionsOperationInputSchema;
14
15
  exports.FormBuilderOperationError = operations.FormBuilderOperationError;
15
16
  exports.createFormBuilderOperations = operations.createFormBuilderOperations;
16
17
  exports.formBuilderPermissions = permissions.formBuilderPermissions;
@@ -1,4 +1,4 @@
1
- export { F as FORM_QUERY_KEYS, a as FormBuilderApiRouter, b as FormBuilderOperationError, c as FormBuilderOperations, d as FormBuilderRouteKey, e as FormListOperationResult, S as SubmissionDetailOperationResult, f as SubmissionListOperationResult, g as SubmitOperationResult, h as createFormBuilderOperations, i as formBuilderBackendPlugin } from '../shared/plugin-form-builder.Bbl9NufB.cjs';
1
+ export { E as ExportSubmissionsOperationInputSchema, F as FORM_QUERY_KEYS, a as FormBuilderApiRouter, b as FormBuilderOperationError, c as FormBuilderOperations, d as FormBuilderRouteKey, e as FormListOperationResult, S as SubmissionDetailOperationResult, f as SubmissionListOperationResult, g as SubmitOperationResult, h as createFormBuilderOperations, i as formBuilderBackendPlugin } from '../shared/plugin-form-builder.DXdOj9vM.cjs';
2
2
  export { formBuilderPermissions } from '../permissions.cjs';
3
3
  import { d as SerializedFormSubmissionSummary } from '../shared/plugin-form-builder.Cmk083dA.cjs';
4
4
  import { SerializedForm, SerializedFormSubmissionWithData, Form, FormSubmission, SerializedFormSubmission, FormSubmissionWithForm } from '../types.cjs';
@@ -1,4 +1,4 @@
1
- export { F as FORM_QUERY_KEYS, a as FormBuilderApiRouter, b as FormBuilderOperationError, c as FormBuilderOperations, d as FormBuilderRouteKey, e as FormListOperationResult, S as SubmissionDetailOperationResult, f as SubmissionListOperationResult, g as SubmitOperationResult, h as createFormBuilderOperations, i as formBuilderBackendPlugin } from '../shared/plugin-form-builder.lp98rFNl.mjs';
1
+ export { E as ExportSubmissionsOperationInputSchema, F as FORM_QUERY_KEYS, a as FormBuilderApiRouter, b as FormBuilderOperationError, c as FormBuilderOperations, d as FormBuilderRouteKey, e as FormListOperationResult, S as SubmissionDetailOperationResult, f as SubmissionListOperationResult, g as SubmitOperationResult, h as createFormBuilderOperations, i as formBuilderBackendPlugin } from '../shared/plugin-form-builder.PWs25HN1.mjs';
2
2
  export { formBuilderPermissions } from '../permissions.mjs';
3
3
  import { d as SerializedFormSubmissionSummary } from '../shared/plugin-form-builder.BZYGyFgT.mjs';
4
4
  import { SerializedForm, SerializedFormSubmissionWithData, Form, FormSubmission, SerializedFormSubmission, FormSubmissionWithForm } from '../types.mjs';
@@ -1,4 +1,4 @@
1
- export { F as FORM_QUERY_KEYS, a as FormBuilderApiRouter, b as FormBuilderOperationError, c as FormBuilderOperations, d as FormBuilderRouteKey, e as FormListOperationResult, S as SubmissionDetailOperationResult, f as SubmissionListOperationResult, g as SubmitOperationResult, h as createFormBuilderOperations, i as formBuilderBackendPlugin } from '../shared/plugin-form-builder.CSP5HR2O.js';
1
+ export { E as ExportSubmissionsOperationInputSchema, F as FORM_QUERY_KEYS, a as FormBuilderApiRouter, b as FormBuilderOperationError, c as FormBuilderOperations, d as FormBuilderRouteKey, e as FormListOperationResult, S as SubmissionDetailOperationResult, f as SubmissionListOperationResult, g as SubmitOperationResult, h as createFormBuilderOperations, i as formBuilderBackendPlugin } from '../shared/plugin-form-builder.65tLgfcd.js';
2
2
  export { formBuilderPermissions } from '../permissions.js';
3
3
  import { d as SerializedFormSubmissionSummary } from '../shared/plugin-form-builder.D258Qpb8.js';
4
4
  import { SerializedForm, SerializedFormSubmissionWithData, Form, FormSubmission, SerializedFormSubmission, FormSubmissionWithForm } from '../types.js';
@@ -1,6 +1,6 @@
1
1
  export { formBuilderBackendPlugin } from './plugin.mjs';
2
2
  export { FORM_BUILDER_LIFECYCLE_HOOK_MIGRATIONS } from './lifecycle-migrations.mjs';
3
- export { FormBuilderOperationError, createFormBuilderOperations } from './operations.mjs';
3
+ export { ExportSubmissionsOperationInputSchema, FormBuilderOperationError, createFormBuilderOperations } from './operations.mjs';
4
4
  export { formBuilderPermissions } from '../permissions.mjs';
5
5
  export { getAllForms, getFormById, getFormBySlug, getFormSubmissions, serializeForm, serializeFormSubmission, serializeFormSubmissionSummary, serializeFormSubmissionWithData } from './getters.mjs';
6
6
  export { FORM_QUERY_KEYS } from './query-key-defs.mjs';
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const submissionsCsv = require('./submissions-csv.cjs');
3
4
  const api = require('@btst/stack/plugins/api');
4
5
  const schemaConverter = require('../ui/src/lib/schema-converter.cjs');
5
6
  const z = require('zod');
@@ -30,6 +31,9 @@ const ListSubmissionsOperationInputSchema = z.z.object({
30
31
  formId: z.z.string(),
31
32
  query: schemas.listSubmissionsQuerySchema
32
33
  });
34
+ const ExportSubmissionsOperationInputSchema = z.z.object({
35
+ formId: z.z.string()
36
+ });
33
37
  const GetSubmissionOperationInputSchema = z.z.object({
34
38
  formId: z.z.string(),
35
39
  submissionId: z.z.string()
@@ -749,6 +753,7 @@ function createFormBuilderOperations(adapter, hooks) {
749
753
  "FORM_NOT_ACTIVE"
750
754
  );
751
755
  }
756
+ let validatedData;
752
757
  try {
753
758
  const jsonSchema = JSON.parse(form.schema);
754
759
  const validation = schemaConverter.formSchemaToZod(jsonSchema).safeParse(
@@ -762,6 +767,7 @@ function createFormBuilderOperations(adapter, hooks) {
762
767
  validation.error.issues
763
768
  );
764
769
  }
770
+ validatedData = validation.data;
765
771
  } catch (error) {
766
772
  if (error instanceof FormBuilderOperationError) throw error;
767
773
  throw new FormBuilderOperationError(
@@ -775,9 +781,7 @@ function createFormBuilderOperations(adapter, hooks) {
775
781
  form.slug,
776
782
  form.id
777
783
  );
778
- const submittedData = JSON.parse(
779
- JSON.stringify(context.input.data)
780
- );
784
+ const submittedData = JSON.parse(JSON.stringify(validatedData));
781
785
  return adapter.transaction(async (tx) => {
782
786
  const current = await findFormBySlug(tx, context.input.slug);
783
787
  if (!sameFormSnapshot(current, authorized)) throw staleFormError();
@@ -796,7 +800,17 @@ function createFormBuilderOperations(adapter, hooks) {
796
800
  400,
797
801
  "SUBMISSION_REJECTED"
798
802
  );
799
- const finalData = hookResult ?? submittedData;
803
+ const finalValidation = schemaConverter.formSchemaToZod(
804
+ JSON.parse(form.schema)
805
+ ).safeParse(hookResult ?? submittedData);
806
+ if (!finalValidation.success)
807
+ throw new FormBuilderOperationError(
808
+ 400,
809
+ "Validation failed",
810
+ "SUBMISSION_VALIDATION_FAILED",
811
+ finalValidation.error.issues
812
+ );
813
+ const finalData = finalValidation.data;
800
814
  const claimed = await findFormById(tx, authorized.id);
801
815
  if (!sameFormSnapshot(claimed, { ...authorized, updatedAt: claimedAt }) || claimed?.status !== "active") {
802
816
  throw staleFormError();
@@ -856,6 +870,50 @@ function createFormBuilderOperations(adapter, hooks) {
856
870
  await notifyError(hooks, error, "submit", lifecycleContext);
857
871
  }
858
872
  });
873
+ const exportSubmissions = api.defineOperation({
874
+ input: ExportSubmissionsOperationInputSchema,
875
+ permission: permissions.formBuilderPermissions.submission.export,
876
+ facts: async ({ input }) => {
877
+ const form = await findFormById(adapter, input.formId);
878
+ formSnapshots.set(input, form ? snapshotForm(form) : null);
879
+ return {
880
+ formId: input.formId,
881
+ formExists: form !== null,
882
+ ...form?.createdBy ? { ownerId: form.createdBy } : {}
883
+ };
884
+ },
885
+ execute: async ({ input }) => adapter.transaction(async (tx) => {
886
+ const expected = formSnapshots.get(input) ?? null;
887
+ const form = await findFormById(tx, input.formId);
888
+ if (!sameFormSnapshot(form, expected)) throw staleFormError();
889
+ if (!form) throw formNotFoundError();
890
+ const items = [];
891
+ let sourceCharacters = 0;
892
+ for (; ; ) {
893
+ const page = await getters.getFormSubmissions(tx, input.formId, {
894
+ limit: 100,
895
+ offset: items.length
896
+ });
897
+ if (page.total > submissionsCsv.CSV_EXPORT_MAX_ROWS) throw submissionsCsv.csvExportLimitError();
898
+ for (const item of page.items) {
899
+ sourceCharacters += item.data.length;
900
+ if (sourceCharacters > submissionsCsv.CSV_EXPORT_MAX_BYTES)
901
+ throw submissionsCsv.csvExportLimitError();
902
+ items.push(item);
903
+ }
904
+ if (!page.items.length || items.length >= page.total) break;
905
+ }
906
+ const current = await findFormById(tx, input.formId);
907
+ if (!sameFormSnapshot(current, expected)) throw staleFormError();
908
+ return {
909
+ csv: submissionsCsv.submissionsToCsv(
910
+ items,
911
+ Object.keys(JSON.parse(form.schema).properties ?? {})
912
+ ),
913
+ filename: `${form.slug}-submissions.csv`
914
+ };
915
+ })
916
+ });
859
917
  const listSubmissions = api.defineOperation({
860
918
  input: ListSubmissionsOperationInputSchema,
861
919
  permission: permissions.formBuilderPermissions.submission.read,
@@ -1067,6 +1125,7 @@ function createFormBuilderOperations(adapter, hooks) {
1067
1125
  onError: ({ error, ...context }) => notifyError(hooks, error, "deleteSubmission", hookContext(context))
1068
1126
  });
1069
1127
  return {
1128
+ exportSubmissions,
1070
1129
  listForms,
1071
1130
  getFormBySlug,
1072
1131
  getFormById,
@@ -1082,6 +1141,7 @@ function createFormBuilderOperations(adapter, hooks) {
1082
1141
  }
1083
1142
 
1084
1143
  exports.DeleteFormOperationInputSchema = DeleteFormOperationInputSchema;
1144
+ exports.ExportSubmissionsOperationInputSchema = ExportSubmissionsOperationInputSchema;
1085
1145
  exports.FormBuilderOperationError = FormBuilderOperationError;
1086
1146
  exports.GetFormByIdOperationInputSchema = GetFormByIdOperationInputSchema;
1087
1147
  exports.GetFormBySlugOperationInputSchema = GetFormBySlugOperationInputSchema;
@@ -1,3 +1,4 @@
1
+ import { CSV_EXPORT_MAX_ROWS, csvExportLimitError, CSV_EXPORT_MAX_BYTES, submissionsToCsv } from './submissions-csv.mjs';
1
2
  import { OperationHttpError, defineOperation } from '@btst/stack/plugins/api';
2
3
  import { formSchemaToZod } from '../ui/src/lib/schema-converter.mjs';
3
4
  import { z } from 'zod';
@@ -28,6 +29,9 @@ const ListSubmissionsOperationInputSchema = z.object({
28
29
  formId: z.string(),
29
30
  query: listSubmissionsQuerySchema
30
31
  });
32
+ const ExportSubmissionsOperationInputSchema = z.object({
33
+ formId: z.string()
34
+ });
31
35
  const GetSubmissionOperationInputSchema = z.object({
32
36
  formId: z.string(),
33
37
  submissionId: z.string()
@@ -747,6 +751,7 @@ function createFormBuilderOperations(adapter, hooks) {
747
751
  "FORM_NOT_ACTIVE"
748
752
  );
749
753
  }
754
+ let validatedData;
750
755
  try {
751
756
  const jsonSchema = JSON.parse(form.schema);
752
757
  const validation = formSchemaToZod(jsonSchema).safeParse(
@@ -760,6 +765,7 @@ function createFormBuilderOperations(adapter, hooks) {
760
765
  validation.error.issues
761
766
  );
762
767
  }
768
+ validatedData = validation.data;
763
769
  } catch (error) {
764
770
  if (error instanceof FormBuilderOperationError) throw error;
765
771
  throw new FormBuilderOperationError(
@@ -773,9 +779,7 @@ function createFormBuilderOperations(adapter, hooks) {
773
779
  form.slug,
774
780
  form.id
775
781
  );
776
- const submittedData = JSON.parse(
777
- JSON.stringify(context.input.data)
778
- );
782
+ const submittedData = JSON.parse(JSON.stringify(validatedData));
779
783
  return adapter.transaction(async (tx) => {
780
784
  const current = await findFormBySlug(tx, context.input.slug);
781
785
  if (!sameFormSnapshot(current, authorized)) throw staleFormError();
@@ -794,7 +798,17 @@ function createFormBuilderOperations(adapter, hooks) {
794
798
  400,
795
799
  "SUBMISSION_REJECTED"
796
800
  );
797
- const finalData = hookResult ?? submittedData;
801
+ const finalValidation = formSchemaToZod(
802
+ JSON.parse(form.schema)
803
+ ).safeParse(hookResult ?? submittedData);
804
+ if (!finalValidation.success)
805
+ throw new FormBuilderOperationError(
806
+ 400,
807
+ "Validation failed",
808
+ "SUBMISSION_VALIDATION_FAILED",
809
+ finalValidation.error.issues
810
+ );
811
+ const finalData = finalValidation.data;
798
812
  const claimed = await findFormById(tx, authorized.id);
799
813
  if (!sameFormSnapshot(claimed, { ...authorized, updatedAt: claimedAt }) || claimed?.status !== "active") {
800
814
  throw staleFormError();
@@ -854,6 +868,50 @@ function createFormBuilderOperations(adapter, hooks) {
854
868
  await notifyError(hooks, error, "submit", lifecycleContext);
855
869
  }
856
870
  });
871
+ const exportSubmissions = defineOperation({
872
+ input: ExportSubmissionsOperationInputSchema,
873
+ permission: formBuilderPermissions.submission.export,
874
+ facts: async ({ input }) => {
875
+ const form = await findFormById(adapter, input.formId);
876
+ formSnapshots.set(input, form ? snapshotForm(form) : null);
877
+ return {
878
+ formId: input.formId,
879
+ formExists: form !== null,
880
+ ...form?.createdBy ? { ownerId: form.createdBy } : {}
881
+ };
882
+ },
883
+ execute: async ({ input }) => adapter.transaction(async (tx) => {
884
+ const expected = formSnapshots.get(input) ?? null;
885
+ const form = await findFormById(tx, input.formId);
886
+ if (!sameFormSnapshot(form, expected)) throw staleFormError();
887
+ if (!form) throw formNotFoundError();
888
+ const items = [];
889
+ let sourceCharacters = 0;
890
+ for (; ; ) {
891
+ const page = await getFormSubmissions(tx, input.formId, {
892
+ limit: 100,
893
+ offset: items.length
894
+ });
895
+ if (page.total > CSV_EXPORT_MAX_ROWS) throw csvExportLimitError();
896
+ for (const item of page.items) {
897
+ sourceCharacters += item.data.length;
898
+ if (sourceCharacters > CSV_EXPORT_MAX_BYTES)
899
+ throw csvExportLimitError();
900
+ items.push(item);
901
+ }
902
+ if (!page.items.length || items.length >= page.total) break;
903
+ }
904
+ const current = await findFormById(tx, input.formId);
905
+ if (!sameFormSnapshot(current, expected)) throw staleFormError();
906
+ return {
907
+ csv: submissionsToCsv(
908
+ items,
909
+ Object.keys(JSON.parse(form.schema).properties ?? {})
910
+ ),
911
+ filename: `${form.slug}-submissions.csv`
912
+ };
913
+ })
914
+ });
857
915
  const listSubmissions = defineOperation({
858
916
  input: ListSubmissionsOperationInputSchema,
859
917
  permission: formBuilderPermissions.submission.read,
@@ -1065,6 +1123,7 @@ function createFormBuilderOperations(adapter, hooks) {
1065
1123
  onError: ({ error, ...context }) => notifyError(hooks, error, "deleteSubmission", hookContext(context))
1066
1124
  });
1067
1125
  return {
1126
+ exportSubmissions,
1068
1127
  listForms,
1069
1128
  getFormBySlug,
1070
1129
  getFormById,
@@ -1079,4 +1138,4 @@ function createFormBuilderOperations(adapter, hooks) {
1079
1138
  };
1080
1139
  }
1081
1140
 
1082
- export { DeleteFormOperationInputSchema, FormBuilderOperationError, GetFormByIdOperationInputSchema, GetFormBySlugOperationInputSchema, GetFormForUpdateOperationInputSchema, GetSubmissionOperationInputSchema, ListSubmissionsOperationInputSchema, SubmitFormOperationInputSchema, UpdateFormOperationInputSchema, createFormBuilderOperations };
1141
+ export { DeleteFormOperationInputSchema, ExportSubmissionsOperationInputSchema, FormBuilderOperationError, GetFormByIdOperationInputSchema, GetFormBySlugOperationInputSchema, GetFormForUpdateOperationInputSchema, GetSubmissionOperationInputSchema, ListSubmissionsOperationInputSchema, SubmitFormOperationInputSchema, UpdateFormOperationInputSchema, createFormBuilderOperations };
@@ -158,6 +158,13 @@ const formBuilderBackendPlugin = (config = {}) => api.defineBackendPlugin({
158
158
  query: ctx.query
159
159
  }))
160
160
  );
161
+ const exportSubmissions = api.createEndpoint(
162
+ "/forms/:formId/submissions/export",
163
+ { method: "POST", requireRequest: true },
164
+ operations$1.exportSubmissions.route((ctx) => ({
165
+ formId: ctx.params.formId
166
+ }))
167
+ );
161
168
  const getSubmission = api.createEndpoint(
162
169
  "/forms/:formId/submissions/:subId",
163
170
  { method: "GET", requireRequest: true },
@@ -175,6 +182,7 @@ const formBuilderBackendPlugin = (config = {}) => api.defineBackendPlugin({
175
182
  }))
176
183
  );
177
184
  return {
185
+ exportSubmissions,
178
186
  listForms,
179
187
  getFormBySlug: getFormBySlugEndpoint,
180
188
  getFormById: getFormByIdEndpoint,
@@ -157,6 +157,13 @@ const formBuilderBackendPlugin = (config = {}) => defineBackendPlugin({
157
157
  query: ctx.query
158
158
  }))
159
159
  );
160
+ const exportSubmissions = createEndpoint(
161
+ "/forms/:formId/submissions/export",
162
+ { method: "POST", requireRequest: true },
163
+ operations.exportSubmissions.route((ctx) => ({
164
+ formId: ctx.params.formId
165
+ }))
166
+ );
160
167
  const getSubmission = createEndpoint(
161
168
  "/forms/:formId/submissions/:subId",
162
169
  { method: "GET", requireRequest: true },
@@ -174,6 +181,7 @@ const formBuilderBackendPlugin = (config = {}) => defineBackendPlugin({
174
181
  }))
175
182
  );
176
183
  return {
184
+ exportSubmissions,
177
185
  listForms,
178
186
  getFormBySlug: getFormBySlugEndpoint,
179
187
  getFormById: getFormByIdEndpoint,
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const api = require('@btst/stack/plugins/api');
4
+
5
+ const CSV_EXPORT_MAX_ROWS = 1e4;
6
+ const CSV_EXPORT_MAX_BYTES = 10 * 1024 * 1024;
7
+ const CSV_EXPORT_MAX_COLUMNS = 1e3;
8
+ const CSV_EXPORT_MAX_CELLS = 1e6;
9
+ function csvExportLimitError() {
10
+ return new api.OperationHttpError(
11
+ 413,
12
+ "CSV export exceeds the supported row, column, cell or 10 MiB size limit.",
13
+ "FORM_EXPORT_TOO_LARGE"
14
+ );
15
+ }
16
+ function csvCell(value) {
17
+ let text = value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
18
+ if (/^[\s]*[=+\-@]/.test(text) || /^[\t\r\n]/.test(text)) text = `'${text}`;
19
+ return `"${text.replaceAll('"', '""')}"`;
20
+ }
21
+ function submissionsToCsv(submissions, fieldNames) {
22
+ const encoder = new TextEncoder();
23
+ const columns = new Set(fieldNames);
24
+ const rows = [];
25
+ let sourceBytes = 0;
26
+ if (submissions.length > CSV_EXPORT_MAX_ROWS || columns.size > CSV_EXPORT_MAX_COLUMNS || columns.size * submissions.length > CSV_EXPORT_MAX_CELLS)
27
+ throw csvExportLimitError();
28
+ for (const submission of submissions) {
29
+ sourceBytes += encoder.encode(submission.data).byteLength;
30
+ if (sourceBytes > CSV_EXPORT_MAX_BYTES) throw csvExportLimitError();
31
+ const data = JSON.parse(submission.data);
32
+ for (const key of Object.keys(data)) {
33
+ columns.add(key);
34
+ if (columns.size > CSV_EXPORT_MAX_COLUMNS || columns.size * submissions.length > CSV_EXPORT_MAX_CELLS)
35
+ throw csvExportLimitError();
36
+ }
37
+ rows.push({ submission, data });
38
+ }
39
+ const fields = [...columns];
40
+ const lines = [];
41
+ let outputBytes = 0;
42
+ const appendRow = (values) => {
43
+ const line = values.map(csvCell).join(",") + "\r\n";
44
+ outputBytes += encoder.encode(line).byteLength;
45
+ if (outputBytes > CSV_EXPORT_MAX_BYTES) throw csvExportLimitError();
46
+ lines.push(line);
47
+ };
48
+ const header = [
49
+ "id",
50
+ "submittedAt",
51
+ ...fields.map((field) => `data.${field}`)
52
+ ];
53
+ appendRow(header);
54
+ for (const { submission, data } of rows) {
55
+ appendRow([
56
+ submission.id,
57
+ new Date(submission.submittedAt).toISOString(),
58
+ ...fields.map(
59
+ (field) => Object.hasOwn(data, field) ? data[field] : void 0
60
+ )
61
+ ]);
62
+ }
63
+ return lines.join("");
64
+ }
65
+
66
+ exports.CSV_EXPORT_MAX_BYTES = CSV_EXPORT_MAX_BYTES;
67
+ exports.CSV_EXPORT_MAX_ROWS = CSV_EXPORT_MAX_ROWS;
68
+ exports.csvExportLimitError = csvExportLimitError;
69
+ exports.submissionsToCsv = submissionsToCsv;
@@ -0,0 +1,64 @@
1
+ import { OperationHttpError } from '@btst/stack/plugins/api';
2
+
3
+ const CSV_EXPORT_MAX_ROWS = 1e4;
4
+ const CSV_EXPORT_MAX_BYTES = 10 * 1024 * 1024;
5
+ const CSV_EXPORT_MAX_COLUMNS = 1e3;
6
+ const CSV_EXPORT_MAX_CELLS = 1e6;
7
+ function csvExportLimitError() {
8
+ return new OperationHttpError(
9
+ 413,
10
+ "CSV export exceeds the supported row, column, cell or 10 MiB size limit.",
11
+ "FORM_EXPORT_TOO_LARGE"
12
+ );
13
+ }
14
+ function csvCell(value) {
15
+ let text = value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
16
+ if (/^[\s]*[=+\-@]/.test(text) || /^[\t\r\n]/.test(text)) text = `'${text}`;
17
+ return `"${text.replaceAll('"', '""')}"`;
18
+ }
19
+ function submissionsToCsv(submissions, fieldNames) {
20
+ const encoder = new TextEncoder();
21
+ const columns = new Set(fieldNames);
22
+ const rows = [];
23
+ let sourceBytes = 0;
24
+ if (submissions.length > CSV_EXPORT_MAX_ROWS || columns.size > CSV_EXPORT_MAX_COLUMNS || columns.size * submissions.length > CSV_EXPORT_MAX_CELLS)
25
+ throw csvExportLimitError();
26
+ for (const submission of submissions) {
27
+ sourceBytes += encoder.encode(submission.data).byteLength;
28
+ if (sourceBytes > CSV_EXPORT_MAX_BYTES) throw csvExportLimitError();
29
+ const data = JSON.parse(submission.data);
30
+ for (const key of Object.keys(data)) {
31
+ columns.add(key);
32
+ if (columns.size > CSV_EXPORT_MAX_COLUMNS || columns.size * submissions.length > CSV_EXPORT_MAX_CELLS)
33
+ throw csvExportLimitError();
34
+ }
35
+ rows.push({ submission, data });
36
+ }
37
+ const fields = [...columns];
38
+ const lines = [];
39
+ let outputBytes = 0;
40
+ const appendRow = (values) => {
41
+ const line = values.map(csvCell).join(",") + "\r\n";
42
+ outputBytes += encoder.encode(line).byteLength;
43
+ if (outputBytes > CSV_EXPORT_MAX_BYTES) throw csvExportLimitError();
44
+ lines.push(line);
45
+ };
46
+ const header = [
47
+ "id",
48
+ "submittedAt",
49
+ ...fields.map((field) => `data.${field}`)
50
+ ];
51
+ appendRow(header);
52
+ for (const { submission, data } of rows) {
53
+ appendRow([
54
+ submission.id,
55
+ new Date(submission.submittedAt).toISOString(),
56
+ ...fields.map(
57
+ (field) => Object.hasOwn(data, field) ? data[field] : void 0
58
+ )
59
+ ]);
60
+ }
61
+ return lines.join("");
62
+ }
63
+
64
+ export { CSV_EXPORT_MAX_BYTES, CSV_EXPORT_MAX_ROWS, csvExportLimitError, submissionsToCsv };
@@ -13,6 +13,7 @@ const formBuilderHooks = require('../../hooks/form-builder-hooks.cjs');
13
13
  const permissions = require('../../../permissions.cjs');
14
14
  const constants = require('../../constants.cjs');
15
15
  const utils = require('../../../utils.cjs');
16
+ const mediaFileField = require('./media-file-field.cjs');
16
17
 
17
18
  function DefaultLoadingComponent() {
18
19
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
@@ -49,6 +50,7 @@ function DefaultSuccessComponent({ message }) {
49
50
  }
50
51
  function FormRenderer({
51
52
  slug,
53
+ onUpload: propOnUpload,
52
54
  onSuccess,
53
55
  onError,
54
56
  fieldComponents: propFieldComponents,
@@ -61,6 +63,10 @@ function FormRenderer({
61
63
  const t = context.useTranslate();
62
64
  const {
63
65
  fieldComponents: overrideFieldComponents,
66
+ onUpload: overrideOnUpload,
67
+ uploadFile,
68
+ imagePicker,
69
+ imageInputField,
64
70
  localization,
65
71
  classNames
66
72
  } = context.usePluginOverrides(constants.FORM_BUILDER_PLUGIN_ID);
@@ -73,10 +79,17 @@ function FormRenderer({
73
79
  );
74
80
  const mergedFieldComponents = React.useMemo(
75
81
  () => ({
82
+ ...!propOnUpload && (imagePicker || imageInputField) ? { file: mediaFileField.MediaFileField } : {},
76
83
  ...overrideFieldComponents,
77
84
  ...propFieldComponents
78
85
  }),
79
- [overrideFieldComponents, propFieldComponents]
86
+ [
87
+ imagePicker,
88
+ imageInputField,
89
+ overrideFieldComponents,
90
+ propFieldComponents,
91
+ propOnUpload
92
+ ]
80
93
  );
81
94
  const { zodSchema, fieldConfig } = React.useMemo(() => {
82
95
  if (!form?.schema) {
@@ -89,11 +102,27 @@ function FormRenderer({
89
102
  parsedSchema,
90
103
  mergedFieldComponents
91
104
  );
105
+ const applyUpload = (fields) => {
106
+ for (const field of Object.values(fields)) {
107
+ if (field.fieldType === "file")
108
+ field.onUpload = propOnUpload ?? uploadFile ?? overrideOnUpload;
109
+ for (const value of Object.values(field))
110
+ if (value && typeof value === "object" && !Array.isArray(value))
111
+ applyUpload({ child: value });
112
+ }
113
+ };
114
+ applyUpload(config);
92
115
  return { zodSchema: zod, fieldConfig: config };
93
116
  } catch {
94
117
  return { zodSchema: null, fieldConfig: {} };
95
118
  }
96
- }, [form?.schema, mergedFieldComponents]);
119
+ }, [
120
+ form?.schema,
121
+ mergedFieldComponents,
122
+ propOnUpload,
123
+ uploadFile,
124
+ overrideOnUpload
125
+ ]);
97
126
  const handleSubmit = async (data) => {
98
127
  try {
99
128
  const result = await submitMutation.mutateAsync({ data });
@@ -11,6 +11,7 @@ import { useFormBySlug, useSubmitForm } from '../../hooks/form-builder-hooks.mjs
11
11
  import { formBuilderPermissions } from '../../../permissions.mjs';
12
12
  import { FORM_BUILDER_PLUGIN_ID } from '../../constants.mjs';
13
13
  import { cn } from '../../../utils.mjs';
14
+ import { MediaFileField } from './media-file-field.mjs';
14
15
 
15
16
  function DefaultLoadingComponent() {
16
17
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
@@ -47,6 +48,7 @@ function DefaultSuccessComponent({ message }) {
47
48
  }
48
49
  function FormRenderer({
49
50
  slug,
51
+ onUpload: propOnUpload,
50
52
  onSuccess,
51
53
  onError,
52
54
  fieldComponents: propFieldComponents,
@@ -59,6 +61,10 @@ function FormRenderer({
59
61
  const t = useTranslate();
60
62
  const {
61
63
  fieldComponents: overrideFieldComponents,
64
+ onUpload: overrideOnUpload,
65
+ uploadFile,
66
+ imagePicker,
67
+ imageInputField,
62
68
  localization,
63
69
  classNames
64
70
  } = usePluginOverrides(FORM_BUILDER_PLUGIN_ID);
@@ -71,10 +77,17 @@ function FormRenderer({
71
77
  );
72
78
  const mergedFieldComponents = useMemo(
73
79
  () => ({
80
+ ...!propOnUpload && (imagePicker || imageInputField) ? { file: MediaFileField } : {},
74
81
  ...overrideFieldComponents,
75
82
  ...propFieldComponents
76
83
  }),
77
- [overrideFieldComponents, propFieldComponents]
84
+ [
85
+ imagePicker,
86
+ imageInputField,
87
+ overrideFieldComponents,
88
+ propFieldComponents,
89
+ propOnUpload
90
+ ]
78
91
  );
79
92
  const { zodSchema, fieldConfig } = useMemo(() => {
80
93
  if (!form?.schema) {
@@ -87,11 +100,27 @@ function FormRenderer({
87
100
  parsedSchema,
88
101
  mergedFieldComponents
89
102
  );
103
+ const applyUpload = (fields) => {
104
+ for (const field of Object.values(fields)) {
105
+ if (field.fieldType === "file")
106
+ field.onUpload = propOnUpload ?? uploadFile ?? overrideOnUpload;
107
+ for (const value of Object.values(field))
108
+ if (value && typeof value === "object" && !Array.isArray(value))
109
+ applyUpload({ child: value });
110
+ }
111
+ };
112
+ applyUpload(config);
90
113
  return { zodSchema: zod, fieldConfig: config };
91
114
  } catch {
92
115
  return { zodSchema: null, fieldConfig: {} };
93
116
  }
94
- }, [form?.schema, mergedFieldComponents]);
117
+ }, [
118
+ form?.schema,
119
+ mergedFieldComponents,
120
+ propOnUpload,
121
+ uploadFile,
122
+ overrideOnUpload
123
+ ]);
95
124
  const handleSubmit = async (data) => {
96
125
  try {
97
126
  const result = await submitMutation.mutateAsync({ data });
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ const jsxRuntime = require('react/jsx-runtime');
5
+ const reactHookForm = require('react-hook-form');
6
+ const context = require('@btst/stack/context');
7
+ const form = require('../../../ui/src/components/form.cjs');
8
+ const label = require('../../../ui/src/components/auto-form/common/label.cjs');
9
+ const tooltip = require('../../../ui/src/components/auto-form/common/tooltip.cjs');
10
+ const constants = require('../../constants.cjs');
11
+
12
+ function MediaFileField({
13
+ field,
14
+ fieldProps,
15
+ fieldConfigItem,
16
+ label: label$1,
17
+ isRequired
18
+ }) {
19
+ const form$1 = reactHookForm.useFormContext();
20
+ const { imageInputField: ImageInput, imagePicker: ImagePicker } = context.usePluginOverrides(constants.FORM_BUILDER_PLUGIN_ID);
21
+ const select = (url) => {
22
+ if (fieldProps.disabled || form$1.formState.isSubmitting) return;
23
+ field.onChange(url);
24
+ void form$1.trigger(field.name);
25
+ };
26
+ return /* @__PURE__ */ jsxRuntime.jsxs(form.FormItem, { children: [
27
+ /* @__PURE__ */ jsxRuntime.jsx(
28
+ label,
29
+ {
30
+ label: fieldConfigItem.label || label$1,
31
+ isRequired
32
+ }
33
+ ),
34
+ /* @__PURE__ */ jsxRuntime.jsx(form.FormControl, { children: /* @__PURE__ */ jsxRuntime.jsx(
35
+ "div",
36
+ {
37
+ role: "group",
38
+ "aria-label": fieldConfigItem.label || label$1,
39
+ inert: fieldProps.disabled || form$1.formState.isSubmitting,
40
+ children: ImageInput ? /* @__PURE__ */ jsxRuntime.jsx(ImageInput, { value: field.value ?? "", onChange: select }) : ImagePicker ? /* @__PURE__ */ jsxRuntime.jsx(ImagePicker, { onSelect: select }) : null
41
+ }
42
+ ) }),
43
+ /* @__PURE__ */ jsxRuntime.jsx(tooltip, { fieldConfigItem }),
44
+ /* @__PURE__ */ jsxRuntime.jsx(form.FormMessage, {})
45
+ ] });
46
+ }
47
+
48
+ exports.MediaFileField = MediaFileField;