strapi-plugin-hubspot 0.15.0 → 0.16.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 (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +28 -13
  3. package/dist/_chunks/{Forms-B7eIm0Fi.mjs → Forms-6EoEYzAc.mjs} +26 -5
  4. package/dist/_chunks/{Forms-ClyI4MJ1.js → Forms-BW_zGc1P.js} +26 -5
  5. package/dist/_chunks/{HubspotFormInput-cZAiaAaq.mjs → HubspotFormInput-V5w4y1fb.mjs} +1 -1
  6. package/dist/_chunks/{HubspotFormInput-Do7F83V6.js → HubspotFormInput-oMhm1UJm.js} +1 -1
  7. package/dist/_chunks/{HubspotObjectInput-DWqk2C26.js → HubspotObjectInput-Bmo6YQgs.js} +3 -3
  8. package/dist/_chunks/{HubspotObjectInput-ZZk_7hWq.mjs → HubspotObjectInput-DhsnSguG.mjs} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-5bkDGmGo.mjs → HubspotPropertyInput-CrYP4pc0.mjs} +3 -3
  10. package/dist/_chunks/{HubspotPropertyInput-v9B3wb1a.js → HubspotPropertyInput-luFfnxqy.js} +3 -3
  11. package/dist/_chunks/{Settings-X3EHnTaO.js → Settings-B3A22i7Z.js} +77 -4
  12. package/dist/_chunks/{Settings-QcFbm3s5.mjs → Settings-CjcWj9I6.mjs} +78 -5
  13. package/dist/_chunks/{en-ZHvaZLi7.js → en-C7ls2EUl.js} +15 -2
  14. package/dist/_chunks/{en-DMJeF7EN.mjs → en-D6GHgMOt.mjs} +15 -2
  15. package/dist/_chunks/{fr-CVAntLmc.mjs → fr-CfohMY8n.mjs} +15 -2
  16. package/dist/_chunks/{fr-Ds_2EAzZ.js → fr-pOP8ZXqM.js} +15 -2
  17. package/dist/_chunks/{index-BSJAhj8r.mjs → index-7cL9PRh1.mjs} +6 -6
  18. package/dist/_chunks/{index-BpcilDy1.js → index-CodpLKZx.js} +6 -6
  19. package/dist/_chunks/{objectLabels-CYXXFpNB.js → objectLabels-CSWGIgNb.js} +1 -1
  20. package/dist/_chunks/{objectLabels-CrYRzsqe.mjs → objectLabels-Dnj14fsp.mjs} +1 -1
  21. package/dist/_chunks/{useHubspotSchema-CBF4GkmK.mjs → useHubspotSchema-BsmQgJgx.mjs} +1 -1
  22. package/dist/_chunks/{useHubspotSchema-Ba6xZUCL.js → useHubspotSchema-DiRN3HrN.js} +1 -1
  23. package/dist/admin/index.js +1 -1
  24. package/dist/admin/index.mjs +1 -1
  25. package/dist/server/index.js +216 -7
  26. package/dist/server/index.mjs +216 -7
  27. package/dist/server/src/__tests__/formSync.test.d.ts +1 -0
  28. package/dist/server/src/formSync.d.ts +37 -0
  29. package/dist/server/src/forms.d.ts +6 -8
  30. package/dist/server/src/hsforms.d.ts +8 -0
  31. package/dist/server/src/importHubspot.d.ts +4 -0
  32. package/dist/server/src/index.d.ts +6 -0
  33. package/dist/server/src/settings.d.ts +32 -3
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.16.0 — 2026-09-22
6
+
7
+ ### Added — submission policies
8
+ The plugin no longer assumes one HubSpot workflow. **Settings → HubSpot**
9
+ (and `forms.*` in `config/plugins.ts`) now expose three independent choices:
10
+
11
+ - **`submissionMode`** (`auto` / `forms` / `crm`): native marketing-form
12
+ conversions when a GUID is linked, CRM upsert only, or automatic.
13
+ - **`writeExtraProperties`** (default `true`): after a Forms API conversion,
14
+ leftover mapped contact fields still land on the contact via CRM upsert.
15
+ - **`syncFieldsOnPublish`** (default `false`): on publish, PATCH missing
16
+ mapped contact fields onto the linked HubSpot form. Off so HubSpot-first
17
+ installs aren't mutated. CRM properties are never created.
18
+
5
19
  ## 0.15.0 — 2026-09-21
6
20
 
7
21
  ### Added — account settings in the admin
package/README.md CHANGED
@@ -44,14 +44,25 @@ Permissions**):
44
44
  | `GET` | `/api/hubspot/forms/:slug?locale=` | The published form — meta, steps, fields, conditions. The CRM mapping is stripped: the browser never learns your property names |
45
45
  | `POST` | `/api/hubspot/forms/:slug/submit` | Validates (bounds, conditions, required), maps server-side, syncs HubSpot, stores the submission |
