strapi-plugin-hubspot 0.16.7 → 0.16.8

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 (29) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/_chunks/{Forms-DyV6KyiO.mjs → Forms-CUNHDvhI.mjs} +52 -6
  3. package/dist/_chunks/{Forms-uUN4XjhW.js → Forms-CylJ0nD3.js} +52 -6
  4. package/dist/_chunks/{HubspotFormInput-BkWat_wu.js → HubspotFormInput-D2H2u9fZ.js} +1 -1
  5. package/dist/_chunks/{HubspotFormInput-DEXPX-6j.mjs → HubspotFormInput-Dppgr_rd.mjs} +1 -1
  6. package/dist/_chunks/{HubspotObjectInput-CFDYW-TM.js → HubspotObjectInput-BjRCaqXr.js} +3 -3
  7. package/dist/_chunks/{HubspotObjectInput-DL8fYK2x.mjs → HubspotObjectInput-CWFddNNw.mjs} +3 -3
  8. package/dist/_chunks/{HubspotPropertyInput-CFMkbQeT.js → HubspotPropertyInput-BlDarkcj.js} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-WU5FMJj3.mjs → HubspotPropertyInput-TGG9ozTa.mjs} +3 -3
  10. package/dist/_chunks/{Settings-B_hhdpHa.js → Settings-DOx7YSqA.js} +2 -2
  11. package/dist/_chunks/{Settings-CiXEps_C.mjs → Settings-tct49GYU.mjs} +2 -2
  12. package/dist/_chunks/{index-BeBn7bJa.mjs → index-Au3W85GX.mjs} +5 -5
  13. package/dist/_chunks/{index-CG_WQzSa.js → index-DhGVA3ZL.js} +5 -5
  14. package/dist/_chunks/{objectLabels-CS_0QCNz.mjs → objectLabels-3rnJ0WUc.mjs} +1 -1
  15. package/dist/_chunks/{objectLabels-BGq-Ypgn.js → objectLabels-Bx7x8fFO.js} +1 -1
  16. package/dist/_chunks/{useHubspotSchema-CP7LRdzO.mjs → useHubspotSchema-BlVXfwju.mjs} +1 -1
  17. package/dist/_chunks/{useHubspotSchema-BZP2qkAu.js → useHubspotSchema-DrGeidXC.js} +1 -1
  18. package/dist/admin/index.js +1 -1
  19. package/dist/admin/index.mjs +1 -1
  20. package/dist/admin/src/builder/types.d.ts +2 -1
  21. package/dist/server/index.js +229 -23
  22. package/dist/server/index.mjs +229 -23
  23. package/dist/server/src/conditions.d.ts +1 -0
  24. package/dist/server/src/formSync.d.ts +20 -0
  25. package/dist/server/src/formsAdmin.d.ts +1 -0
  26. package/dist/server/src/hsforms.d.ts +2 -0
  27. package/dist/server/src/importHubspot.d.ts +2 -1
  28. package/dist/server/src/index.d.ts +1 -0
  29. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.16.8 — 2026-09-24
6
+
7
+ ### Added
8
+ - Publishing a form linked to HubSpot is blocked when a required marketing
9
+ field is not collected. Submissions show the conversion error next to
10
+ **Contact only**.
11
+ - **Create in HubSpot** builds a marketing form from the Strapi definition
12
+ and stores its GUID.
13
+ - Resync updates conditions that came from HubSpot and leaves editor-written
14
+ ones. With field sync on, `visibleIf` is pushed back as HubSpot dependent
15
+ fields. Progressive groups are reported on import.
16
+
5
17
  ## 0.16.7 — 2026-09-23
6
18
 
7
19
  ### Fixed
@@ -5,12 +5,12 @@ import { u as useIntl } from "./index-CEh8vkxY.mjs";
5
5
  import { Typography, Flex, Box, SingleSelect, SingleSelectOption, MultiSelect, MultiSelectOption, TextInput, IconButton, Button, Combobox, ComboboxOption, Field, Textarea, Checkbox, Divider, Loader, Badge, Table, Thead, Tr, Th, Tbody, Td, Modal } from "@strapi/design-system";
6
6
  import { Trash, Plus, ArrowClockwise, ArrowLeft, Drag, 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-BeBn7bJa.mjs";
8
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-Au3W85GX.mjs";
9
9
  import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, useDroppable, closestCorners } from "@dnd-kit/core";
10
10
  import { arrayMove, sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable";
11
11
  import { CSS } from "@dnd-kit/utilities";
12
- import { o as objectLabel } from "./objectLabels-CS_0QCNz.mjs";
13
- import { u as useHubspotSchema } from "./useHubspotSchema-CP7LRdzO.mjs";
12
+ import { o as objectLabel } from "./objectLabels-3rnJ0WUc.mjs";
13
+ import { u as useHubspotSchema } from "./useHubspotSchema-BlVXfwju.mjs";
14
14
  const OPERATORS = [
15
15
  "eq",
16
16
  "neq",
@@ -682,7 +682,7 @@ const FieldPanel = ({ field, candidates, problem, onChange }) => {
682
682
  {
683
683
  condition: field.visibleIf,
684
684
  candidates,
685
- onChange: (visibleIf) => onChange({ visibleIf })
685
+ onChange: (visibleIf) => onChange({ visibleIf, visibleIfSource: "editor" })
686
686
  }
687
687
  )
688
688
  ] })
@@ -882,9 +882,14 @@ const FormEditor = () => {
882
882
  const data = err.response?.data;
883
883
  setErrors(data?.errors ?? []);
884
884
  setProblems(data?.problems ?? []);
885
+ const missing = data?.missingHubspot ?? [];
885
886
  setFeedback({
886
887
  tone: "danger",
887
- text: t("editor.publish-blocked", "Publishing is blocked — fix the flagged mappings first.")
888
+ text: missing.length ? t(
889
+ "editor.publish-missing-hubspot",
890
+ "Publishing is blocked — the linked HubSpot form requires {fields}, which this form does not collect.",
891
+ { fields: missing.map((field) => `${field.object}.${field.name}`).join(", ") }
892
+ ) : t("editor.publish-blocked", "Publishing is blocked — fix the flagged mappings first.")
888
893
  });
889
894
  } finally {
890
895
  setBusy(false);
@@ -902,6 +907,29 @@ const FormEditor = () => {
902
907
  setBusy(false);
903
908
  }
904
909
  };
910
+ const createInHubspot = async () => {
911
+ if (dirty && !await save()) return;
912
+ setBusy(true);
913
+ setFeedback(null);
914
+ try {
915
+ const { data } = await post(
916
+ `/${PLUGIN_ID}/builder/forms/${documentId}/create-hubspot${query}`
917
+ );
918
+ if (data.form) setEntry(data.form);
919
+ setFeedback({
920
+ tone: "success",
921
+ text: t("editor.created-hubspot", "HubSpot form created and linked.")
922
+ });
923
+ } catch (err) {
924
+ const message = err.response?.data?.error?.message;
925
+ setFeedback({
926
+ tone: "danger",
927
+ text: message || t("editor.create-hubspot-error", "Could not create the HubSpot form.")
928
+ });
929
+ } finally {
930
+ setBusy(false);
931
+ }
932
+ };
905
933
  const resync = async () => {
906
934
  if (dirty && !await save()) return;
907
935
  setBusy(true);
@@ -982,6 +1010,15 @@ const FormEditor = () => {
982
1010
  children: t("editor.resync", "Resync from HubSpot")
983
1011
  }
984
1012
  ),
1013
+ /* @__PURE__ */ jsx(
1014
+ Button,
1015
+ {
1016
+ variant: "tertiary",
1017
+ onClick: createInHubspot,
1018
+ disabled: busy || Boolean(entry.hubspotFormId),
1019
+ children: t("editor.create-hubspot", "Create in HubSpot")
1020
+ }
1021
+ ),
985
1022
  /* @__PURE__ */ jsx(Button, { onClick: publish, disabled: busy, children: t("editor.publish", "Publish") }),
986
1023
  published && /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: unpublish, disabled: busy, children: t("editor.unpublish", "Unpublish") })
987
1024
  ] })
