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
@@ -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 } 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-CPVfElYB.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-6OUnIDB5.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;
@@ -172,19 +172,37 @@ const FailuresSection = ({ configured }) => {
172
172
  ) : null
173
173
  ] });
174
174
  };
175
+ const REGIONS = ["eu1", "na1"];
175
176
  const HubspotSettings = () => {
176
177
  const { formatMessage } = useIntl();
177
178
  const { get, put, del } = useFetchClient();
178
179
  const [settings, setSettings] = React.useState(null);
179
180
  const [apiKey, setApiKey] = React.useState("");
181
+ const [portalId, setPortalId] = React.useState("");
182
+ const [region, setRegion] = React.useState("eu1");
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);
187
+ const [forms, setForms] = React.useState([]);
180
188
  const [busy, setBusy] = React.useState(false);
181
189
  const [feedback, setFeedback] = React.useState(
182
190
  null
183
191
  );
184
192
  const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
193
+ const apply = (data) => {
194
+ setSettings(data);
195
+ setPortalId(data.portalId ?? "");
196
+ setRegion(data.region || "eu1");
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);
201
+ setForms(Array.isArray(data.forms) ? data.forms : []);
202
+ };
185
203
  const load = React.useCallback(async () => {
186
204
  const { data } = await get(`/${PLUGIN_ID}/settings`);
187
- setSettings(data);
205
+ apply(data);
188
206
  }, [get]);
189
207
  React.useEffect(() => {
190
208
  load().catch(
@@ -195,12 +213,20 @@ const HubspotSettings = () => {
195
213
  setBusy(true);
196
214
  setFeedback(null);
197
215
  try {
198
- const { data } = await put(`/${PLUGIN_ID}/settings`, { apiKey });
199
- setSettings(data);
216
+ await put(`/${PLUGIN_ID}/settings`, {
217
+ ...apiKey.trim() ? { apiKey: apiKey.trim() } : {},
218
+ portalId,
219
+ region,
220
+ defaultFormId,
221
+ submissionMode,
222
+ writeExtraProperties,
223
+ syncFieldsOnPublish
224
+ });
200
225
  setApiKey("");
201
- setFeedback({ tone: "success", text: t("settings.saved", "Key saved.") });
226
+ await load();
227
+ setFeedback({ tone: "success", text: t("settings.saved", "Settings saved.") });
202
228
  } catch {
203
- setFeedback({ tone: "danger", text: t("settings.save-error", "Could not save the key.") });
229
+ setFeedback({ tone: "danger", text: t("settings.save-error", "Could not save settings.") });
204
230
  } finally {
205
231
  setBusy(false);
206
232
  }
@@ -209,9 +235,9 @@ const HubspotSettings = () => {
209
235
  setBusy(true);
210
236
  setFeedback(null);
211
237
  try {
212
- const { data } = await del(`/${PLUGIN_ID}/settings`);
213
- setSettings(data);
238
+ await del(`/${PLUGIN_ID}/settings`);
214
239
  setApiKey("");
240
+ await load();
215
241
  setFeedback({ tone: "success", text: t("settings.removed", "Key removed.") });
216
242
  } catch {
217
243
  setFeedback({ tone: "danger", text: t("settings.remove-error", "Could not remove the key.") });
@@ -234,6 +260,7 @@ const HubspotSettings = () => {
234
260
  })
235
261
  } : { tone: "danger", text: t("settings.test-no-key", "No key configured.") }
236
262
  );
263
+ if (data.configured) await load();
237
264
  } catch {
238
265
  setFeedback({
239
266
  tone: "danger",
@@ -251,44 +278,186 @@ const HubspotSettings = () => {
251
278
  config: t("settings.source.config", "config/plugins.ts"),
252
279
  env: t("settings.source.env", "environment variable")
253
280
  };
281
+ const sourceHint = (source) => source && source !== "settings" ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t("settings.source", "source: {source}", { source: sourceLabel[source] }) }) : null;
282
+ const regionOptions = REGIONS.includes(region) ? [...REGIONS] : [region, ...REGIONS];
254
283
  return /* @__PURE__ */ jsx(Box, { padding: 8, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
255
284
  /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 2, children: [
256
285
  /* @__PURE__ */ jsx(Typography, { variant: "alpha", children: "HubSpot" }),
257
286
  /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: t(
258
287
  "settings.subtitle",
259
- "The private app token used to read the portal's properties and validate form mappings."
288
+ "Private app token and the HubSpot account that receives form conversions. Switch test → production here — nothing is hardcoded."
260
289
  ) })
261
290
  ] }),
262
291
  /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
263
292
  /* @__PURE__ */ jsx(Badge, { active: settings.configured, children: settings.configured ? t("settings.configured", "Configured {hint}", { hint: settings.hint }) : t("settings.not-configured", "Not configured") }),
264
293
  settings.keySource ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t("settings.source", "source: {source}", { source: sourceLabel[settings.keySource] }) }) : null
