strapi-plugin-hubspot 0.7.0 → 0.9.0

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 (40) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +28 -0
  3. package/dist/_chunks/{Forms-CKA6ukB6.mjs → Forms-BsKys-Hc.mjs} +404 -42
  4. package/dist/_chunks/{Forms-HMZbTk_B.js → Forms-JuzNfsbL.js} +401 -39
  5. package/dist/_chunks/{HubspotFormInput-CKYRcNNU.mjs → HubspotFormInput-CIvHS5rp.mjs} +1 -1
  6. package/dist/_chunks/{HubspotFormInput-BequMb12.js → HubspotFormInput-ph9pe6xY.js} +1 -1
  7. package/dist/_chunks/{HubspotObjectInput-V3uhGK8k.mjs → HubspotObjectInput-BX1qYD2c.mjs} +3 -3
  8. package/dist/_chunks/{HubspotObjectInput-D-FGwsyS.js → HubspotObjectInput-dAd-lMdD.js} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-Dn1QvOcu.js → HubspotPropertyInput-BM3ZaY8f.js} +3 -3
  10. package/dist/_chunks/{HubspotPropertyInput-DrBsx9jQ.mjs → HubspotPropertyInput-C2Z6472q.mjs} +3 -3
  11. package/dist/_chunks/{Settings-Cgp7M1Cv.js → Settings-Cdcjy9DA.js} +2 -2
  12. package/dist/_chunks/{Settings-BjkNbyna.mjs → Settings-DnwGetcc.mjs} +2 -2
  13. package/dist/_chunks/{en-Cun5LoUP.mjs → en-CcQWnXra.mjs} +50 -3
  14. package/dist/_chunks/{en-D5Ch9YFo.js → en-Cqfair98.js} +50 -3
  15. package/dist/_chunks/{fr-ClOWflse.mjs → fr-CBRZ9q8U.mjs} +50 -3
  16. package/dist/_chunks/{fr-CqlhHltZ.js → fr-D117-3Ta.js} +50 -3
  17. package/dist/_chunks/{index-D2Q34BuH.js → index-BbRtXnAL.js} +6 -6
  18. package/dist/_chunks/{index-FKsiOEPB.mjs → index-LTaVoyWJ.mjs} +6 -6
  19. package/dist/_chunks/{objectLabels-DBKevJle.mjs → objectLabels-DE2xGq7f.mjs} +1 -1
  20. package/dist/_chunks/{objectLabels-LAQ0M6-a.js → objectLabels-DU0Cagt_.js} +1 -1
  21. package/dist/_chunks/{useHubspotSchema-BC0Od-Wl.mjs → useHubspotSchema-BSKow0X3.mjs} +1 -1
  22. package/dist/_chunks/{useHubspotSchema-C08qBAyy.js → useHubspotSchema-DzBMAxv7.js} +1 -1
  23. package/dist/admin/index.js +1 -1
  24. package/dist/admin/index.mjs +1 -1
  25. package/dist/admin/src/builder/types.d.ts +26 -0
  26. package/dist/admin/src/pages/Forms.d.ts +5 -1
  27. package/dist/admin/src/pages/Submissions.d.ts +8 -0
  28. package/dist/server/index.js +396 -21
  29. package/dist/server/index.mjs +396 -21
  30. package/dist/server/src/__tests__/importHubspot.test.d.ts +1 -0
  31. package/dist/server/src/__tests__/submissions.test.d.ts +1 -0
  32. package/dist/server/src/content-types.d.ts +3 -0
  33. package/dist/server/src/forms.d.ts +2 -0
  34. package/dist/server/src/formsAdmin.d.ts +19 -0
  35. package/dist/server/src/importHubspot.d.ts +85 -0
  36. package/dist/server/src/importLegacy.d.ts +1 -0
  37. package/dist/server/src/index.d.ts +32 -1
  38. package/dist/server/src/submissions.d.ts +28 -0
  39. package/dist/shared/types.d.ts +2 -0
  40. package/package.json +2 -2
@@ -1,13 +1,13 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { useParams, useSearchParams, Link, useNavigate, Routes, Route } from "react-router-dom";
3
3
  import * as React from "react";
