strapi-plugin-hubspot 0.14.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 (35) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +39 -15
  3. package/dist/_chunks/{Forms-ws6d8rdC.mjs → Forms-6EoEYzAc.mjs} +26 -5
  4. package/dist/_chunks/{Forms-Cch-7zei.js → Forms-BW_zGc1P.js} +26 -5
  5. package/dist/_chunks/{HubspotFormInput-18pWTITT.mjs → HubspotFormInput-V5w4y1fb.mjs} +1 -1
  6. package/dist/_chunks/{HubspotFormInput-M2l_8w_e.js → HubspotFormInput-oMhm1UJm.js} +1 -1
  7. package/dist/_chunks/{HubspotObjectInput-BDO99qXs.js → HubspotObjectInput-Bmo6YQgs.js} +3 -3
  8. package/dist/_chunks/{HubspotObjectInput-DshusXZP.mjs → HubspotObjectInput-DhsnSguG.mjs} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-C-kHDgwx.mjs → HubspotPropertyInput-CrYP4pc0.mjs} +3 -3
  10. package/dist/_chunks/{HubspotPropertyInput-XGnH2tHn.js → HubspotPropertyInput-luFfnxqy.js} +3 -3
  11. package/dist/_chunks/{Settings-CRnsu6ob.js → Settings-B3A22i7Z.js} +204 -35
  12. package/dist/_chunks/{Settings-Bm7WkzK1.mjs → Settings-CjcWj9I6.mjs} +205 -36
  13. package/dist/_chunks/{en-BXMU5CjT.js → en-C7ls2EUl.js} +28 -6
  14. package/dist/_chunks/{en-DKV-8FWP.mjs → en-D6GHgMOt.mjs} +28 -6
  15. package/dist/_chunks/{fr-GB6T3ZyU.mjs → fr-CfohMY8n.mjs} +27 -5
  16. package/dist/_chunks/{fr-Dx4XmZ4X.js → fr-pOP8ZXqM.js} +27 -5
  17. package/dist/_chunks/{index-CPVfElYB.mjs → index-7cL9PRh1.mjs} +6 -6
  18. package/dist/_chunks/{index-BlaG8oIq.js → index-CodpLKZx.js} +6 -6
  19. package/dist/_chunks/{objectLabels-CPc21Tdd.js → objectLabels-CSWGIgNb.js} +1 -1
  20. package/dist/_chunks/{objectLabels-6OUnIDB5.mjs → objectLabels-Dnj14fsp.mjs} +1 -1
  21. package/dist/_chunks/{useHubspotSchema-qCUs2gIX.mjs → useHubspotSchema-BsmQgJgx.mjs} +1 -1
  22. package/dist/_chunks/{useHubspotSchema-0MvCM1AC.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 +301 -15
  26. package/dist/server/index.mjs +301 -15
  27. package/dist/server/src/__tests__/formSync.test.d.ts +1 -0
  28. package/dist/server/src/__tests__/settings.test.d.ts +1 -0
  29. package/dist/server/src/formSync.d.ts +37 -0
  30. package/dist/server/src/forms.d.ts +6 -8
  31. package/dist/server/src/hsforms.d.ts +8 -0
  32. package/dist/server/src/importHubspot.d.ts +4 -0
  33. package/dist/server/src/index.d.ts +11 -1
  34. package/dist/server/src/settings.d.ts +65 -5
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
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
+
19
+ ## 0.15.0 — 2026-09-21
20
+
21
+ ### Added — account settings in the admin
22
+ - **Settings → HubSpot** now holds the portal id, region (`eu1` / `na1`) and
23
+ the default marketing form (picked from the connected portal), not just the
24
+ private app token. Values saved here override `config/plugins.ts` and env.
25
+ Switching test → production is an admin change, not a deploy.
26
+
5
27
  ## 0.14.0 — 2026-09-21
6
28
 
7
29
  ### Changed — native HubSpot form conversions
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,10 +379,18 @@ its equivalent per object you send to).
365
379
 
366
380
  ### A settings screen
367
381
 
368
- **Settings → HubSpot** holds the private app token. It is stored server-side and
369
- never returned to the browser: the UI only receives whether a key exists, where
370
- it comes from, and its last four characters. A "Test connection" button
371
- round-trips to HubSpot and reports how many properties it can read.
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.
388
+
389
+ The token is stored server-side and never returned to the browser: the UI only
390
+ receives whether a key exists, where it comes from, and its last four
391
+ characters. Portal id / region / form GUID are not secrets and round-trip so
392
+ they can be edited. A "Test connection" button round-trips to HubSpot and
393
+ reports how many properties it can read.
372
394
 