265
294
  ] }),
266
- /* @__PURE__ */ jsx(Box, { maxWidth: "32rem", children: /* @__PURE__ */ jsxs(
267
- Field.Root,
268
- {
269
- name: "apiKey",
270
- hint: t(
271
- "settings.key.hint",
272
- "A key entered here overrides the ones from config/plugins.ts and HUBSPOT_API_KEY."
295
+ /* @__PURE__ */ jsx(Box, { maxWidth: "32rem", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
296
+ /* @__PURE__ */ jsxs(
297
+ Field.Root,
298
+ {
299
+ name: "apiKey",
300
+ hint: t(
301
+ "settings.key.hint",
302
+ "A key entered here overrides config/plugins.ts and HUBSPOT_API_KEY."
303
+ ),
304
+ children: [
305
+ /* @__PURE__ */ jsx(Field.Label, { children: t("settings.key.label", "Private app token") }),
306
+ /* @__PURE__ */ jsx(
307
+ Field.Input,
308
+ {
309
+ type: "password",
310
+ value: apiKey,
311
+ autoComplete: "off",
312
+ placeholder: settings.configured ? t("settings.key.placeholder.set", "•••••••• (leave empty to keep)") : t("settings.key.placeholder.empty", "pat-eu1-…"),
313
+ onChange: (e) => setApiKey(e.target.value)
314
+ }
315
+ ),
316
+ /* @__PURE__ */ jsx(Field.Hint, {})
317
+ ]
318
+ }
319
+ ),
320
+ /* @__PURE__ */ jsxs(
321
+ Field.Root,
322
+ {
323
+ name: "portalId",
324
+ hint: t(
325
+ "settings.portal.hint",
326
+ "HubSpot account id (Settings → Account → Account setup). Test portal today, production later."
327
+ ),
328
+ children: [
329
+ /* @__PURE__ */ jsx(Field.Label, { children: t("settings.portal.label", "Portal ID") }),
330
+ /* @__PURE__ */ jsx(
331
+ Field.Input,
332
+ {
333
+ type: "text",
334
+ value: portalId,
335
+ autoComplete: "off",
336
+ placeholder: "148991818",
337
+ onChange: (e) => setPortalId(e.target.value)
338
+ }
339
+ ),
340
+ /* @__PURE__ */ jsx(Field.Hint, {}),
341
+ sourceHint(settings.portalSource)
342
+ ]
343
+ }
344
+ ),
345
+ /* @__PURE__ */ jsxs(Field.Root, { name: "region", children: [
346
+ /* @__PURE__ */ jsx(Field.Label, { children: t("settings.region.label", "Region") }),
347
+ /* @__PURE__ */ jsx(
348
+ SingleSelect,
349
+ {
350
+ value: region,
351
+ onChange: (value) => setRegion(String(value)),
352
+ children: regionOptions.map((r) => /* @__PURE__ */ jsx(SingleSelectOption, { value: r, children: r === "eu1" ? t("settings.region.eu1", "Europe (eu1)") : r === "na1" ? t("settings.region.na1", "North America (na1)") : r }, r))
353
+ }
273
354
  ),
274
- children: [
275
- /* @__PURE__ */ jsx(Field.Label, { children: t("settings.key.label", "Private app token") }),
276
- /* @__PURE__ */ jsx(
277
- Field.Input,
278
- {
279
- type: "password",
280
- value: apiKey,
281
- autoComplete: "off",
282
- placeholder: settings.configured ? t("settings.key.placeholder.set", "•••••••• (leave empty to keep)") : t("settings.key.placeholder.empty", "pat-eu1-…"),
283
- onChange: (e) => setApiKey(e.target.value)
284
- }
355
+ sourceHint(settings.regionSource)
356
+ ] }),
357
+ /* @__PURE__ */ jsxs(
358
+ Field.Root,
359
+ {
360
+ name: "defaultFormId",
361
+ hint: t(
362
+ "settings.form.hint",
363
+ "Marketing form that receives submissions when a builder form doesn't pick its own. Create the form in HubSpot, then pick it here."
285
364
  ),
286
- /* @__PURE__ */ jsx(Field.Hint, {})
287
- ]
288
- }
289
- ) }),
365
+ children: [
366
+ /* @__PURE__ */ jsx(Field.Label, { children: t("settings.form.label", "Default marketing form") }),
367
+ forms.length ? /* @__PURE__ */ jsxs(
368
+ SingleSelect,
369
+ {
370
+ value: defaultFormId,
371
+ placeholder: t("settings.form.placeholder", "Pick a form of the connected portal"),
372
+ onChange: (value) => setDefaultFormId(String(value)),
373
+ children: [
374
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "", children: t("settings.form.none", "None — CRM upsert only") }),
375
+ defaultFormId && !forms.some((f) => f.id === defaultFormId) && /* @__PURE__ */ jsx(SingleSelectOption, { value: defaultFormId, children: defaultFormId }),
376
+ forms.map((f) => /* @__PURE__ */ jsx(SingleSelectOption, { value: f.id, children: f.name }, f.id))
377
+ ]
378
+ }
379
+ ) : /* @__PURE__ */ jsx(
380
+ Field.Input,
381
+ {
382
+ type: "text",
383
+ value: defaultFormId,
384
+ autoComplete: "off",
385
+ placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
386
+ onChange: (e) => setDefaultFormId(e.target.value.trim())
387
+ }
388
+ ),
389
+ /* @__PURE__ */ jsx(Field.Hint, {}),
390
+ sourceHint(settings.formSource)
391
+ ]
392
+ }
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
+ ) })
458
+ ] }) }),
290
459
  /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