4
4
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
5
- import { Typography, Flex, Box, SingleSelect, SingleSelectOption, TextInput, IconButton, Button, Field, Checkbox, Divider, Combobox, ComboboxOption, Loader, Badge, Textarea, Table, Thead, Tr, Th, Tbody, Td } from "@strapi/design-system";
6
- import { Trash, Plus, ArrowClockwise, ArrowLeft, ArrowUp, ArrowDown } from "@strapi/icons";
5
+ import { Typography, Flex, Box, SingleSelect, SingleSelectOption, TextInput, IconButton, Button, Field, Checkbox, Divider, Combobox, ComboboxOption, Loader, Badge, Textarea, Table, Thead, Tr, Th, Tbody, Td, Modal } from "@strapi/design-system";
6
+ import { Trash, Plus, ArrowClockwise, ArrowLeft, ArrowUp, ArrowDown, List, Duplicate, Download } from "@strapi/icons";
7
7
  import { useFetchClient } from "@strapi/strapi/admin";
8
- import { g as getTranslation, P as PLUGIN_ID } from "./index-FKsiOEPB.mjs";
9
- import { o as objectLabel } from "./objectLabels-DBKevJle.mjs";
10
- import { u as useHubspotSchema } from "./useHubspotSchema-BC0Od-Wl.mjs";
8
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-LTaVoyWJ.mjs";
9
+ import { o as objectLabel } from "./objectLabels-DE2xGq7f.mjs";
10
+ import { u as useHubspotSchema } from "./useHubspotSchema-BSKow0X3.mjs";
11
11
  const OPERATORS = ["eq", "neq", "contains", "empty", "notEmpty", "gt", "lt"];
12
12
  const NEEDS_VALUE = /* @__PURE__ */ new Set(["eq", "neq", "contains", "gt", "lt"]);
13
13
  const ConditionEditor = ({ condition, candidates, onChange }) => {
@@ -224,6 +224,43 @@ const FieldPanel = ({ field, candidates, problem, onChange }) => {
224
224
  children: t("panel.required", "Required")
225
225
  }
226
226
  ),
227
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, children: [
228
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
229
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.icon", "Icon") }),
230
+ /* @__PURE__ */ jsx(
231
+ TextInput,
232
+ {
233
+ value: field.icon ?? "",
234
+ placeholder: "i-lucide-building-2",
235
+ onChange: (e) => onChange({ icon: e.target.value })
236
+ }
237
+ )
238
+ ] }) }),
239
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
240
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.persist", "Answer memory") }),
241
+ /* @__PURE__ */ jsxs(
242
+ SingleSelect,
243
+ {
244
+ value: field.persist ?? "session",
245
+ onChange: (v) => onChange({ persist: v }),
246
+ children: [
247
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "session", children: t("panel.persist-session", "This visit only") }),
248
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "days30", children: t("panel.persist-days30", "30 days (non-identifying fields only)") })
249
+ ]
250
+ }
251
+ )
252
+ ] }) })
253
+ ] }),
254
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
255
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.class", "CSS classes") }),
256
+ /* @__PURE__ */ jsx(
257
+ TextInput,
258
+ {
259
+ value: field.class ?? "",
260
+ onChange: (e) => onChange({ class: e.target.value })
261
+ }
262
+ )
263
+ ] }),
227
264
  isChoice && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
228
265
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("panel.options", "Choices") }),
229
266
  (field.options ?? []).map((option, index) => /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
@@ -370,6 +407,14 @@ const FormEditor = () => {
370
407
  const [feedback, setFeedback] = React.useState(null);
371
408
  const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
372
409
  const query = locale ? `?locale=${locale}` : "";
410
+ React.useEffect(() => {
411
+ if (!dirty) return void 0;
412
+ const warn = (event) => {
413
+ event.preventDefault();
414
+ };
415
+ window.addEventListener("beforeunload", warn);
416
+ return () => window.removeEventListener("beforeunload", warn);
417
+ }, [dirty]);
373
418
  React.useEffect(() => {
374
419
  get("/i18n/locales").then(({ data }) => Array.isArray(data) && setLocales(data)).catch(() => setLocales([]));
375
420
  }, [get]);
@@ -462,6 +507,7 @@ const FormEditor = () => {
462
507
  nextLabel: entry.nextLabel,
463
508
  submitLabel: entry.submitLabel,
464
509
  successMessage: entry.successMessage,
510
+ class: entry.class,
465
511
  definition
466
512
  }
467
513
  );
@@ -567,6 +613,8 @@ const FormEditor = () => {
567
613
  hasRadius: true,
568
614
  shadow: "tableShadow",
569
615
  padding: 4,
616
+ onClick: () => setSelection({ kind: "step", stepId: step.id }),
617
+ style: { cursor: "pointer" },
570
618
  borderColor: selection.kind === "step" && selection.stepId === step.id ? "primary600" : void 0,
571
619
  borderStyle: selection.kind === "step" && selection.stepId === step.id ? "solid" : void 0,
572
620
  borderWidth: selection.kind === "step" && selection.stepId === step.id ? "1px" : void 0,
@@ -589,7 +637,7 @@ const FormEditor = () => {
589
637
  ]
590
638
  }
591
639
  ),