373
395
  Access is gated by a dedicated RBAC permission — **Settings →
374
396
  Roles → Plugins → Hubspot**. A role without it neither sees the settings link
@@ -479,11 +501,13 @@ lives on `app-eu1.hubspot.com` and submits to `api-eu1.hsforms.com`. Set
479
501
  `config.region` (`eu1`, `na1`, …) to match. Deep links are built from the
480
502
  `uiDomain` HubSpot reports for your portal rather than a hardcoded host.
481
503
 
482
- The key is resolved in this order, first match wins:
504
+ The key, portal id, region and default form GUID are resolved in this order,
505
+ first match wins:
483
506
 
484
507
  1. saved from **Settings → HubSpot**
485
- 2. `config.apiKey` in `config/plugins.ts`
486
- 3. the `HUBSPOT_API_KEY` environment variable
508
+ 2. `config/plugins.ts` (`apiKey`, `portalId`, `region`, `forms.defaultFormId`)
509
+ 3. environment variables (`HUBSPOT_API_KEY`, `HUBSPOT_PORTAL_ID`,
510
+ `HUBSPOT_REGION`, `HUBSPOT_DEFAULT_FORM_ID`)
487
511
 
488
512
  ## Migrating an existing field
489
513
 
@@ -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-CPVfElYB.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-6OUnIDB5.mjs";
13
- import { u as useHubspotSchema } from "./useHubspotSchema-qCUs2gIX.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-BlaG8oIq.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-CPc21Tdd.js");
15
- const useHubspotSchema = require("./useHubspotSchema-0MvCM1AC.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-CPVfElYB.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-BlaG8oIq.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-BlaG8oIq.js");
8
- const objectLabels = require("./objectLabels-CPc21Tdd.js");
9
- const useHubspotSchema = require("./useHubspotSchema-0MvCM1AC.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-CPVfElYB.mjs";
6
- import { o as objectLabel } from "./objectLabels-6OUnIDB5.mjs";
7
- import { u as useHubspotSchema } from "./useHubspotSchema-qCUs2gIX.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-CPVfElYB.mjs";
8
- import { o as objectLabel } from "./objectLabels-6OUnIDB5.mjs";
9
- import { u as useHubspotSchema } from "./useHubspotSchema-qCUs2gIX.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-BlaG8oIq.js");
10
- const objectLabels = require("./objectLabels-CPc21Tdd.js");
11
- const useHubspotSchema = require("./useHubspotSchema-0MvCM1AC.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-BlaG8oIq.js");
8
+ const index$1 = require("./index-CodpLKZx.js");
9
9
  const reactRouterDom = require("react-router-dom");
10
- const objectLabels = require("./objectLabels-CPc21Tdd.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" } });
@@ -192,19 +192,37 @@ const FailuresSection = ({ configured }) => {
192
192
  ) : null
193
193
  ] });
194
194
  };
195
+ const REGIONS = ["eu1", "na1"];
195
196
  const HubspotSettings = () => {
196
197
  const { formatMessage } = index.useIntl();
197
198
  const { get, put, del } = admin.useFetchClient();
198
199
  const [settings, setSettings] = React__namespace.useState(null);
199
200
  const [apiKey, setApiKey] = React__namespace.useState("");
201
+ const [portalId, setPortalId] = React__namespace.useState("");
202
+ const [region, setRegion] = React__namespace.useState("eu1");
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);
207
+ const [forms, setForms] = React__namespace.useState([]);
200
208
  const [busy, setBusy] = React__namespace.useState(false);
201
209
  const [feedback, setFeedback] = React__namespace.useState(
202
210
  null
203
211
  );
204
212
  const t = (id, defaultMessage, values) => formatMessage({ id: index$1.getTranslation(id), defaultMessage }, values);
213
+ const apply = (data) => {
214
+ setSettings(data);
215
+ setPortalId(data.portalId ?? "");
216
+ setRegion(data.region || "eu1");
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);
221
+ setForms(Array.isArray(data.forms) ? data.forms : []);
222
+ };
205
223
  const load = React__namespace.useCallback(async () => {
206
224
  const { data } = await get(`/${index$1.PLUGIN_ID}/settings`);
207
- setSettings(data);
225
+ apply(data);
208
226
  }, [get]);