46
46
 
47
- The submit pipeline, in order: HubSpot **Forms API** (native conversion —
48
- Original Source, workflows, lead-center notifications) when a marketing form
49
- GUID is set on the builder form or via `forms.defaultFormId`; the visitor's
50
- `hutk` is forwarded so attribution sticks. Then the contact is looked up by
51
- email and, when the address is on a corporate domain, the company is
52
- found-or-created and associated. A CRM contact upsert is the **fallback**
53
- when no GUID is configured — it does not count as a form conversion.
54
- Timeline notes are opt-in and only used on that fallback.
47
+ The submit pipeline is a **policy**, not a single path. Settings → HubSpot
48
+ (or `forms.*` in `config/plugins.ts`) pick how this install talks to HubSpot —
49
+ so a portal that wants native conversions and a portal that only wants CRM
50
+ upserts can share the same plugin:
51
+
52
+ | Setting | Default | What it does |
53
+ |---|---|---|
54
+ | `forms.submissionMode` | `auto` | `auto`: Forms API when a marketing GUID is linked, else CRM upsert. `forms`: always try the Forms API. `crm`: never count as a conversion |
55
+ | `forms.writeExtraProperties` | `true` | After a conversion, CRM-write mapped contact fields the HubSpot form dropped (they 400 the Forms API) |
56
+ | `forms.syncFieldsOnPublish` | `false` | On publish, PATCH missing mapped **contact** fields onto the HubSpot form. Off by default so HubSpot-first installs aren't mutated |
57
+
58
+ The visitor's `hutk` is forwarded so attribution sticks. Then the contact is
59
+ looked up by email and, when the address is on a corporate domain, the company
60
+ is found-or-created and associated. Timeline notes are opt-in and only used
61
+ on the CRM-upsert path.
62
+
63
+ CRM properties are **never created**. Map to properties that already exist in
64
+ the portal; opting into field sync only adds form fields that point at those
65
+ properties.
55
66
 
56
67
  Portal id, region and form GUIDs are configuration. A test portal today,
57
68
  production tomorrow: swap `portalId` / `region` / the GUID, no code change.
@@ -66,6 +77,9 @@ hubspot: {
66
77
  companyFromDomain: true, // Company by corporate domain + association
67
78
  timelineNote: false, // recap note on the CRM-upsert fallback only
68
79
  defaultFormId: env("HUBSPOT_DEFAULT_FORM_ID", ""),
80
+ submissionMode: "auto", // auto | forms | crm
81
+ writeExtraProperties: true,
82
+ syncFieldsOnPublish: false,
69
83
  },
70
84
  },
71
85
  }
@@ -365,11 +379,12 @@ its equivalent per object you send to).
365
379
 
366
380
  ### A settings screen
367
381
 
368
- **Settings → HubSpot** holds the private app token **and** the HubSpot account
369
- that receives conversions: portal id, region (`eu1` / `na1`), and the default
370
- marketing form (picked from the connected portal). Values saved here override
371
- `config/plugins.ts` and env — switching test → production is an admin change,
372
- not a deploy.
382
+ **Settings → HubSpot** holds the private app token, the HubSpot account that
383
+ receives conversions (portal id, region, default marketing form), **and** the
384
+ submission policy: conversion vs CRM upsert, leftover contact writes, form
385
+ field sync on publish. Values saved here override `config/plugins.ts` and env
386
+ — switching test → production, or switching HubSpot-first ↔ Strapi-first, is
387
+ an admin change, not a deploy.
373
388
 
374
389
  The token is stored server-side and never returned to the browser: the UI only
375
390
  receives whether a key exists, where it comes from, and its last four
@@ -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-BSJAhj8r.mjs";
8
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-7cL9PRh1.mjs";
9
9
  import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from "@dnd-kit/core";
10
10
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from "@dnd-kit/sortable";
11
11
  import { CSS } from "@dnd-kit/utilities";