592
- /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
640
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, onClick: (e) => e.stopPropagation(), children: [
593
641
  /* @__PURE__ */ jsx(
594
642
  IconButton,
595
643
  {
@@ -631,7 +679,10 @@ const FormEditor = () => {
631
679
  background: isSelected ? "primary100" : "neutral100",
632
680
  hasRadius: true,
633
681
  padding: 3,
634
- onClick: () => setSelection({ kind: "field", fieldId: field.id }),
682
+ onClick: (e) => {
683
+ e.stopPropagation();
684
+ setSelection({ kind: "field", fieldId: field.id });
685
+ },
635
686
  style: { cursor: "pointer" },
636
687
  children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
637
688
  /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
@@ -689,7 +740,8 @@ const FormEditor = () => {
689
740
  {
690
741
  variant: "tertiary",
691
742
  startIcon: /* @__PURE__ */ jsx(Plus, {}),
692
- onClick: () => {
743
+ onClick: (e) => {
744
+ e.stopPropagation();
693
745
  const field = newField(t("editor.new-field", "New field"));
694
746
  patchStep(step.id, { fields: [...step.fields, field] });
695
747
  setSelection({ kind: "field", fieldId: field.id });
@@ -746,6 +798,16 @@ const FormEditor = () => {
746
798
  }
747
799
  )
748
800
  ] }),
801
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
802
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.class", "CSS classes") }),
803
+ /* @__PURE__ */ jsx(
804
+ TextInput,
805
+ {
806
+ value: selectedStep.class ?? "",
807
+ onChange: (e) => patchStep(selectedStep.id, { class: e.target.value })
808
+ }
809
+ )
810
+ ] }),
749
811
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("editor.step-condition", "Show this step only when…") }),
750
812
  /* @__PURE__ */ jsx(
751
813
  ConditionEditor,
@@ -819,6 +881,16 @@ const FormEditor = () => {
819
881
  }
820
882
  )
821
883
  ] }),
884
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
885
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.class", "CSS classes") }),
886
+ /* @__PURE__ */ jsx(
887
+ TextInput,
888
+ {
889
+ value: entry.class ?? "",
890
+ onChange: (e) => patchEntry({ class: e.target.value })
891
+ }
892
+ )
893
+ ] }),
822
894
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
823
895
  "editor.form-hint",
824
896
  "Select a step or a field to edit it. The public endpoint serves this form at /api/hubspot/forms/{slug}.",