209
227
  React__namespace.useEffect(() => {
210
228
  load().catch(
@@ -215,12 +233,20 @@ const HubspotSettings = () => {
215
233
  setBusy(true);
216
234
  setFeedback(null);
217
235
  try {
218
- const { data } = await put(`/${index$1.PLUGIN_ID}/settings`, { apiKey });
219
- setSettings(data);
236
+ await put(`/${index$1.PLUGIN_ID}/settings`, {
237
+ ...apiKey.trim() ? { apiKey: apiKey.trim() } : {},
238
+ portalId,
239
+ region,
240
+ defaultFormId,
241
+ submissionMode,
242
+ writeExtraProperties,
243
+ syncFieldsOnPublish
244
+ });
220
245
  setApiKey("");
221
- setFeedback({ tone: "success", text: t("settings.saved", "Key saved.") });
246
+ await load();
247
+ setFeedback({ tone: "success", text: t("settings.saved", "Settings saved.") });
222
248
  } catch {
223
- setFeedback({ tone: "danger", text: t("settings.save-error", "Could not save the key.") });
249
+ setFeedback({ tone: "danger", text: t("settings.save-error", "Could not save settings.") });
224
250
  } finally {
225
251
  setBusy(false);
226
252
  }
@@ -229,9 +255,9 @@ const HubspotSettings = () => {
229
255
  setBusy(true);
230
256
  setFeedback(null);
231
257
  try {
232
- const { data } = await del(`/${index$1.PLUGIN_ID}/settings`);
233
- setSettings(data);
258
+ await del(`/${index$1.PLUGIN_ID}/settings`);
234
259
  setApiKey("");
260
+ await load();
235
261
  setFeedback({ tone: "success", text: t("settings.removed", "Key removed.") });
236
262
  } catch {
237
263
  setFeedback({ tone: "danger", text: t("settings.remove-error", "Could not remove the key.") });
@@ -254,6 +280,7 @@ const HubspotSettings = () => {
254
280
  })
255
281
  } : { tone: "danger", text: t("settings.test-no-key", "No key configured.") }
256
282
  );
283
+ if (data.configured) await load();
257
284
  } catch {
258
285
  setFeedback({
259
286
  tone: "danger",
@@ -271,44 +298,186 @@ const HubspotSettings = () => {
271
298
  config: t("settings.source.config", "config/plugins.ts"),
272
299
  env: t("settings.source.env", "environment variable")
273
300
  };
301
+ const sourceHint = (source) => source && source !== "settings" ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t("settings.source", "source: {source}", { source: sourceLabel[source] }) }) : null;
302
+ const regionOptions = REGIONS.includes(region) ? [...REGIONS] : [region, ...REGIONS];
274
303
  return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 8, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
275
304
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "flex-start", gap: 2, children: [
276
305
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", children: "HubSpot" }),
277
306
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "epsilon", textColor: "neutral600", children: t(
278
307
  "settings.subtitle",
279
- "The private app token used to read the portal's properties and validate form mappings."
308
+ "Private app token and the HubSpot account that receives form conversions. Switch test → production here — nothing is hardcoded."
280
309
  ) })
281
310
  ] }),
282
311
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
283
312
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { active: settings.configured, children: settings.configured ? t("settings.configured", "Configured {hint}", { hint: settings.hint }) : t("settings.not-configured", "Not configured") }),
284
313
  settings.keySource ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t("settings.source", "source: {source}", { source: sourceLabel[settings.keySource] }) }) : null
285
314
  ] }),
286
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { maxWidth: "32rem", children: /* @__PURE__ */ jsxRuntime.jsxs(
287
- designSystem.Field.Root,
288
- {
289
- name: "apiKey",
290
- hint: t(
291
- "settings.key.hint",
292
- "A key entered here overrides the ones from config/plugins.ts and HUBSPOT_API_KEY."
315
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { maxWidth: "32rem", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
316
+ /* @__PURE__ */ jsxRuntime.jsxs(
317
+ designSystem.Field.Root,
318
+ {
319
+ name: "apiKey",
320
+ hint: t(
321
+ "settings.key.hint",
322
+ "A key entered here overrides config/plugins.ts and HUBSPOT_API_KEY."
323
+ ),
324
+ children: [
325
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("settings.key.label", "Private app token") }),
326
+ /* @__PURE__ */ jsxRuntime.jsx(
327
+ designSystem.Field.Input,
328
+ {
329
+ type: "password",
330
+ value: apiKey,
331
+ autoComplete: "off",
332
+ placeholder: settings.configured ? t("settings.key.placeholder.set", "•••••••• (leave empty to keep)") : t("settings.key.placeholder.empty", "pat-eu1-…"),
333
+ onChange: (e) => setApiKey(e.target.value)
334
+ }
335
+ ),
336
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
337
+ ]
338
+ }
339
+ ),
340
+ /* @__PURE__ */ jsxRuntime.jsxs(
341
+ designSystem.Field.Root,
342
+ {
343
+ name: "portalId",
344
+ hint: t(
345
+ "settings.portal.hint",
346
+ "HubSpot account id (Settings → Account → Account setup). Test portal today, production later."
347
+ ),
348
+ children: [
349
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("settings.portal.label", "Portal ID") }),
350
+ /* @__PURE__ */ jsxRuntime.jsx(
351
+ designSystem.Field.Input,
352
+ {
353
+ type: "text",
354
+ value: portalId,
355
+ autoComplete: "off",
356
+ placeholder: "148991818",
357
+ onChange: (e) => setPortalId(e.target.value)
358
+ }
359
+ ),
360
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
361
+ sourceHint(settings.portalSource)
362
+ ]
363
+ }
364
+ ),
365
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "region", children: [
366
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("settings.region.label", "Region") }),
367
+ /* @__PURE__ */ jsxRuntime.jsx(
368
+ designSystem.SingleSelect,
369
+ {
370
+ value: region,
371
+ onChange: (value) => setRegion(String(value)),
372
+ children: regionOptions.map((r) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: r, children: r === "eu1" ? t("settings.region.eu1", "Europe (eu1)") : r === "na1" ? t("settings.region.na1", "North America (na1)") : r }, r))
373
+ }
293
374
  ),