12
- import { o as objectLabel } from "./objectLabels-CrYRzsqe.mjs";
13
- import { u as useHubspotSchema } from "./useHubspotSchema-CBF4GkmK.mjs";
12
+ import { o as objectLabel } from "./objectLabels-Dnj14fsp.mjs";
13
+ import { u as useHubspotSchema } from "./useHubspotSchema-BsmQgJgx.mjs";
14
14
  const OPERATORS = [
15
15
  "eq",
16
16
  "neq",
@@ -774,11 +774,32 @@ const FormEditor = () => {
774
774
  setBusy(true);
775
775
  setFeedback(null);
776
776
  try {
777
- await post(`/${PLUGIN_ID}/builder/forms/${documentId}/publish${query}`);
777
+ const { data } = await post(`/${PLUGIN_ID}/builder/forms/${documentId}/publish${query}`);
778
778
  setPublished(true);
779
779
  setErrors([]);
780
780
  setProblems([]);
781
- setFeedback({ tone: "success", text: t("editor.published", "Form published.") });
781
+ const added = Array.isArray(data?.addedFields) ? data.addedFields.length : 0;
782
+ if (data?.syncWarning) {
783
+ setFeedback({
784
+ tone: "success",
785
+ text: t(
786
+ "editor.published-sync-warn",
787
+ "Form published. HubSpot field sync failed: {error}",
788
+ { error: data.syncWarning }
789
+ )
790
+ });
791
+ } else if (added) {
792
+ setFeedback({
793
+ tone: "success",
794
+ text: t(
795
+ "editor.published-synced",
796
+ "Form published. {count, plural, one {# field added} other {# fields added}} to the HubSpot form.",
797
+ { count: added }
798
+ )
799
+ });
800
+ } else {
801
+ setFeedback({ tone: "success", text: t("editor.published", "Form published.") });
802
+ }
782
803
  } catch (err) {
783
804
  const data = err.response?.data;
784
805
  setErrors(data?.errors ?? []);
@@ -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-BpcilDy1.js");
10
+ const index$1 = require("./index-CodpLKZx.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-CYXXFpNB.js");
15
- const useHubspotSchema = require("./useHubspotSchema-Ba6xZUCL.js");
14
+ const objectLabels = require("./objectLabels-CSWGIgNb.js");
15
+ const useHubspotSchema = require("./useHubspotSchema-DiRN3HrN.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" } });
@@ -794,11 +794,32 @@ const FormEditor = () => {
794
794
  setBusy(true);
795
795
  setFeedback(null);
796
796
  try {
797
- await post(`/${index$1.PLUGIN_ID}/builder/forms/${documentId}/publish${query}`);
797
+ const { data } = await post(`/${index$1.PLUGIN_ID}/builder/forms/${documentId}/publish${query}`);
798
798
  setPublished(true);
799
799
  setErrors([]);
800
800
  setProblems([]);
801
- setFeedback({ tone: "success", text: t("editor.published", "Form published.") });
801
+ const added = Array.isArray(data?.addedFields) ? data.addedFields.length : 0;
802
+ if (data?.syncWarning) {
803
+ setFeedback({
804
+ tone: "success",
805
+ text: t(
806
+ "editor.published-sync-warn",
807
+ "Form published. HubSpot field sync failed: {error}",
808
+ { error: data.syncWarning }
809
+ )
810
+ });
811
+ } else if (added) {
812
+ setFeedback({
813
+ tone: "success",
814
+ text: t(
815
+ "editor.published-synced",
816
+ "Form published. {count, plural, one {# field added} other {# fields added}} to the HubSpot form.",
817
+ { count: added }
818
+ )
819
+ });
820
+ } else {
821
+ setFeedback({ tone: "success", text: t("editor.published", "Form published.") });
822
+ }
802
823
  } catch (err) {
803
824
  const data = err.response?.data;
804
825
  setErrors(data?.errors ?? []);
@@ -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-BSJAhj8r.mjs";
6
+ import { P as PLUGIN_ID, g as getTranslation } from "./index-7cL9PRh1.mjs";
7
7
  const HubspotFormInput = React.forwardRef(
8
8
  ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
9
9
  const { formatMessage } = useIntl();
@@ -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-BpcilDy1.js");
8
+ const index$1 = require("./index-CodpLKZx.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" } });
@@ -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-BpcilDy1.js");
8
- const objectLabels = require("./objectLabels-CYXXFpNB.js");
9
- const useHubspotSchema = require("./useHubspotSchema-Ba6xZUCL.js");
7
+ const index$1 = require("./index-CodpLKZx.js");
8
+ const objectLabels = require("./objectLabels-CSWGIgNb.js");
9
+ const useHubspotSchema = require("./useHubspotSchema-DiRN3HrN.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-BSJAhj8r.mjs";
6
- import { o as objectLabel } from "./objectLabels-CrYRzsqe.mjs";
7
- import { u as useHubspotSchema } from "./useHubspotSchema-CBF4GkmK.mjs";
5
+ import { g as getTranslation } from "./index-7cL9PRh1.mjs";
6
+ import { o as objectLabel } from "./objectLabels-Dnj14fsp.mjs";
7
+ import { u as useHubspotSchema } from "./useHubspotSchema-BsmQgJgx.mjs";
8
8
  const HubspotObjectInput = React.forwardRef(
9
9
  ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
10
10
  const intl = useIntl();
@@ -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-BSJAhj8r.mjs";
8
- import { o as objectLabel } from "./objectLabels-CrYRzsqe.mjs";
9
- import { u as useHubspotSchema } from "./useHubspotSchema-CBF4GkmK.mjs";
7
+ import { g as getTranslation } from "./index-7cL9PRh1.mjs";
8
+ import { o as objectLabel } from "./objectLabels-Dnj14fsp.mjs";
9
+ import { u as useHubspotSchema } from "./useHubspotSchema-BsmQgJgx.mjs";
10
10
  const oneLine = {
11
11
  display: "block",
12
12
  whiteSpace: "nowrap",
@@ -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-BpcilDy1.js");
10
- const objectLabels = require("./objectLabels-CYXXFpNB.js");
11
- const useHubspotSchema = require("./useHubspotSchema-Ba6xZUCL.js");
9
+ const index$1 = require("./index-CodpLKZx.js");
10
+ const objectLabels = require("./objectLabels-CSWGIgNb.js");
11
+ const useHubspotSchema = require("./useHubspotSchema-DiRN3HrN.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" } });
@@ -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-BpcilDy1.js");
8
+ const index$1 = require("./index-CodpLKZx.js");
9
9
  const reactRouterDom = require("react-router-dom");
10
- const objectLabels = require("./objectLabels-CYXXFpNB.js");
10
+ const objectLabels = require("./objectLabels-CSWGIgNb.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" } });
@@ -201,6 +201,9 @@ const HubspotSettings = () => {
201
201
  const [portalId, setPortalId] = React__namespace.useState("");
202
202
  const [region, setRegion] = React__namespace.useState("eu1");
203
203
  const [defaultFormId, setDefaultFormId] = React__namespace.useState("");
204
+ const [submissionMode, setSubmissionMode] = React__namespace.useState("auto");
205
+ const [writeExtraProperties, setWriteExtraProperties] = React__namespace.useState(true);
206
+ const [syncFieldsOnPublish, setSyncFieldsOnPublish] = React__namespace.useState(false);
204
207
  const [forms, setForms] = React__namespace.useState([]);
205
208
  const [busy, setBusy] = React__namespace.useState(false);
206
209
  const [feedback, setFeedback] = React__namespace.useState(
@@ -212,6 +215,9 @@ const HubspotSettings = () => {
212
215
  setPortalId(data.portalId ?? "");
213
216
  setRegion(data.region || "eu1");
214
217
  setDefaultFormId(data.defaultFormId ?? "");
218
+ setSubmissionMode(data.submissionMode === "crm" || data.submissionMode === "forms" ? data.submissionMode : "auto");
219
+ setWriteExtraProperties(data.writeExtraProperties !== false);
220
+ setSyncFieldsOnPublish(data.syncFieldsOnPublish === true);
215
221
  setForms(Array.isArray(data.forms) ? data.forms : []);
216
222
  };
217
223
  const load = React__namespace.useCallback(async () => {
@@ -231,7 +237,10 @@ const HubspotSettings = () => {
231
237
  ...apiKey.trim() ? { apiKey: apiKey.trim() } : {},
232
238
  portalId,
233
239
  region,
234
- defaultFormId
240
+ defaultFormId,
241
+ submissionMode,
242
+ writeExtraProperties,
243
+ syncFieldsOnPublish
235
244
  });
236
245
  setApiKey("");
237
246
  await load();
@@ -401,7 +410,71 @@ const HubspotSettings = () => {
401
410
  sourceHint(settings.formSource)
402
411
  ]
403
412
  }
404
- )
413
+ ),
414
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", children: t("settings.policy.title", "How submissions reach HubSpot") }),
415
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t(
416
+ "settings.policy.hint",
417
+ "Pick the policy that matches this portal. Defaults stay safe for every install: conversions when a form is linked, leftover answers still written on the contact, HubSpot forms never mutated unless you ask."
418
+ ) }),
419
+ /* @__PURE__ */ jsxRuntime.jsxs(
420
+ designSystem.Field.Root,
421
+ {
422
+ name: "submissionMode",
423
+ hint: t(
424
+ "settings.mode.hint",
425
+ "Automatic uses the marketing form when one is linked, otherwise a CRM upsert. CRM-only never counts as a conversion. Forms-only prefers the Forms API and falls back to the CRM if HubSpot refuses the submit."
426
+ ),
427
+ children: [
428
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("settings.mode.label", "Submission path") }),
429
+ /* @__PURE__ */ jsxRuntime.jsxs(
430
+ designSystem.SingleSelect,
431
+ {
432
+ value: submissionMode,
433
+ onChange: (value) => {
434
+ const next = String(value);
435
+ if (next === "crm" || next === "forms" || next === "auto") setSubmissionMode(next);
436
+ },
437
+ children: [
438
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "auto", children: t("settings.mode.auto", "Automatic — form conversion if linked, else CRM") }),
439
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "forms", children: t("settings.mode.forms", "Marketing form — native HubSpot conversions") }),
440
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "crm", children: t("settings.mode.crm", "CRM only — upsert the contact, no form conversion") })
441
+ ]
442
+ }
443
+ ),
444
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
445
+ ]
446
+ }
447
+ ),
448
+ /* @__PURE__ */ jsxRuntime.jsx(
449
+ designSystem.Checkbox,
450
+ {
451
+ checked: writeExtraProperties,
452
+ onCheckedChange: (checked) => setWriteExtraProperties(checked === true),
453
+ children: t(
454
+ "settings.extra.label",
455
+ "After a conversion, also write mapped contact fields that the HubSpot form doesn't have"
456
+ )
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t(
460
+ "settings.extra.hint",
461
+ "The Forms API drops unknown fields (they 400 the submit). With this on, leftovers still land on the contact. Turn it off if HubSpot must only receive what the marketing form declares."
462
+ ) }),
463
+ /* @__PURE__ */ jsxRuntime.jsx(
464
+ designSystem.Checkbox,
465
+ {
466
+ checked: syncFieldsOnPublish,
467
+ onCheckedChange: (checked) => setSyncFieldsOnPublish(checked === true),
468
+ children: t(
469
+ "settings.sync.label",
470
+ "On publish, add mapped contact fields that are missing on the HubSpot form"
471
+ )
472
+ }
473
+ ),
474
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t(
475
+ "settings.sync.hint",
476
+ "Strapi-first installs: a field added here appears on the HubSpot form next publish. HubSpot-first installs should leave this off — importing a form already carries its fields. Properties themselves are never created; map to ones that exist in the portal."
477
+ ) })
405
478
  ] }) }),