@@ -838,6 +910,12 @@ const FormsList = () => {
838
910
  const [error, setError] = React.useState(null);
839
911
  const [sources, setSources] = React.useState([]);
840
912
  const [sourceId, setSourceId] = React.useState("");
913
+ const [hsForms, setHsForms] = React.useState([]);
914
+ const [hsFormId, setHsFormId] = React.useState("");
915
+ const [hsError, setHsError] = React.useState(null);
916
+ const [report, setReport] = React.useState(
917
+ null
918
+ );
841
919
  const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
842
920
  const load = React.useCallback(async () => {
843
921
  const { data } = await get(`/${PLUGIN_ID}/builder/forms`);
@@ -848,6 +926,17 @@ const FormsList = () => {
848
926
  get(
849
927
  `/${PLUGIN_ID}/builder/import/sources`
850
928
  ).then(({ data }) => setSources(data.sources ?? [])).catch(() => setSources([]));
929
+ get(`/${PLUGIN_ID}/builder/import/hubspot`).then(({ data }) => setHsForms(data.configured ? data.forms ?? [] : [])).catch(
930
+ () => (
931
+ // Reachable settings but no portal list: almost always the missing scope.
932
+ setHsError(
933
+ t(
934
+ "forms.hubspot-list-error",
935
+ "Could not list the portal's forms — the token needs the `forms` read scope."
936
+ )
937
+ )
938
+ )
939
+ );
851
940
  }, [load]);
852
941
  const runImport = async () => {
853
942
  if (!sourceId) return;
@@ -863,6 +952,48 @@ const FormsList = () => {
863
952
  setBusy(false);
864
953
  }
865
954
  };
955
+ const runHubspotImport = async () => {
956
+ if (!hsFormId) return;
957
+ setBusy(true);
958
+ setError(null);
959
+ setReport(null);
960
+ try {
961
+ const { data } = await post(
962
+ `/${PLUGIN_ID}/builder/import/hubspot`,
963
+ { formId: hsFormId }
964
+ );
965
+ if (data.skipped.length) {
966
+ setReport(data);
967
+ setBusy(false);
968
+ await load();
969
+ } else {
970
+ navigate(data.documentId);
971
+ }
972
+ } catch {
973
+ setError(t("forms.import-error", "Could not import the form."));
974
+ setBusy(false);
975
+ }
976
+ };
977
+ const skippedLine = (item) => {
978
+ const label = item.label ?? "";
979
+ const detail = item.detail ?? "";
980
+ switch (item.code) {
981
+ case "field-type":
982
+ return t("forms.skipped-field-type", '"{label}" skipped — unsupported type ({detail})', { label, detail });
983
+ case "hidden-field":
984
+ return t("forms.skipped-hidden", '"{label}" skipped — hidden fields are not supported yet', { label });
985
+ case "object":
986
+ return t("forms.skipped-object", '"{label}" skipped — unmapped object ({detail})', { label, detail });
987
+ case "duplicate":
988
+ return t("forms.skipped-duplicate", '"{label}" skipped — the name "{detail}" is already used', { label, detail });
989
+ case "condition":
990
+ return t("forms.skipped-condition", 'Condition on "{label}" dropped — no equivalent operator ({detail})', { label, detail });
991
+ case "rich-text":
992
+ return t("forms.skipped-rich-text", "A content block was skipped — the builder has no rich-text element");
993
+ case "legal-consent":
994
+ return t("forms.skipped-consent", "The GDPR consent block was skipped — rebuild it as a field if needed");
995
+ }
996
+ };
866
997
  const create = async () => {
867
998
  if (!name.trim()) return;
868
999
  setBusy(true);
@@ -878,6 +1009,19 @@ const FormsList = () => {
878
1009
  setBusy(false);
879
1010
  }
880
1011
  };
1012
+ const duplicate = async (row) => {
1013
+ setBusy(true);
1014
+ setError(null);
1015
+ try {
1016
+ const { data } = await post(
1017
+ `/${PLUGIN_ID}/builder/forms/${row.documentId}/duplicate`
1018
+ );
1019
+ navigate(data.documentId);
1020
+ } catch {
1021
+ setError(t("forms.duplicate-error", "Could not duplicate the form."));
1022
+ setBusy(false);
1023
+ }
1024
+ };
881
1025
  const remove = async (row) => {
882
1026
  if (!window.confirm(t("forms.delete-confirm", 'Delete "{name}" and all its locales?', { name: row.name }))) {
883
1027
  return;
@@ -902,6 +1046,7 @@ const FormsList = () => {
902
1046
  ) })
903
1047
  ] }),
904
1048
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1049
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(List, {}), onClick: () => navigate("submissions"), children: t("forms.submissions", "Submissions") }),
905
1050
  /* @__PURE__ */ jsx(
906
1051
  TextInput,
907
1052
  {
@@ -916,40 +1061,76 @@ const FormsList = () => {
916
1061
  ] })
917
1062
  ] }),
918
1063
  error && /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }) }),
