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.
- package/CHANGELOG.md +22 -0
- package/README.md +39 -15
- package/dist/_chunks/{Forms-ws6d8rdC.mjs → Forms-6EoEYzAc.mjs} +26 -5
- package/dist/_chunks/{Forms-Cch-7zei.js → Forms-BW_zGc1P.js} +26 -5
- package/dist/_chunks/{HubspotFormInput-18pWTITT.mjs → HubspotFormInput-V5w4y1fb.mjs} +1 -1
- package/dist/_chunks/{HubspotFormInput-M2l_8w_e.js → HubspotFormInput-oMhm1UJm.js} +1 -1
- package/dist/_chunks/{HubspotObjectInput-BDO99qXs.js → HubspotObjectInput-Bmo6YQgs.js} +3 -3
- package/dist/_chunks/{HubspotObjectInput-DshusXZP.mjs → HubspotObjectInput-DhsnSguG.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-C-kHDgwx.mjs → HubspotPropertyInput-CrYP4pc0.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-XGnH2tHn.js → HubspotPropertyInput-luFfnxqy.js} +3 -3
- package/dist/_chunks/{Settings-CRnsu6ob.js → Settings-B3A22i7Z.js} +204 -35
- package/dist/_chunks/{Settings-Bm7WkzK1.mjs → Settings-CjcWj9I6.mjs} +205 -36
- package/dist/_chunks/{en-BXMU5CjT.js → en-C7ls2EUl.js} +28 -6
- package/dist/_chunks/{en-DKV-8FWP.mjs → en-D6GHgMOt.mjs} +28 -6
- package/dist/_chunks/{fr-GB6T3ZyU.mjs → fr-CfohMY8n.mjs} +27 -5
- package/dist/_chunks/{fr-Dx4XmZ4X.js → fr-pOP8ZXqM.js} +27 -5
- package/dist/_chunks/{index-CPVfElYB.mjs → index-7cL9PRh1.mjs} +6 -6
- package/dist/_chunks/{index-BlaG8oIq.js → index-CodpLKZx.js} +6 -6
- package/dist/_chunks/{objectLabels-CPc21Tdd.js → objectLabels-CSWGIgNb.js} +1 -1
- package/dist/_chunks/{objectLabels-6OUnIDB5.mjs → objectLabels-Dnj14fsp.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-qCUs2gIX.mjs → useHubspotSchema-BsmQgJgx.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-0MvCM1AC.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 +301 -15
- package/dist/server/index.mjs +301 -15
- package/dist/server/src/__tests__/formSync.test.d.ts +1 -0
- package/dist/server/src/__tests__/settings.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 +11 -1
- package/dist/server/src/settings.d.ts +65 -5
- package/package.json +1 -1
|
@@ -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
|
@@ -159,6 +159,9 @@ function resolveObjects(configured) {
|
|
|
159
159
|
return out;
|
|
160
160
|
}
|
|
161
161
|
const ENV_VAR = "HUBSPOT_API_KEY";
|
|
162
|
+
const ENV_PORTAL = "HUBSPOT_PORTAL_ID";
|
|
163
|
+
const ENV_REGION = "HUBSPOT_REGION";
|
|
164
|
+
const ENV_DEFAULT_FORM = "HUBSPOT_DEFAULT_FORM_ID";
|
|
162
165
|
const store = (strapi) => strapi.store({ type: "plugin", name: "hubspot" });
|
|
163
166
|
async function getStoredSettings(strapi) {
|
|
164
167
|
return await store(strapi).get({ key: "settings" }) ?? {};
|
|
@@ -167,6 +170,33 @@ async function setStoredSettings(strapi, value) {
|
|
|
167
170
|
await store(strapi).set({ key: "settings", value });
|
|
168
171
|
clearCache();
|
|
169
172
|
}
|
|
173
|
+
const first = (values, fallback = "") => {
|
|
174
|
+
for (const item of values) {
|
|
175
|
+
const value = (item.value ?? "").trim();
|
|
176
|
+
if (value) return { value, source: item.source };
|
|
177
|
+
}
|
|
178
|
+
return { value: fallback, source: null };
|
|
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
|
+
}
|
|
170
200
|
async function resolveApiKey(strapi) {
|
|
171
201
|
const stored = await getStoredSettings(strapi);
|
|
172
202
|
if (stored.apiKey) return { apiKey: stored.apiKey, source: "settings" };
|
|
@@ -176,14 +206,83 @@ async function resolveApiKey(strapi) {
|
|
|
176
206
|
if (fromEnv) return { apiKey: fromEnv, source: "env" };
|
|
177
207
|
return { apiKey: "", source: null };
|
|
178
208
|
}
|
|
179
|
-
async function
|
|
180
|
-
const
|
|
209
|
+
async function resolveAccount(strapi) {
|
|
210
|
+
const resolved = await resolveAccountSources(strapi);
|
|
211
|
+
return {
|
|
212
|
+
portalId: resolved.portalId,
|
|
213
|
+
region: resolved.region || "eu1",
|
|
214
|
+
defaultFormId: resolved.defaultFormId
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async function resolveAccountSources(strapi) {
|
|
218
|
+
const stored = await getStoredSettings(strapi);
|
|
219
|
+
const forms = formsCfg(strapi);
|
|
220
|
+
const portal = first([
|
|
221
|
+
{ value: stored.portalId, source: "settings" },
|
|
222
|
+
{ value: strapi.plugin("hubspot").config("portalId", ""), source: "config" },
|
|
223
|
+
{ value: process.env[ENV_PORTAL], source: "env" }
|
|
224
|
+
]);
|
|
225
|
+
const region = first(
|
|
226
|
+
[
|
|
227
|
+
{ value: stored.region, source: "settings" },
|
|
228
|
+
{ value: strapi.plugin("hubspot").config("region", ""), source: "config" },
|
|
229
|
+
{ value: process.env[ENV_REGION], source: "env" }
|
|
230
|
+
],
|
|
231
|
+
"eu1"
|
|
232
|
+
);
|
|
233
|
+
const form = first([
|
|
234
|
+
{ value: stored.defaultFormId, source: "settings" },
|
|
235
|
+
{ value: forms.defaultFormId, source: "config" },
|
|
236
|
+
{ value: process.env[ENV_DEFAULT_FORM], source: "env" }
|
|
237
|
+
]);
|
|
238
|
+
const { apiKey, source: keySource } = await resolveApiKey(strapi);
|
|
239
|
+
const policy = policyFrom(stored, forms);
|
|
181
240
|
return {
|
|
182
241
|
configured: Boolean(apiKey),
|
|
183
|
-
keySource
|
|
184
|
-
hint: apiKey ? `…${apiKey.slice(-4)}` : ""
|
|
242
|
+
keySource,
|
|
243
|
+
hint: apiKey ? `…${apiKey.slice(-4)}` : "",
|
|
244
|
+
portalId: portal.value,
|
|
245
|
+
region: region.value || "eu1",
|
|
246
|
+
defaultFormId: form.value,
|
|
247
|
+
portalSource: portal.source,
|
|
248
|
+
regionSource: region.source,
|
|
249
|
+
formSource: form.source,
|
|
250
|
+
...policy
|
|
185
251
|
};
|
|
186
252
|
}
|
|
253
|
+
async function publicSettings(strapi) {
|
|
254
|
+
return resolveAccountSources(strapi);
|
|
255
|
+
}
|
|
256
|
+
async function patchStoredSettings(strapi, body) {
|
|
257
|
+
const current = await getStoredSettings(strapi);
|
|
258
|
+
const next = { ...current };
|
|
259
|
+
if (typeof body.apiKey === "string" && body.apiKey.trim()) {
|
|
260
|
+
next.apiKey = body.apiKey.trim();
|
|
261
|
+
}
|
|
262
|
+
if ("portalId" in body) next.portalId = String(body.portalId ?? "").trim();
|
|
263
|
+
if ("region" in body) {
|
|
264
|
+
const region = String(body.region ?? "").trim().toLowerCase();
|
|
265
|
+
next.region = region || "eu1";
|
|
266
|
+
}
|
|
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
|
+
}
|
|
278
|
+
await setStoredSettings(strapi, next);
|
|
279
|
+
}
|
|
280
|
+
async function clearStoredApiKey(strapi) {
|
|
281
|
+
const current = await getStoredSettings(strapi);
|
|
282
|
+
const { apiKey: _apiKey, ...rest } = current;
|
|
283
|
+
const empty = !rest.portalId && !rest.region && !rest.defaultFormId;
|
|
284
|
+
await setStoredSettings(strapi, empty ? null : rest);
|
|
285
|
+
}
|
|
187
286
|
function collectMappings(node, target, found = []) {
|
|
188
287
|
if (Array.isArray(node)) {
|
|
189
288
|
for (const item of node) collectMappings(item, target, found);
|
|
@@ -1457,6 +1556,96 @@ function resolveSubmission(definition, rawValues) {
|
|
|
1457
1556
|
const unknown = Object.keys(rawValues).filter((key) => !declared.has(key));
|
|
1458
1557
|
return { values, hidden, missingRequired, unknown };
|
|
1459
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
|
+
}
|
|
1460
1649
|
const HS_BASE$2 = "https://api.hubapi.com";
|
|
1461
1650
|
const OBJECT_BY_TYPE_ID = {
|
|
1462
1651
|
"0-1": "contact",
|
|
@@ -1620,6 +1809,23 @@ async function listHubspotForms(apiKey) {
|
|
|
1620
1809
|
async function fetchHubspotForm(apiKey, formId) {
|
|
1621
1810
|
return hsGet(apiKey, `/marketing/v3/forms/${encodeURIComponent(formId)}`);
|
|
1622
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
|
+
}
|
|
1623
1829
|
const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1624
1830
|
const PORTAL = /^\d{1,12}$/;
|
|
1625
1831
|
const HUTK = /^[a-f0-9]{8,128}$/i;
|
|
@@ -1668,6 +1874,10 @@ function formFieldNames(raw) {
|
|
|
1668
1874
|
return names;
|
|
1669
1875
|
}
|
|
1670
1876
|
const shapeCache = /* @__PURE__ */ new Map();
|
|
1877
|
+
function clearFormShapeCache(formGuid) {
|
|
1878
|
+
if (formGuid) shapeCache.delete(formGuid);
|
|
1879
|
+
else shapeCache.clear();
|
|
1880
|
+
}
|
|
1671
1881
|
async function loadFormShape(apiKey, formGuid) {
|
|
1672
1882
|
const cached = shapeCache.get(formGuid);
|
|
1673
1883
|
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
@@ -1763,6 +1973,16 @@ async function submitMarketingForm(input) {
|
|
|
1763
1973
|
}
|
|
1764
1974
|
return result;
|
|
1765
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
|
+
}
|
|
1766
1986
|
function publicForm(entry) {
|
|
1767
1987
|
return {
|
|
1768
1988
|
slug: entry.slug,
|
|
@@ -2130,6 +2350,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2130
2350
|
}
|
|
2131
2351
|
}
|
|
2132
2352
|
let usedFormsApi = false;
|
|
2353
|
+
let sentFieldNames = [];
|
|
2133
2354
|
if (email && apiKey) {
|
|
2134
2355
|
const groups = groupByObject(form.definition, resolution.values);
|
|
2135
2356
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -2137,20 +2358,25 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2137
2358
|
}
|
|
2138
2359
|
const partition = await partitionBySchema(apiKey, groups);
|
|
2139
2360
|
rejected = partition.rejected;
|
|
2140
|
-
const
|
|
2141
|
-
const
|
|
2361
|
+
const contactProps = stringProps(partition.accepted.contact ?? {});
|
|
2362
|
+
const account = await resolveAccount(strapi);
|
|
2363
|
+
const policy = await resolvePolicy(strapi);
|
|
2364
|
+
const portalId = sanitizePortalId(account.portalId);
|
|
2365
|
+
const region = account.region || "eu1";
|
|
2142
2366
|
const ownGuid = typeof form.hubspotFormId === "string" ? form.hubspotFormId.trim() : "";
|
|
2143
|
-
const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(
|
|
2144
|
-
|
|
2367
|
+
const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
|
|
2368
|
+
const tryFormsApi = policy.submissionMode !== "crm" && Boolean(portalId && formGuid);
|
|
2369
|
+
if (tryFormsApi && portalId && formGuid) {
|
|
2145
2370
|
try {
|
|
2146
2371
|
const shape = await loadFormShape(apiKey, formGuid);
|
|
2147
2372
|
const fields = filterToFormFields(
|
|
2148
|
-
toHsFields({ ...
|
|
2373
|
+
toHsFields({ ...contactProps, email }),
|
|
2149
2374
|
shape?.names ?? null
|
|
2150
2375
|
);
|
|
2151
2376
|
if (fields.length && !fields.some((f) => f.name === "email")) {
|
|
2152
2377
|
fields.unshift({ name: "email", value: email });
|
|
2153
2378
|
}
|
|
2379
|
+
sentFieldNames = fields.map((f) => f.name);
|
|
2154
2380
|
const submitted = await submitMarketingForm({
|
|
2155
2381
|
apiKey,
|
|
2156
2382
|
portalId,
|
|
@@ -2187,12 +2413,28 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2187
2413
|
const result = await strapi.plugin("hubspot").service("submit").upsert({
|
|
2188
2414
|
object: "contact",
|
|
2189
2415
|
idProperty: "email",
|
|
2190
|
-
properties: { ...
|
|
2416
|
+
properties: { ...contactProps, email }
|
|
2191
2417
|
});
|
|
2192
2418
|
if (result.ok) {
|
|
2193
2419
|
hubspotSynced = true;
|
|
2194
2420
|
contactId = result.id;
|
|
2195
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
|
+
}
|
|
2196
2438
|
}
|
|
2197
2439
|
const domain = corporateDomain(email);
|
|
2198
2440
|
const companyProps = partition.accepted.company ?? {};
|
|
@@ -2491,7 +2733,32 @@ function createFormsAdminController(strapi) {
|
|
|
2491
2733
|
documentId: ctx.params.documentId,
|
|
2492
2734
|
locale: ctx.query.locale
|
|
2493
2735
|
});
|
|
2494
|
-
|
|
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
|
+
};
|
|
2495
2762
|
},
|
|
2496
2763
|
async unpublish(ctx) {
|
|
2497
2764
|
await documents().unpublish({
|
|
@@ -2955,10 +3222,20 @@ const config = {
|
|
|
2955
3222
|
// submissions go through the marketing Forms API);
|
|
2956
3223
|
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
2957
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.
|
|
2958
3227
|
forms: {
|
|
2959
3228
|
companyFromDomain: true,
|
|
2960
3229
|
timelineNote: true,
|
|
2961
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,
|
|
2962
3239
|
// Per-IP brakes on the public routes; 0 disables one.
|
|
2963
3240
|
rateLimit: { submitPerMinute: 6, searchPerMinute: 30 }
|
|
2964
3241
|
}
|
|
@@ -3141,15 +3418,24 @@ const controllers = {
|
|
|
3141
3418
|
},
|
|
3142
3419
|
settings: ({ strapi }) => ({
|
|
3143
3420
|
async get(ctx) {
|
|
3144
|
-
|
|
3421
|
+
const settings = await publicSettings(strapi);
|
|
3422
|
+
let forms = [];
|
|
3423
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
3424
|
+
if (apiKey) {
|
|
3425
|
+
try {
|
|
3426
|
+
forms = await listHubspotForms(apiKey);
|
|
3427
|
+
} catch {
|
|
3428
|
+
forms = [];
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
ctx.body = { ...settings, forms };
|
|
3145
3432
|
},
|
|
3146
3433
|
async update(ctx) {
|
|
3147
|
-
|
|
3148
|
-
await setStoredSettings(strapi, apiKey ? { apiKey } : null);
|
|
3434
|
+
await patchStoredSettings(strapi, ctx.request.body ?? {});
|
|
3149
3435
|
ctx.body = await publicSettings(strapi);
|
|
3150
3436
|
},
|
|
3151
3437
|
async reset(ctx) {
|
|
3152
|
-
await
|
|
3438
|
+
await clearStoredApiKey(strapi);
|
|
3153
3439
|
ctx.body = await publicSettings(strapi);
|
|
3154
3440
|
}
|
|
3155
3441
|
})
|