@@ -1468,6 +1505,12 @@ const FormsList = () => {
1468
1505
  return t("forms.skipped-rich-text", "A content block was skipped — the builder has no rich-text element");
1469
1506
  case "legal-consent":
1470
1507
  return t("forms.skipped-consent", "The GDPR consent block was skipped — rebuild it as a field if needed");
1508
+ case "progressive":
1509
+ return t(
1510
+ "forms.skipped-progressive",
1511
+ "A progressive field group was skipped — it is not a visibility condition ({detail})",
1512
+ { detail: item.detail ?? "" }
1513
+ );
1471
1514
  }
1472
1515
  };
1473
1516
  const create = async () => {
@@ -1774,7 +1817,10 @@ const Submissions = () => {
1774
1817
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: row.formTitle || row.form }) }),
1775
1818
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: row.email || "—" }) }),
1776
1819
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", ellipsis: true, children: preview(row.values) }) }),
1777
- /* @__PURE__ */ jsx(Td, { children: row.meta?.hubspotConversion === true || row.meta?.hubspotConversion === "true" ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.converted", "Conversion") }) : row.hubspotSynced ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.contact-only", "Contact only") }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1820
+ /* @__PURE__ */ jsx(Td, { children: row.meta?.hubspotConversion === true || row.meta?.hubspotConversion === "true" ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.converted", "Conversion") }) : row.hubspotSynced ? /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
1821
+ /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.contact-only", "Contact only") }),
1822
+ typeof row.meta?.hubspotError === "string" && row.meta.hubspotError && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: row.meta.hubspotError })
1823
+ ] }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1778
1824
  ]
1779
1825
  },
1780
1826
  row.documentId
@@ -7,12 +7,12 @@ const index = require("./index-DkTxsEqL.js");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
9
  const admin = require("@strapi/strapi/admin");
10
- const index$1 = require("./index-CG_WQzSa.js");
10
+ const index$1 = require("./index-DhGVA3ZL.js");
11
11
  const core = require("@dnd-kit/core");
12
12
  const sortable = require("@dnd-kit/sortable");
13
13
  const utilities = require("@dnd-kit/utilities");
14
- const objectLabels = require("./objectLabels-BGq-Ypgn.js");
15
- const useHubspotSchema = require("./useHubspotSchema-BZP2qkAu.js");
14
+ const objectLabels = require("./objectLabels-Bx7x8fFO.js");
15
+ const useHubspotSchema = require("./useHubspotSchema-DrGeidXC.js");
16
16
  function _interopNamespace(e) {
17
17
  if (e && e.__esModule) return e;
18
18
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -702,7 +702,7 @@ const FieldPanel = ({ field, candidates, problem, onChange }) => {
702
702
  {
703
703
  condition: field.visibleIf,
704
704
  candidates,
705
- onChange: (visibleIf) => onChange({ visibleIf })
705
+ onChange: (visibleIf) => onChange({ visibleIf, visibleIfSource: "editor" })
706
706
  }
707
707
  )
708
708
  ] })
@@ -902,9 +902,14 @@ const FormEditor = () => {
902
902
  const data = err.response?.data;
903
903
  setErrors(data?.errors ?? []);
904
904
  setProblems(data?.problems ?? []);
905
+ const missing = data?.missingHubspot ?? [];
905
906
  setFeedback({
906
907
  tone: "danger",
907
- text: t("editor.publish-blocked", "Publishing is blocked — fix the flagged mappings first.")
908
+ text: missing.length ? t(
909
+ "editor.publish-missing-hubspot",
910
+ "Publishing is blocked — the linked HubSpot form requires {fields}, which this form does not collect.",
911
+ { fields: missing.map((field) => `${field.object}.${field.name}`).join(", ") }
912
+ ) : t("editor.publish-blocked", "Publishing is blocked — fix the flagged mappings first.")
908
913
  });
909
914
  } finally {
910
915
  setBusy(false);
@@ -922,6 +927,29 @@ const FormEditor = () => {
922
927
  setBusy(false);
923
928
  }
924
929
  };
930
+ const createInHubspot = async () => {
931
+ if (dirty && !await save()) return;
932
+ setBusy(true);
933
+ setFeedback(null);
934
+ try {
935
+ const { data } = await post(
936
+ `/${index$1.PLUGIN_ID}/builder/forms/${documentId}/create-hubspot${query}`
937
+ );
938
+ if (data.form) setEntry(data.form);
939
+ setFeedback({
940
+ tone: "success",
941
+ text: t("editor.created-hubspot", "HubSpot form created and linked.")
942
+ });
943
+ } catch (err) {
944
+ const message = err.response?.data?.error?.message;
945
+ setFeedback({
946
+ tone: "danger",
947
+ text: message || t("editor.create-hubspot-error", "Could not create the HubSpot form.")
948
+ });
949
+ } finally {
950
+ setBusy(false);
951
+ }
952
+ };
925
953
  const resync = async () => {
926
954
  if (dirty && !await save()) return;
927
955
  setBusy(true);
@@ -1002,6 +1030,15 @@ const FormEditor = () => {
1002
1030
  children: t("editor.resync", "Resync from HubSpot")
1003
1031
  }
1004
1032
  ),
1033
+ /* @__PURE__ */ jsxRuntime.jsx(
1034
+ designSystem.Button,
1035
+ {
1036
+ variant: "tertiary",
1037
+ onClick: createInHubspot,
1038
+ disabled: busy || Boolean(entry.hubspotFormId),
1039
+ children: t("editor.create-hubspot", "Create in HubSpot")
1040
+ }
1041
+ ),
1005
1042
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: publish, disabled: busy, children: t("editor.publish", "Publish") }),
1006
1043
  published && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: unpublish, disabled: busy, children: t("editor.unpublish", "Unpublish") })
1007
1044
  ] })
@@ -1488,6 +1525,12 @@ const FormsList = () => {
1488
1525
  return t("forms.skipped-rich-text", "A content block was skipped — the builder has no rich-text element");
1489
1526
  case "legal-consent":
1490
1527
  return t("forms.skipped-consent", "The GDPR consent block was skipped — rebuild it as a field if needed");
1528
+ case "progressive":
1529
+ return t(
1530
+ "forms.skipped-progressive",
1531
+ "A progressive field group was skipped — it is not a visibility condition ({detail})",
1532
+ { detail: item.detail ?? "" }
1533
+ );
1491
1534
  }
1492
1535
  };
1493
1536
  const create = async () => {
@@ -1794,7 +1837,10 @@ const Submissions = () => {
1794
1837
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { children: row.formTitle || row.form }) }),
1795
1838
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "semiBold", children: row.email || "—" }) }),
1796
1839
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", ellipsis: true, children: preview(row.values) }) }),
1797
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: row.meta?.hubspotConversion === true || row.meta?.hubspotConversion === "true" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.converted", "Conversion") }) : row.hubspotSynced ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.contact-only", "Contact only") }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1840
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: row.meta?.hubspotConversion === true || row.meta?.hubspotConversion === "true" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.converted", "Conversion") }) : row.hubspotSynced ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
1841
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.contact-only", "Contact only") }),
1842
+ typeof row.meta?.hubspotError === "string" && row.meta.hubspotError && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: row.meta.hubspotError })
1843
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
1798
1844
  ]
1799
1845
  },
1800
1846
  row.documentId
@@ -5,7 +5,7 @@ const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const admin = require("@strapi/strapi/admin");
8
- const index$1 = require("./index-CG_WQzSa.js");
8
+ const index$1 = require("./index-DhGVA3ZL.js");
9
9
  function _interopNamespace(e) {
10
10
  if (e && e.__esModule) return e;
11
11
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -3,7 +3,7 @@ import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, Combobox, ComboboxOption } from "@strapi/design-system";
5
5
  import { useFetchClient } from "@strapi/strapi/admin";
