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.
- package/CHANGELOG.md +14 -0
- package/README.md +28 -13
- package/dist/_chunks/{Forms-B7eIm0Fi.mjs → Forms-6EoEYzAc.mjs} +26 -5
- package/dist/_chunks/{Forms-ClyI4MJ1.js → Forms-BW_zGc1P.js} +26 -5
- package/dist/_chunks/{HubspotFormInput-cZAiaAaq.mjs → HubspotFormInput-V5w4y1fb.mjs} +1 -1
- package/dist/_chunks/{HubspotFormInput-Do7F83V6.js → HubspotFormInput-oMhm1UJm.js} +1 -1
- package/dist/_chunks/{HubspotObjectInput-DWqk2C26.js → HubspotObjectInput-Bmo6YQgs.js} +3 -3
- package/dist/_chunks/{HubspotObjectInput-ZZk_7hWq.mjs → HubspotObjectInput-DhsnSguG.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-5bkDGmGo.mjs → HubspotPropertyInput-CrYP4pc0.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-v9B3wb1a.js → HubspotPropertyInput-luFfnxqy.js} +3 -3
- package/dist/_chunks/{Settings-X3EHnTaO.js → Settings-B3A22i7Z.js} +77 -4
- package/dist/_chunks/{Settings-QcFbm3s5.mjs → Settings-CjcWj9I6.mjs} +78 -5
- package/dist/_chunks/{en-ZHvaZLi7.js → en-C7ls2EUl.js} +15 -2
- package/dist/_chunks/{en-DMJeF7EN.mjs → en-D6GHgMOt.mjs} +15 -2
- package/dist/_chunks/{fr-CVAntLmc.mjs → fr-CfohMY8n.mjs} +15 -2
- package/dist/_chunks/{fr-Ds_2EAzZ.js → fr-pOP8ZXqM.js} +15 -2
- package/dist/_chunks/{index-BSJAhj8r.mjs → index-7cL9PRh1.mjs} +6 -6
- package/dist/_chunks/{index-BpcilDy1.js → index-CodpLKZx.js} +6 -6
- package/dist/_chunks/{objectLabels-CYXXFpNB.js → objectLabels-CSWGIgNb.js} +1 -1
- package/dist/_chunks/{objectLabels-CrYRzsqe.mjs → objectLabels-Dnj14fsp.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-CBF4GkmK.mjs → useHubspotSchema-BsmQgJgx.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-Ba6xZUCL.js → useHubspotSchema-DiRN3HrN.js} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +216 -7
- package/dist/server/index.mjs +216 -7
- package/dist/server/src/__tests__/formSync.test.d.ts +1 -0
- package/dist/server/src/formSync.d.ts +37 -0
- package/dist/server/src/forms.d.ts +6 -8
- package/dist/server/src/hsforms.d.ts +8 -0
- package/dist/server/src/importHubspot.d.ts +4 -0
- package/dist/server/src/index.d.ts +6 -0
- package/dist/server/src/settings.d.ts +32 -3
- package/package.json +1 -1
|
@@ -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": "Jeton d'application privée
|
|
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}",
|
|
@@ -62,7 +73,7 @@ const fr = {
|
|
|
62
73
|
"settings.test-ok": "Connexion établie — {count} propriétés lisibles.",
|
|
63
74
|
"settings.test-no-key": "Aucune clé configurée.",
|
|
64
75
|
"settings.test-error": "HubSpot injoignable — clé invalide ou révoquée ?",
|
|
65
|
-
"settings.scopes": "Le jeton a besoin de crm.schemas.contacts.read, crm.schemas.companies.read et forms (lister et
|
|
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.",
|
|
66
77
|
"audit.title": "Audit des mappings",
|
|
67
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.",
|
|
68
79
|
"audit.run": "Lancer l'audit",
|
|
@@ -122,6 +133,8 @@ const fr = {
|
|
|
122
133
|
"editor.invalid": "La structure a des problèmes — voir les cartes signalées.",
|
|
123
134
|
"editor.publish": "Publier",
|
|
124
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}",
|
|
125
138
|
"editor.publish-blocked": "Publication bloquée — corrigez d'abord les mappings signalés.",
|
|
126
139
|
"editor.unpublish": "Dépublier",
|
|
127
140
|
"editor.unpublished": "Formulaire dépublié.",
|
|
@@ -25,7 +25,7 @@ const index = {
|
|
|
25
25
|
icon: PaperPlane,
|
|
26
26
|
intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
|
|
27
27
|
permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
|
|
28
|
-
Component: async () => (await import("./Forms-
|
|
28
|
+
Component: async () => (await import("./Forms-6EoEYzAc.mjs")).default
|
|
29
29
|
});
|
|
30
30
|
app.customFields.register({
|
|
31
31
|
name: "property",
|
|
@@ -40,7 +40,7 @@ const index = {
|
|
|
40
40
|
defaultMessage: "Picked from the portal's real properties"
|
|
41
41
|
},
|
|
42
42
|
components: {
|
|
43
|
-
Input: async () => import("./HubspotPropertyInput-
|
|
43
|
+
Input: async () => import("./HubspotPropertyInput-CrYP4pc0.mjs")
|
|
44
44
|
},
|
|
45
45
|
// Exposed in the Content-Type Builder. Without this, `objectField` could
|
|
46
46
|
// only be set by hand-editing schema.json — which is not how anyone
|
|
@@ -103,7 +103,7 @@ const index = {
|
|
|
103
103
|
defaultMessage: "Picked from the portal's configured objects"
|
|
104
104
|
},
|
|
105
105
|
components: {
|
|
106
|
-
Input: async () => import("./HubspotObjectInput-
|
|
106
|
+
Input: async () => import("./HubspotObjectInput-DhsnSguG.mjs")
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
app.customFields.register({
|
|
@@ -119,7 +119,7 @@ const index = {
|
|
|
119
119
|
defaultMessage: "A form built in the HubSpot form builder, stored by slug"
|
|
120
120
|
},
|
|
121
121
|
components: {
|
|
122
|
-
Input: async () => import("./HubspotFormInput-
|
|
122
|
+
Input: async () => import("./HubspotFormInput-V5w4y1fb.mjs")
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
125
|
app.createSettingSection(
|
|
@@ -135,7 +135,7 @@ const index = {
|
|
|
135
135
|
// Mirrors the RBAC action registered server-side: a role without it
|
|
136
136
|
// doesn't see the link at all — the routes refuse it anyway.
|
|
137
137
|
permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
|
|
138
|
-
Component: async () => (await import("./Settings-
|
|
138
|
+
Component: async () => (await import("./Settings-CjcWj9I6.mjs")).default
|
|
139
139
|
}
|
|
140
140
|
]
|
|
141
141
|
);
|
|
@@ -145,7 +145,7 @@ const index = {
|
|
|
145
145
|
return Promise.all(
|
|
146
146
|
locales.map(async (locale) => {
|
|
147
147
|
try {
|
|
148
|
-
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-
|
|
148
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-D6GHgMOt.mjs"), "./translations/fr.json": () => import("./fr-CfohMY8n.mjs") }), `./translations/${locale}.json`, 3);
|
|
149
149
|
return { data: prefixPluginTranslations(data), locale };
|
|
150
150
|
} catch {
|
|
151
151
|
return { data: {}, locale };
|
|
@@ -26,7 +26,7 @@ const index = {
|
|
|
26
26
|
icon: icons.PaperPlane,
|
|
27
27
|
intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
|
|
28
28
|
permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
|
|
29
|
-
Component: async () => (await Promise.resolve().then(() => require("./Forms-
|
|
29
|
+
Component: async () => (await Promise.resolve().then(() => require("./Forms-BW_zGc1P.js"))).default
|
|
30
30
|
});
|
|
31
31
|
app.customFields.register({
|
|
32
32
|
name: "property",
|
|
@@ -41,7 +41,7 @@ const index = {
|
|
|
41
41
|
defaultMessage: "Picked from the portal's real properties"
|
|
42
42
|
},
|
|
43
43
|
components: {
|
|
44
|
-
Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-
|
|
44
|
+
Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-luFfnxqy.js"))
|
|
45
45
|
},
|
|
46
46
|
// Exposed in the Content-Type Builder. Without this, `objectField` could
|
|
47
47
|
// only be set by hand-editing schema.json — which is not how anyone
|
|
@@ -104,7 +104,7 @@ const index = {
|
|
|
104
104
|
defaultMessage: "Picked from the portal's configured objects"
|
|
105
105
|
},
|
|
106
106
|
components: {
|
|
107
|
-
Input: async () => Promise.resolve().then(() => require("./HubspotObjectInput-
|
|
107
|
+
Input: async () => Promise.resolve().then(() => require("./HubspotObjectInput-Bmo6YQgs.js"))
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
app.customFields.register({
|
|
@@ -120,7 +120,7 @@ const index = {
|
|
|
120
120
|
defaultMessage: "A form built in the HubSpot form builder, stored by slug"
|
|
121
121
|
},
|
|
122
122
|
components: {
|
|
123
|
-
Input: async () => Promise.resolve().then(() => require("./HubspotFormInput-
|
|
123
|
+
Input: async () => Promise.resolve().then(() => require("./HubspotFormInput-oMhm1UJm.js"))
|
|
124
124
|
}
|
|
125
125
|
});
|
|
126
126
|
app.createSettingSection(
|
|
@@ -136,7 +136,7 @@ const index = {
|
|
|
136
136
|
// Mirrors the RBAC action registered server-side: a role without it
|
|
137
137
|
// doesn't see the link at all — the routes refuse it anyway.
|
|
138
138
|
permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
|
|
139
|
-
Component: async () => (await Promise.resolve().then(() => require("./Settings-
|
|
139
|
+
Component: async () => (await Promise.resolve().then(() => require("./Settings-B3A22i7Z.js"))).default
|
|
140
140
|
}
|
|
141
141
|
]
|
|
142
142
|
);
|
|
@@ -146,7 +146,7 @@ const index = {
|
|
|
146
146
|
return Promise.all(
|
|
147
147
|
locales.map(async (locale) => {
|
|
148
148
|
try {
|
|
149
|
-
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-
|
|
149
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-C7ls2EUl.js")), "./translations/fr.json": () => Promise.resolve().then(() => require("./fr-pOP8ZXqM.js")) }), `./translations/${locale}.json`, 3);
|
|
150
150
|
return { data: prefixPluginTranslations(data), locale };
|
|
151
151
|
} catch {
|
|
152
152
|
return { data: {}, locale };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
3
|
-
import { P as PLUGIN_ID } from "./index-
|
|
3
|
+
import { P as PLUGIN_ID } from "./index-7cL9PRh1.mjs";
|
|
4
4
|
let cached = null;
|
|
5
5
|
let inFlight = null;
|
|
6
6
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const React = require("react");
|
|
3
3
|
const admin = require("@strapi/strapi/admin");
|
|
4
|
-
const index = require("./index-
|
|
4
|
+
const index = require("./index-CodpLKZx.js");
|
|
5
5
|
function _interopNamespace(e) {
|
|
6
6
|
if (e && e.__esModule) return e;
|
|
7
7
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -177,6 +177,26 @@ const first = (values, fallback = "") => {
|
|
|
177
177
|
}
|
|
178
178
|
return { value: fallback, source: null };
|
|
179
179
|
};
|
|
180
|
+
const MODES = /* @__PURE__ */ new Set(["auto", "crm", "forms"]);
|
|
181
|
+
function asSubmissionMode(value) {
|
|
182
|
+
return typeof value === "string" && MODES.has(value) ? value : void 0;
|
|
183
|
+
}
|
|
184
|
+
function asBool(value) {
|
|
185
|
+
return typeof value === "boolean" ? value : void 0;
|
|
186
|
+
}
|
|
187
|
+
function formsCfg(strapi) {
|
|
188
|
+
return strapi.plugin("hubspot").config("forms", {}) ?? {};
|
|
189
|
+
}
|
|
190
|
+
function policyFrom(stored, forms) {
|
|
191
|
+
return {
|
|
192
|
+
submissionMode: asSubmissionMode(stored.submissionMode) ?? asSubmissionMode(forms.submissionMode) ?? "auto",
|
|
193
|
+
writeExtraProperties: asBool(stored.writeExtraProperties) ?? asBool(forms.writeExtraProperties) ?? true,
|
|
194
|
+
syncFieldsOnPublish: asBool(stored.syncFieldsOnPublish) ?? asBool(forms.syncFieldsOnPublish) ?? false
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function resolvePolicy(strapi) {
|
|
198
|
+
return policyFrom(await getStoredSettings(strapi), formsCfg(strapi));
|
|
199
|
+
}
|
|
180
200
|
async function resolveApiKey(strapi) {
|
|
181
201
|
const stored = await getStoredSettings(strapi);
|
|
182
202
|
if (stored.apiKey) return { apiKey: stored.apiKey, source: "settings" };
|
|
@@ -196,7 +216,7 @@ async function resolveAccount(strapi) {
|
|
|
196
216
|
}
|
|
197
217
|
async function resolveAccountSources(strapi) {
|
|
198
218
|
const stored = await getStoredSettings(strapi);
|
|
199
|
-
const
|
|
219
|
+
const forms = formsCfg(strapi);
|
|
200
220
|
const portal = first([
|
|
201
221
|
{ value: stored.portalId, source: "settings" },
|
|
202
222
|
{ value: strapi.plugin("hubspot").config("portalId", ""), source: "config" },
|
|
@@ -212,10 +232,11 @@ async function resolveAccountSources(strapi) {
|
|
|
212
232
|
);
|
|
213
233
|
const form = first([
|
|
214
234
|
{ value: stored.defaultFormId, source: "settings" },
|
|
215
|
-
{ value:
|
|
235
|
+
{ value: forms.defaultFormId, source: "config" },
|
|
216
236
|
{ value: process.env[ENV_DEFAULT_FORM], source: "env" }
|
|
217
237
|
]);
|
|
218
238
|
const { apiKey, source: keySource } = await resolveApiKey(strapi);
|
|
239
|
+
const policy = policyFrom(stored, forms);
|
|
219
240
|
return {
|
|
220
241
|
configured: Boolean(apiKey),
|
|
221
242
|
keySource,
|
|
@@ -225,7 +246,8 @@ async function resolveAccountSources(strapi) {
|
|
|
225
246
|
defaultFormId: form.value,
|
|
226
247
|
portalSource: portal.source,
|
|
227
248
|
regionSource: region.source,
|
|
228
|
-
formSource: form.source
|
|
249
|
+
formSource: form.source,
|
|
250
|
+
...policy
|
|
229
251
|
};
|
|
230
252
|
}
|
|
231
253
|
async function publicSettings(strapi) {
|
|
@@ -243,6 +265,16 @@ async function patchStoredSettings(strapi, body) {
|
|
|
243
265
|
next.region = region || "eu1";
|
|
244
266
|
}
|
|
245
267
|
if ("defaultFormId" in body) next.defaultFormId = String(body.defaultFormId ?? "").trim();
|
|
268
|
+
if ("submissionMode" in body) {
|
|
269
|
+
const mode = asSubmissionMode(body.submissionMode);
|
|
270
|
+
if (mode) next.submissionMode = mode;
|
|
271
|
+
}
|
|
272
|
+
if ("writeExtraProperties" in body && typeof body.writeExtraProperties === "boolean") {
|
|
273
|
+
next.writeExtraProperties = body.writeExtraProperties;
|
|
274
|
+
}
|
|
275
|
+
if ("syncFieldsOnPublish" in body && typeof body.syncFieldsOnPublish === "boolean") {
|
|
276
|
+
next.syncFieldsOnPublish = body.syncFieldsOnPublish;
|
|
277
|
+
}
|
|
246
278
|
await setStoredSettings(strapi, next);
|
|
247
279
|
}
|
|
248
280
|
async function clearStoredApiKey(strapi) {
|
|
@@ -1524,6 +1556,96 @@ function resolveSubmission(definition, rawValues) {
|
|
|
1524
1556
|
const unknown = Object.keys(rawValues).filter((key) => !declared.has(key));
|
|
1525
1557
|
return { values, hidden, missingRequired, unknown };
|
|
1526
1558
|
}
|
|
1559
|
+
const OBJECT_TYPE_ID = {
|
|
1560
|
+
contact: "0-1",
|
|
1561
|
+
company: "0-2"
|
|
1562
|
+
};
|
|
1563
|
+
function leftoverContactProps(contact, sentFieldNames) {
|
|
1564
|
+
const sent = new Set(sentFieldNames);
|
|
1565
|
+
const extra = {};
|
|
1566
|
+
for (const [key, value] of Object.entries(contact)) {
|
|
1567
|
+
if (key === "email" || sent.has(key) || value === "") continue;
|
|
1568
|
+
extra[key] = value;
|
|
1569
|
+
}
|
|
1570
|
+
return extra;
|
|
1571
|
+
}
|
|
1572
|
+
function mappedContactFields(definition) {
|
|
1573
|
+
const out = [];
|
|
1574
|
+
for (const step of definition.steps ?? []) {
|
|
1575
|
+
for (const fld of step.fields ?? []) {
|
|
1576
|
+
if (fld.type === "company") continue;
|
|
1577
|
+
const mapping = fld.hubspot ?? {};
|
|
1578
|
+
const object = mapping.object?.trim() || "contact";
|
|
1579
|
+
if (object !== "contact") continue;
|
|
1580
|
+
const property = mapping.property?.trim() || fld.name;
|
|
1581
|
+
if (!property) continue;
|
|
1582
|
+
out.push({
|
|
1583
|
+
name: fld.name,
|
|
1584
|
+
label: typeof fld.label === "string" ? fld.label : void 0,
|
|
1585
|
+
type: fld.type,
|
|
1586
|
+
required: Boolean(fld.required),
|
|
1587
|
+
object,
|
|
1588
|
+
property,
|
|
1589
|
+
options: fld.options
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return out;
|
|
1594
|
+
}
|
|
1595
|
+
function missingMappedFields(definition, existingNames) {
|
|
1596
|
+
const existing = new Set(existingNames);
|
|
1597
|
+
return mappedContactFields(definition).filter((field) => !existing.has(field.property));
|
|
1598
|
+
}
|
|
1599
|
+
function hsFieldType(type, options) {
|
|
1600
|
+
switch (type) {
|
|
1601
|
+
case "email":
|
|
1602
|
+
return "email";
|
|
1603
|
+
case "tel":
|
|
1604
|
+
return "phone";
|
|
1605
|
+
case "textarea":
|
|
1606
|
+
return "multi_line_text";
|
|
1607
|
+
case "number":
|
|
1608
|
+
return "number";
|
|
1609
|
+
case "select":
|
|
1610
|
+
return "dropdown";
|
|
1611
|
+
case "radio":
|
|
1612
|
+
return "radio";
|
|
1613
|
+
case "checkbox":
|
|
1614
|
+
return Array.isArray(options) && options.length ? "multiple_checkboxes" : "booleancheckbox";
|
|
1615
|
+
default:
|
|
1616
|
+
return "single_line_text";
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function toHsFormField(field) {
|
|
1620
|
+
const options = (field.options ?? []).filter((option) => option.value).map((option) => ({
|
|
1621
|
+
value: String(option.value),
|
|
1622
|
+
label: option.label || String(option.value)
|
|
1623
|
+
}));
|
|
1624
|
+
return {
|
|
1625
|
+
objectTypeId: OBJECT_TYPE_ID[field.object || "contact"] || "0-1",
|
|
1626
|
+
name: field.property,
|
|
1627
|
+
label: field.label || field.property,
|
|
1628
|
+
required: Boolean(field.required),
|
|
1629
|
+
hidden: false,
|
|
1630
|
+
fieldType: hsFieldType(field.type, field.options),
|
|
1631
|
+
...options.length ? { options } : {}
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function appendFieldGroups(existing, extra) {
|
|
1635
|
+
return [
|
|
1636
|
+
...existing.map((group) => ({
|
|
1637
|
+
groupType: group.groupType || "default_group",
|
|
1638
|
+
richTextType: group.richTextType || "text",
|
|
1639
|
+
...group.richText ? { richText: group.richText } : {},
|
|
1640
|
+
fields: group.fields ?? []
|
|
1641
|
+
})),
|
|
1642
|
+
...extra.map((field) => ({
|
|
1643
|
+
groupType: "default_group",
|
|
1644
|
+
richTextType: "text",
|
|
1645
|
+
fields: [toHsFormField(field)]
|
|
1646
|
+
}))
|
|
1647
|
+
];
|
|
1648
|
+
}
|
|
1527
1649
|
const HS_BASE$2 = "https://api.hubapi.com";
|
|
1528
1650
|
const OBJECT_BY_TYPE_ID = {
|
|
1529
1651
|
"0-1": "contact",
|
|
@@ -1687,6 +1809,23 @@ async function listHubspotForms(apiKey) {
|
|
|
1687
1809
|
async function fetchHubspotForm(apiKey, formId) {
|
|
1688
1810
|
return hsGet(apiKey, `/marketing/v3/forms/${encodeURIComponent(formId)}`);
|
|
1689
1811
|
}
|
|
1812
|
+
async function patchHubspotForm(apiKey, formId, body) {
|
|
1813
|
+
const res = await fetch(`${HS_BASE$2}/marketing/v3/forms/${encodeURIComponent(formId)}`, {
|
|
1814
|
+
method: "PATCH",
|
|
1815
|
+
headers: {
|
|
1816
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1817
|
+
"Content-Type": "application/json"
|
|
1818
|
+
},
|
|
1819
|
+
body: JSON.stringify(body)
|
|
1820
|
+
});
|
|
1821
|
+
if (!res.ok) {
|
|
1822
|
+
const payload = await res.json().catch(() => ({}));
|
|
1823
|
+
throw Object.assign(new Error(payload.message || `HubSpot ${res.status}`), {
|
|
1824
|
+
status: res.status
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
return await res.json();
|
|
1828
|
+
}
|
|
1690
1829
|
const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1691
1830
|
const PORTAL = /^\d{1,12}$/;
|
|
1692
1831
|
const HUTK = /^[a-f0-9]{8,128}$/i;
|
|
@@ -1735,6 +1874,10 @@ function formFieldNames(raw) {
|
|
|
1735
1874
|
return names;
|
|
1736
1875
|
}
|
|
1737
1876
|
const shapeCache = /* @__PURE__ */ new Map();
|
|
1877
|
+
function clearFormShapeCache(formGuid) {
|
|
1878
|
+
if (formGuid) shapeCache.delete(formGuid);
|
|
1879
|
+
else shapeCache.clear();
|
|
1880
|
+
}
|
|
1738
1881
|
async function loadFormShape(apiKey, formGuid) {
|
|
1739
1882
|
const cached = shapeCache.get(formGuid);
|
|
1740
1883
|
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
@@ -1830,6 +1973,16 @@ async function submitMarketingForm(input) {
|
|
|
1830
1973
|
}
|
|
1831
1974
|
return result;
|
|
1832
1975
|
}
|
|
1976
|
+
async function syncFieldsToHubspotForm(apiKey, formGuid, definition) {
|
|
1977
|
+
const raw = await fetchHubspotForm(apiKey, formGuid);
|
|
1978
|
+
const extra = missingMappedFields(definition, formFieldNames(raw));
|
|
1979
|
+
if (!extra.length) return [];
|
|
1980
|
+
await patchHubspotForm(apiKey, formGuid, {
|
|
1981
|
+
fieldGroups: appendFieldGroups(raw.fieldGroups ?? [], extra)
|
|
1982
|
+
});
|
|
1983
|
+
clearFormShapeCache(formGuid);
|
|
1984
|
+
return extra.map((field) => field.property);
|
|
1985
|
+
}
|
|
1833
1986
|
function publicForm(entry) {
|
|
1834
1987
|
return {
|
|
1835
1988
|
slug: entry.slug,
|
|
@@ -2197,6 +2350,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2197
2350
|
}
|
|
2198
2351
|
}
|
|
2199
2352
|
let usedFormsApi = false;
|
|
2353
|
+
let sentFieldNames = [];
|
|
2200
2354
|
if (email && apiKey) {
|
|
2201
2355
|
const groups = groupByObject(form.definition, resolution.values);
|
|
2202
2356
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -2204,21 +2358,25 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2204
2358
|
}
|
|
2205
2359
|
const partition = await partitionBySchema(apiKey, groups);
|
|
2206
2360
|
rejected = partition.rejected;
|
|
2361
|
+
const contactProps = stringProps(partition.accepted.contact ?? {});
|
|
2207
2362
|
const account = await resolveAccount(strapi);
|
|
2363
|
+
const policy = await resolvePolicy(strapi);
|
|
2208
2364
|
const portalId = sanitizePortalId(account.portalId);
|
|
2209
2365
|
const region = account.region || "eu1";
|
|
2210
2366
|
const ownGuid = typeof form.hubspotFormId === "string" ? form.hubspotFormId.trim() : "";
|
|
2211
2367
|
const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
|
|
2212
|
-
|
|
2368
|
+
const tryFormsApi = policy.submissionMode !== "crm" && Boolean(portalId && formGuid);
|
|
2369
|
+
if (tryFormsApi && portalId && formGuid) {
|
|
2213
2370
|
try {
|
|
2214
2371
|
const shape = await loadFormShape(apiKey, formGuid);
|
|
2215
2372
|
const fields = filterToFormFields(
|
|
2216
|
-
toHsFields({ ...
|
|
2373
|
+
toHsFields({ ...contactProps, email }),
|
|
2217
2374
|
shape?.names ?? null
|
|
2218
2375
|
);
|
|
2219
2376
|
if (fields.length && !fields.some((f) => f.name === "email")) {
|
|
2220
2377
|
fields.unshift({ name: "email", value: email });
|
|
2221
2378
|
}
|
|
2379
|
+
sentFieldNames = fields.map((f) => f.name);
|
|
2222
2380
|
const submitted = await submitMarketingForm({
|
|
2223
2381
|
apiKey,
|
|
2224
2382
|
portalId,
|
|
@@ -2255,12 +2413,28 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2255
2413
|
const result = await strapi.plugin("hubspot").service("submit").upsert({
|
|
2256
2414
|
object: "contact",
|
|
2257
2415
|
idProperty: "email",
|
|
2258
|
-
properties: { ...
|
|
2416
|
+
properties: { ...contactProps, email }
|
|
2259
2417
|
});
|
|
2260
2418
|
if (result.ok) {
|
|
2261
2419
|
hubspotSynced = true;
|
|
2262
2420
|
contactId = result.id;
|
|
2263
2421
|
}
|
|
2422
|
+
} else if (policy.writeExtraProperties) {
|
|
2423
|
+
const extra = leftoverContactProps(contactProps, sentFieldNames);
|
|
2424
|
+
if (Object.keys(extra).length) {
|
|
2425
|
+
try {
|
|
2426
|
+
const result = await strapi.plugin("hubspot").service("submit").upsert({
|
|
2427
|
+
object: "contact",
|
|
2428
|
+
idProperty: "email",
|
|
2429
|
+
properties: { ...extra, email }
|
|
2430
|
+
});
|
|
2431
|
+
if (result.ok) contactId = result.id ?? contactId;
|
|
2432
|
+
} catch (err) {
|
|
2433
|
+
strapi.log.warn(
|
|
2434
|
+
`[hubspot] leftover contact properties failed (conversion OK) — ${err.message}`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2264
2438
|
}
|
|
2265
2439
|
const domain = corporateDomain(email);
|
|
2266
2440
|
const companyProps = partition.accepted.company ?? {};
|
|
@@ -2559,7 +2733,32 @@ function createFormsAdminController(strapi) {
|
|
|
2559
2733
|
documentId: ctx.params.documentId,
|
|
2560
2734
|
locale: ctx.query.locale
|
|
2561
2735
|
});
|
|
2562
|
-
|
|
2736
|
+
const policy = await resolvePolicy(strapi);
|
|
2737
|
+
let addedFields = [];
|
|
2738
|
+
let syncWarning;
|
|
2739
|
+
if (policy.syncFieldsOnPublish) {
|
|
2740
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
2741
|
+
const account = await resolveAccount(strapi);
|
|
2742
|
+
const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
|
|
2743
|
+
const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
|
|
2744
|
+
if (apiKey && formGuid) {
|
|
2745
|
+
try {
|
|
2746
|
+
addedFields = await syncFieldsToHubspotForm(
|
|
2747
|
+
apiKey,
|
|
2748
|
+
formGuid,
|
|
2749
|
+
entry.definition ?? EMPTY_DEFINITION
|
|
2750
|
+
);
|
|
2751
|
+
} catch (err) {
|
|
2752
|
+
syncWarning = err.message;
|
|
2753
|
+
strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
ctx.body = {
|
|
2758
|
+
ok: true,
|
|
2759
|
+
...addedFields.length ? { addedFields } : {},
|
|
2760
|
+
...syncWarning ? { syncWarning } : {}
|
|
2761
|
+
};
|
|
2563
2762
|
},
|
|
2564
2763
|
async unpublish(ctx) {
|
|
2565
2764
|
await documents().unpublish({
|
|
@@ -3023,10 +3222,20 @@ const config = {
|
|
|
3023
3222
|
// submissions go through the marketing Forms API);
|
|
3024
3223
|
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
3025
3224
|
// doesn't set its own — swap with the portal at production cutover.
|
|
3225
|
+
// - submissionMode / writeExtraProperties / syncFieldsOnPublish: see
|
|
3226
|
+
// Settings → HubSpot; each install picks its HubSpot will.
|
|
3026
3227
|
forms: {
|
|
3027
3228
|
companyFromDomain: true,
|
|
3028
3229
|
timelineNote: true,
|
|
3029
3230
|
defaultFormId: "",
|
|
3231
|
+
// How submissions reach HubSpot. `auto` = Forms API when a GUID is set,
|
|
3232
|
+
// else CRM upsert. `crm` skips conversions. `forms` prefers the Forms API.
|
|
3233
|
+
submissionMode: "auto",
|
|
3234
|
+
// After a conversion, CRM-write mapped contact props the HubSpot form dropped.
|
|
3235
|
+
writeExtraProperties: true,
|
|
3236
|
+
// On publish, PATCH missing mapped contact fields onto the HubSpot form.
|
|
3237
|
+
// Off by default: HubSpot-first installs must not have Strapi mutate forms.
|
|
3238
|
+
syncFieldsOnPublish: false,
|
|
3030
3239
|
// Per-IP brakes on the public routes; 0 disables one.
|
|
3031
3240
|
rateLimit: { submitPerMinute: 6, searchPerMinute: 30 }
|
|
3032
3241
|
}
|