strapi-plugin-hubspot 0.16.2 → 0.16.5
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 +26 -0
- package/README.md +8 -5
- package/dist/_chunks/{Forms-BOQWely0.mjs → Forms-DyV6KyiO.mjs} +20 -6
- package/dist/_chunks/{Forms-mMNP7hc9.js → Forms-uUN4XjhW.js} +20 -6
- package/dist/_chunks/{HubspotFormInput-DI35Aqiu.js → HubspotFormInput-BkWat_wu.js} +1 -1
- package/dist/_chunks/{HubspotFormInput-DCkoavrN.mjs → HubspotFormInput-DEXPX-6j.mjs} +1 -1
- package/dist/_chunks/{HubspotObjectInput-Cq1TuL_l.js → HubspotObjectInput-CFDYW-TM.js} +3 -3
- package/dist/_chunks/{HubspotObjectInput-BbnvhdbD.mjs → HubspotObjectInput-DL8fYK2x.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-BLZzsW08.js → HubspotPropertyInput-CFMkbQeT.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-BHfpBD36.mjs → HubspotPropertyInput-WU5FMJj3.mjs} +3 -3
- package/dist/_chunks/{Settings-BAsx0LGv.js → Settings-B_hhdpHa.js} +21 -3
- package/dist/_chunks/{Settings-DqRpHtnT.mjs → Settings-CiXEps_C.mjs} +21 -3
- package/dist/_chunks/{en-Djd3TID0.mjs → en-BIUBYzHv.mjs} +5 -1
- package/dist/_chunks/{en-sfzopB5K.js → en-CFhEtgZJ.js} +5 -1
- package/dist/_chunks/{fr-C7t1elmR.mjs → fr-CMYcnFH0.mjs} +5 -1
- package/dist/_chunks/{fr-sF5Josfh.js → fr-zdUuuV0M.js} +5 -1
- package/dist/_chunks/{index-CLAy5dVN.mjs → index-BeBn7bJa.mjs} +6 -6
- package/dist/_chunks/{index-R3ReZXF1.js → index-CG_WQzSa.js} +6 -6
- package/dist/_chunks/{objectLabels-CW2iBOkD.js → objectLabels-BGq-Ypgn.js} +1 -1
- package/dist/_chunks/{objectLabels--zOmqsfm.mjs → objectLabels-CS_0QCNz.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-BRzzvs9t.js → useHubspotSchema-BZP2qkAu.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-Bz2o1BU-.mjs → useHubspotSchema-CP7LRdzO.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/builder/types.d.ts +1 -1
- package/dist/server/index.js +108 -30
- package/dist/server/index.mjs +108 -30
- package/dist/server/src/forms.d.ts +3 -2
- package/dist/server/src/hsforms.d.ts +53 -7
- package/dist/server/src/importHubspot.d.ts +9 -0
- package/dist/server/src/index.d.ts +1 -0
- package/dist/server/src/settings.d.ts +6 -0
- package/package.json +1 -1
|
@@ -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-uUN4XjhW.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-CFMkbQeT.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-CFDYW-TM.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-BkWat_wu.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-B_hhdpHa.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-CFhEtgZJ.js")), "./translations/fr.json": () => Promise.resolve().then(() => require("./fr-zdUuuV0M.js")) }), `./translations/${locale}.json`, 3);
|
|
150
150
|
return { data: prefixPluginTranslations(data), locale };
|
|
151
151
|
} catch {
|
|
152
152
|
return { data: {}, locale };
|
|
@@ -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-CG_WQzSa.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" } });
|
|
@@ -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-BeBn7bJa.mjs";
|
|
4
4
|
let cached = null;
|
|
5
5
|
let inFlight = null;
|
|
6
6
|
const listeners = /* @__PURE__ */ new Set();
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
|
@@ -103,7 +103,7 @@ export interface SubmissionRowDto {
|
|
|
103
103
|
formTitle?: string | null;
|
|
104
104
|
email?: string | null;
|
|
105
105
|
values: Record<string, string | number | boolean>;
|
|
106
|
-
meta?: Record<string,
|
|
106
|
+
meta?: Record<string, unknown> | null;
|
|
107
107
|
locale?: string | null;
|
|
108
108
|
hubspotSynced?: boolean;
|
|
109
109
|
createdAt?: string;
|
package/dist/server/index.js
CHANGED
|
@@ -191,7 +191,8 @@ function policyFrom(stored, forms) {
|
|
|
191
191
|
return {
|
|
192
192
|
submissionMode: asSubmissionMode(stored.submissionMode) ?? asSubmissionMode(forms.submissionMode) ?? "auto",
|
|
193
193
|
writeExtraProperties: asBool(stored.writeExtraProperties) ?? asBool(forms.writeExtraProperties) ?? true,
|
|
194
|
-
syncFieldsOnPublish: asBool(stored.syncFieldsOnPublish) ?? asBool(forms.syncFieldsOnPublish) ?? false
|
|
194
|
+
syncFieldsOnPublish: asBool(stored.syncFieldsOnPublish) ?? asBool(forms.syncFieldsOnPublish) ?? false,
|
|
195
|
+
timelineNote: asBool(stored.timelineNote) ?? asBool(forms.timelineNote) ?? true
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
198
|
async function resolvePolicy(strapi) {
|
|
@@ -275,6 +276,9 @@ async function patchStoredSettings(strapi, body) {
|
|
|
275
276
|
if ("syncFieldsOnPublish" in body && typeof body.syncFieldsOnPublish === "boolean") {
|
|
276
277
|
next.syncFieldsOnPublish = body.syncFieldsOnPublish;
|
|
277
278
|
}
|
|
279
|
+
if ("timelineNote" in body && typeof body.timelineNote === "boolean") {
|
|
280
|
+
next.timelineNote = body.timelineNote;
|
|
281
|
+
}
|
|
278
282
|
await setStoredSettings(strapi, next);
|
|
279
283
|
}
|
|
280
284
|
async function clearStoredApiKey(strapi) {
|
|
@@ -1932,6 +1936,59 @@ function formFieldNames(raw) {
|
|
|
1932
1936
|
}
|
|
1933
1937
|
return names;
|
|
1934
1938
|
}
|
|
1939
|
+
function formRequiredNames(raw) {
|
|
1940
|
+
const names = [];
|
|
1941
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
1942
|
+
for (const field of group.fields ?? []) {
|
|
1943
|
+
if (field.name && field.required) names.push(field.name);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
return names;
|
|
1947
|
+
}
|
|
1948
|
+
function parseFormShape(raw) {
|
|
1949
|
+
const legal = raw.legalConsentOptions;
|
|
1950
|
+
const hasLegalConsent = Boolean(
|
|
1951
|
+
legal && typeof legal === "object" && legal.type && legal.type !== "none"
|
|
1952
|
+
);
|
|
1953
|
+
const communications = [];
|
|
1954
|
+
for (const box of legal?.communicationsCheckboxes ?? []) {
|
|
1955
|
+
const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
|
|
1956
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
1957
|
+
communications.push({
|
|
1958
|
+
subscriptionTypeId: id,
|
|
1959
|
+
text: String(box.label || box.text || "").trim() || "I agree to receive communications."
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
const consentToProcessText = legal?.consentToProcessText?.trim();
|
|
1963
|
+
return {
|
|
1964
|
+
names: new Set(formFieldNames(raw)),
|
|
1965
|
+
required: new Set(formRequiredNames(raw)),
|
|
1966
|
+
hasLegalConsent,
|
|
1967
|
+
captcha: raw.configuration?.captchaEnabled === true,
|
|
1968
|
+
...consentToProcessText ? { consentToProcessText } : {},
|
|
1969
|
+
communications
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
function canRetryEmailOnly(required) {
|
|
1973
|
+
const names = [...required ?? []];
|
|
1974
|
+
return names.length === 0 || names.every((name) => name === "email");
|
|
1975
|
+
}
|
|
1976
|
+
function buildLegalConsent(shape, given, fallbackText) {
|
|
1977
|
+
if (!given) return void 0;
|
|
1978
|
+
if (shape && !shape.hasLegalConsent) return void 0;
|
|
1979
|
+
const consent = {
|
|
1980
|
+
consentToProcess: true,
|
|
1981
|
+
text: shape?.consentToProcessText?.trim() || fallbackText?.trim() || DEFAULT_CONSENT_TEXT
|
|
1982
|
+
};
|
|
1983
|
+
if (shape?.communications.length) {
|
|
1984
|
+
consent.communications = shape.communications.map((item) => ({
|
|
1985
|
+
value: true,
|
|
1986
|
+
subscriptionTypeId: item.subscriptionTypeId,
|
|
1987
|
+
text: item.text
|
|
1988
|
+
}));
|
|
1989
|
+
}
|
|
1990
|
+
return { consent };
|
|
1991
|
+
}
|
|
1935
1992
|
const shapeCache = /* @__PURE__ */ new Map();
|
|
1936
1993
|
function clearFormShapeCache(formGuid) {
|
|
1937
1994
|
if (formGuid) shapeCache.delete(formGuid);
|
|
@@ -1940,16 +1997,13 @@ function clearFormShapeCache(formGuid) {
|
|
|
1940
1997
|
async function loadFormShape(apiKey, formGuid) {
|
|
1941
1998
|
const cached = shapeCache.get(formGuid);
|
|
1942
1999
|
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
1943
|
-
return
|
|
2000
|
+
return cached.shape;
|
|
1944
2001
|
}
|
|
1945
2002
|
try {
|
|
1946
2003
|
const raw = await fetchHubspotForm(apiKey, formGuid);
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
);
|
|
1951
|
-
shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
|
|
1952
|
-
return { names, hasLegalConsent };
|
|
2004
|
+
const shape = parseFormShape(raw);
|
|
2005
|
+
shapeCache.set(formGuid, { at: Date.now(), shape });
|
|
2006
|
+
return shape;
|
|
1953
2007
|
} catch {
|
|
1954
2008
|
return null;
|
|
1955
2009
|
}
|
|
@@ -2007,28 +2061,45 @@ async function submitMarketingForm(input) {
|
|
|
2007
2061
|
if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
|
|
2008
2062
|
return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
|
|
2009
2063
|
}
|
|
2064
|
+
if (input.shape?.captcha) {
|
|
2065
|
+
return {
|
|
2066
|
+
ok: false,
|
|
2067
|
+
status: 0,
|
|
2068
|
+
error: "HubSpot form has CAPTCHA enabled — the Forms API cannot submit it. Turn CAPTCHA off on that marketing form."
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2010
2071
|
const url = formsSubmitUrl(portalId, formGuid, input.region);
|
|
2011
2072
|
const context = contextPayload(input.context);
|
|
2073
|
+
const fields = input.fields.map((field) => ({
|
|
2074
|
+
objectTypeId: field.objectTypeId || "0-1",
|
|
2075
|
+
name: field.name,
|
|
2076
|
+
value: field.value
|
|
2077
|
+
}));
|
|
2012
2078
|
const base = {
|
|
2013
|
-
fields
|
|
2079
|
+
fields,
|
|
2014
2080
|
...Object.keys(context).length ? { context } : {}
|
|
2015
2081
|
};
|
|
2016
|
-
const
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
} : base;
|
|
2082
|
+
const legal = buildLegalConsent(
|
|
2083
|
+
input.shape,
|
|
2084
|
+
input.consent?.given === true,
|
|
2085
|
+
input.consent?.text
|
|
2086
|
+
);
|
|
2087
|
+
const withConsent = legal ? { ...base, legalConsentOptions: legal } : base;
|
|
2025
2088
|
let result = await postForm(url, input.apiKey, withConsent);
|
|
2026
|
-
|
|
2089
|
+
const mustKeepConsent = Boolean(input.shape?.hasLegalConsent && legal);
|
|
2090
|
+
if (!result.ok && result.status === 400 && withConsent !== base && !mustKeepConsent) {
|
|
2027
2091
|
result = await postForm(url, input.apiKey, base);
|
|
2028
2092
|
}
|
|
2029
|
-
if (!result.ok && result.status === 400 && input.fields.length > 1) {
|
|
2030
|
-
const email =
|
|
2031
|
-
if (email)
|
|
2093
|
+
if (!result.ok && result.status === 400 && input.fields.length > 1 && canRetryEmailOnly(input.shape?.required)) {
|
|
2094
|
+
const email = fields.find((f) => f.name === "email");
|
|
2095
|
+
if (email) {
|
|
2096
|
+
const slim = { ...base, fields: [email] };
|
|
2097
|
+
result = await postForm(
|
|
2098
|
+
url,
|
|
2099
|
+
input.apiKey,
|
|
2100
|
+
legal && mustKeepConsent ? { ...slim, legalConsentOptions: legal } : slim
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2032
2103
|
}
|
|
2033
2104
|
return result;
|
|
2034
2105
|
}
|
|
@@ -2410,6 +2481,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2410
2481
|
}
|
|
2411
2482
|
let usedFormsApi = false;
|
|
2412
2483
|
let sentFieldNames = [];
|
|
2484
|
+
let conversionError;
|
|
2413
2485
|
if (email && apiKey) {
|
|
2414
2486
|
const groups = groupByObject(form.definition, resolution.values);
|
|
2415
2487
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -2445,11 +2517,11 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2445
2517
|
context: {
|
|
2446
2518
|
hutk: sanitizeHutk(meta.hutk),
|
|
2447
2519
|
pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
|
|
2448
|
-
pageName: typeof meta.pageName === "string" && meta.pageName || (form.title || form.name) || void 0,
|
|
2520
|
+
pageName: typeof meta.pageName === "string" && meta.pageName || typeof meta.originLabel === "string" && meta.originLabel || (form.title || form.name) || void 0,
|
|
2449
2521
|
ipAddress: sanitizeIp(meta.ipAddress)
|
|
2450
2522
|
},
|
|
2451
2523
|
consent: { given: meta.consent === true },
|
|
2452
|
-
|
|
2524
|
+
shape
|
|
2453
2525
|
});
|
|
2454
2526
|
if (submitted.ok) {
|
|
2455
2527
|
hubspotSynced = true;
|
|
@@ -2462,10 +2534,12 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2462
2534
|
);
|
|
2463
2535
|
}
|
|
2464
2536
|
} else {
|
|
2537
|
+
conversionError = submitted.error;
|
|
2465
2538
|
strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
|
|
2466
2539
|
}
|
|
2467
2540
|
} catch (err) {
|
|
2468
|
-
|
|
2541
|
+
conversionError = err.message;
|
|
2542
|
+
strapi.log.warn(`[hubspot] Forms API submit failed — ${conversionError}`);
|
|
2469
2543
|
}
|
|
2470
2544
|
}
|
|
2471
2545
|
if (!usedFormsApi) {
|
|
@@ -2525,7 +2599,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2525
2599
|
strapi.log.warn(`[hubspot] company sync failed (contact OK) — ${err.message}`);
|
|
2526
2600
|
}
|
|
2527
2601
|
}
|
|
2528
|
-
if (contactId &&
|
|
2602
|
+
if (contactId && policy.timelineNote) {
|
|
2529
2603
|
try {
|
|
2530
2604
|
await createLeadNote(apiKey, {
|
|
2531
2605
|
contactId,
|
|
@@ -2547,7 +2621,11 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2547
2621
|
formTitle: form.title || form.name,
|
|
2548
2622
|
email,
|
|
2549
2623
|
values: resolution.values,
|
|
2550
|
-
meta
|
|
2624
|
+
meta: {
|
|
2625
|
+
...meta,
|
|
2626
|
+
hubspotConversion: usedFormsApi,
|
|
2627
|
+
...conversionError ? { hubspotError: conversionError } : {}
|
|
2628
|
+
},
|
|
2551
2629
|
locale: form.locale ?? null,
|
|
2552
2630
|
hubspotSynced,
|
|
2553
2631
|
contactId,
|
|
@@ -2734,7 +2812,7 @@ function createFormsAdminController(strapi) {
|
|
|
2734
2812
|
locale: ctx.query.locale,
|
|
2735
2813
|
status: "published"
|
|
2736
2814
|
});
|
|
2737
|
-
ctx.body = { form: entry, published: Boolean(published) };
|
|
2815
|
+
ctx.body = { form: entry, published: Boolean(published), publishedHubspotFormId: published?.hubspotFormId ?? null };
|
|
2738
2816
|
},
|
|
2739
2817
|
async create(ctx) {
|
|
2740
2818
|
const body = ctx.request.body ?? {};
|
|
@@ -3334,8 +3412,8 @@ const config = {
|
|
|
3334
3412
|
// Submission pipeline of the built forms:
|
|
3335
3413
|
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
3336
3414
|
// domain) and associates it to the contact;
|
|
3337
|
-
// - timelineNote: recap note on the
|
|
3338
|
-
//
|
|
3415
|
+
// - timelineNote: recap note on the contact after a successful sync
|
|
3416
|
+
// (page URL, subject, answers) — alongside a Forms API conversion;
|
|
3339
3417
|
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
3340
3418
|
// doesn't set its own — swap with the portal at production cutover.
|
|
3341
3419
|
// - submissionMode / writeExtraProperties / syncFieldsOnPublish: see
|
package/dist/server/index.mjs
CHANGED
|
@@ -190,7 +190,8 @@ function policyFrom(stored, forms) {
|
|
|
190
190
|
return {
|
|
191
191
|
submissionMode: asSubmissionMode(stored.submissionMode) ?? asSubmissionMode(forms.submissionMode) ?? "auto",
|
|
192
192
|
writeExtraProperties: asBool(stored.writeExtraProperties) ?? asBool(forms.writeExtraProperties) ?? true,
|
|
193
|
-
syncFieldsOnPublish: asBool(stored.syncFieldsOnPublish) ?? asBool(forms.syncFieldsOnPublish) ?? false
|
|
193
|
+
syncFieldsOnPublish: asBool(stored.syncFieldsOnPublish) ?? asBool(forms.syncFieldsOnPublish) ?? false,
|
|
194
|
+
timelineNote: asBool(stored.timelineNote) ?? asBool(forms.timelineNote) ?? true
|
|
194
195
|
};
|
|
195
196
|
}
|
|
196
197
|
async function resolvePolicy(strapi) {
|
|
@@ -274,6 +275,9 @@ async function patchStoredSettings(strapi, body) {
|
|
|
274
275
|
if ("syncFieldsOnPublish" in body && typeof body.syncFieldsOnPublish === "boolean") {
|
|
275
276
|
next.syncFieldsOnPublish = body.syncFieldsOnPublish;
|
|
276
277
|
}
|
|
278
|
+
if ("timelineNote" in body && typeof body.timelineNote === "boolean") {
|
|
279
|
+
next.timelineNote = body.timelineNote;
|
|
280
|
+
}
|
|
277
281
|
await setStoredSettings(strapi, next);
|
|
278
282
|
}
|
|
279
283
|
async function clearStoredApiKey(strapi) {
|
|
@@ -1931,6 +1935,59 @@ function formFieldNames(raw) {
|
|
|
1931
1935
|
}
|
|
1932
1936
|
return names;
|
|
1933
1937
|
}
|
|
1938
|
+
function formRequiredNames(raw) {
|
|
1939
|
+
const names = [];
|
|
1940
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
1941
|
+
for (const field of group.fields ?? []) {
|
|
1942
|
+
if (field.name && field.required) names.push(field.name);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return names;
|
|
1946
|
+
}
|
|
1947
|
+
function parseFormShape(raw) {
|
|
1948
|
+
const legal = raw.legalConsentOptions;
|
|
1949
|
+
const hasLegalConsent = Boolean(
|
|
1950
|
+
legal && typeof legal === "object" && legal.type && legal.type !== "none"
|
|
1951
|
+
);
|
|
1952
|
+
const communications = [];
|
|
1953
|
+
for (const box of legal?.communicationsCheckboxes ?? []) {
|
|
1954
|
+
const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
|
|
1955
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
1956
|
+
communications.push({
|
|
1957
|
+
subscriptionTypeId: id,
|
|
1958
|
+
text: String(box.label || box.text || "").trim() || "I agree to receive communications."
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
const consentToProcessText = legal?.consentToProcessText?.trim();
|
|
1962
|
+
return {
|
|
1963
|
+
names: new Set(formFieldNames(raw)),
|
|
1964
|
+
required: new Set(formRequiredNames(raw)),
|
|
1965
|
+
hasLegalConsent,
|
|
1966
|
+
captcha: raw.configuration?.captchaEnabled === true,
|
|
1967
|
+
...consentToProcessText ? { consentToProcessText } : {},
|
|
1968
|
+
communications
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
function canRetryEmailOnly(required) {
|
|
1972
|
+
const names = [...required ?? []];
|
|
1973
|
+
return names.length === 0 || names.every((name) => name === "email");
|
|
1974
|
+
}
|
|
1975
|
+
function buildLegalConsent(shape, given, fallbackText) {
|
|
1976
|
+
if (!given) return void 0;
|
|
1977
|
+
if (shape && !shape.hasLegalConsent) return void 0;
|
|
1978
|
+
const consent = {
|
|
1979
|
+
consentToProcess: true,
|
|
1980
|
+
text: shape?.consentToProcessText?.trim() || fallbackText?.trim() || DEFAULT_CONSENT_TEXT
|
|
1981
|
+
};
|
|
1982
|
+
if (shape?.communications.length) {
|
|
1983
|
+
consent.communications = shape.communications.map((item) => ({
|
|
1984
|
+
value: true,
|
|
1985
|
+
subscriptionTypeId: item.subscriptionTypeId,
|
|
1986
|
+
text: item.text
|
|
1987
|
+
}));
|
|
1988
|
+
}
|
|
1989
|
+
return { consent };
|
|
1990
|
+
}
|
|
1934
1991
|
const shapeCache = /* @__PURE__ */ new Map();
|
|
1935
1992
|
function clearFormShapeCache(formGuid) {
|
|
1936
1993
|
if (formGuid) shapeCache.delete(formGuid);
|
|
@@ -1939,16 +1996,13 @@ function clearFormShapeCache(formGuid) {
|
|
|
1939
1996
|
async function loadFormShape(apiKey, formGuid) {
|
|
1940
1997
|
const cached = shapeCache.get(formGuid);
|
|
1941
1998
|
if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
|
|
1942
|
-
return
|
|
1999
|
+
return cached.shape;
|
|
1943
2000
|
}
|
|
1944
2001
|
try {
|
|
1945
2002
|
const raw = await fetchHubspotForm(apiKey, formGuid);
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
);
|
|
1950
|
-
shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
|
|
1951
|
-
return { names, hasLegalConsent };
|
|
2003
|
+
const shape = parseFormShape(raw);
|
|
2004
|
+
shapeCache.set(formGuid, { at: Date.now(), shape });
|
|
2005
|
+
return shape;
|
|
1952
2006
|
} catch {
|
|
1953
2007
|
return null;
|
|
1954
2008
|
}
|
|
@@ -2006,28 +2060,45 @@ async function submitMarketingForm(input) {
|
|
|
2006
2060
|
if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
|
|
2007
2061
|
return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
|
|
2008
2062
|
}
|
|
2063
|
+
if (input.shape?.captcha) {
|
|
2064
|
+
return {
|
|
2065
|
+
ok: false,
|
|
2066
|
+
status: 0,
|
|
2067
|
+
error: "HubSpot form has CAPTCHA enabled — the Forms API cannot submit it. Turn CAPTCHA off on that marketing form."
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2009
2070
|
const url = formsSubmitUrl(portalId, formGuid, input.region);
|
|
2010
2071
|
const context = contextPayload(input.context);
|
|
2072
|
+
const fields = input.fields.map((field) => ({
|
|
2073
|
+
objectTypeId: field.objectTypeId || "0-1",
|
|
2074
|
+
name: field.name,
|
|
2075
|
+
value: field.value
|
|
2076
|
+
}));
|
|
2011
2077
|
const base = {
|
|
2012
|
-
fields
|
|
2078
|
+
fields,
|
|
2013
2079
|
...Object.keys(context).length ? { context } : {}
|
|
2014
2080
|
};
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
} : base;
|
|
2081
|
+
const legal = buildLegalConsent(
|
|
2082
|
+
input.shape,
|
|
2083
|
+
input.consent?.given === true,
|
|
2084
|
+
input.consent?.text
|
|
2085
|
+
);
|
|
2086
|
+
const withConsent = legal ? { ...base, legalConsentOptions: legal } : base;
|
|
2024
2087
|
let result = await postForm(url, input.apiKey, withConsent);
|
|
2025
|
-
|
|
2088
|
+
const mustKeepConsent = Boolean(input.shape?.hasLegalConsent && legal);
|
|
2089
|
+
if (!result.ok && result.status === 400 && withConsent !== base && !mustKeepConsent) {
|
|
2026
2090
|
result = await postForm(url, input.apiKey, base);
|
|
2027
2091
|
}
|
|
2028
|
-
if (!result.ok && result.status === 400 && input.fields.length > 1) {
|
|
2029
|
-
const email =
|
|
2030
|
-
if (email)
|
|
2092
|
+
if (!result.ok && result.status === 400 && input.fields.length > 1 && canRetryEmailOnly(input.shape?.required)) {
|
|
2093
|
+
const email = fields.find((f) => f.name === "email");
|
|
2094
|
+
if (email) {
|
|
2095
|
+
const slim = { ...base, fields: [email] };
|
|
2096
|
+
result = await postForm(
|
|
2097
|
+
url,
|
|
2098
|
+
input.apiKey,
|
|
2099
|
+
legal && mustKeepConsent ? { ...slim, legalConsentOptions: legal } : slim
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2031
2102
|
}
|
|
2032
2103
|
return result;
|
|
2033
2104
|
}
|
|
@@ -2409,6 +2480,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2409
2480
|
}
|
|
2410
2481
|
let usedFormsApi = false;
|
|
2411
2482
|
let sentFieldNames = [];
|
|
2483
|
+
let conversionError;
|
|
2412
2484
|
if (email && apiKey) {
|
|
2413
2485
|
const groups = groupByObject(form.definition, resolution.values);
|
|
2414
2486
|
for (const [object, props] of Object.entries(companyGroups)) {
|
|
@@ -2444,11 +2516,11 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2444
2516
|
context: {
|
|
2445
2517
|
hutk: sanitizeHutk(meta.hutk),
|
|
2446
2518
|
pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
|
|
2447
|
-
pageName: typeof meta.pageName === "string" && meta.pageName || (form.title || form.name) || void 0,
|
|
2519
|
+
pageName: typeof meta.pageName === "string" && meta.pageName || typeof meta.originLabel === "string" && meta.originLabel || (form.title || form.name) || void 0,
|
|
2448
2520
|
ipAddress: sanitizeIp(meta.ipAddress)
|
|
2449
2521
|
},
|
|
2450
2522
|
consent: { given: meta.consent === true },
|
|
2451
|
-
|
|
2523
|
+
shape
|
|
2452
2524
|
});
|
|
2453
2525
|
if (submitted.ok) {
|
|
2454
2526
|
hubspotSynced = true;
|
|
@@ -2461,10 +2533,12 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2461
2533
|
);
|
|
2462
2534
|
}
|
|
2463
2535
|
} else {
|
|
2536
|
+
conversionError = submitted.error;
|
|
2464
2537
|
strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
|
|
2465
2538
|
}
|
|
2466
2539
|
} catch (err) {
|
|
2467
|
-
|
|
2540
|
+
conversionError = err.message;
|
|
2541
|
+
strapi.log.warn(`[hubspot] Forms API submit failed — ${conversionError}`);
|
|
2468
2542
|
}
|
|
2469
2543
|
}
|
|
2470
2544
|
if (!usedFormsApi) {
|
|
@@ -2524,7 +2598,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2524
2598
|
strapi.log.warn(`[hubspot] company sync failed (contact OK) — ${err.message}`);
|
|
2525
2599
|
}
|
|
2526
2600
|
}
|
|
2527
|
-
if (contactId &&
|
|
2601
|
+
if (contactId && policy.timelineNote) {
|
|
2528
2602
|
try {
|
|
2529
2603
|
await createLeadNote(apiKey, {
|
|
2530
2604
|
contactId,
|
|
@@ -2546,7 +2620,11 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2546
2620
|
formTitle: form.title || form.name,
|
|
2547
2621
|
email,
|
|
2548
2622
|
values: resolution.values,
|
|
2549
|
-
meta
|
|
2623
|
+
meta: {
|
|
2624
|
+
...meta,
|
|
2625
|
+
hubspotConversion: usedFormsApi,
|
|
2626
|
+
...conversionError ? { hubspotError: conversionError } : {}
|
|
2627
|
+
},
|
|
2550
2628
|
locale: form.locale ?? null,
|
|
2551
2629
|
hubspotSynced,
|
|
2552
2630
|
contactId,
|
|
@@ -2733,7 +2811,7 @@ function createFormsAdminController(strapi) {
|
|
|
2733
2811
|
locale: ctx.query.locale,
|
|
2734
2812
|
status: "published"
|
|
2735
2813
|
});
|
|
2736
|
-
ctx.body = { form: entry, published: Boolean(published) };
|
|
2814
|
+
ctx.body = { form: entry, published: Boolean(published), publishedHubspotFormId: published?.hubspotFormId ?? null };
|
|
2737
2815
|
},
|
|
2738
2816
|
async create(ctx) {
|
|
2739
2817
|
const body = ctx.request.body ?? {};
|
|
@@ -3333,8 +3411,8 @@ const config = {
|
|
|
3333
3411
|
// Submission pipeline of the built forms:
|
|
3334
3412
|
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
3335
3413
|
// domain) and associates it to the contact;
|
|
3336
|
-
// - timelineNote: recap note on the
|
|
3337
|
-
//
|
|
3414
|
+
// - timelineNote: recap note on the contact after a successful sync
|
|
3415
|
+
// (page URL, subject, answers) — alongside a Forms API conversion;
|
|
3338
3416
|
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
3339
3417
|
// doesn't set its own — swap with the portal at production cutover.
|
|
3340
3418
|
// - submissionMode / writeExtraProperties / syncFieldsOnPublish: see
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* conversion (with leftover CRM writes for mapped fields the HubSpot form
|
|
7
7
|
* dropped), CRM upsert only, or automatic (Forms API when a GUID is linked).
|
|
8
8
|
* Company find-or-create still runs after a successful submit. Timeline notes
|
|
9
|
-
*
|
|
10
|
-
* the lead — a CRM
|
|
9
|
+
* (page URL, subject, answers) are optional extra context on the contact,
|
|
10
|
+
* alongside a native conversion. The form submission is the lead — a CRM
|
|
11
|
+
* outage never loses it.
|
|
11
12
|
*/
|
|
12
13
|
import type { Core } from "@strapi/strapi";
|
|
13
14
|
import { type FormDefinition, type Primitive } from "./conditions";
|