919
- sources.length > 0 && /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, marginBottom: 4, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "flex-end", justifyContent: "space-between", children: [
1064
+ (sources.length > 0 || hsForms.length > 0 || hsError) && /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, marginBottom: 4, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
1065
+ hsForms.length > 0 && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "flex-end", justifyContent: "space-between", children: [
1066
+ /* @__PURE__ */ jsxs(Box, { children: [
1067
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("forms.import-hubspot-title", "Import from your HubSpot portal") }),
1068
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", tag: "p", children: t(
1069
+ "forms.import-hubspot-hint",
1070
+ "Translates a form built in HubSpot into a draft here, CRM mapping included. What the builder can't express is skipped and reported. Re-importing overwrites the draft."
1071
+ ) })
1072
+ ] }),
1073
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1074
+ /* @__PURE__ */ jsx(
1075
+ SingleSelect,
1076
+ {
1077
+ "aria-label": t("forms.import-hubspot-source", "HubSpot form to import"),
1078
+ placeholder: t("forms.import-hubspot-source", "HubSpot form to import"),
1079
+ value: hsFormId,
1080
+ onChange: (v) => setHsFormId(String(v)),
1081
+ children: hsForms.map((f) => /* @__PURE__ */ jsx(SingleSelectOption, { value: f.id, children: f.name }, f.id))
1082
+ }
1083
+ ),
1084
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: runHubspotImport, disabled: busy || !hsFormId, children: t("forms.import", "Import") })
1085
+ ] })
1086
+ ] }),
1087
+ hsError && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "warning700", children: hsError }),
1088
+ sources.length > 0 && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "flex-end", justifyContent: "space-between", children: [
1089
+ /* @__PURE__ */ jsxs(Box, { children: [
1090
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("forms.import-title", "Import an existing form") }),
1091
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", tag: "p", children: t(
1092
+ "forms.import-hint",
1093
+ "Converts a legacy content-type form (all locales) into a draft here — same slug, source untouched. Re-importing overwrites the draft."
1094
+ ) })
1095
+ ] }),
1096
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1097
+ /* @__PURE__ */ jsx(
1098
+ SingleSelect,
1099
+ {
1100
+ "aria-label": t("forms.import-source", "Form to import"),
1101
+ placeholder: t("forms.import-source", "Form to import"),
1102
+ value: sourceId,
1103
+ onChange: (v) => setSourceId(String(v)),
1104
+ children: sources.map((s) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: s.documentId, children: [
1105
+ s.name,
1106
+ " ",
1107
+ s.slug ? `(${s.slug})` : ""
1108
+ ] }, s.documentId))
1109
+ }
1110
+ ),
1111
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: runImport, disabled: busy || !sourceId, children: t("forms.import", "Import") })
1112
+ ] })
1113
+ ] })
1114
+ ] }) }),
1115
+ report && /* @__PURE__ */ jsx(Box, { background: "warning100", hasRadius: true, padding: 4, marginBottom: 4, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-start", gap: 4, children: [
920
1116
  /* @__PURE__ */ jsxs(Box, { children: [
921
- /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("forms.import-title", "Import an existing form") }),
922
- /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", tag: "p", children: t(
923
- "forms.import-hint",
924
- "Converts a legacy content-type form (all locales) into a draft here — same slug, source untouched. Re-importing overwrites the draft."
925
- ) })
1117
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", textColor: "warning700", children: t("forms.report-title", "Imported with gaps") }),
1118
+ /* @__PURE__ */ jsx(Box, { tag: "ul", paddingTop: 2, style: { listStyle: "disc", paddingLeft: 16 }, children: report.skipped.map((item, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "warning700", children: skippedLine(item) }) }, i)) })
926
1119
  ] }),
927
- /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
928
- /* @__PURE__ */ jsx(
929
- SingleSelect,
930
- {
931
- "aria-label": t("forms.import-source", "Form to import"),
932
- placeholder: t("forms.import-source", "Form to import"),
933
- value: sourceId,
934
- onChange: (v) => setSourceId(String(v)),
935
- children: sources.map((s) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: s.documentId, children: [
936
- s.name,
937
- " ",
938
- s.slug ? `(${s.slug})` : ""
939
- ] }, s.documentId))
940
- }
941
- ),
942
- /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: runImport, disabled: busy || !sourceId, children: t("forms.import", "Import") })
1120
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, shrink: 0, children: [
1121
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: () => setReport(null), children: t("forms.report-dismiss", "Dismiss") }),
1122
+ /* @__PURE__ */ jsx(Button, { onClick: () => navigate(report.documentId), children: t("forms.report-open", "Open the form") })
943
1123
  ] })