406
479
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
407
480
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: save, loading: busy, children: t("settings.save", "Save") }),
@@ -1,11 +1,11 @@
1
1
  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
- import { Flex, Divider, Typography, Button, Box, Link, Badge, Loader, Field, SingleSelect, SingleSelectOption } from "@strapi/design-system";
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-BSJAhj8r.mjs";
6
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-7cL9PRh1.mjs";
7
7
  import { Link as Link$1 } from "react-router-dom";
8
- import { o as objectLabel } from "./objectLabels-CrYRzsqe.mjs";
8
+ import { o as objectLabel } from "./objectLabels-Dnj14fsp.mjs";
9
9
  const AuditSection = ({ configured }) => {
10
10
  const intl = useIntl();
11
11
  const { formatMessage } = intl;
@@ -181,6 +181,9 @@ const HubspotSettings = () => {
181
181
  const [portalId, setPortalId] = React.useState("");
182
182
  const [region, setRegion] = React.useState("eu1");
183
183
  const [defaultFormId, setDefaultFormId] = React.useState("");
184
+ const [submissionMode, setSubmissionMode] = React.useState("auto");
185
+ const [writeExtraProperties, setWriteExtraProperties] = React.useState(true);
186
+ const [syncFieldsOnPublish, setSyncFieldsOnPublish] = React.useState(false);
184
187
  const [forms, setForms] = React.useState([]);
185
188
  const [busy, setBusy] = React.useState(false);
186
189
  const [feedback, setFeedback] = React.useState(
@@ -192,6 +195,9 @@ const HubspotSettings = () => {
192
195
  setPortalId(data.portalId ?? "");
193
196
  setRegion(data.region || "eu1");
194
197
  setDefaultFormId(data.defaultFormId ?? "");
198
+ setSubmissionMode(data.submissionMode === "crm" || data.submissionMode === "forms" ? data.submissionMode : "auto");
199
+ setWriteExtraProperties(data.writeExtraProperties !== false);
200
+ setSyncFieldsOnPublish(data.syncFieldsOnPublish === true);
195
201
  setForms(Array.isArray(data.forms) ? data.forms : []);
196
202
  };
197
203
  const load = React.useCallback(async () => {
@@ -211,7 +217,10 @@ const HubspotSettings = () => {
211
217
  ...apiKey.trim() ? { apiKey: apiKey.trim() } : {},
212
218
  portalId,
213
219
  region,
214
- defaultFormId
220
+ defaultFormId,
221
+ submissionMode,
222
+ writeExtraProperties,
223
+ syncFieldsOnPublish
215
224
  });
216
225
  setApiKey("");
217
226
  await load();
@@ -381,7 +390,71 @@ const HubspotSettings = () => {
381
390
  sourceHint(settings.formSource)
382
391
  ]
383
392
  }
384
- )
393
+ ),
394
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", children: t("settings.policy.title", "How submissions reach HubSpot") }),
395
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
396
+ "settings.policy.hint",
397
+ "Pick the policy that matches this portal. Defaults stay safe for every install: conversions when a form is linked, leftover answers still written on the contact, HubSpot forms never mutated unless you ask."
398
+ ) }),
399
+ /* @__PURE__ */ jsxs(
400
+ Field.Root,
401
+ {
402
+ name: "submissionMode",
403
+ hint: t(
404
+ "settings.mode.hint",
405
+ "Automatic uses the marketing form when one is linked, otherwise a CRM upsert. CRM-only never counts as a conversion. Forms-only prefers the Forms API and falls back to the CRM if HubSpot refuses the submit."
406
+ ),
407
+ children: [
408
+ /* @__PURE__ */ jsx(Field.Label, { children: t("settings.mode.label", "Submission path") }),
409
+ /* @__PURE__ */ jsxs(
410
+ SingleSelect,
411
+ {
412
+ value: submissionMode,
413
+ onChange: (value) => {
414
+ const next = String(value);
415
+ if (next === "crm" || next === "forms" || next === "auto") setSubmissionMode(next);
416
+ },
417
+ children: [
418
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "auto", children: t("settings.mode.auto", "Automatic — form conversion if linked, else CRM") }),
419
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "forms", children: t("settings.mode.forms", "Marketing form — native HubSpot conversions") }),
420
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "crm", children: t("settings.mode.crm", "CRM only — upsert the contact, no form conversion") })
421
+ ]
422
+ }
423
+ ),
424
+ /* @__PURE__ */ jsx(Field.Hint, {})
425
+ ]
426
+ }
427
+ ),
428
+ /* @__PURE__ */ jsx(
429
+ Checkbox,
430
+ {
431
+ checked: writeExtraProperties,
432
+ onCheckedChange: (checked) => setWriteExtraProperties(checked === true),
433
+ children: t(
434
+ "settings.extra.label",
435
+ "After a conversion, also write mapped contact fields that the HubSpot form doesn't have"
436
+ )
437
+ }
438
+ ),
439
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
440
+ "settings.extra.hint",
441
+ "The Forms API drops unknown fields (they 400 the submit). With this on, leftovers still land on the contact. Turn it off if HubSpot must only receive what the marketing form declares."
442
+ ) }),
443
+ /* @__PURE__ */ jsx(
444
+ Checkbox,
445
+ {
446
+ checked: syncFieldsOnPublish,
447
+ onCheckedChange: (checked) => setSyncFieldsOnPublish(checked === true),
448
+ children: t(
449
+ "settings.sync.label",
450
+ "On publish, add mapped contact fields that are missing on the HubSpot form"
451
+ )
452
+ }
453
+ ),
454
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
455
+ "settings.sync.hint",
456
+ "Strapi-first installs: a field added here appears on the HubSpot form next publish. HubSpot-first installs should leave this off — importing a form already carries its fields. Properties themselves are never created; map to ones that exist in the portal."
457
+ ) })
385
458
  ] }) }),
