strapi-plugin-hubspot 0.8.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 (36) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +28 -0
  3. package/dist/_chunks/{Forms-77Mornxt.mjs → Forms-BsKys-Hc.mjs} +295 -29
  4. package/dist/_chunks/{Forms-B8kkPY0c.js → Forms-JuzNfsbL.js} +292 -26
  5. package/dist/_chunks/{HubspotFormInput-BVknnlmR.mjs → HubspotFormInput-CIvHS5rp.mjs} +1 -1
  6. package/dist/_chunks/{HubspotFormInput-kx3LlQYF.js → HubspotFormInput-ph9pe6xY.js} +1 -1
  7. package/dist/_chunks/{HubspotObjectInput-B-Y1PZ6c.mjs → HubspotObjectInput-BX1qYD2c.mjs} +3 -3
  8. package/dist/_chunks/{HubspotObjectInput-BOO1PvOj.js → HubspotObjectInput-dAd-lMdD.js} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-awfD1opH.js → HubspotPropertyInput-BM3ZaY8f.js} +3 -3
  10. package/dist/_chunks/{HubspotPropertyInput-Drxn_Ddt.mjs → HubspotPropertyInput-C2Z6472q.mjs} +3 -3
  11. package/dist/_chunks/{Settings-BT1NhvxO.js → Settings-Cdcjy9DA.js} +2 -2
  12. package/dist/_chunks/{Settings-DrujhMxS.mjs → Settings-DnwGetcc.mjs} +2 -2
  13. package/dist/_chunks/{en-BYKKi1LK.mjs → en-CcQWnXra.mjs} +41 -1
  14. package/dist/_chunks/{en-B8adhJhO.js → en-Cqfair98.js} +41 -1
  15. package/dist/_chunks/{fr-DzgArgi3.mjs → fr-CBRZ9q8U.mjs} +41 -1
  16. package/dist/_chunks/{fr-DnKtGXaB.js → fr-D117-3Ta.js} +41 -1
  17. package/dist/_chunks/{index-P7p8ASb6.js → index-BbRtXnAL.js} +6 -6
  18. package/dist/_chunks/{index-LrCSAHoH.mjs → index-LTaVoyWJ.mjs} +6 -6
  19. package/dist/_chunks/{objectLabels-yNrnTOBo.mjs → objectLabels-DE2xGq7f.mjs} +1 -1
  20. package/dist/_chunks/{objectLabels-Dn-zriuZ.js → objectLabels-DU0Cagt_.js} +1 -1
  21. package/dist/_chunks/{useHubspotSchema-DccG2KbZ.mjs → useHubspotSchema-BSKow0X3.mjs} +1 -1
  22. package/dist/_chunks/{useHubspotSchema-BottzdRR.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 +25 -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 +329 -13
  29. package/dist/server/index.mjs +329 -13
  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/formsAdmin.d.ts +13 -0
  33. package/dist/server/src/importHubspot.d.ts +85 -0
  34. package/dist/server/src/index.d.ts +28 -1
  35. package/dist/server/src/submissions.d.ts +28 -0
  36. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-08-29
4
+
5
+ ### Added
6
+ - **Import from HubSpot**: forms built in HubSpot (marketing v3 Forms API,
7
+ `forms` read scope) can be translated into builder drafts — fields, options,
8
+ two-per-row layouts and dependent-field conditions carried over, with a
9
+ **CRM mapping that is valid on arrival** since a HubSpot form field is a CRM
10
+ property. What the builder can't express (file uploads, hidden fields,
11
+ content blocks, GDPR consent, exotic operators) is skipped and **reported**
12
+ after the import instead of silently dropped. Re-importing overwrites the
13
+ draft; the HubSpot original is never touched.
14
+ - **Submissions browser**: a Submissions page in the admin (button on the
15
+ forms list + per-form counts in the table) — newest first, filterable by
16
+ form, sync state at a glance, full answers in a modal, and a per-form
17
+ **CSV export** whose columns follow the field order of the form (historical
18
+ keys appended, never dropped).
19
+ - Admin routes: `GET/POST /hubspot/builder/import/hubspot`,
20
+ `GET /hubspot/builder/submissions`, `GET /hubspot/builder/submissions/export`
21
+ (all RBAC-gated by `plugin::hubspot.forms`); the forms list now carries a
22
+ `submissions` count per row.
23
+
3
24
  ## 0.8.0 — 2026-08-29
4
25
 
5
26
  ### Improved (builder UX pass, from a live session)
package/README.md CHANGED
@@ -66,6 +66,16 @@ Every submission is stored in **HubSpot form submissions** (Content Manager),
66
66
  synced or not, with the CRM ids when the sync succeeded — the source of truth
67
67
  lives in your database, not in HubSpot's availability.
68
68
 
69
+ ### Browsing submissions
70
+
71
+ The **Submissions** button on the forms list (also reachable from a form's
72
+ count in the table) opens the submissions browser: every stored answer,
73
+ newest first, filterable by form, with the sync state at a glance and the
74
+ full answer set one click away. **Export CSV** downloads one form's whole
75
+ history — columns follow the form's field order, and keys found only in older
76
+ submissions (a renamed or removed field) are appended rather than dropped.
77
+ Deleting stays in the Content Manager, where the type remains visible.
78
+
69
79
  Typed payloads for your frontend:
70
80
 