944
1124
  ] }) }),
945
- !rows ? /* @__PURE__ */ jsx(Loader, { children: t("forms.loading", "Loading…") }) : rows.length === 0 ? /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 8, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.empty", "No form yet — name one above to open the builder.") }) }) : /* @__PURE__ */ jsxs(Table, { colCount: 6, rowCount: rows.length, children: [
1125
+ !rows ? /* @__PURE__ */ jsx(Loader, { children: t("forms.loading", "Loading…") }) : rows.length === 0 ? /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 8, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.empty", "No form yet — name one above to open the builder.") }) }) : /* @__PURE__ */ jsxs(Table, { colCount: 7, rowCount: rows.length, children: [
946
1126
  /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
947
1127
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-name", "Name") }) }),
948
1128
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-slug", "Slug") }) }),
949
1129
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-structure", "Structure") }) }),
950
1130
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-status", "Status") }) }),
1131
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-submissions", "Submissions") }) }),
951
1132
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-updated", "Updated") }) }),
952
- /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-actions", "") }) })
1133
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-actions", "Actions") }) })
953
1134
  ] }) }),
954
1135
  /* @__PURE__ */ jsx(Tbody, { children: rows.map((row) => /* @__PURE__ */ jsxs(
955
1136
  Tr,
@@ -959,21 +1140,41 @@ const FormsList = () => {
959
1140
  children: [
960
1141
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: row.name }) }),
961
1142
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.slug }) }),
962
- /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.structure", "{steps} steps · {fields} fields", {
1143
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.structure", "{steps, plural, one {# step} other {# steps}} · {fields, plural, one {# field} other {# fields}}", {
963
1144
  steps: row.steps,
964
1145
  fields: row.fields
965
1146
  }) }) }),
966
1147
  /* @__PURE__ */ jsx(Td, { children: row.published ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: t("forms.published", "Published") }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: t("forms.draft", "Draft") }) }),
967
- /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.updatedAt ? new Date(row.updatedAt).toLocaleString() : "—" }) }),
968
- /* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
969
- IconButton,
1148
+ /* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: row.submissions > 0 ? /* @__PURE__ */ jsx(
1149
+ Button,
970
1150
  {
971
- label: t("forms.delete", "Delete"),
972
- onClick: () => remove(row),
973
- disabled: busy,
974
- children: /* @__PURE__ */ jsx(Trash, {})
1151
+ variant: "ghost",
1152
+ startIcon: /* @__PURE__ */ jsx(List, {}),
1153
+ onClick: () => navigate(`submissions?form=${encodeURIComponent(row.slug)}`),
1154
+ children: row.submissions
975
1155
  }
976
- ) })
1156
+ ) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "—" }) }),
1157
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.updatedAt ? new Date(row.updatedAt).toLocaleString() : "—" }) }),
1158
+ /* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
1159
+ /* @__PURE__ */ jsx(
1160
+ IconButton,
1161
+ {
1162
+ label: t("forms.duplicate", "Duplicate"),
1163
+ onClick: () => duplicate(row),
1164
+ disabled: busy,
1165
+ children: /* @__PURE__ */ jsx(Duplicate, {})
1166
+ }
1167
+ ),
1168
+ /* @__PURE__ */ jsx(
1169
+ IconButton,
1170
+ {
1171
+ label: t("forms.delete", "Delete"),
1172
+ onClick: () => remove(row),
1173
+ disabled: busy,
1174
+ children: /* @__PURE__ */ jsx(Trash, {})
1175
+ }
1176
+ )
1177
+ ] }) })
977
1178
  ]
978
1179
  },
979
1180
  row.documentId
@@ -981,8 +1182,169 @@ const FormsList = () => {
981
1182
  ] })
982
1183
  ] });
983
1184
  };