386
459
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
387
460
  /* @__PURE__ */ jsx(Button, { onClick: save, loading: busy, children: t("settings.save", "Save") }),
@@ -30,7 +30,18 @@ const en = {
30
30
  "picker.refresh": "Refresh the properties from HubSpot",
31
31
  "picker.import": "Import the {count} options from HubSpot",
32
32
  "picker.imported": "Options imported — save to keep them.",
33
- "settings.subtitle": "Private app token and the HubSpot account that receives form conversions. Switch test → production here — nothing is hardcoded.",
33
+ "settings.subtitle": "Private app token, HubSpot account, and how submissions reach the CRM. Switch test → production here — nothing is hardcoded.",
34
+ "settings.policy.title": "How submissions reach HubSpot",
35
+ "settings.policy.hint": "Pick the policy that matches this portal. Defaults stay safe for every install: conversions when a form is linked, leftover answers still written on the contact, HubSpot forms never mutated unless you ask.",
36
+ "settings.mode.label": "Submission path",
37
+ "settings.mode.hint": "Automatic uses the marketing form when one is linked, otherwise a CRM upsert. CRM-only never counts as a conversion. Forms-only prefers the Forms API and falls back to the CRM if HubSpot refuses the submit.",
38
+ "settings.mode.auto": "Automatic — form conversion if linked, else CRM",
39
+ "settings.mode.forms": "Marketing form — native HubSpot conversions",
40
+ "settings.mode.crm": "CRM only — upsert the contact, no form conversion",
41
+ "settings.extra.label": "After a conversion, also write mapped contact fields that the HubSpot form doesn't have",
42
+ "settings.extra.hint": "The Forms API drops unknown fields (they 400 the submit). With this on, leftovers still land on the contact. Turn it off if HubSpot must only receive what the marketing form declares.",
43
+ "settings.sync.label": "On publish, add mapped contact fields that are missing on the HubSpot form",
44
+ "settings.sync.hint": "Strapi-first installs: a field added here appears on the HubSpot form next publish. HubSpot-first installs should leave this off — importing a form already carries its fields. Properties themselves are never created; map to ones that exist in the portal.",
34
45
  "settings.configured": "Configured {hint}",
35
46
  "settings.not-configured": "Not configured",
36
47
  "settings.source": "source: {source}",
@@ -62,7 +73,7 @@ const en = {
62
73
  "settings.test-ok": "Connection established — {count} readable properties.",
63
74
  "settings.test-no-key": "No key configured.",
64
75
  "settings.test-error": "HubSpot unreachable — invalid or revoked key?",
65
- "settings.scopes": "The token needs crm.schemas.contacts.read, crm.schemas.companies.read and forms (to list and submit marketing forms). The token is never returned to the browser.",
76
+ "settings.scopes": "The token needs crm.schemas.contacts.read, crm.schemas.companies.read and forms (to list, submit, and optionally update marketing forms). The token is never returned to the browser.",
66
77
  "audit.title": "Mapping audit",
67
78
  "audit.subtitle": "Scans every entry of the validated content types and lists the mappings the portal would reject today — including properties deleted in HubSpot since they were saved.",
68
79
  "audit.run": "Run the audit",
@@ -122,6 +133,8 @@ const en = {
122
133
  "editor.invalid": "The structure has problems — see the flagged cards.",
123
134
  "editor.publish": "Publish",
124
135
  "editor.published": "Form published.",
136
+ "editor.published-synced": "Form published. {count, plural, one {# field added} other {# fields added}} to the HubSpot form.",
137
+ "editor.published-sync-warn": "Form published. HubSpot field sync failed: {error}",
125
138
  "editor.publish-blocked": "Publishing is blocked — fix the flagged mappings first.",
126
139
  "editor.unpublish": "Unpublish",
127
140
  "editor.unpublished": "Form unpublished.",
@@ -28,7 +28,18 @@ const en = {
28
28
  "picker.refresh": "Refresh the properties from HubSpot",
29
29
  "picker.import": "Import the {count} options from HubSpot",
30
30
  "picker.imported": "Options imported — save to keep them.",
31
- "settings.subtitle": "Private app token and the HubSpot account that receives form conversions. Switch test → production here — nothing is hardcoded.",
31
+ "settings.subtitle": "Private app token, HubSpot account, and how submissions reach the CRM. Switch test → production here — nothing is hardcoded.",
32
+ "settings.policy.title": "How submissions reach HubSpot",
33
+ "settings.policy.hint": "Pick the policy that matches this portal. Defaults stay safe for every install: conversions when a form is linked, leftover answers still written on the contact, HubSpot forms never mutated unless you ask.",
34
+ "settings.mode.label": "Submission path",
35
+ "settings.mode.hint": "Automatic uses the marketing form when one is linked, otherwise a CRM upsert. CRM-only never counts as a conversion. Forms-only prefers the Forms API and falls back to the CRM if HubSpot refuses the submit.",
36
+ "settings.mode.auto": "Automatic — form conversion if linked, else CRM",
37
+ "settings.mode.forms": "Marketing form — native HubSpot conversions",
38
+ "settings.mode.crm": "CRM only — upsert the contact, no form conversion",
39
+ "settings.extra.label": "After a conversion, also write mapped contact fields that the HubSpot form doesn't have",
40
+ "settings.extra.hint": "The Forms API drops unknown fields (they 400 the submit). With this on, leftovers still land on the contact. Turn it off if HubSpot must only receive what the marketing form declares.",
41
+ "settings.sync.label": "On publish, add mapped contact fields that are missing on the HubSpot form",
42
+ "settings.sync.hint": "Strapi-first installs: a field added here appears on the HubSpot form next publish. HubSpot-first installs should leave this off — importing a form already carries its fields. Properties themselves are never created; map to ones that exist in the portal.",
32
43
  "settings.configured": "Configured {hint}",
33
44
  "settings.not-configured": "Not configured",
34
45
  "settings.source": "source: {source}",
@@ -60,7 +71,7 @@ const en = {
60
71
  "settings.test-ok": "Connection established — {count} readable properties.",
61
72
  "settings.test-no-key": "No key configured.",
62
73
  "settings.test-error": "HubSpot unreachable — invalid or revoked key?",
63
- "settings.scopes": "The token needs crm.schemas.contacts.read, crm.schemas.companies.read and forms (to list and submit marketing forms). The token is never returned to the browser.",
74
+ "settings.scopes": "The token needs crm.schemas.contacts.read, crm.schemas.companies.read and forms (to list, submit, and optionally update marketing forms). The token is never returned to the browser.",
64
75
  "audit.title": "Mapping audit",
65
76
  "audit.subtitle": "Scans every entry of the validated content types and lists the mappings the portal would reject today — including properties deleted in HubSpot since they were saved.",
66
77
  "audit.run": "Run the audit",
@@ -120,6 +131,8 @@ const en = {
120
131
  "editor.invalid": "The structure has problems — see the flagged cards.",
121
132
  "editor.publish": "Publish",
122
133
  "editor.published": "Form published.",
134
+ "editor.published-synced": "Form published. {count, plural, one {# field added} other {# fields added}} to the HubSpot form.",
135
+ "editor.published-sync-warn": "Form published. HubSpot field sync failed: {error}",
123
136
  "editor.publish-blocked": "Publishing is blocked — fix the flagged mappings first.",
124
137
  "editor.unpublish": "Unpublish",
125
138
  "editor.unpublished": "Form unpublished.",
@@ -28,7 +28,18 @@ const fr = {
28
28
  "picker.refresh": "Rafraîchir les propriétés depuis HubSpot",
29
29
  "picker.import": "Importer les {count} options depuis HubSpot",
30
30
  "picker.imported": "Options importées — enregistrez pour les conserver.",
31
- "settings.subtitle": "Jeton d'application privée et compte HubSpot qui reçoit les conversions. Passez du test à la prod ici — rien n'est figé dans le code.",
31
+ "settings.subtitle": "Jeton d'application privée, compte HubSpot, et la façon dont les envois atteignent le CRM. Passez du test à la prod ici — rien n'est figé dans le code.",
32
+ "settings.policy.title": "Comment les envois atteignent HubSpot",
33
+ "settings.policy.hint": "Choisissez la politique de ce portail. Les défauts restent sûrs pour toute install : conversion si un formulaire est lié, réponses en trop écrites sur le contact, formulaires HubSpot jamais mutés sauf si vous le demandez.",
34
+ "settings.mode.label": "Chemin d'envoi",
35
+ "settings.mode.hint": "Automatique utilise le formulaire marketing s'il est lié, sinon un upsert CRM. CRM seul ne compte jamais comme conversion. Formulaire marketing privilégie l'API Forms et retombe sur le CRM si HubSpot refuse l'envoi.",
36
+ "settings.mode.auto": "Automatique — conversion si un formulaire est lié, sinon CRM",
37
+ "settings.mode.forms": "Formulaire marketing — conversions HubSpot natives",
38
+ "settings.mode.crm": "CRM uniquement — upsert du contact, pas de conversion",
39
+ "settings.extra.label": "Après une conversion, écrire aussi sur le contact les champs mappés absents du formulaire HubSpot",
40
+ "settings.extra.hint": "L'API Forms écarte les champs inconnus (ils font échouer tout l'envoi). Activé, les champs restants atterrissent quand même sur le contact. Désactivez si HubSpot ne doit recevoir que ce que le formulaire marketing déclare.",
41
+ "settings.sync.label": "À la publication, ajouter au formulaire HubSpot les champs mappés manquants",
42
+ "settings.sync.hint": "Install Strapi-first : un champ ajouté ici apparaît sur le formulaire HubSpot à la publication suivante. Install HubSpot-first : laissez désactivé — l'import porte déjà les champs. Les propriétés CRM ne sont jamais créées ; mappez vers des propriétés déjà présentes dans le portail.",
32
43
  "settings.configured": "Configurée {hint}",
33
44
  "settings.not-configured": "Non configurée",
34
45
  "settings.source": "source : {source}",
@@ -60,7 +71,7 @@ const fr = {
60
71
  "settings.test-ok": "Connexion établie — {count} propriétés lisibles.",
61
72
  "settings.test-no-key": "Aucune clé configurée.",
62
73
  "settings.test-error": "HubSpot injoignable — clé invalide ou révoquée ?",
63
- "settings.scopes": "Le jeton a besoin de crm.schemas.contacts.read, crm.schemas.companies.read et forms (lister et soumettre les formulaires marketing). Il n'est jamais renvoyé au navigateur.",
74
+ "settings.scopes": "Le jeton a besoin de crm.schemas.contacts.read, crm.schemas.companies.read et forms (lister, soumettre et éventuellement mettre à jour les formulaires marketing). Il n'est jamais renvoyé au navigateur.",
64
75
  "audit.title": "Audit des mappings",
65
76
  "audit.subtitle": "Parcourt toutes les entrées des content-types validés et liste les mappings que le portail refuserait aujourd'hui — y compris les propriétés supprimées dans HubSpot depuis leur enregistrement.",
66
77
  "audit.run": "Lancer l'audit",
@@ -120,6 +131,8 @@ const fr = {
120
131
  "editor.invalid": "La structure a des problèmes — voir les cartes signalées.",
121
132
  "editor.publish": "Publier",
122
133
  "editor.published": "Formulaire publié.",
134
+ "editor.published-synced": "Formulaire publié. {count, plural, one {# champ ajouté} other {# champs ajoutés}} au formulaire HubSpot.",
135
+ "editor.published-sync-warn": "Formulaire publié. La sync des champs HubSpot a échoué : {error}",
123
136
  "editor.publish-blocked": "Publication bloquée — corrigez d'abord les mappings signalés.",
124
137
  "editor.unpublish": "Dépublier",
125
138
  "editor.unpublished": "Formulaire dépublié.",