294
- children: [
295
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("settings.key.label", "Private app token") }),
296
- /* @__PURE__ */ jsxRuntime.jsx(
297
- designSystem.Field.Input,
298
- {
299
- type: "password",
300
- value: apiKey,
301
- autoComplete: "off",
302
- placeholder: settings.configured ? t("settings.key.placeholder.set", "•••••••• (leave empty to keep)") : t("settings.key.placeholder.empty", "pat-eu1-…"),
303
- onChange: (e) => setApiKey(e.target.value)
304
- }
375
+ sourceHint(settings.regionSource)
376
+ ] }),
377
+ /* @__PURE__ */ jsxRuntime.jsxs(
378
+ designSystem.Field.Root,
379
+ {
380
+ name: "defaultFormId",
381
+ hint: t(
382
+ "settings.form.hint",
383
+ "Marketing form that receives submissions when a builder form doesn't pick its own. Create the form in HubSpot, then pick it here."
305
384
  ),
306
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
307
- ]
308
- }
309
- ) }),
385
+ children: [
386
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("settings.form.label", "Default marketing form") }),
387
+ forms.length ? /* @__PURE__ */ jsxRuntime.jsxs(
388
+ designSystem.SingleSelect,
389
+ {
390
+ value: defaultFormId,
391
+ placeholder: t("settings.form.placeholder", "Pick a form of the connected portal"),
392
+ onChange: (value) => setDefaultFormId(String(value)),
393
+ children: [
394
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "", children: t("settings.form.none", "None — CRM upsert only") }),
395
+ defaultFormId && !forms.some((f) => f.id === defaultFormId) && /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: defaultFormId, children: defaultFormId }),
396
+ forms.map((f) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: f.id, children: f.name }, f.id))
397
+ ]
398
+ }
399
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
400
+ designSystem.Field.Input,
401
+ {
402
+ type: "text",
403
+ value: defaultFormId,
404
+ autoComplete: "off",
405
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
406
+ onChange: (e) => setDefaultFormId(e.target.value.trim())
407
+ }
408
+ ),
409
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
410
+ sourceHint(settings.formSource)
411
+ ]
412
+ }
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
+ ) })
478
+ ] }) }),
310
479
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
311
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: save, loading: busy, disabled: !apiKey.trim(), children: t("settings.save", "Save") }),
480
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: save, loading: busy, children: t("settings.save", "Save") }),
312
481
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", onClick: test, loading: busy, disabled: !settings.configured, children: t("settings.test", "Test connection") }),
313
482
  /* @__PURE__ */ jsxRuntime.jsx(
314
483
  designSystem.Button,
@@ -317,7 +486,7 @@ const HubspotSettings = () => {
317
486
  onClick: remove,
318
487
  loading: busy,
319
488
  disabled: settings.keySource !== "settings",
320
- children: t("settings.remove", "Remove")
489
+ children: t("settings.remove", "Remove key")
321
490
  }
322
491
  )
323
492
  ] }),
@@ -331,7 +500,7 @@ const HubspotSettings = () => {
331
500
  ) : null,
332
501
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t(
333
502
  "settings.scopes",
334
- "The token needs the crm.schemas.contacts.read and crm.schemas.companies.read scopes to list properties. It is never returned to the browser: only its existence, its source and its last four characters are."
503
+ "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."
335
504
  ) }) }),
336
505
  /* @__PURE__ */ jsxRuntime.jsx(AuditSection, { configured: settings.configured }),
337
506
  /* @__PURE__ */ jsxRuntime.jsx(FailuresSection, { configured: settings.configured })