6
- import { P as PLUGIN_ID, g as getTranslation } from "./index-BeBn7bJa.mjs";
6
+ import { P as PLUGIN_ID, g as getTranslation } from "./index-Au3W85GX.mjs";
7
7
  const HubspotFormInput = React.forwardRef(
8
8
  ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
9
9
  const { formatMessage } = useIntl();
@@ -4,9 +4,9 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
- const index$1 = require("./index-CG_WQzSa.js");
8
- const objectLabels = require("./objectLabels-BGq-Ypgn.js");
9
- const useHubspotSchema = require("./useHubspotSchema-BZP2qkAu.js");
7
+ const index$1 = require("./index-DhGVA3ZL.js");
8
+ const objectLabels = require("./objectLabels-Bx7x8fFO.js");
9
+ const useHubspotSchema = require("./useHubspotSchema-DrGeidXC.js");
10
10
  function _interopNamespace(e) {
11
11
  if (e && e.__esModule) return e;
12
12
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -2,9 +2,9 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, SingleSelect, SingleSelectOption } from "@strapi/design-system";
5
- import { g as getTranslation } from "./index-BeBn7bJa.mjs";
6
- import { o as objectLabel } from "./objectLabels-CS_0QCNz.mjs";
7
- import { u as useHubspotSchema } from "./useHubspotSchema-CP7LRdzO.mjs";
5
+ import { g as getTranslation } from "./index-Au3W85GX.mjs";
6
+ import { o as objectLabel } from "./objectLabels-3rnJ0WUc.mjs";
7
+ import { u as useHubspotSchema } from "./useHubspotSchema-BlVXfwju.mjs";
8
8
  const HubspotObjectInput = React.forwardRef(
9
9
  ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
10
10
  const intl = useIntl();
@@ -6,9 +6,9 @@ const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const icons = require("@strapi/icons");
8
8
  const admin = require("@strapi/strapi/admin");
9
- const index$1 = require("./index-CG_WQzSa.js");
10
- const objectLabels = require("./objectLabels-BGq-Ypgn.js");
11
- const useHubspotSchema = require("./useHubspotSchema-BZP2qkAu.js");
9
+ const index$1 = require("./index-DhGVA3ZL.js");
10
+ const objectLabels = require("./objectLabels-Bx7x8fFO.js");
11
+ const useHubspotSchema = require("./useHubspotSchema-DrGeidXC.js");
12
12
  function _interopNamespace(e) {
13
13
  if (e && e.__esModule) return e;
14
14
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -4,9 +4,9 @@ import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, Combobox, ComboboxOption, IconButton, Button, Typography, Link } from "@strapi/design-system";
5
5
  import { ArrowClockwise } from "@strapi/icons";
6
6
  import { useField } from "@strapi/strapi/admin";
7
- import { g as getTranslation } from "./index-BeBn7bJa.mjs";
8
- import { o as objectLabel } from "./objectLabels-CS_0QCNz.mjs";
9
- import { u as useHubspotSchema } from "./useHubspotSchema-CP7LRdzO.mjs";
7
+ import { g as getTranslation } from "./index-Au3W85GX.mjs";
8
+ import { o as objectLabel } from "./objectLabels-3rnJ0WUc.mjs";
9
+ import { u as useHubspotSchema } from "./useHubspotSchema-BlVXfwju.mjs";
10
10
  const oneLine = {
11
11
  display: "block",
12
12
  whiteSpace: "nowrap",
@@ -5,9 +5,9 @@ const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const admin = require("@strapi/strapi/admin");
8
- const index$1 = require("./index-CG_WQzSa.js");
8
+ const index$1 = require("./index-DhGVA3ZL.js");
9
9
  const reactRouterDom = require("react-router-dom");
10
- const objectLabels = require("./objectLabels-BGq-Ypgn.js");
10
+ const objectLabels = require("./objectLabels-Bx7x8fFO.js");
11
11
  function _interopNamespace(e) {
12
12
  if (e && e.__esModule) return e;
13
13
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -3,9 +3,9 @@ import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Flex, Divider, Typography, Button, Box, Link, Badge, Loader, Field, SingleSelect, SingleSelectOption, Checkbox } from "@strapi/design-system";
5
5
  import { useFetchClient } from "@strapi/strapi/admin";
6
- import { g as getTranslation, P as PLUGIN_ID } from "./index-BeBn7bJa.mjs";
6
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-Au3W85GX.mjs";
7
7
  import { Link as Link$1 } from "react-router-dom";
8
- import { o as objectLabel } from "./objectLabels-CS_0QCNz.mjs";
8
+ import { o as objectLabel } from "./objectLabels-3rnJ0WUc.mjs";
9
9
  const AuditSection = ({ configured }) => {
10
10
  const intl = useIntl();
11
11
  const { formatMessage } = intl;
@@ -25,7 +25,7 @@ const index = {
25
25
  icon: PaperPlane,
26
26
  intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
27
27
  permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
28
- Component: async () => (await import("./Forms-DyV6KyiO.mjs")).default
28
+ Component: async () => (await import("./Forms-CUNHDvhI.mjs")).default
29
29
  });
30
30
  app.customFields.register({
31
31
  name: "property",
@@ -40,7 +40,7 @@ const index = {
40
40
  defaultMessage: "Picked from the portal's real properties"
41
41
  },
42
42
  components: {
43
- Input: async () => import("./HubspotPropertyInput-WU5FMJj3.mjs")
43
+ Input: async () => import("./HubspotPropertyInput-TGG9ozTa.mjs")
44
44
  },
45
45
  // Exposed in the Content-Type Builder. Without this, `objectField` could
46
46
  // only be set by hand-editing schema.json — which is not how anyone
@@ -103,7 +103,7 @@ const index = {
103
103
  defaultMessage: "Picked from the portal's configured objects"
104
104
  },
105
105
  components: {
106
- Input: async () => import("./HubspotObjectInput-DL8fYK2x.mjs")
106
+ Input: async () => import("./HubspotObjectInput-CWFddNNw.mjs")
107
107
  }
108
108
  });
109
109
  app.customFields.register({
@@ -119,7 +119,7 @@ const index = {
119
119
  defaultMessage: "A form built in the HubSpot form builder, stored by slug"
120
120
  },
121
121
  components: {
122
- Input: async () => import("./HubspotFormInput-DEXPX-6j.mjs")
122
+ Input: async () => import("./HubspotFormInput-Dppgr_rd.mjs")
123
123
  }
124
124
  });
125
125
  app.createSettingSection(
@@ -135,7 +135,7 @@ const index = {
135
135
  // Mirrors the RBAC action registered server-side: a role without it
136
136
  // doesn't see the link at all — the routes refuse it anyway.
137
137
  permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
138
- Component: async () => (await import("./Settings-CiXEps_C.mjs")).default
138
+ Component: async () => (await import("./Settings-tct49GYU.mjs")).default
139
139
  }
140
140
  ]
141
141
  );
@@ -26,7 +26,7 @@ const index = {
26
26
  icon: icons.PaperPlane,
27
27
  intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
28
28
  permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
29
- Component: async () => (await Promise.resolve().then(() => require("./Forms-uUN4XjhW.js"))).default
29
+ Component: async () => (await Promise.resolve().then(() => require("./Forms-CylJ0nD3.js"))).default
30
30
  });
31
31
  app.customFields.register({
32
32
  name: "property",
@@ -41,7 +41,7 @@ const index = {
41
41
  defaultMessage: "Picked from the portal's real properties"
42
42
  },
43
43
  components: {
44
- Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-CFMkbQeT.js"))
44
+ Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-BlDarkcj.js"))
45
45
  },
46
46
  // Exposed in the Content-Type Builder. Without this, `objectField` could
47
47
  // only be set by hand-editing schema.json — which is not how anyone
@@ -104,7 +104,7 @@ const index = {
104
104
  defaultMessage: "Picked from the portal's configured objects"
105
105
  },
106
106
  components: {
107
- Input: async () => Promise.resolve().then(() => require("./HubspotObjectInput-CFDYW-TM.js"))
107
+ Input: async () => Promise.resolve().then(() => require("./HubspotObjectInput-BjRCaqXr.js"))
108
108
  }
109
109
  });
110
110
  app.customFields.register({
@@ -120,7 +120,7 @@ const index = {
120
120
  defaultMessage: "A form built in the HubSpot form builder, stored by slug"
121
121
  },
122
122
  components: {
123
- Input: async () => Promise.resolve().then(() => require("./HubspotFormInput-BkWat_wu.js"))
123
+ Input: async () => Promise.resolve().then(() => require("./HubspotFormInput-D2H2u9fZ.js"))
124
124
  }
125
125
  });
126
126
  app.createSettingSection(
@@ -136,7 +136,7 @@ const index = {
136
136
  // Mirrors the RBAC action registered server-side: a role without it
137
137
  // doesn't see the link at all — the routes refuse it anyway.
138
138
  permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
139
- Component: async () => (await Promise.resolve().then(() => require("./Settings-B_hhdpHa.js"))).default
139
+ Component: async () => (await Promise.resolve().then(() => require("./Settings-DOx7YSqA.js"))).default
140
140
  }
141
141
  ]
142
142
  );