291
- /* @__PURE__ */ jsx(Button, { onClick: save, loading: busy, disabled: !apiKey.trim(), children: t("settings.save", "Save") }),
460
+ /* @__PURE__ */ jsx(Button, { onClick: save, loading: busy, children: t("settings.save", "Save") }),
292
461
  /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: test, loading: busy, disabled: !settings.configured, children: t("settings.test", "Test connection") }),
293
462
  /* @__PURE__ */ jsx(
294
463
  Button,
@@ -297,7 +466,7 @@ const HubspotSettings = () => {
297
466
  onClick: remove,
298
467
  loading: busy,
299
468
  disabled: settings.keySource !== "settings",
300
- children: t("settings.remove", "Remove")
469
+ children: t("settings.remove", "Remove key")
301
470
  }
302
471
  )
303
472
  ] }),
@@ -311,7 +480,7 @@ const HubspotSettings = () => {
311
480
  ) : null,
312
481
  /* @__PURE__ */ jsx(Box, { paddingTop: 4, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
313
482
  "settings.scopes",
314
- "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."
483
+ "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."
315
484
  ) }) }),
316
485
  /* @__PURE__ */ jsx(AuditSection, { configured: settings.configured }),
317
486
  /* @__PURE__ */ jsx(FailuresSection, { configured: settings.configured })