71
81
  ```ts
@@ -99,6 +109,24 @@ never modified, re-importing overwrites the draft only, and unpublishing rolls
99
109
  a migrated form back. Migrate form by form; the validation middleware keeps
100
110
  protecting the ones that stay.
101
111
 
112
+ ### Importing forms from HubSpot
113
+
114
+ Forms built **in HubSpot itself** can be imported too — give the private app
115
+ token the `forms` read scope and the list page offers every regular form of
116
+ the portal. The translation carries over what the builder can express:
117
+
118
+ - fields, labels, placeholders, help texts, required flags and options —
119
+ and since a HubSpot form field *is* a CRM property, the imported form
120
+ arrives with a **mapping that is already valid** for the portal;
121
+ - two-fields-per-row layouts become two half-width fields;
122
+ - dependent fields become `visibleIf` conditions (value lists are expanded
123
+ into OR'd/AND'd rules).
124
+
125
+ Everything else — file uploads, hidden fields, content blocks, GDPR consent
126
+ blocks, an operator with no equivalent — is **skipped and reported** after the
127
+ import, so nothing is silently half-migrated. The HubSpot original is never
128
+ modified; re-importing overwrites the draft, never the published version.
129
+
102
130
  ### i18n and publishing
103
131
 
104
132
  Forms are draft & publish, localized like your content: `name` and `slug` are
@@ -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, Duplicate } 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-LrCSAHoH.mjs";
9
- import { o as objectLabel } from "./objectLabels-yNrnTOBo.mjs";
10
- import { u as useHubspotSchema } from "./useHubspotSchema-DccG2KbZ.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 }) => {
@@ -910,6 +910,12 @@ const FormsList = () => {
910
910
  const [error, setError] = React.useState(null);
911
911
  const [sources, setSources] = React.useState([]);
912
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
+ );
913
919
  const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
914
920
  const load = React.useCallback(async () => {
915
921
  const { data } = await get(`/${PLUGIN_ID}/builder/forms`);
@@ -920,6 +926,17 @@ const FormsList = () => {
920
926
  get(
921
927
  `/${PLUGIN_ID}/builder/import/sources`
922
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
+ );
923
940
  }, [load]);
924
941
  const runImport = async () => {
925
942
  if (!sourceId) return;
@@ -935,6 +952,48 @@ const FormsList = () => {
935
952
  setBusy(false);
936
953
  }
937
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
+ };
938
997
  const create = async () => {
939
998
  if (!name.trim()) return;
940
999
  setBusy(true);
@@ -987,6 +1046,7 @@ const FormsList = () => {
987
1046
  ) })
988
1047
  ] }),
989
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") }),
990
1050
  /* @__PURE__ */ jsx(
991
1051
  TextInput,
992
1052
  {
@@ -1001,38 +1061,74 @@ const FormsList = () => {
1001
1061
  ] })
1002
1062
  ] }),
1003
1063
  error && /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }) }),
1004
- 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: [
1005
1116
  /* @__PURE__ */ jsxs(Box, { children: [
1006
- /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("forms.import-title", "Import an existing form") }),
1007
- /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", tag: "p", children: t(
1008
- "forms.import-hint",
1009
- "Converts a legacy content-type form (all locales) into a draft here — same slug, source untouched. Re-importing overwrites the draft."
1010
- ) })
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)) })
1011
1119
  ] }),
1012
- /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1013
- /* @__PURE__ */ jsx(
1014
- SingleSelect,
1015
- {
1016
- "aria-label": t("forms.import-source", "Form to import"),
1017
- placeholder: t("forms.import-source", "Form to import"),
1018
- value: sourceId,
1019
- onChange: (v) => setSourceId(String(v)),
1020
- children: sources.map((s) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: s.documentId, children: [
1021
- s.name,
1022
- " ",
1023
- s.slug ? `(${s.slug})` : ""
1024
- ] }, s.documentId))
1025
- }
1026
- ),
1027
- /* @__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") })
1028
1123
  ] })
1029
1124
  ] }) }),
1030
- !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: [
1031
1126
  /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1032
1127
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-name", "Name") }) }),
1033
1128
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-slug", "Slug") }) }),
1034
1129
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-structure", "Structure") }) }),
1035
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") }) }),
1036
1132
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-updated", "Updated") }) }),
1037
1133
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-actions", "Actions") }) })
1038
1134
  ] }) }),
@@ -1049,6 +1145,15 @@ const FormsList = () => {
1049
1145
  fields: row.fields
1050
1146
  }) }) }),
1051
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") }) }),
1148
+ /* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: row.submissions > 0 ? /* @__PURE__ */ jsx(
1149
+ Button,
1150
+ {
1151
+ variant: "ghost",
1152
+ startIcon: /* @__PURE__ */ jsx(List, {}),
1153
+ onClick: () => navigate(`submissions?form=${encodeURIComponent(row.slug)}`),
1154
+ children: row.submissions
1155
+ }
1156
+ ) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "—" }) }),
1052
1157
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.updatedAt ? new Date(row.updatedAt).toLocaleString() : "—" }) }),
1053
1158
  /* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
1054
1159
  /* @__PURE__ */ jsx(
@@ -1077,8 +1182,169 @@ const FormsList = () => {
1077
1182
  ] })
1078
1183
  ] });
1079
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
+ };
1080
1345
  const Forms = () => /* @__PURE__ */ jsxs(Routes, { children: [
1081
1346
  /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(FormsList, {}) }),
1347
+ /* @__PURE__ */ jsx(Route, { path: "submissions", element: /* @__PURE__ */ jsx(Submissions, {}) }),
1082
1348
  /* @__PURE__ */ jsx(Route, { path: ":documentId", element: /* @__PURE__ */ jsx(FormEditor, {}) })
1083
1349
  ] });
1084
1350
  export {