@@ -1,4 +1,4 @@
1
- import { g as getTranslation } from "./index-BeBn7bJa.mjs";
1
+ import { g as getTranslation } from "./index-Au3W85GX.mjs";
2
2
  const STANDARD = /* @__PURE__ */ new Set([
3
3
  "contact",
4
4
  "company",
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const index = require("./index-CG_WQzSa.js");
2
+ const index = require("./index-DhGVA3ZL.js");
3
3
  const STANDARD = /* @__PURE__ */ new Set([
4
4
  "contact",
5
5
  "company",
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { useFetchClient } from "@strapi/strapi/admin";
3
- import { P as PLUGIN_ID } from "./index-BeBn7bJa.mjs";
3
+ import { P as PLUGIN_ID } from "./index-Au3W85GX.mjs";
4
4
  let cached = null;
5
5
  let inFlight = null;
6
6
  const listeners = /* @__PURE__ */ new Set();
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  const React = require("react");
3
3
  const admin = require("@strapi/strapi/admin");
4
- const index = require("./index-CG_WQzSa.js");
4
+ const index = require("./index-DhGVA3ZL.js");
5
5
  function _interopNamespace(e) {
6
6
  if (e && e.__esModule) return e;
7
7
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-CG_WQzSa.js");
2
+ const index = require("../_chunks/index-DhGVA3ZL.js");
3
3
  require("@strapi/icons");
4
4
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-BeBn7bJa.mjs";
1
+ import { i } from "../_chunks/index-Au3W85GX.mjs";
2
2
  import "@strapi/icons";
3
3
  export {
4
4
  i as default
@@ -45,6 +45,7 @@ export interface FormField {
45
45
  /** Company fields: rotating placeholder examples, one per line. */
46
46
  placeholderExamples?: string[];
47
47
  visibleIf?: Condition | null;
48
+ visibleIfSource?: "hubspot" | "editor" | null;
48
49
  }
49
50
  export interface FormStep {
50
51
  id: string;
@@ -92,7 +93,7 @@ export interface HubspotSource {
92
93
  }
93
94
  /** What a HubSpot import couldn't carry over (mirrors server SkippedItem). */
94
95
  export interface SkippedItem {
95
- code: "field-type" | "hidden-field" | "object" | "duplicate" | "condition" | "rich-text" | "legal-consent";
96
+ code: "field-type" | "hidden-field" | "object" | "duplicate" | "condition" | "rich-text" | "legal-consent" | "progressive";
96
97
  label?: string;
97
98
  detail?: string;
98
99
  }
@@ -1600,6 +1600,48 @@ function missingMappedFields(definition, existingNames) {
1600
1600
  const existing = new Set(existingNames);
1601
1601
  return mappedContactFields(definition).filter((field) => !existing.has(field.property));
1602
1602
  }
1603
+ const OBJECT_TYPE_BY_NAME = {
1604
+ contact: "0-1",
1605
+ company: "0-2"
1606
+ };
1607
+ function collectedHubspotKeys(definition) {
1608
+ const keys = /* @__PURE__ */ new Set();
1609
+ const add = (object, property) => {
1610
+ const typeId = OBJECT_TYPE_BY_NAME[object] || object;
1611
+ if (property) keys.add(`${typeId}:${property}`);
1612
+ };
1613
+ for (const step of definition.steps ?? []) {
1614
+ for (const fld of step.fields ?? []) {
1615
+ if (fld.type === "company") {
1616
+ const map = fld.companyMap ?? {};
1617
+ for (const slot of Object.values(map)) {
1618
+ if (slot?.property?.trim()) add(slot.object?.trim() || "company", slot.property.trim());
1619
+ }
1620
+ continue;
1621
+ }
1622
+ const mapping = fld.hubspot ?? {};
1623
+ const object = mapping.object?.trim() || "contact";
1624
+ const property = mapping.property?.trim() || fld.name;
1625
+ if (property) add(object, property);
1626
+ }
1627
+ }
1628
+ return keys;
1629
+ }
1630
+ function missingRequiredHubspotFields(definition, required) {
1631
+ const collected = collectedHubspotKeys(definition);
1632
+ const missing = [];
1633
+ for (const field of required) {
1634
+ if (!field.name) continue;
1635
+ const objectTypeId = field.objectTypeId?.trim() || "0-1";
1636
+ if (collected.has(`${objectTypeId}:${field.name}`)) continue;
1637
+ missing.push({
1638
+ name: field.name,
1639
+ objectTypeId,
1640
+ object: objectTypeId === "0-2" ? "company" : "contact"
1641
+ });
1642
+ }
1643
+ return missing;
1644
+ }
1603
1645
  function hsFieldType(type, options) {
1604
1646
  switch (type) {
1605
1647
  case "email":
@@ -1650,6 +1692,74 @@ function appendFieldGroups(existing, extra) {
1650
1692
  }))
1651
1693
  ];
1652
1694
  }
1695
+ function conditionToHubspot(condition, parentId) {
1696
+ const rules = (condition.rules ?? []).filter((rule) => rule.field === parentId);
1697
+ if (!rules.length || rules.length !== (condition.rules ?? []).length) return null;
1698
+ const op = rules[0]?.operator;
1699
+ if (rules.some((rule) => rule.operator !== op)) return null;
1700
+ const values = rules.map((rule) => rule.value).filter((value) => Boolean(value));
1701
+ switch (op) {
1702
+ case "eq":
1703
+ return { operator: values.length > 1 ? "SET_ANY" : "EQ", values };
1704
+ case "neq":
1705
+ return { operator: values.length > 1 ? "NOT_SET_ANY" : "NEQ", values };
1706
+ case "contains":
1707
+ return { operator: "CONTAINS", values };
1708
+ case "gt":
1709
+ return { operator: "GT", values };
1710
+ case "lt":
1711
+ return { operator: "LT", values };
1712
+ case "notEmpty":
1713
+ return { operator: "SET", values: [] };
1714
+ case "empty":
1715
+ return { operator: "NOT_SET", values: [] };
1716
+ default:
1717
+ return null;
1718
+ }
1719
+ }
1720
+ function applyVisibleIfToFieldGroups(groups, definition) {
1721
+ const byId = /* @__PURE__ */ new Map();
1722
+ for (const step of definition.steps ?? []) {
1723
+ for (const fld of step.fields ?? []) {
1724
+ const mapping = fld.hubspot ?? {};
1725
+ byId.set(fld.id, { name: fld.name, property: mapping.property?.trim() || fld.name });
1726
+ }
1727
+ }
1728
+ const moves = /* @__PURE__ */ new Map();
1729
+ for (const step of definition.steps ?? []) {
1730
+ for (const fld of step.fields ?? []) {
1731
+ const visibleIf = fld.visibleIf;
1732
+ const parentId = visibleIf?.rules?.[0]?.field;
1733
+ if (!visibleIf || !parentId) continue;
1734
+ const converted = conditionToHubspot(visibleIf, parentId);
1735
+ const parent = byId.get(parentId);
1736
+ const mapping = fld.hubspot ?? {};
1737
+ const property = mapping.property?.trim() || fld.name;
1738
+ if (!converted || !parent) continue;
1739
+ moves.set(property, { parent: parent.property, ...converted });
1740
+ }
1741
+ }
1742
+ if (!moves.size) return groups;
1743
+ const cloned = JSON.parse(JSON.stringify(groups));
1744
+ const flat = [];
1745
+ for (const group of cloned) {
1746
+ (group.fields ?? []).forEach((field, index2) => flat.push({ group, index: index2, field }));
1747
+ }
1748
+ for (const [property, move] of moves) {
1749
+ const child = flat.find((item) => item.field.name === property);
1750
+ const parent = flat.find((item) => item.field.name === move.parent);
1751
+ if (!child || !parent || child === parent) continue;
1752
+ parent.field.dependentFields = [
1753
+ ...parent.field.dependentFields ?? [],
1754
+ {
1755
+ dependentCondition: { operator: move.operator, values: move.values },
1756
+ field: child.field
1757
+ }
1758
+ ];
1759
+ child.group.fields?.splice(child.index, 1);
1760
+ }
1761
+ return cloned;
1762
+ }
1653
1763
  const HS_BASE$2 = "https://api.hubapi.com";
1654
1764
  const OBJECT_BY_TYPE_ID = {
1655
1765
  "0-1": "contact",
@@ -1731,7 +1841,7 @@ function convertField(raw, ctx) {
1731
1841
  helpText: raw.description || void 0,
1732
1842
  ...options.length ? { options } : {},
1733
1843
  hubspot: { object, property: name },
1734
- ...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
1844
+ ...ctx.parentCondition ? { visibleIf: ctx.parentCondition, visibleIfSource: "hubspot" } : {}
1735
1845
  };
1736
1846
  const out = [field];
1737
1847
  for (const dependent of raw.dependentFields ?? []) {
@@ -1754,6 +1864,12 @@ function convertHubspotForm(raw) {
1754
1864
  const usedNames = /* @__PURE__ */ new Set();
1755
1865
  const fields = [];
1756
1866
  for (const group of raw.fieldGroups ?? []) {
1867
+ if (group.groupType === "progressive" || group.groupType === "queued") {
1868
+ skipped.push({
1869
+ code: "progressive",
1870
+ detail: group.groupType
1871
+ });
1872
+ }
1757
1873
  if (group.richText && !(group.fields ?? []).length) {
1758
1874
  skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
1759
1875
  continue;
@@ -1825,7 +1941,10 @@ function mergeHubspotImport(current, incoming) {
1825
1941
  existing.helpText = next.helpText;
1826
1942
  if (next.options) existing.options = next.options;
1827
1943
  else delete existing.options;
1828
- if (!existing.visibleIf && next.visibleIf) existing.visibleIf = next.visibleIf;
1944
+ if (next.visibleIf && (existing.visibleIfSource === "hubspot" || !existing.visibleIf)) {
1945
+ existing.visibleIf = next.visibleIf;
1946
+ if (existing.visibleIfSource !== "editor") existing.visibleIfSource = "hubspot";
1947
+ }
1829
1948
  updated.push(key);
1830
1949
  continue;
1831
1950
  }
@@ -2146,13 +2265,62 @@ async function submitMarketingForm(input) {
2146
2265
  async function syncFieldsToHubspotForm(apiKey, formGuid, definition) {
2147
2266
  const raw = await fetchHubspotForm(apiKey, formGuid);
2148
2267
  const extra = missingMappedFields(definition, formFieldNames(raw));
2149
- if (!extra.length) return [];
2150
- await patchHubspotForm(apiKey, formGuid, {
2151
- fieldGroups: appendFieldGroups(raw.fieldGroups ?? [], extra)
2152
- });
2268
+ const fieldGroups = applyVisibleIfToFieldGroups(
2269
+ appendFieldGroups(raw.fieldGroups ?? [], extra),
2270
+ definition
2271
+ );
2272
+ if (!extra.length && JSON.stringify(fieldGroups) === JSON.stringify(appendFieldGroups(raw.fieldGroups ?? [], []))) {
2273
+ return [];
2274
+ }
2275
+ await patchHubspotForm(apiKey, formGuid, { fieldGroups });
2153
2276
  clearFormShapeCache(formGuid);
2154
2277
  return extra.map((field) => field.property);
2155
2278
  }
2279
+ async function createMarketingForm(apiKey, name, definition) {
2280
+ const fields = mappedContactFields(definition).map((field) => toHsFormField(field));
2281
+ if (!fields.length) {
2282
+ throw new Error("Add at least one mapped contact field before creating a HubSpot form");
2283
+ }
2284
+ const res = await fetch("https://api.hubapi.com/marketing/v3/forms", {
2285
+ method: "POST",
2286
+ headers: {
2287
+ Authorization: `Bearer ${apiKey}`,
2288
+ "Content-Type": "application/json"
2289
+ },
2290
+ body: JSON.stringify({
2291
+ name,
2292
+ formType: "hubspot",
2293
+ fieldGroups: applyVisibleIfToFieldGroups(
2294
+ fields.map((field) => ({
2295
+ groupType: "default_group",
2296
+ richTextType: "text",
2297
+ fields: [field]
2298
+ })),
2299
+ definition
2300
+ ),
2301
+ configuration: {
2302
+ language: "fr",
2303
+ recaptchaEnabled: false,
2304
+ createNewContactForNewEmail: true,
2305
+ prePopulateKnownValues: true,
2306
+ notifyContactOwner: false,
2307
+ notifyRecipients: [],
2308
+ editable: true,
2309
+ archivable: true,
2310
+ cloneable: true,
2311
+ allowLinkToResetKnownValues: false,
2312
+ postSubmitAction: { type: "thank_you", value: "Merci." }
2313
+ },
2314
+ displayOptions: { submitButtonText: "Envoyer", style: { submitButtonText: "Envoyer" } },
2315
+ legalConsentOptions: { type: "none" }
2316
+ })
2317
+ });
2318
+ const body = await res.json().catch(() => ({}));
2319
+ if (!res.ok || !body.id) {
2320
+ throw new Error(body.message || `HubSpot ${res.status}`);
2321
+ }
2322
+ return body.id;
2323
+ }
2156
2324
  function publicForm(entry) {
2157
2325
  return {
2158
2326
  slug: entry.slug,
@@ -2166,7 +2334,9 @@ function publicForm(entry) {
2166
2334
  steps: (entry.definition?.steps ?? []).map((step) => ({
2167
2335
  ...step,
2168
2336
  // The CRM mappings are the server's business — never the browser's.
2169
- fields: (step.fields ?? []).map(({ hubspot: _hubspot, companyMap: _companyMap, ...fld }) => fld)
2337
+ fields: (step.fields ?? []).map(
2338
+ ({ hubspot: _hubspot, companyMap: _companyMap, visibleIfSource: _source, ...fld }) => fld
2339
+ )
2170
2340
  }))
2171
2341
  };
2172
2342
  }
@@ -2910,6 +3080,22 @@ function createFormsAdminController(strapi) {
2910
3080
  ctx.body = { errors: structural, problems: mappings };
2911
3081
  return;
2912
3082
  }
3083
+ const { apiKey } = await resolveApiKey(strapi);
3084
+ const account = await resolveAccount(strapi);
3085
+ const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
3086
+ const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
3087
+ if (apiKey && formGuid) {
3088
+ const shape = await loadFormShape(apiKey, formGuid);
3089
+ const missingHubspot = shape ? missingRequiredHubspotFields(
3090
+ entry.definition ?? EMPTY_DEFINITION,
3091
+ [...shape.fields.filter((field) => field.required)]
3092
+ ) : [];
3093
+ if (missingHubspot.length) {
3094
+ ctx.status = 400;
3095
+ ctx.body = { errors: structural, problems: mappings, missingHubspot };
3096
+ return;
3097
+ }
3098
+ }
2913
3099
  await documents().publish({
2914
3100
  documentId: ctx.params.documentId,
2915
3101
  locale: ctx.query.locale
@@ -2917,22 +3103,16 @@ function createFormsAdminController(strapi) {
2917
3103
  const policy = await resolvePolicy(strapi);
2918
3104
  let addedFields = [];
2919
3105
  let syncWarning;
2920
- if (policy.syncFieldsOnPublish) {
2921
- const { apiKey } = await resolveApiKey(strapi);
2922
- const account = await resolveAccount(strapi);
2923
- const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
2924
- const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
2925
- if (apiKey && formGuid) {
2926
- try {
2927
- addedFields = await syncFieldsToHubspotForm(
2928
- apiKey,
2929
- formGuid,
2930
- entry.definition ?? EMPTY_DEFINITION
2931
- );
2932
- } catch (err) {
2933
- syncWarning = err.message;
2934
- strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
2935
- }
3106
+ if (policy.syncFieldsOnPublish && apiKey && formGuid) {
3107
+ try {
3108
+ addedFields = await syncFieldsToHubspotForm(
3109
+ apiKey,
3110
+ formGuid,
3111
+ entry.definition ?? EMPTY_DEFINITION
3112
+ );
3113
+ } catch (err) {
3114
+ syncWarning = err.message;
3115
+ strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
2936
3116
  }
2937
3117
  }
2938
3118
  ctx.body = {
@@ -2941,6 +3121,31 @@ function createFormsAdminController(strapi) {
2941
3121
  ...syncWarning ? { syncWarning } : {}
2942
3122
  };
2943
3123
  },
3124
+ async createInHubspot(ctx) {
3125
+ const entry = await documents().findOne({
3126
+ documentId: ctx.params.documentId,
3127
+ locale: ctx.query.locale,
3128
+ status: "draft"
3129
+ });
3130
+ if (!entry) ctx.throw(404, "Form not found");
3131
+ const { apiKey } = await resolveApiKey(strapi);
3132
+ if (!apiKey) ctx.throw(400, "HubSpot API key is missing");
3133
+ try {
3134
+ const id = await createMarketingForm(
3135
+ apiKey,
3136
+ entry.name || entry.slug || "Formulaire",
3137
+ entry.definition ?? EMPTY_DEFINITION
3138
+ );
3139
+ const updated = await documents().update({
3140
+ documentId: ctx.params.documentId,
3141
+ locale: ctx.query.locale,
3142
+ data: { hubspotFormId: id }
3143
+ });
3144
+ ctx.body = { form: updated, hubspotFormId: id };
3145
+ } catch (err) {
3146
+ ctx.throw(400, err.message);
3147
+ }
3148
+ },
2944
3149
  async unpublish(ctx) {
2945
3150
  await documents().unpublish({
2946
3151
  documentId: ctx.params.documentId,
@@ -3707,6 +3912,7 @@ const routes = {
3707
3912
  adminRoute("GET", "/builder/forms/:documentId", "formsAdmin.findOne", [FORMS_ACTION]),
3708
3913
  adminRoute("PUT", "/builder/forms/:documentId", "formsAdmin.update", [FORMS_ACTION]),
3709
3914
  adminRoute("POST", "/builder/forms/:documentId/publish", "formsAdmin.publish", [FORMS_ACTION]),
3915
+ adminRoute("POST", "/builder/forms/:documentId/create-hubspot", "formsAdmin.createInHubspot", [FORMS_ACTION]),
3710
3916
  adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
3711
3917
  adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
3712
3918
  adminRoute("POST", "/builder/forms/:documentId/duplicate", "formsAdmin.duplicate", [FORMS_ACTION]),
@@ -1599,6 +1599,48 @@ function missingMappedFields(definition, existingNames) {
1599
1599
  const existing = new Set(existingNames);
1600
1600
  return mappedContactFields(definition).filter((field) => !existing.has(field.property));
1601
1601
  }
1602
+ const OBJECT_TYPE_BY_NAME = {
1603
+ contact: "0-1",
1604
+ company: "0-2"
1605
+ };
1606
+ function collectedHubspotKeys(definition) {
1607
+ const keys = /* @__PURE__ */ new Set();
1608
+ const add = (object, property) => {
1609
+ const typeId = OBJECT_TYPE_BY_NAME[object] || object;
1610
+ if (property) keys.add(`${typeId}:${property}`);
1611
+ };
1612
+ for (const step of definition.steps ?? []) {
1613
+ for (const fld of step.fields ?? []) {
1614
+ if (fld.type === "company") {
1615
+ const map = fld.companyMap ?? {};
1616
+ for (const slot of Object.values(map)) {
1617
+ if (slot?.property?.trim()) add(slot.object?.trim() || "company", slot.property.trim());
1618
+ }
1619
+ continue;
1620
+ }
1621
+ const mapping = fld.hubspot ?? {};
1622
+ const object = mapping.object?.trim() || "contact";
1623
+ const property = mapping.property?.trim() || fld.name;
1624
+ if (property) add(object, property);
1625
+ }
1626
+ }
1627
+ return keys;
1628
+ }
1629
+ function missingRequiredHubspotFields(definition, required) {
1630
+ const collected = collectedHubspotKeys(definition);
1631
+ const missing = [];
1632
+ for (const field of required) {
1633
+ if (!field.name) continue;
1634
+ const objectTypeId = field.objectTypeId?.trim() || "0-1";
1635
+ if (collected.has(`${objectTypeId}:${field.name}`)) continue;
1636
+ missing.push({
1637
+ name: field.name,
1638
+ objectTypeId,
1639
+ object: objectTypeId === "0-2" ? "company" : "contact"
1640
+ });
1641
+ }
1642
+ return missing;
1643
+ }
1602
1644
  function hsFieldType(type, options) {
1603
1645
  switch (type) {
1604
1646
  case "email":
@@ -1649,6 +1691,74 @@ function appendFieldGroups(existing, extra) {
1649
1691
  }))
1650
1692
  ];
1651
1693
  }
1694
+ function conditionToHubspot(condition, parentId) {
1695
+ const rules = (condition.rules ?? []).filter((rule) => rule.field === parentId);
1696
+ if (!rules.length || rules.length !== (condition.rules ?? []).length) return null;
1697
+ const op = rules[0]?.operator;
1698
+ if (rules.some((rule) => rule.operator !== op)) return null;
1699
+ const values = rules.map((rule) => rule.value).filter((value) => Boolean(value));
1700
+ switch (op) {
1701
+ case "eq":
1702
+ return { operator: values.length > 1 ? "SET_ANY" : "EQ", values };
1703
+ case "neq":
1704
+ return { operator: values.length > 1 ? "NOT_SET_ANY" : "NEQ", values };
1705
+ case "contains":
1706
+ return { operator: "CONTAINS", values };
1707
+ case "gt":
1708
+ return { operator: "GT", values };
1709
+ case "lt":
1710
+ return { operator: "LT", values };
1711
+ case "notEmpty":
1712
+ return { operator: "SET", values: [] };
1713
+ case "empty":
1714
+ return { operator: "NOT_SET", values: [] };
1715
+ default:
1716
+ return null;
1717
+ }
1718
+ }
1719
+ function applyVisibleIfToFieldGroups(groups, definition) {
1720
+ const byId = /* @__PURE__ */ new Map();
1721
+ for (const step of definition.steps ?? []) {
1722
+ for (const fld of step.fields ?? []) {
1723
+ const mapping = fld.hubspot ?? {};
1724
+ byId.set(fld.id, { name: fld.name, property: mapping.property?.trim() || fld.name });
1725
+ }
1726
+ }
1727
+ const moves = /* @__PURE__ */ new Map();
1728
+ for (const step of definition.steps ?? []) {
1729
+ for (const fld of step.fields ?? []) {
1730
+ const visibleIf = fld.visibleIf;
1731
+ const parentId = visibleIf?.rules?.[0]?.field;
1732
+ if (!visibleIf || !parentId) continue;
1733
+ const converted = conditionToHubspot(visibleIf, parentId);
1734
+ const parent = byId.get(parentId);
1735
+ const mapping = fld.hubspot ?? {};
1736
+ const property = mapping.property?.trim() || fld.name;
1737
+ if (!converted || !parent) continue;
1738
+ moves.set(property, { parent: parent.property, ...converted });
1739
+ }
1740
+ }
1741
+ if (!moves.size) return groups;
1742
+ const cloned = JSON.parse(JSON.stringify(groups));
1743
+ const flat = [];
1744
+ for (const group of cloned) {
1745
+ (group.fields ?? []).forEach((field, index2) => flat.push({ group, index: index2, field }));
1746
+ }
1747
+ for (const [property, move] of moves) {
1748
+ const child = flat.find((item) => item.field.name === property);
1749
+ const parent = flat.find((item) => item.field.name === move.parent);
1750
+ if (!child || !parent || child === parent) continue;
1751
+ parent.field.dependentFields = [
1752
+ ...parent.field.dependentFields ?? [],
1753
+ {
1754
+ dependentCondition: { operator: move.operator, values: move.values },
1755
+ field: child.field
1756
+ }
1757
+ ];
1758
+ child.group.fields?.splice(child.index, 1);
1759
+ }
1760
+ return cloned;
1761
+ }
1652
1762
  const HS_BASE$2 = "https://api.hubapi.com";
1653
1763
  const OBJECT_BY_TYPE_ID = {
1654
1764
  "0-1": "contact",
@@ -1730,7 +1840,7 @@ function convertField(raw, ctx) {
1730
1840
  helpText: raw.description || void 0,
1731
1841
  ...options.length ? { options } : {},
1732
1842
  hubspot: { object, property: name },
1733
- ...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
1843
+ ...ctx.parentCondition ? { visibleIf: ctx.parentCondition, visibleIfSource: "hubspot" } : {}
1734
1844
  };
1735
1845
  const out = [field];
1736
1846
  for (const dependent of raw.dependentFields ?? []) {
@@ -1753,6 +1863,12 @@ function convertHubspotForm(raw) {
1753
1863
  const usedNames = /* @__PURE__ */ new Set();
1754
1864
  const fields = [];
1755
1865
  for (const group of raw.fieldGroups ?? []) {
1866
+ if (group.groupType === "progressive" || group.groupType === "queued") {
1867
+ skipped.push({
1868
+ code: "progressive",
1869
+ detail: group.groupType
1870
+ });
1871
+ }
1756
1872
  if (group.richText && !(group.fields ?? []).length) {
1757
1873
  skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
1758
1874
  continue;
@@ -1824,7 +1940,10 @@ function mergeHubspotImport(current, incoming) {
1824
1940
  existing.helpText = next.helpText;
1825
1941
  if (next.options) existing.options = next.options;
1826
1942
  else delete existing.options;
1827
- if (!existing.visibleIf && next.visibleIf) existing.visibleIf = next.visibleIf;
1943
+ if (next.visibleIf && (existing.visibleIfSource === "hubspot" || !existing.visibleIf)) {
1944
+ existing.visibleIf = next.visibleIf;
1945
+ if (existing.visibleIfSource !== "editor") existing.visibleIfSource = "hubspot";
1946
+ }
1828
1947
  updated.push(key);
1829
1948
  continue;
1830
1949
  }
@@ -2145,13 +2264,62 @@ async function submitMarketingForm(input) {
2145
2264
  async function syncFieldsToHubspotForm(apiKey, formGuid, definition) {
2146
2265
  const raw = await fetchHubspotForm(apiKey, formGuid);
2147
2266
  const extra = missingMappedFields(definition, formFieldNames(raw));
2148
- if (!extra.length) return [];
2149
- await patchHubspotForm(apiKey, formGuid, {
2150
- fieldGroups: appendFieldGroups(raw.fieldGroups ?? [], extra)
2151
- });
2267
+ const fieldGroups = applyVisibleIfToFieldGroups(
2268
+ appendFieldGroups(raw.fieldGroups ?? [], extra),
2269
+ definition
2270
+ );
2271
+ if (!extra.length && JSON.stringify(fieldGroups) === JSON.stringify(appendFieldGroups(raw.fieldGroups ?? [], []))) {
2272
+ return [];
2273
+ }
2274
+ await patchHubspotForm(apiKey, formGuid, { fieldGroups });
2152
2275
  clearFormShapeCache(formGuid);
2153
2276
  return extra.map((field) => field.property);
2154
2277
  }
2278
+ async function createMarketingForm(apiKey, name, definition) {
2279
+ const fields = mappedContactFields(definition).map((field) => toHsFormField(field));
2280
+ if (!fields.length) {
2281
+ throw new Error("Add at least one mapped contact field before creating a HubSpot form");
2282
+ }
2283
+ const res = await fetch("https://api.hubapi.com/marketing/v3/forms", {
2284
+ method: "POST",
2285
+ headers: {
2286
+ Authorization: `Bearer ${apiKey}`,
2287
+ "Content-Type": "application/json"
2288
+ },
2289
+ body: JSON.stringify({
2290
+ name,
2291
+ formType: "hubspot",
2292
+ fieldGroups: applyVisibleIfToFieldGroups(
2293
+ fields.map((field) => ({
2294
+ groupType: "default_group",
2295
+ richTextType: "text",
2296
+ fields: [field]
2297
+ })),
2298
+ definition
2299
+ ),
2300
+ configuration: {
2301
+ language: "fr",
2302
+ recaptchaEnabled: false,
2303
+ createNewContactForNewEmail: true,
2304
+ prePopulateKnownValues: true,
2305
+ notifyContactOwner: false,
2306
+ notifyRecipients: [],
2307
+ editable: true,
2308
+ archivable: true,
2309
+ cloneable: true,
2310
+ allowLinkToResetKnownValues: false,
2311
+ postSubmitAction: { type: "thank_you", value: "Merci." }
2312
+ },
2313
+ displayOptions: { submitButtonText: "Envoyer", style: { submitButtonText: "Envoyer" } },
2314
+ legalConsentOptions: { type: "none" }
2315
+ })
2316
+ });
2317
+ const body = await res.json().catch(() => ({}));
2318
+ if (!res.ok || !body.id) {
2319
+ throw new Error(body.message || `HubSpot ${res.status}`);
2320
+ }
2321
+ return body.id;
2322
+ }
2155
2323
  function publicForm(entry) {
2156
2324
  return {
2157
2325
  slug: entry.slug,
@@ -2165,7 +2333,9 @@ function publicForm(entry) {
2165
2333
  steps: (entry.definition?.steps ?? []).map((step) => ({
2166
2334
  ...step,
2167
2335
  // The CRM mappings are the server's business — never the browser's.
2168
- fields: (step.fields ?? []).map(({ hubspot: _hubspot, companyMap: _companyMap, ...fld }) => fld)
2336
+ fields: (step.fields ?? []).map(
2337
+ ({ hubspot: _hubspot, companyMap: _companyMap, visibleIfSource: _source, ...fld }) => fld
2338
+ )
2169
2339
  }))
2170
2340
  };
2171
2341
  }
@@ -2909,6 +3079,22 @@ function createFormsAdminController(strapi) {
2909
3079
  ctx.body = { errors: structural, problems: mappings };
2910
3080
  return;
2911
3081
  }
3082
+ const { apiKey } = await resolveApiKey(strapi);
3083
+ const account = await resolveAccount(strapi);
3084
+ const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
3085
+ const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
3086
+ if (apiKey && formGuid) {
3087
+ const shape = await loadFormShape(apiKey, formGuid);
3088
+ const missingHubspot = shape ? missingRequiredHubspotFields(
3089
+ entry.definition ?? EMPTY_DEFINITION,
3090
+ [...shape.fields.filter((field) => field.required)]
3091
+ ) : [];
3092
+ if (missingHubspot.length) {
3093
+ ctx.status = 400;
3094
+ ctx.body = { errors: structural, problems: mappings, missingHubspot };
3095
+ return;
3096
+ }
3097
+ }
2912
3098
  await documents().publish({
2913
3099
  documentId: ctx.params.documentId,
2914
3100
  locale: ctx.query.locale
@@ -2916,22 +3102,16 @@ function createFormsAdminController(strapi) {
2916
3102
  const policy = await resolvePolicy(strapi);
2917
3103
  let addedFields = [];
2918
3104
  let syncWarning;
2919
- if (policy.syncFieldsOnPublish) {
2920
- const { apiKey } = await resolveApiKey(strapi);
2921
- const account = await resolveAccount(strapi);
2922
- const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
2923
- const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
2924
- if (apiKey && formGuid) {
2925
- try {
2926
- addedFields = await syncFieldsToHubspotForm(
2927
- apiKey,
2928
- formGuid,
2929
- entry.definition ?? EMPTY_DEFINITION
2930
- );
2931
- } catch (err) {
2932
- syncWarning = err.message;
2933
- strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
2934
- }
3105
+ if (policy.syncFieldsOnPublish && apiKey && formGuid) {
3106
+ try {
3107
+ addedFields = await syncFieldsToHubspotForm(
3108
+ apiKey,
3109
+ formGuid,
3110
+ entry.definition ?? EMPTY_DEFINITION
3111
+ );
3112
+ } catch (err) {
3113
+ syncWarning = err.message;
3114
+ strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
2935
3115
  }
2936
3116
  }
2937
3117
  ctx.body = {
@@ -2940,6 +3120,31 @@ function createFormsAdminController(strapi) {
2940
3120
  ...syncWarning ? { syncWarning } : {}
2941
3121
  };
2942
3122
  },
3123
+ async createInHubspot(ctx) {
3124
+ const entry = await documents().findOne({
3125
+ documentId: ctx.params.documentId,
3126
+ locale: ctx.query.locale,
3127
+ status: "draft"
3128
+ });
3129
+ if (!entry) ctx.throw(404, "Form not found");
3130
+ const { apiKey } = await resolveApiKey(strapi);
3131
+ if (!apiKey) ctx.throw(400, "HubSpot API key is missing");
3132
+ try {
3133
+ const id = await createMarketingForm(
3134
+ apiKey,
3135
+ entry.name || entry.slug || "Formulaire",
3136
+ entry.definition ?? EMPTY_DEFINITION
3137
+ );
3138
+ const updated = await documents().update({
3139
+ documentId: ctx.params.documentId,
3140
+ locale: ctx.query.locale,
3141
+ data: { hubspotFormId: id }
3142
+ });
3143
+ ctx.body = { form: updated, hubspotFormId: id };
3144
+ } catch (err) {
3145
+ ctx.throw(400, err.message);
3146
+ }
3147
+ },
2943
3148
  async unpublish(ctx) {
2944
3149
  await documents().unpublish({
2945
3150
  documentId: ctx.params.documentId,
@@ -3706,6 +3911,7 @@ const routes = {
3706
3911
  adminRoute("GET", "/builder/forms/:documentId", "formsAdmin.findOne", [FORMS_ACTION]),
3707
3912
  adminRoute("PUT", "/builder/forms/:documentId", "formsAdmin.update", [FORMS_ACTION]),
3708
3913
  adminRoute("POST", "/builder/forms/:documentId/publish", "formsAdmin.publish", [FORMS_ACTION]),
3914
+ adminRoute("POST", "/builder/forms/:documentId/create-hubspot", "formsAdmin.createInHubspot", [FORMS_ACTION]),
3709
3915
  adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
3710
3916
  adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
3711
3917
  adminRoute("POST", "/builder/forms/:documentId/duplicate", "formsAdmin.duplicate", [FORMS_ACTION]),
@@ -22,6 +22,7 @@ export interface FormFieldDef {
22
22
  type: string;
23
23
  required?: boolean;
24
24
  visibleIf?: Condition | null;
25
+ visibleIfSource?: "hubspot" | "editor" | null;
25
26
  [key: string]: unknown;
26
27
  }
27
28
  export interface FormStepDef {
@@ -26,6 +26,21 @@ export interface MappedFormField {
26
26
  export declare function leftoverContactProps(contact: Record<string, string>, sentFieldNames: Iterable<string>): Record<string, string>;
27
27
  export declare function mappedContactFields(definition: FormDefinition): MappedFormField[];
28
28
  export declare function missingMappedFields(definition: FormDefinition, existingNames: Iterable<string>): MappedFormField[];
29
+ export interface MissingHubspotRequired {
30
+ name: string;
31
+ objectTypeId: string;
32
+ object: string;
33
+ }
34
+ /** Properties the Strapi form can actually send (`objectTypeId:property`). */
35
+ export declare function collectedHubspotKeys(definition: FormDefinition): Set<string>;
36
+ /**
37
+ * Required marketing-form fields the Strapi form never collects. HubSpot
38
+ * rejects the whole conversion when one of these is absent.
39
+ */
40
+ export declare function missingRequiredHubspotFields(definition: FormDefinition, required: {
41
+ name: string;
42
+ objectTypeId?: string;
43
+ }[]): MissingHubspotRequired[];
29
44
  export declare function hsFieldType(type: string, options?: unknown[]): string;
30
45
  export declare function toHsFormField(field: MappedFormField): Record<string, unknown>;
31
46
  export interface RawFieldGroupLike {
@@ -35,3 +50,8 @@ export interface RawFieldGroupLike {
35
50
  fields?: unknown[];
36
51
  }
37
52
  export declare function appendFieldGroups(existing: RawFieldGroupLike[], extra: MappedFormField[]): Record<string, unknown>[];
53
+ /**
54
+ * Nest Strapi `visibleIf` fields under their HubSpot parent as dependentFields.
55
+ * Conditions edited in Strapi are included; unsupported multi-parent rules stay flat.
56
+ */
57
+ export declare function applyVisibleIfToFieldGroups(groups: Record<string, unknown>[], definition: FormDefinition): Record<string, unknown>[];
@@ -26,6 +26,7 @@ export declare function createFormsAdminController(strapi: Core.Strapi): {
26
26
  create(ctx: Ctx): Promise<void>;
27
27
  update(ctx: Ctx): Promise<void>;
28
28
  publish(ctx: Ctx): Promise<void>;
29
+ createInHubspot(ctx: Ctx): Promise<void>;
29
30
  unpublish(ctx: Ctx): Promise<void>;
30
31
  /**
31
32
  * Feeds the `hubspot.form-picker` custom field: every form of the builder
@@ -153,3 +153,5 @@ export declare function submitMarketingForm(input: FormSubmitInput): Promise<{
153
153
  * that already exist. Returns the property names that were added.
154
154
  */
155
155
  export declare function syncFieldsToHubspotForm(apiKey: string, formGuid: string, definition: FormDefinition): Promise<string[]>;
156
+ /** Create a marketing form from the Strapi definition and return its GUID. */
157
+ export declare function createMarketingForm(apiKey: string, name: string, definition: FormDefinition): Promise<string>;
@@ -21,7 +21,7 @@ export interface HubspotFormSummary {
21
21
  }
22
22
  /** Something the converter could not express in the builder's model. */
23
23
  export interface SkippedItem {
24
- code: "field-type" | "hidden-field" | "object" | "duplicate" | "condition" | "rich-text" | "legal-consent";
24
+ code: "field-type" | "hidden-field" | "object" | "duplicate" | "condition" | "rich-text" | "legal-consent" | "progressive";
25
25
  /** Field label or name, when the item is a field. */
26
26
  label?: string;
27
27
  /** Raw detail (the fieldType, the operator…) for the report. */
@@ -58,6 +58,7 @@ interface RawField {
58
58
  dependentFields?: RawDependent[];
59
59
  }
60
60
  interface RawFieldGroup {
61
+ groupType?: string;
61
62
  richText?: string;
62
63
  fields?: RawField[];
63
64
  }
@@ -274,6 +274,7 @@ declare const _default: {
274
274
  create(ctx: import("./formsAdmin").Ctx): Promise<void>;
275
275
  update(ctx: import("./formsAdmin").Ctx): Promise<void>;
276
276
  publish(ctx: import("./formsAdmin").Ctx): Promise<void>;
277
+ createInHubspot(ctx: import("./formsAdmin").Ctx): Promise<void>;
277
278
  unpublish(ctx: import("./formsAdmin").Ctx): Promise<void>;
278
279
  options(ctx: import("./formsAdmin").Ctx): Promise<void>;
279
280
  listSources(ctx: import("./formsAdmin").Ctx): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-hubspot",
3
- "version": "0.16.7",
3
+ "version": "0.16.8",
4
4
  "description": "The form builder for HubSpot in Strapi — multi-step forms with conditional fields mapped to real CRM properties, a public submit pipeline with retries, and save-time mapping validation.",
5
5
  "keywords": [
6
6
  "strapi",