strapi-plugin-hubspot 0.16.1 → 0.16.3
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 +16 -0
- package/README.md +12 -9
- package/dist/_chunks/{Forms-DEoZGrEt.mjs → Forms-BBaw53Tz.mjs} +48 -5
- package/dist/_chunks/{Forms-D0QXkYAN.js → Forms-CIJrUB_n.js} +48 -5
- package/dist/_chunks/{HubspotFormInput-CPep55GL.mjs → HubspotFormInput-DIo6hCNz.mjs} +1 -1
- package/dist/_chunks/{HubspotFormInput-4ZMYXWM_.js → HubspotFormInput-DSTuQ_bx.js} +1 -1
- package/dist/_chunks/{HubspotObjectInput-CQszIx1W.mjs → HubspotObjectInput-BA16dswz.mjs} +3 -3
- package/dist/_chunks/{HubspotObjectInput-BTqYXJIa.js → HubspotObjectInput-BIqC8tl2.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-2MgIKlR_.js → HubspotPropertyInput-C77Op1Wm.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-ClNph0f1.mjs → HubspotPropertyInput-Dsn7ikop.mjs} +3 -3
- package/dist/_chunks/{Settings-CZ-qQSKT.mjs → Settings-DxX9Uh_2.mjs} +21 -3
- package/dist/_chunks/{Settings-DhiHQC6U.js → Settings-PVhJVPqq.js} +21 -3
- package/dist/_chunks/{en-Dh3bMS6N.mjs → en-Djd3TID0.mjs} +6 -2
- package/dist/_chunks/{en-CSgFmKmM.js → en-sfzopB5K.js} +6 -2
- package/dist/_chunks/{fr-aWOIOlS9.mjs → fr-C7t1elmR.mjs} +6 -2
- package/dist/_chunks/{fr-D_EhZS0P.js → fr-sF5Josfh.js} +6 -2
- package/dist/_chunks/{index-gKM4jVf8.mjs → index-B8GYi6sw.mjs} +6 -6
- package/dist/_chunks/{index-ciliGv0E.js → index-XEuSPuUj.js} +6 -6
- package/dist/_chunks/{objectLabels-DQpSLUaJ.mjs → objectLabels-DybqhBdT.mjs} +1 -1
- package/dist/_chunks/{objectLabels-BjYfdhRB.js → objectLabels-wBwfLdZt.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-CE7tJgOM.mjs → useHubspotSchema-DtcHHUOb.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-KKVppjC2.js → useHubspotSchema-WvgW3od8.js} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +138 -17
- package/dist/server/index.mjs +138 -17
- package/dist/server/src/forms.d.ts +3 -2
- package/dist/server/src/formsAdmin.d.ts +8 -3
- package/dist/server/src/importHubspot.d.ts +12 -0
- package/dist/server/src/index.d.ts +2 -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-CIJrUB_n.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-C77Op1Wm.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-BIqC8tl2.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-DSTuQ_bx.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-PVhJVPqq.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-sfzopB5K.js")), "./translations/fr.json": () => Promise.resolve().then(() => require("./fr-sF5Josfh.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-B8GYi6sw.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-XEuSPuUj.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
|
@@ -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) {
|
|
@@ -1778,6 +1782,65 @@ function convertHubspotForm(raw) {
|
|
|
1778
1782
|
skipped
|
|
1779
1783
|
};
|
|
1780
1784
|
}
|
|
1785
|
+
function mappingKey(field) {
|
|
1786
|
+
const mapping = field.hubspot ?? {};
|
|
1787
|
+
const property = mapping.property?.trim();
|
|
1788
|
+
if (!property) return null;
|
|
1789
|
+
return `${mapping.object?.trim() || "contact"}:${property}`;
|
|
1790
|
+
}
|
|
1791
|
+
function uniqueName(base, used) {
|
|
1792
|
+
if (!used.has(base)) return base;
|
|
1793
|
+
let n = 2;
|
|
1794
|
+
while (used.has(`${base}_${n}`)) n += 1;
|
|
1795
|
+
return `${base}_${n}`;
|
|
1796
|
+
}
|
|
1797
|
+
function mergeHubspotImport(current, incoming) {
|
|
1798
|
+
const steps = (current.steps ?? []).map((step) => ({
|
|
1799
|
+
...step,
|
|
1800
|
+
fields: [...step.fields ?? []]
|
|
1801
|
+
}));
|
|
1802
|
+
if (!steps.length) {
|
|
1803
|
+
steps.push({ id: makeId$1("stp"), fields: [] });
|
|
1804
|
+
}
|
|
1805
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1806
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
1807
|
+
for (const step of steps) {
|
|
1808
|
+
for (const field of step.fields) {
|
|
1809
|
+
usedNames.add(field.name);
|
|
1810
|
+
const key = mappingKey(field);
|
|
1811
|
+
if (key) byKey.set(key, field);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
const added = [];
|
|
1815
|
+
const updated = [];
|
|
1816
|
+
const incomingFields = (incoming.steps ?? []).flatMap((step) => step.fields ?? []);
|
|
1817
|
+
for (const next of incomingFields) {
|
|
1818
|
+
const key = mappingKey(next);
|
|
1819
|
+
const existing = key ? byKey.get(key) : void 0;
|
|
1820
|
+
if (existing) {
|
|
1821
|
+
existing.label = next.label;
|
|
1822
|
+
existing.type = next.type;
|
|
1823
|
+
existing.required = next.required;
|
|
1824
|
+
existing.placeholder = next.placeholder;
|
|
1825
|
+
existing.helpText = next.helpText;
|
|
1826
|
+
if (next.options) existing.options = next.options;
|
|
1827
|
+
else delete existing.options;
|
|
1828
|
+
if (!existing.visibleIf && next.visibleIf) existing.visibleIf = next.visibleIf;
|
|
1829
|
+
updated.push(key);
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
const field = {
|
|
1833
|
+
...next,
|
|
1834
|
+
id: makeId$1("fld"),
|
|
1835
|
+
name: uniqueName(next.name, usedNames)
|
|
1836
|
+
};
|
|
1837
|
+
usedNames.add(field.name);
|
|
1838
|
+
if (key) byKey.set(key, field);
|
|
1839
|
+
steps[steps.length - 1].fields.push(field);
|
|
1840
|
+
added.push(key || field.name);
|
|
1841
|
+
}
|
|
1842
|
+
return { definition: { version: 1, steps }, added, updated };
|
|
1843
|
+
}
|
|
1781
1844
|
async function hsGet(apiKey, path) {
|
|
1782
1845
|
const res = await fetch(`${HS_BASE$2}${path}`, {
|
|
1783
1846
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
@@ -2386,7 +2449,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2386
2449
|
context: {
|
|
2387
2450
|
hutk: sanitizeHutk(meta.hutk),
|
|
2388
2451
|
pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
|
|
2389
|
-
pageName: typeof meta.pageName === "string" && meta.pageName || (form.title || form.name) || void 0,
|
|
2452
|
+
pageName: typeof meta.pageName === "string" && meta.pageName || typeof meta.originLabel === "string" && meta.originLabel || (form.title || form.name) || void 0,
|
|
2390
2453
|
ipAddress: sanitizeIp(meta.ipAddress)
|
|
2391
2454
|
},
|
|
2392
2455
|
consent: { given: meta.consent === true },
|
|
@@ -2466,7 +2529,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2466
2529
|
strapi.log.warn(`[hubspot] company sync failed (contact OK) — ${err.message}`);
|
|
2467
2530
|
}
|
|
2468
2531
|
}
|
|
2469
|
-
if (contactId &&
|
|
2532
|
+
if (contactId && policy.timelineNote) {
|
|
2470
2533
|
try {
|
|
2471
2534
|
await createLeadNote(apiKey, {
|
|
2472
2535
|
contactId,
|
|
@@ -2895,9 +2958,9 @@ function createFormsAdminController(strapi) {
|
|
|
2895
2958
|
},
|
|
2896
2959
|
/**
|
|
2897
2960
|
* Converts one portal form into a plugin form draft, in the current
|
|
2898
|
-
* locale.
|
|
2899
|
-
*
|
|
2900
|
-
*
|
|
2961
|
+
* locale. Matching prefers the linked HubSpot GUID, then the slug.
|
|
2962
|
+
* Re-importing merges: mapped fields update in place, new ones append,
|
|
2963
|
+
* Strapi steps stay. Never the published version, never the portal.
|
|
2901
2964
|
*/
|
|
2902
2965
|
async runHubspotImport(ctx) {
|
|
2903
2966
|
const formId = ctx.request.body?.formId;
|
|
@@ -2916,24 +2979,81 @@ function createFormsAdminController(strapi) {
|
|
|
2916
2979
|
const { skipped, definition, ...meta } = converted;
|
|
2917
2980
|
const slug = slugify(name);
|
|
2918
2981
|
const existing = await documents().findFirst({
|
|
2982
|
+
filters: { hubspotFormId: formId }
|
|
2983
|
+
}) ?? await documents().findFirst({
|
|
2919
2984
|
filters: { slug }
|
|
2920
2985
|
});
|
|
2921
|
-
let documentId;
|
|
2922
2986
|
if (existing) {
|
|
2923
|
-
await documents().
|
|
2987
|
+
const current = await documents().findOne({
|
|
2924
2988
|
documentId: existing.documentId,
|
|
2925
2989
|
locale: ctx.query.locale,
|
|
2926
|
-
|
|
2990
|
+
status: "draft"
|
|
2927
2991
|
});
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2992
|
+
const merged = mergeHubspotImport(current?.definition ?? EMPTY_DEFINITION, definition);
|
|
2993
|
+
await documents().update({
|
|
2994
|
+
documentId: existing.documentId,
|
|
2931
2995
|
locale: ctx.query.locale,
|
|
2932
|
-
data: {
|
|
2996
|
+
data: {
|
|
2997
|
+
hubspotFormId: meta.hubspotFormId ?? formId,
|
|
2998
|
+
definition: merged.definition
|
|
2999
|
+
}
|
|
2933
3000
|
});
|
|
2934
|
-
|
|
3001
|
+
ctx.body = {
|
|
3002
|
+
documentId: existing.documentId,
|
|
3003
|
+
skipped,
|
|
3004
|
+
added: merged.added,
|
|
3005
|
+
updated: merged.updated
|
|
3006
|
+
};
|
|
3007
|
+
return;
|
|
2935
3008
|
}
|
|
2936
|
-
|
|
3009
|
+
const created = await documents().create({
|
|
3010
|
+
locale: ctx.query.locale,
|
|
3011
|
+
data: { ...meta, name, slug, definition }
|
|
3012
|
+
});
|
|
3013
|
+
ctx.body = { documentId: created.documentId, skipped, added: [], updated: [] };
|
|
3014
|
+
},
|
|
3015
|
+
/**
|
|
3016
|
+
* Pull the linked HubSpot marketing form back into this draft: mapped
|
|
3017
|
+
* fields update in place, new ones append, Strapi steps and extras stay.
|
|
3018
|
+
*/
|
|
3019
|
+
async resync(ctx) {
|
|
3020
|
+
const entry = await documents().findOne({
|
|
3021
|
+
documentId: ctx.params.documentId,
|
|
3022
|
+
locale: ctx.query.locale,
|
|
3023
|
+
status: "draft"
|
|
3024
|
+
});
|
|
3025
|
+
if (!entry) ctx.throw(404, "Form not found");
|
|
3026
|
+
const guid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
|
|
3027
|
+
if (!isFormGuid(guid)) {
|
|
3028
|
+
ctx.throw(400, "This form is not linked to a HubSpot marketing form.");
|
|
3029
|
+
}
|
|
3030
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
3031
|
+
if (!apiKey) ctx.throw(400, "No HubSpot API key configured");
|
|
3032
|
+
let converted;
|
|
3033
|
+
try {
|
|
3034
|
+
converted = convertHubspotForm(await fetchHubspotForm(apiKey, guid));
|
|
3035
|
+
} catch (err) {
|
|
3036
|
+
strapi.log.warn(`[hubspot] form resync failed — ${err.message}`);
|
|
3037
|
+
ctx.throw(502, err.message || "Cannot reach HubSpot");
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
const merged = mergeHubspotImport(entry.definition ?? EMPTY_DEFINITION, converted.definition);
|
|
3041
|
+
await documents().update({
|
|
3042
|
+
documentId: ctx.params.documentId,
|
|
3043
|
+
locale: ctx.query.locale,
|
|
3044
|
+
data: { definition: merged.definition }
|
|
3045
|
+
});
|
|
3046
|
+
const form = await documents().findOne({
|
|
3047
|
+
documentId: ctx.params.documentId,
|
|
3048
|
+
locale: ctx.query.locale,
|
|
3049
|
+
status: "draft"
|
|
3050
|
+
});
|
|
3051
|
+
ctx.body = {
|
|
3052
|
+
form,
|
|
3053
|
+
added: merged.added,
|
|
3054
|
+
updated: merged.updated,
|
|
3055
|
+
skipped: converted.skipped
|
|
3056
|
+
};
|
|
2937
3057
|
},
|
|
2938
3058
|
/**
|
|
2939
3059
|
* Copies a form (every locale of its draft) into a new draft document —
|
|
@@ -3218,8 +3338,8 @@ const config = {
|
|
|
3218
3338
|
// Submission pipeline of the built forms:
|
|
3219
3339
|
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
3220
3340
|
// domain) and associates it to the contact;
|
|
3221
|
-
// - timelineNote: recap note on the
|
|
3222
|
-
//
|
|
3341
|
+
// - timelineNote: recap note on the contact after a successful sync
|
|
3342
|
+
// (page URL, subject, answers) — alongside a Forms API conversion;
|
|
3223
3343
|
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
3224
3344
|
// doesn't set its own — swap with the portal at production cutover.
|
|
3225
3345
|
// - submissionMode / writeExtraProperties / syncFieldsOnPublish: see
|
|
@@ -3472,6 +3592,7 @@ const routes = {
|
|
|
3472
3592
|
adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
|
|
3473
3593
|
adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
|
|
3474
3594
|
adminRoute("POST", "/builder/forms/:documentId/duplicate", "formsAdmin.duplicate", [FORMS_ACTION]),
|
|
3595
|
+
adminRoute("POST", "/builder/forms/:documentId/resync", "formsAdmin.resync", [FORMS_ACTION]),
|
|
3475
3596
|
// No FORMS_ACTION: editors pick forms from the Content Manager, like
|
|
3476
3597
|
// they pick properties — the builder itself stays gated.
|
|
3477
3598
|
adminRoute("GET", "/forms-options", "formsAdmin.options"),
|
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) {
|
|
@@ -1777,6 +1781,65 @@ function convertHubspotForm(raw) {
|
|
|
1777
1781
|
skipped
|
|
1778
1782
|
};
|
|
1779
1783
|
}
|
|
1784
|
+
function mappingKey(field) {
|
|
1785
|
+
const mapping = field.hubspot ?? {};
|
|
1786
|
+
const property = mapping.property?.trim();
|
|
1787
|
+
if (!property) return null;
|
|
1788
|
+
return `${mapping.object?.trim() || "contact"}:${property}`;
|
|
1789
|
+
}
|
|
1790
|
+
function uniqueName(base, used) {
|
|
1791
|
+
if (!used.has(base)) return base;
|
|
1792
|
+
let n = 2;
|
|
1793
|
+
while (used.has(`${base}_${n}`)) n += 1;
|
|
1794
|
+
return `${base}_${n}`;
|
|
1795
|
+
}
|
|
1796
|
+
function mergeHubspotImport(current, incoming) {
|
|
1797
|
+
const steps = (current.steps ?? []).map((step) => ({
|
|
1798
|
+
...step,
|
|
1799
|
+
fields: [...step.fields ?? []]
|
|
1800
|
+
}));
|
|
1801
|
+
if (!steps.length) {
|
|
1802
|
+
steps.push({ id: makeId$1("stp"), fields: [] });
|
|
1803
|
+
}
|
|
1804
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1805
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
1806
|
+
for (const step of steps) {
|
|
1807
|
+
for (const field of step.fields) {
|
|
1808
|
+
usedNames.add(field.name);
|
|
1809
|
+
const key = mappingKey(field);
|
|
1810
|
+
if (key) byKey.set(key, field);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
const added = [];
|
|
1814
|
+
const updated = [];
|
|
1815
|
+
const incomingFields = (incoming.steps ?? []).flatMap((step) => step.fields ?? []);
|
|
1816
|
+
for (const next of incomingFields) {
|
|
1817
|
+
const key = mappingKey(next);
|
|
1818
|
+
const existing = key ? byKey.get(key) : void 0;
|
|
1819
|
+
if (existing) {
|
|
1820
|
+
existing.label = next.label;
|
|
1821
|
+
existing.type = next.type;
|
|
1822
|
+
existing.required = next.required;
|
|
1823
|
+
existing.placeholder = next.placeholder;
|
|
1824
|
+
existing.helpText = next.helpText;
|
|
1825
|
+
if (next.options) existing.options = next.options;
|
|
1826
|
+
else delete existing.options;
|
|
1827
|
+
if (!existing.visibleIf && next.visibleIf) existing.visibleIf = next.visibleIf;
|
|
1828
|
+
updated.push(key);
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
const field = {
|
|
1832
|
+
...next,
|
|
1833
|
+
id: makeId$1("fld"),
|
|
1834
|
+
name: uniqueName(next.name, usedNames)
|
|
1835
|
+
};
|
|
1836
|
+
usedNames.add(field.name);
|
|
1837
|
+
if (key) byKey.set(key, field);
|
|
1838
|
+
steps[steps.length - 1].fields.push(field);
|
|
1839
|
+
added.push(key || field.name);
|
|
1840
|
+
}
|
|
1841
|
+
return { definition: { version: 1, steps }, added, updated };
|
|
1842
|
+
}
|
|
1780
1843
|
async function hsGet(apiKey, path) {
|
|
1781
1844
|
const res = await fetch(`${HS_BASE$2}${path}`, {
|
|
1782
1845
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
@@ -2385,7 +2448,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2385
2448
|
context: {
|
|
2386
2449
|
hutk: sanitizeHutk(meta.hutk),
|
|
2387
2450
|
pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
|
|
2388
|
-
pageName: typeof meta.pageName === "string" && meta.pageName || (form.title || form.name) || void 0,
|
|
2451
|
+
pageName: typeof meta.pageName === "string" && meta.pageName || typeof meta.originLabel === "string" && meta.originLabel || (form.title || form.name) || void 0,
|
|
2389
2452
|
ipAddress: sanitizeIp(meta.ipAddress)
|
|
2390
2453
|
},
|
|
2391
2454
|
consent: { given: meta.consent === true },
|
|
@@ -2465,7 +2528,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
2465
2528
|
strapi.log.warn(`[hubspot] company sync failed (contact OK) — ${err.message}`);
|
|
2466
2529
|
}
|
|
2467
2530
|
}
|
|
2468
|
-
if (contactId &&
|
|
2531
|
+
if (contactId && policy.timelineNote) {
|
|
2469
2532
|
try {
|
|
2470
2533
|
await createLeadNote(apiKey, {
|
|
2471
2534
|
contactId,
|
|
@@ -2894,9 +2957,9 @@ function createFormsAdminController(strapi) {
|
|
|
2894
2957
|
},
|
|
2895
2958
|
/**
|
|
2896
2959
|
* Converts one portal form into a plugin form draft, in the current
|
|
2897
|
-
* locale.
|
|
2898
|
-
*
|
|
2899
|
-
*
|
|
2960
|
+
* locale. Matching prefers the linked HubSpot GUID, then the slug.
|
|
2961
|
+
* Re-importing merges: mapped fields update in place, new ones append,
|
|
2962
|
+
* Strapi steps stay. Never the published version, never the portal.
|
|
2900
2963
|
*/
|
|
2901
2964
|
async runHubspotImport(ctx) {
|
|
2902
2965
|
const formId = ctx.request.body?.formId;
|
|
@@ -2915,24 +2978,81 @@ function createFormsAdminController(strapi) {
|
|
|
2915
2978
|
const { skipped, definition, ...meta } = converted;
|
|
2916
2979
|
const slug = slugify(name);
|
|
2917
2980
|
const existing = await documents().findFirst({
|
|
2981
|
+
filters: { hubspotFormId: formId }
|
|
2982
|
+
}) ?? await documents().findFirst({
|
|
2918
2983
|
filters: { slug }
|
|
2919
2984
|
});
|
|
2920
|
-
let documentId;
|
|
2921
2985
|
if (existing) {
|
|
2922
|
-
await documents().
|
|
2986
|
+
const current = await documents().findOne({
|
|
2923
2987
|
documentId: existing.documentId,
|
|
2924
2988
|
locale: ctx.query.locale,
|
|
2925
|
-
|
|
2989
|
+
status: "draft"
|
|
2926
2990
|
});
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2991
|
+
const merged = mergeHubspotImport(current?.definition ?? EMPTY_DEFINITION, definition);
|
|
2992
|
+
await documents().update({
|
|
2993
|
+
documentId: existing.documentId,
|
|
2930
2994
|
locale: ctx.query.locale,
|
|
2931
|
-
data: {
|
|
2995
|
+
data: {
|
|
2996
|
+
hubspotFormId: meta.hubspotFormId ?? formId,
|
|
2997
|
+
definition: merged.definition
|
|
2998
|
+
}
|
|
2932
2999
|
});
|
|
2933
|
-
|
|
3000
|
+
ctx.body = {
|
|
3001
|
+
documentId: existing.documentId,
|
|
3002
|
+
skipped,
|
|
3003
|
+
added: merged.added,
|
|
3004
|
+
updated: merged.updated
|
|
3005
|
+
};
|
|
3006
|
+
return;
|
|
2934
3007
|
}
|
|
2935
|
-
|
|
3008
|
+
const created = await documents().create({
|
|
3009
|
+
locale: ctx.query.locale,
|
|
3010
|
+
data: { ...meta, name, slug, definition }
|
|
3011
|
+
});
|
|
3012
|
+
ctx.body = { documentId: created.documentId, skipped, added: [], updated: [] };
|
|
3013
|
+
},
|
|
3014
|
+
/**
|
|
3015
|
+
* Pull the linked HubSpot marketing form back into this draft: mapped
|
|
3016
|
+
* fields update in place, new ones append, Strapi steps and extras stay.
|
|
3017
|
+
*/
|
|
3018
|
+
async resync(ctx) {
|
|
3019
|
+
const entry = await documents().findOne({
|
|
3020
|
+
documentId: ctx.params.documentId,
|
|
3021
|
+
locale: ctx.query.locale,
|
|
3022
|
+
status: "draft"
|
|
3023
|
+
});
|
|
3024
|
+
if (!entry) ctx.throw(404, "Form not found");
|
|
3025
|
+
const guid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
|
|
3026
|
+
if (!isFormGuid(guid)) {
|
|
3027
|
+
ctx.throw(400, "This form is not linked to a HubSpot marketing form.");
|
|
3028
|
+
}
|
|
3029
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
3030
|
+
if (!apiKey) ctx.throw(400, "No HubSpot API key configured");
|
|
3031
|
+
let converted;
|
|
3032
|
+
try {
|
|
3033
|
+
converted = convertHubspotForm(await fetchHubspotForm(apiKey, guid));
|
|
3034
|
+
} catch (err) {
|
|
3035
|
+
strapi.log.warn(`[hubspot] form resync failed — ${err.message}`);
|
|
3036
|
+
ctx.throw(502, err.message || "Cannot reach HubSpot");
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
const merged = mergeHubspotImport(entry.definition ?? EMPTY_DEFINITION, converted.definition);
|
|
3040
|
+
await documents().update({
|
|
3041
|
+
documentId: ctx.params.documentId,
|
|
3042
|
+
locale: ctx.query.locale,
|
|
3043
|
+
data: { definition: merged.definition }
|
|
3044
|
+
});
|
|
3045
|
+
const form = await documents().findOne({
|
|
3046
|
+
documentId: ctx.params.documentId,
|
|
3047
|
+
locale: ctx.query.locale,
|
|
3048
|
+
status: "draft"
|
|
3049
|
+
});
|
|
3050
|
+
ctx.body = {
|
|
3051
|
+
form,
|
|
3052
|
+
added: merged.added,
|
|
3053
|
+
updated: merged.updated,
|
|
3054
|
+
skipped: converted.skipped
|
|
3055
|
+
};
|
|
2936
3056
|
},
|
|
2937
3057
|
/**
|
|
2938
3058
|
* Copies a form (every locale of its draft) into a new draft document —
|
|
@@ -3217,8 +3337,8 @@ const config = {
|
|
|
3217
3337
|
// Submission pipeline of the built forms:
|
|
3218
3338
|
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
3219
3339
|
// domain) and associates it to the contact;
|
|
3220
|
-
// - timelineNote: recap note on the
|
|
3221
|
-
//
|
|
3340
|
+
// - timelineNote: recap note on the contact after a successful sync
|
|
3341
|
+
// (page URL, subject, answers) — alongside a Forms API conversion;
|
|
3222
3342
|
// - defaultFormId: portal-wide marketing form GUID when a builder form
|
|
3223
3343
|
// doesn't set its own — swap with the portal at production cutover.
|
|
3224
3344
|
// - submissionMode / writeExtraProperties / syncFieldsOnPublish: see
|
|
@@ -3471,6 +3591,7 @@ const routes = {
|
|
|
3471
3591
|
adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
|
|
3472
3592
|
adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
|
|
3473
3593
|
adminRoute("POST", "/builder/forms/:documentId/duplicate", "formsAdmin.duplicate", [FORMS_ACTION]),
|
|
3594
|
+
adminRoute("POST", "/builder/forms/:documentId/resync", "formsAdmin.resync", [FORMS_ACTION]),
|
|
3474
3595
|
// No FORMS_ACTION: editors pick forms from the Content Manager, like
|
|
3475
3596
|
// they pick properties — the builder itself stays gated.
|
|
3476
3597
|
adminRoute("GET", "/forms-options", "formsAdmin.options"),
|
|
@@ -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";
|
|
@@ -50,11 +50,16 @@ export declare function createFormsAdminController(strapi: Core.Strapi): {
|
|
|
50
50
|
listHubspotSources(ctx: Ctx): Promise<void>;
|
|
51
51
|
/**
|
|
52
52
|
* Converts one portal form into a plugin form draft, in the current
|
|
53
|
-
* locale.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* locale. Matching prefers the linked HubSpot GUID, then the slug.
|
|
54
|
+
* Re-importing merges: mapped fields update in place, new ones append,
|
|
55
|
+
* Strapi steps stay. Never the published version, never the portal.
|
|
56
56
|
*/
|
|
57
57
|
runHubspotImport(ctx: Ctx): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Pull the linked HubSpot marketing form back into this draft: mapped
|
|
60
|
+
* fields update in place, new ones append, Strapi steps and extras stay.
|
|
61
|
+
*/
|
|
62
|
+
resync(ctx: Ctx): Promise<void>;
|
|
58
63
|
/**
|
|
59
64
|
* Copies a form (every locale of its draft) into a new draft document —
|
|
60
65
|
* the quickest way to A/B a variant or to start from an existing form.
|
|
@@ -79,6 +79,18 @@ export interface RawHubspotForm {
|
|
|
79
79
|
} | null;
|
|
80
80
|
}
|
|
81
81
|
export declare function convertHubspotForm(raw: RawHubspotForm): ConvertedHubspotForm;
|
|
82
|
+
export interface HubspotMergeResult {
|
|
83
|
+
definition: FormDefinition;
|
|
84
|
+
added: string[];
|
|
85
|
+
updated: string[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fold a freshly converted HubSpot form into an existing builder definition.
|
|
89
|
+
* Mapped fields are updated in place (label, type, required, options…) so
|
|
90
|
+
* Strapi steps, ids, conditions and extra fields survive. New HubSpot fields
|
|
91
|
+
* are appended to the last step. Nothing is deleted.
|
|
92
|
+
*/
|
|
93
|
+
export declare function mergeHubspotImport(current: FormDefinition, incoming: FormDefinition): HubspotMergeResult;
|
|
82
94
|
/** Every regular form of the portal, name-sorted. Needs the `forms` scope. */
|
|
83
95
|
export declare function listHubspotForms(apiKey: string): Promise<HubspotFormSummary[]>;
|
|
84
96
|
/** One form, full definition. */
|
|
@@ -280,6 +280,7 @@ declare const _default: {
|
|
|
280
280
|
runImport(ctx: import("./formsAdmin").Ctx): Promise<void>;
|
|
281
281
|
listHubspotSources(ctx: import("./formsAdmin").Ctx): Promise<void>;
|
|
282
282
|
runHubspotImport(ctx: import("./formsAdmin").Ctx): Promise<void>;
|
|
283
|
+
resync(ctx: import("./formsAdmin").Ctx): Promise<void>;
|
|
283
284
|
duplicate(ctx: import("./formsAdmin").Ctx): Promise<void>;
|
|
284
285
|
remove(ctx: import("./formsAdmin").Ctx): Promise<void>;
|
|
285
286
|
};
|
|
@@ -370,6 +371,7 @@ declare const _default: {
|
|
|
370
371
|
submissionMode?: string;
|
|
371
372
|
writeExtraProperties?: boolean;
|
|
372
373
|
syncFieldsOnPublish?: boolean;
|
|
374
|
+
timelineNote?: boolean;
|
|
373
375
|
};
|
|
374
376
|
};
|
|
375
377
|
body: unknown;
|
|
@@ -24,6 +24,7 @@ export interface StoredSettings {
|
|
|
24
24
|
submissionMode?: SubmissionMode;
|
|
25
25
|
writeExtraProperties?: boolean;
|
|
26
26
|
syncFieldsOnPublish?: boolean;
|
|
27
|
+
timelineNote?: boolean;
|
|
27
28
|
}
|
|
28
29
|
export interface HubspotPolicy {
|
|
29
30
|
/** How submissions reach HubSpot. `auto` uses Forms API when a GUID is set. */
|
|
@@ -32,6 +33,8 @@ export interface HubspotPolicy {
|
|
|
32
33
|
writeExtraProperties: boolean;
|
|
33
34
|
/** On publish, PATCH missing mapped contact fields onto the HubSpot form. */
|
|
34
35
|
syncFieldsOnPublish: boolean;
|
|
36
|
+
/** Recap note on the contact (page URL, subject, answers) after a successful sync. */
|
|
37
|
+
timelineNote: boolean;
|
|
35
38
|
}
|
|
36
39
|
export interface PublicSettings {
|
|
37
40
|
configured: boolean;
|
|
@@ -48,6 +51,7 @@ export interface PublicSettings {
|
|
|
48
51
|
submissionMode: SubmissionMode;
|
|
49
52
|
writeExtraProperties: boolean;
|
|
50
53
|
syncFieldsOnPublish: boolean;
|
|
54
|
+
timelineNote: boolean;
|
|
51
55
|
}
|
|
52
56
|
export interface HubspotAccount {
|
|
53
57
|
portalId: string;
|
|
@@ -62,6 +66,7 @@ interface FormsCfg {
|
|
|
62
66
|
submissionMode?: unknown;
|
|
63
67
|
writeExtraProperties?: unknown;
|
|
64
68
|
syncFieldsOnPublish?: unknown;
|
|
69
|
+
timelineNote?: unknown;
|
|
65
70
|
}
|
|
66
71
|
export declare function policyFrom(stored: StoredSettings, forms: FormsCfg): HubspotPolicy;
|
|
67
72
|
export declare function resolvePolicy(strapi: Core.Strapi): Promise<HubspotPolicy>;
|
|
@@ -80,6 +85,7 @@ export interface SettingsPatch {
|
|
|
80
85
|
submissionMode?: unknown;
|
|
81
86
|
writeExtraProperties?: unknown;
|
|
82
87
|
syncFieldsOnPublish?: unknown;
|
|
88
|
+
timelineNote?: unknown;
|
|
83
89
|
}
|
|
84
90
|
/** Merge a PATCH-like body into the store without dropping the saved key. */
|
|
85
91
|
export declare function patchStoredSettings(strapi: Core.Strapi, body: SettingsPatch): Promise<void>;
|