@@ -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": "The private app token used to read the portal's properties and validate form mappings.",
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}",
@@ -38,22 +49,31 @@ const en = {
38
49
  "settings.source.config": "config/plugins.ts",
39
50
  "settings.source.env": "environment variable",
40
51
  "settings.key.label": "Private app token",
41
- "settings.key.hint": "A key entered here overrides the ones from config/plugins.ts and HUBSPOT_API_KEY.",
52
+ "settings.key.hint": "A key entered here overrides config/plugins.ts and HUBSPOT_API_KEY.",
42
53
  "settings.key.placeholder.set": "•••••••• (leave empty to keep)",
43
54
  "settings.key.placeholder.empty": "pat-eu1-…",
55
+ "settings.portal.label": "Portal ID",
56
+ "settings.portal.hint": "HubSpot account id (Settings → Account → Account setup). Test portal today, production later.",
57
+ "settings.region.label": "Region",
58
+ "settings.region.eu1": "Europe (eu1)",
59
+ "settings.region.na1": "North America (na1)",
60
+ "settings.form.label": "Default marketing form",
61
+ "settings.form.hint": "Marketing form that receives submissions when a builder form doesn't pick its own. Create the form in HubSpot, then pick it here.",
62
+ "settings.form.placeholder": "Pick a form of the connected portal",
63
+ "settings.form.none": "None — CRM upsert only",
44
64
  "settings.save": "Save",
45
65
  "settings.test": "Test connection",
46
- "settings.remove": "Remove",
66
+ "settings.remove": "Remove key",
47
67
  "settings.loading": "Loading…",
48
68
  "settings.load-error": "Could not load settings.",
49
- "settings.saved": "Key saved.",
50
- "settings.save-error": "Could not save the key.",
69
+ "settings.saved": "Settings saved.",
70
+ "settings.save-error": "Could not save settings.",
51
71
  "settings.removed": "Key removed.",
52
72
  "settings.remove-error": "Could not remove the key.",
53
73
  "settings.test-ok": "Connection established — {count} readable properties.",
54
74
  "settings.test-no-key": "No key configured.",
55
75
  "settings.test-error": "HubSpot unreachable — invalid or revoked key?",
56
- "settings.scopes": "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.",
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.",
57
77
  "audit.title": "Mapping audit",
58
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.",
59
79
  "audit.run": "Run the audit",
@@ -113,6 +133,8 @@ const en = {
113
133
  "editor.invalid": "The structure has problems — see the flagged cards.",
114
134
  "editor.publish": "Publish",
115
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}",
116
138
  "editor.publish-blocked": "Publishing is blocked — fix the flagged mappings first.",
117
139
  "editor.unpublish": "Unpublish",
118
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": "The private app token used to read the portal's properties and validate form mappings.",
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}",
@@ -36,22 +47,31 @@ const en = {
36
47
  "settings.source.config": "config/plugins.ts",
37
48
  "settings.source.env": "environment variable",
38
49
  "settings.key.label": "Private app token",
39
- "settings.key.hint": "A key entered here overrides the ones from config/plugins.ts and HUBSPOT_API_KEY.",
50
+ "settings.key.hint": "A key entered here overrides config/plugins.ts and HUBSPOT_API_KEY.",
40
51
  "settings.key.placeholder.set": "•••••••• (leave empty to keep)",
41
52
  "settings.key.placeholder.empty": "pat-eu1-…",
53
+ "settings.portal.label": "Portal ID",
54
+ "settings.portal.hint": "HubSpot account id (Settings → Account → Account setup). Test portal today, production later.",
55
+ "settings.region.label": "Region",
56
+ "settings.region.eu1": "Europe (eu1)",
57
+ "settings.region.na1": "North America (na1)",
58
+ "settings.form.label": "Default marketing form",
59
+ "settings.form.hint": "Marketing form that receives submissions when a builder form doesn't pick its own. Create the form in HubSpot, then pick it here.",
60
+ "settings.form.placeholder": "Pick a form of the connected portal",
61
+ "settings.form.none": "None — CRM upsert only",
42
62
  "settings.save": "Save",
43
63
  "settings.test": "Test connection",
44
- "settings.remove": "Remove",
64
+ "settings.remove": "Remove key",
45
65
  "settings.loading": "Loading…",
46
66
  "settings.load-error": "Could not load settings.",
47
- "settings.saved": "Key saved.",
48
- "settings.save-error": "Could not save the key.",
67
+ "settings.saved": "Settings saved.",
68
+ "settings.save-error": "Could not save settings.",
49
69
  "settings.removed": "Key removed.",
50
70
  "settings.remove-error": "Could not remove the key.",
51
71
  "settings.test-ok": "Connection established — {count} readable properties.",
52
72
  "settings.test-no-key": "No key configured.",
53
73
  "settings.test-error": "HubSpot unreachable — invalid or revoked key?",
54
- "settings.scopes": "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.",
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.",
55
75
  "audit.title": "Mapping audit",
56
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.",
57
77
  "audit.run": "Run the audit",
@@ -111,6 +131,8 @@ const en = {
111
131
  "editor.invalid": "The structure has problems — see the flagged cards.",
112
132
  "editor.publish": "Publish",
113
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}",
114
136
  "editor.publish-blocked": "Publishing is blocked — fix the flagged mappings first.",
115
137
  "editor.unpublish": "Unpublish",
116
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": "Le jeton d'application privée utilisé pour lire les propriétés du portail et valider les mappings de formulaires.",
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}",
@@ -36,22 +47,31 @@ const fr = {
36
47
  "settings.source.config": "config/plugins.ts",
37
48
  "settings.source.env": "variable d'environnement",
38
49
  "settings.key.label": "Jeton d'application privée",
39
- "settings.key.hint": "Saisir une clé ici remplace celles venant de config/plugins.ts et de HUBSPOT_API_KEY.",
50
+ "settings.key.hint": "Saisir une clé ici remplace config/plugins.ts et HUBSPOT_API_KEY.",
40
51
  "settings.key.placeholder.set": "•••••••• (laisser vide pour conserver)",
41
52
  "settings.key.placeholder.empty": "pat-eu1-…",
53
+ "settings.portal.label": "ID de portail",
54
+ "settings.portal.hint": "Identifiant du compte HubSpot (Paramètres → Compte). Portail de test aujourd'hui, production plus tard.",
55
+ "settings.region.label": "Région",
56
+ "settings.region.eu1": "Europe (eu1)",
57
+ "settings.region.na1": "Amérique du Nord (na1)",
58
+ "settings.form.label": "Formulaire marketing par défaut",
59
+ "settings.form.hint": "Formulaire qui reçoit les envois quand un formulaire du builder n'en choisit pas un. Créez-le dans HubSpot, puis sélectionnez-le ici.",
60
+ "settings.form.placeholder": "Choisir un formulaire du portail connecté",
61
+ "settings.form.none": "Aucun — upsert CRM uniquement",
42
62
  "settings.save": "Enregistrer",
43
63
  "settings.test": "Tester la connexion",
44
- "settings.remove": "Supprimer",
64
+ "settings.remove": "Supprimer la clé",
45
65
  "settings.loading": "Chargement…",
46
66
  "settings.load-error": "Réglages illisibles.",
47
- "settings.saved": "Clé enregistrée.",
67
+ "settings.saved": "Réglages enregistrés.",
48
68
  "settings.save-error": "Enregistrement impossible.",
49
69
  "settings.removed": "Clé supprimée.",
50
70
  "settings.remove-error": "Suppression impossible.",
51
71
  "settings.test-ok": "Connexion établie — {count} propriétés lisibles.",
52
72
  "settings.test-no-key": "Aucune clé configurée.",
53
73
  "settings.test-error": "HubSpot injoignable — clé invalide ou révoquée ?",
54
- "settings.scopes": "Le jeton a besoin des portées crm.schemas.contacts.read et crm.schemas.companies.read pour lister les propriétés. Il n'est jamais renvoyé au navigateur : seuls son existence, sa provenance et ses quatre derniers caractères le sont.",
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.",
55
75
  "audit.title": "Audit des mappings",
56
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.",
57
77
  "audit.run": "Lancer l'audit",
@@ -111,6 +131,8 @@ const fr = {
111
131
  "editor.invalid": "La structure a des problèmes — voir les cartes signalées.",
112
132
  "editor.publish": "Publier",
113
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}",
114
136
  "editor.publish-blocked": "Publication bloquée — corrigez d'abord les mappings signalés.",
115
137
  "editor.unpublish": "Dépublier",
116
138
  "editor.unpublished": "Formulaire dépublié.",
@@ -30,7 +30,18 @@ const fr = {
30
30
  "picker.refresh": "Rafraîchir les propriétés depuis HubSpot",
31
31
  "picker.import": "Importer les {count} options depuis HubSpot",
32
32
  "picker.imported": "Options importées — enregistrez pour les conserver.",
33
- "settings.subtitle": "Le jeton d'application privée utilisé pour lire les propriétés du portail et valider les mappings de formulaires.",
33
+ "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.",
34
+ "settings.policy.title": "Comment les envois atteignent HubSpot",
35
+ "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.",
36
+ "settings.mode.label": "Chemin d'envoi",
37
+ "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.",
38
+ "settings.mode.auto": "Automatique — conversion si un formulaire est lié, sinon CRM",
39
+ "settings.mode.forms": "Formulaire marketing — conversions HubSpot natives",
40
+ "settings.mode.crm": "CRM uniquement — upsert du contact, pas de conversion",
41
+ "settings.extra.label": "Après une conversion, écrire aussi sur le contact les champs mappés absents du formulaire HubSpot",
42
+ "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.",
43
+ "settings.sync.label": "À la publication, ajouter au formulaire HubSpot les champs mappés manquants",
44
+ "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.",
34
45
  "settings.configured": "Configurée {hint}",
35
46
  "settings.not-configured": "Non configurée",
36
47
  "settings.source": "source : {source}",
@@ -38,22 +49,31 @@ const fr = {
38
49
  "settings.source.config": "config/plugins.ts",
39
50
  "settings.source.env": "variable d'environnement",
40
51
  "settings.key.label": "Jeton d'application privée",
41
- "settings.key.hint": "Saisir une clé ici remplace celles venant de config/plugins.ts et de HUBSPOT_API_KEY.",
52
+ "settings.key.hint": "Saisir une clé ici remplace config/plugins.ts et HUBSPOT_API_KEY.",
42
53
  "settings.key.placeholder.set": "•••••••• (laisser vide pour conserver)",
43
54
  "settings.key.placeholder.empty": "pat-eu1-…",
55
+ "settings.portal.label": "ID de portail",
56
+ "settings.portal.hint": "Identifiant du compte HubSpot (Paramètres → Compte). Portail de test aujourd'hui, production plus tard.",
57
+ "settings.region.label": "Région",
58
+ "settings.region.eu1": "Europe (eu1)",
59
+ "settings.region.na1": "Amérique du Nord (na1)",
60
+ "settings.form.label": "Formulaire marketing par défaut",
61
+ "settings.form.hint": "Formulaire qui reçoit les envois quand un formulaire du builder n'en choisit pas un. Créez-le dans HubSpot, puis sélectionnez-le ici.",
62
+ "settings.form.placeholder": "Choisir un formulaire du portail connecté",
63
+ "settings.form.none": "Aucun — upsert CRM uniquement",
44
64
  "settings.save": "Enregistrer",
45
65
  "settings.test": "Tester la connexion",
46
- "settings.remove": "Supprimer",
66
+ "settings.remove": "Supprimer la clé",
47
67
  "settings.loading": "Chargement…",
48
68
  "settings.load-error": "Réglages illisibles.",
49
- "settings.saved": "Clé enregistrée.",
69
+ "settings.saved": "Réglages enregistrés.",
50
70
  "settings.save-error": "Enregistrement impossible.",
51
71
  "settings.removed": "Clé supprimée.",
52
72
  "settings.remove-error": "Suppression impossible.",
53
73
  "settings.test-ok": "Connexion établie — {count} propriétés lisibles.",
54
74
  "settings.test-no-key": "Aucune clé configurée.",
55
75
  "settings.test-error": "HubSpot injoignable — clé invalide ou révoquée ?",
56
- "settings.scopes": "Le jeton a besoin des portées crm.schemas.contacts.read et crm.schemas.companies.read pour lister les propriétés. Il n'est jamais renvoyé au navigateur : seuls son existence, sa provenance et ses quatre derniers caractères le sont.",
76
+ "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.",
57
77
  "audit.title": "Audit des mappings",
58
78
  "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.",
59
79
  "audit.run": "Lancer l'audit",
@@ -113,6 +133,8 @@ const fr = {
113
133
  "editor.invalid": "La structure a des problèmes — voir les cartes signalées.",
114
134
  "editor.publish": "Publier",
115
135
  "editor.published": "Formulaire publié.",
136
+ "editor.published-synced": "Formulaire publié. {count, plural, one {# champ ajouté} other {# champs ajoutés}} au formulaire HubSpot.",
137
+ "editor.published-sync-warn": "Formulaire publié. La sync des champs HubSpot a échoué : {error}",
116
138
  "editor.publish-blocked": "Publication bloquée — corrigez d'abord les mappings signalés.",
117
139
  "editor.unpublish": "Dépublier",
118
140
  "editor.unpublished": "Formulaire dépublié.",