1185
+ const PAGE_SIZE = 20;
1186
+ const preview = (values, max = 3) => {
1187
+ const parts = Object.entries(values ?? {}).slice(0, max).map(([k, v]) => `${k}: ${String(v)}`);
1188
+ const more = Object.keys(values ?? {}).length - max;
1189
+ return parts.join(" · ") + (more > 0 ? ` · +${more}` : "");
1190
+ };
1191
+ const Submissions = () => {
1192
+ const { formatMessage } = useIntl();
1193
+ const { get } = useFetchClient();
1194
+ const navigate = useNavigate();
1195
+ const [searchParams, setSearchParams] = useSearchParams();
1196
+ const form = searchParams.get("form") ?? "";
1197
+ const [forms, setForms] = React.useState([]);
1198
+ const [rows, setRows] = React.useState(null);
1199
+ const [total, setTotal] = React.useState(0);
1200
+ const [page, setPage] = React.useState(1);
1201
+ const [selected, setSelected] = React.useState(null);
1202
+ const [error, setError] = React.useState(null);
1203
+ const [exporting, setExporting] = React.useState(false);
1204
+ const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
1205
+ React.useEffect(() => {
1206
+ get(`/${PLUGIN_ID}/builder/forms`).then(({ data }) => setForms(data.forms)).catch(() => setForms([]));
1207
+ }, [get]);
1208
+ React.useEffect(() => {
1209
+ setRows(null);
1210
+ const query = new URLSearchParams({ page: String(page), pageSize: String(PAGE_SIZE) });
1211
+ if (form) query.set("form", form);
1212
+ get(
1213
+ `/${PLUGIN_ID}/builder/submissions?${query}`
1214
+ ).then(({ data }) => {
1215
+ setRows(data.submissions);
1216
+ setTotal(data.total);
1217
+ }).catch(() => setError(t("submissions.load-error", "Could not load the submissions.")));
1218
+ }, [get, form, page]);
1219
+ const changeForm = (slug) => {
1220
+ setPage(1);
1221
+ setSearchParams(slug ? { form: slug } : {});
1222
+ };
1223
+ const exportCsv = async () => {
1224
+ if (!form) return;
1225
+ setExporting(true);
1226
+ setError(null);
1227
+ try {
1228
+ const { data } = await get(
1229
+ `/${PLUGIN_ID}/builder/submissions/export?form=${encodeURIComponent(form)}`
1230
+ );
1231
+ const url = URL.createObjectURL(new Blob([data.csv], { type: "text/csv;charset=utf-8" }));
1232
+ const link = document.createElement("a");
1233
+ link.href = url;
1234
+ link.download = data.filename;
1235
+ link.click();
1236
+ URL.revokeObjectURL(url);
1237
+ } catch {
1238
+ setError(t("submissions.export-error", "Could not export the submissions."));
1239
+ } finally {
1240
+ setExporting(false);
1241
+ }
1242
+ };
1243
+ const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
1244
+ return /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
1245
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", paddingBottom: 6, children: [
1246
+ /* @__PURE__ */ jsxs(Box, { children: [
1247
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1248
+ /* @__PURE__ */ jsx(IconButton, { label: t("submissions.back", "Back to the forms"), onClick: () => navigate(".."), children: /* @__PURE__ */ jsx(ArrowLeft, {}) }),
1249
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: t("submissions.title", "Submissions") })
1250
+ ] }),
1251
+ /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", tag: "p", children: t(
1252
+ "submissions.subtitle",
1253
+ "Every answer received by the built forms — stored whatever the CRM's mood."
1254
+ ) })
1255
+ ] }),
1256
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1257
+ /* @__PURE__ */ jsx(
1258
+ SingleSelect,
1259
+ {
1260
+ "aria-label": t("submissions.filter", "Filter by form"),
1261
+ placeholder: t("submissions.all-forms", "All forms"),
1262
+ value: form,
1263
+ onChange: (v) => changeForm(String(v)),
1264
+ onClear: () => changeForm(""),
1265
+ children: forms.map((f) => /* @__PURE__ */ jsx(SingleSelectOption, { value: f.slug, children: f.name }, f.slug))
1266
+ }
1267
+ ),
1268
+ /* @__PURE__ */ jsx(
1269
+ Button,
1270
+ {
1271
+ variant: "secondary",
1272
+ startIcon: /* @__PURE__ */ jsx(Download, {}),
1273
+ onClick: exportCsv,
1274
+ disabled: !form || exporting,
1275
+ title: form ? void 0 : t("submissions.export-pick", "Pick a form to export"),
1276
+ children: t("submissions.export", "Export CSV")
1277
+ }
1278
+ )
1279
+ ] })
1280
+ ] }),
1281
+ error && /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }) }),
1282
+ !rows ? /* @__PURE__ */ jsx(Loader, { children: t("submissions.loading", "Loading…") }) : rows.length === 0 ? /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 8, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: form ? t("submissions.empty-form", "No submission for this form yet.") : t("submissions.empty", "No submission yet.") }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1283
+ /* @__PURE__ */ jsxs(Table, { colCount: 5, rowCount: rows.length, children: [
1284
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1285
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-date", "Date") }) }),
1286
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-form", "Form") }) }),
1287
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-email", "Email") }) }),
1288
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-answers", "Answers") }) }),
1289
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-crm", "CRM") }) })
1290
+ ] }) }),
1291
+ /* @__PURE__ */ jsx(Tbody, { children: rows.map((row) => /* @__PURE__ */ jsxs(
1292
+ Tr,
1293
+ {
1294
+ onClick: () => setSelected(row),
1295
+ style: { cursor: "pointer" },
1296
+ children: [
1297
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.createdAt ? new Date(row.createdAt).toLocaleString() : "—" }) }),
1298
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: row.formTitle || row.form }) }),
1299
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: row.email || "—" }) }),
1300
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", ellipsis: true, children: preview(row.values) }) }),
1301
+ /* @__PURE__ */ jsx(Td, { children: row.hubspotSynced ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.synced", "Synced") }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1302
+ ]
1303
+ },
1304
+ row.documentId
1305
+ )) })
1306
+ ] }),
1307
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", paddingTop: 4, children: [
1308
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("submissions.count", "{total, plural, one {# submission} other {# submissions}}", {
1309
+ total
1310
+ }) }),
1311
+ pages > 1 && /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1312
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", disabled: page <= 1, onClick: () => setPage(page - 1), children: t("submissions.prev", "Previous") }),
1313
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("submissions.page", "{page} / {pages}", { page, pages }) }),
1314
+ /* @__PURE__ */ jsx(
1315
+ Button,
1316
+ {
1317
+ variant: "tertiary",
1318
+ disabled: page >= pages,
1319
+ onClick: () => setPage(page + 1),
1320
+ children: t("submissions.next", "Next")
1321
+ }
1322
+ )
1323
+ ] })
1324
+ ] })
1325
+ ] }),
1326
+ /* @__PURE__ */ jsx(Modal.Root, { open: Boolean(selected), onOpenChange: (open) => !open && setSelected(null), children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
1327
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsxs(Modal.Title, { children: [
1328
+ selected?.formTitle || selected?.form,
1329
+ " ",
1330
+ selected?.createdAt ? `— ${new Date(selected.createdAt).toLocaleString()}` : ""
1331
+ ] }) }),
1332
+ /* @__PURE__ */ jsx(Modal.Body, { children: selected && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
1333
+ Object.entries(selected.values ?? {}).map(([key, value]) => /* @__PURE__ */ jsxs(Box, { children: [
1334
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "p", children: key }),
1335
+ /* @__PURE__ */ jsx(Typography, { tag: "p", children: String(value) })
1336
+ ] }, key)),
1337
+ selected.meta?.pagePath && /* @__PURE__ */ jsxs(Box, { children: [
1338
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "p", children: t("submissions.meta-page", "Submitted from") }),
1339
+ /* @__PURE__ */ jsx(Typography, { tag: "p", children: selected.meta.pagePath })
1340
+ ] })
1341
+ ] }) })
1342
+ ] }) })
1343
+ ] });
1344
+ };
984
1345
  const Forms = () => /* @__PURE__ */ jsxs(Routes, { children: [
985
1346
  /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(FormsList, {}) }),
1347
+ /* @__PURE__ */ jsx(Route, { path: "submissions", element: /* @__PURE__ */ jsx(Submissions, {}) }),
986
1348
  /* @__PURE__ */ jsx(Route, { path: ":documentId", element: /* @__PURE__ */ jsx(FormEditor, {}) })
987
1349
  ] });
988
1350
  export {