strapi-plugin-hubspot 0.4.2 → 0.6.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 +48 -0
- package/README.md +116 -4
- package/dist/_chunks/Forms-BF4_zA-m.js +1007 -0
- package/dist/_chunks/Forms-CW1KFO9U.mjs +989 -0
- package/dist/_chunks/{HubspotObjectInput-DutelVp6.mjs → HubspotObjectInput-CpxeM2J0.mjs} +4 -16
- package/dist/_chunks/{HubspotObjectInput-Br2jTFkG.js → HubspotObjectInput-hjAYWtaU.js} +4 -16
- package/dist/_chunks/HubspotPropertyInput-C-52Lu24.mjs +227 -0
- package/dist/_chunks/HubspotPropertyInput-OSeIBaRC.js +245 -0
- package/dist/_chunks/Settings-2V7TH5CX.mjs +321 -0
- package/dist/_chunks/Settings-DcA9M5Tw.js +339 -0
- package/dist/_chunks/en-CJ9wYHgN.mjs +197 -0
- package/dist/_chunks/en-C_-N8cjv.js +197 -0
- package/dist/_chunks/fr-BHObPoUp.js +197 -0
- package/dist/_chunks/fr-DttDn-2Y.mjs +197 -0
- package/dist/_chunks/{Settings-CQsB0B2T.mjs → index-C1UJXE_o.mjs} +576 -317
- package/dist/_chunks/{index-BUlB1rK9.js → index-CMzg-x5i.js} +12 -4
- package/dist/_chunks/{Settings-CvbKVl-t.js → index-Dgbid2LK.js} +580 -322
- package/dist/_chunks/{index-CKoBcX1T.mjs → index-DkGOTp8p.mjs} +12 -4
- package/dist/_chunks/{objectLabels-dM4o67Fz.mjs → objectLabels-D9UEuOGr.mjs} +1 -1
- package/dist/_chunks/{objectLabels-B76t8qqg.js → objectLabels-Th7aYW-B.js} +1 -1
- package/dist/_chunks/useHubspotSchema-Bgrgg8Mt.js +75 -0
- package/dist/_chunks/useHubspotSchema-DQkQcA_V.mjs +58 -0
- package/dist/admin/index.js +2 -1
- package/dist/admin/index.mjs +2 -1
- package/dist/admin/src/builder/ConditionEditor.d.ts +17 -0
- package/dist/admin/src/builder/FieldPanel.d.ts +16 -0
- package/dist/admin/src/builder/types.d.ts +90 -0
- package/dist/admin/src/index.d.ts +1 -0
- package/dist/admin/src/pages/FormEditor.d.ts +9 -0
- package/dist/admin/src/pages/Forms.d.ts +4 -0
- package/dist/admin/src/pages/FormsList.d.ts +8 -0
- package/dist/admin/src/useHubspotSchema.d.ts +28 -0
- package/dist/server/index.js +880 -5
- package/dist/server/index.mjs +880 -5
- package/dist/server/src/__tests__/conditions.test.d.ts +1 -0
- package/dist/server/src/__tests__/forms.test.d.ts +1 -0
- package/dist/server/src/__tests__/formsSubmit.test.d.ts +1 -0
- package/dist/server/src/__tests__/importLegacy.test.d.ts +1 -0
- package/dist/server/src/conditions.d.ts +88 -0
- package/dist/server/src/content-types.d.ts +161 -9
- package/dist/server/src/forms.d.ts +81 -0
- package/dist/server/src/formsAdmin.d.ts +39 -0
- package/dist/server/src/importLegacy.d.ts +25 -0
- package/dist/server/src/index.d.ts +214 -16
- package/dist/shared/types.d.ts +103 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.mjs +2 -0
- package/package.json +23 -6
- package/dist/_chunks/HubspotPropertyInput-CK6Yj9rC.mjs +0 -177
- package/dist/_chunks/HubspotPropertyInput-DIPDPRXJ.js +0 -195
- package/dist/_chunks/en-D4ueeUcJ.mjs +0 -82
- package/dist/_chunks/en-PBG7-vMR.js +0 -82
- package/dist/_chunks/fr-DL4wdV8I.js +0 -82
- package/dist/_chunks/fr-DYnxVmIO.mjs +0 -82
package/dist/server/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errors } from "@strapi/utils";
|
|
2
|
-
const HS_BASE$
|
|
2
|
+
const HS_BASE$2 = "https://api.hubapi.com";
|
|
3
3
|
const TTL_MS = 10 * 60 * 1e3;
|
|
4
4
|
const STANDARD_OBJECTS = [
|
|
5
5
|
{ name: "contact", path: "contacts" },
|
|
@@ -13,7 +13,7 @@ const STANDARD_OBJECTS = [
|
|
|
13
13
|
let cache = null;
|
|
14
14
|
let inFlight = null;
|
|
15
15
|
async function hsGet(apiKey, path) {
|
|
16
|
-
const res = await fetch(`${HS_BASE$
|
|
16
|
+
const res = await fetch(`${HS_BASE$2}${path}`, {
|
|
17
17
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
18
18
|
});
|
|
19
19
|
if (!res.ok) {
|
|
@@ -352,7 +352,92 @@ async function runAudit(strapi, targets, schema) {
|
|
|
352
352
|
}
|
|
353
353
|
return reports;
|
|
354
354
|
}
|
|
355
|
+
const localizedString = (type = "string") => ({
|
|
356
|
+
type,
|
|
357
|
+
pluginOptions: { i18n: { localized: true } }
|
|
358
|
+
});
|
|
355
359
|
const contentTypes = {
|
|
360
|
+
/**
|
|
361
|
+
* A form built in the plugin's builder page. The flat, queryable identity
|
|
362
|
+
* lives as attributes; the structure (steps → fields → conditions) lives in
|
|
363
|
+
* `definition`, a versioned JSON document — plugins cannot ship components,
|
|
364
|
+
* and the builder is a better editor for it than the Content Manager, which
|
|
365
|
+
* is why the type is hidden there.
|
|
366
|
+
*/
|
|
367
|
+
form: {
|
|
368
|
+
schema: {
|
|
369
|
+
kind: "collectionType",
|
|
370
|
+
collectionName: "hubspot_forms",
|
|
371
|
+
info: {
|
|
372
|
+
singularName: "form",
|
|
373
|
+
pluralName: "forms",
|
|
374
|
+
displayName: "HubSpot forms",
|
|
375
|
+
description: "Forms built in the HubSpot form builder"
|
|
376
|
+
},
|
|
377
|
+
options: { draftAndPublish: true },
|
|
378
|
+
pluginOptions: {
|
|
379
|
+
"content-manager": { visible: false },
|
|
380
|
+
"content-type-builder": { visible: false },
|
|
381
|
+
i18n: { localized: true }
|
|
382
|
+
},
|
|
383
|
+
attributes: {
|
|
384
|
+
name: {
|
|
385
|
+
type: "string",
|
|
386
|
+
required: true,
|
|
387
|
+
pluginOptions: { i18n: { localized: false } }
|
|
388
|
+
},
|
|
389
|
+
slug: {
|
|
390
|
+
type: "uid",
|
|
391
|
+
targetField: "name",
|
|
392
|
+
required: true
|
|
393
|
+
},
|
|
394
|
+
title: localizedString(),
|
|
395
|
+
subtitle: localizedString("text"),
|
|
396
|
+
nextLabel: localizedString(),
|
|
397
|
+
submitLabel: localizedString(),
|
|
398
|
+
successMessage: localizedString("text"),
|
|
399
|
+
definition: {
|
|
400
|
+
type: "json",
|
|
401
|
+
required: true,
|
|
402
|
+
pluginOptions: { i18n: { localized: true } }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
/**
|
|
408
|
+
* One visitor submission of a built form — the source of truth for the
|
|
409
|
+
* lead, stored whatever the CRM's mood. Visible in the Content Manager so
|
|
410
|
+
* the team can browse and export; hidden from the Content-Type Builder.
|
|
411
|
+
*/
|
|
412
|
+
submission: {
|
|
413
|
+
schema: {
|
|
414
|
+
kind: "collectionType",
|
|
415
|
+
collectionName: "hubspot_submissions",
|
|
416
|
+
info: {
|
|
417
|
+
singularName: "submission",
|
|
418
|
+
pluralName: "submissions",
|
|
419
|
+
displayName: "HubSpot form submissions",
|
|
420
|
+
description: "Visitor submissions of the forms built with the plugin"
|
|
421
|
+
},
|
|
422
|
+
options: { draftAndPublish: false },
|
|
423
|
+
pluginOptions: {
|
|
424
|
+
"content-manager": { visible: true },
|
|
425
|
+
"content-type-builder": { visible: false }
|
|
426
|
+
},
|
|
427
|
+
attributes: {
|
|
428
|
+
form: { type: "string", required: true },
|
|
429
|
+
formTitle: { type: "string" },
|
|
430
|
+
email: { type: "email" },
|
|
431
|
+
values: { type: "json", required: true },
|
|
432
|
+
meta: { type: "json" },
|
|
433
|
+
locale: { type: "string" },
|
|
434
|
+
hubspotSynced: { type: "boolean", default: false },
|
|
435
|
+
contactId: { type: "string" },
|
|
436
|
+
companyId: { type: "string" },
|
|
437
|
+
rejected: { type: "json" }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
},
|
|
356
441
|
failure: {
|
|
357
442
|
schema: {
|
|
358
443
|
kind: "collectionType",
|
|
@@ -378,6 +463,728 @@ const contentTypes = {
|
|
|
378
463
|
}
|
|
379
464
|
}
|
|
380
465
|
};
|
|
466
|
+
const NEEDS_VALUE = /* @__PURE__ */ new Set(["eq", "neq", "contains", "gt", "lt"]);
|
|
467
|
+
function validateDefinition(definition) {
|
|
468
|
+
const errors2 = [];
|
|
469
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
470
|
+
for (const step of definition.steps ?? []) {
|
|
471
|
+
for (const fld of step.fields ?? []) allIds.add(fld.id);
|
|
472
|
+
}
|
|
473
|
+
const checkCondition = (condition, seen2, owner) => {
|
|
474
|
+
for (const rule of condition?.rules ?? []) {
|
|
475
|
+
if (!allIds.has(rule.field)) {
|
|
476
|
+
errors2.push({ code: "unknown-field", ...owner, target: rule.field });
|
|
477
|
+
} else if (!seen2.has(rule.field)) {
|
|
478
|
+
errors2.push({ code: "forward-reference", ...owner, target: rule.field });
|
|
479
|
+
}
|
|
480
|
+
if (NEEDS_VALUE.has(rule.operator) && (rule.value === void 0 || rule.value === "")) {
|
|
481
|
+
errors2.push({ code: "missing-value", ...owner, target: rule.field });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
const seen = /* @__PURE__ */ new Set();
|
|
486
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
487
|
+
for (const step of definition.steps ?? []) {
|
|
488
|
+
checkCondition(step.visibleIf, seen, { stepId: step.id });
|
|
489
|
+
for (const fld of step.fields ?? []) {
|
|
490
|
+
if (!fld.name?.trim()) {
|
|
491
|
+
errors2.push({ code: "missing-name", fieldId: fld.id, name: fld.name ?? "" });
|
|
492
|
+
} else if (seenNames.has(fld.name)) {
|
|
493
|
+
errors2.push({ code: "duplicate-name", fieldId: fld.id, name: fld.name });
|
|
494
|
+
} else {
|
|
495
|
+
seenNames.add(fld.name);
|
|
496
|
+
}
|
|
497
|
+
checkCondition(fld.visibleIf, seen, { fieldId: fld.id });
|
|
498
|
+
seen.add(fld.id);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return errors2;
|
|
502
|
+
}
|
|
503
|
+
const isPrimitive = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
504
|
+
const isEmpty = (v) => v === void 0 || v === null || v === false || typeof v === "string" && v.trim() === "";
|
|
505
|
+
const asString = (v) => typeof v === "string" ? v.trim() : v === void 0 || v === null ? "" : String(v);
|
|
506
|
+
const asNumber = (v) => typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
|
|
507
|
+
function evaluateRule(rule, get) {
|
|
508
|
+
const value = get(rule.field);
|
|
509
|
+
switch (rule.operator) {
|
|
510
|
+
case "eq":
|
|
511
|
+
return asString(value) === asString(rule.value);
|
|
512
|
+
case "neq":
|
|
513
|
+
return asString(value) !== asString(rule.value);
|
|
514
|
+
case "contains":
|
|
515
|
+
return asString(value).toLowerCase().includes(asString(rule.value).toLowerCase());
|
|
516
|
+
case "empty":
|
|
517
|
+
return isEmpty(value);
|
|
518
|
+
case "notEmpty":
|
|
519
|
+
return !isEmpty(value);
|
|
520
|
+
case "gt": {
|
|
521
|
+
const [a, b] = [asNumber(value), asNumber(rule.value)];
|
|
522
|
+
return Number.isFinite(a) && Number.isFinite(b) && a > b;
|
|
523
|
+
}
|
|
524
|
+
case "lt": {
|
|
525
|
+
const [a, b] = [asNumber(value), asNumber(rule.value)];
|
|
526
|
+
return Number.isFinite(a) && Number.isFinite(b) && a < b;
|
|
527
|
+
}
|
|
528
|
+
default:
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function evaluateCondition(condition, get) {
|
|
533
|
+
if (!condition || !condition.rules?.length) return true;
|
|
534
|
+
const results = condition.rules.map((rule) => evaluateRule(rule, get));
|
|
535
|
+
return condition.logic === "or" ? results.some(Boolean) : results.every(Boolean);
|
|
536
|
+
}
|
|
537
|
+
function resolveSubmission(definition, rawValues) {
|
|
538
|
+
const visibleById = /* @__PURE__ */ new Map();
|
|
539
|
+
const get = (fieldId) => visibleById.get(fieldId);
|
|
540
|
+
const values = {};
|
|
541
|
+
const hidden = [];
|
|
542
|
+
const missingRequired = [];
|
|
543
|
+
const declared = /* @__PURE__ */ new Set();
|
|
544
|
+
for (const step of definition.steps ?? []) {
|
|
545
|
+
const stepVisible = evaluateCondition(step.visibleIf, get);
|
|
546
|
+
for (const fld of step.fields ?? []) {
|
|
547
|
+
declared.add(fld.name);
|
|
548
|
+
const visible = stepVisible && evaluateCondition(fld.visibleIf, get);
|
|
549
|
+
if (!visible) {
|
|
550
|
+
hidden.push(fld.name);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const raw = rawValues[fld.name];
|
|
554
|
+
const value = typeof raw === "string" ? raw.trim() : raw;
|
|
555
|
+
const submitted = isPrimitive(value) && !isEmpty(value) ? value : void 0;
|
|
556
|
+
visibleById.set(fld.id, submitted);
|
|
557
|
+
if (submitted !== void 0) {
|
|
558
|
+
values[fld.name] = submitted;
|
|
559
|
+
} else if (fld.required) {
|
|
560
|
+
missingRequired.push({ id: fld.id, name: fld.name, label: fld.label ?? fld.name });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const unknown = Object.keys(rawValues).filter((key) => !declared.has(key));
|
|
565
|
+
return { values, hidden, missingRequired, unknown };
|
|
566
|
+
}
|
|
567
|
+
function publicForm(entry) {
|
|
568
|
+
return {
|
|
569
|
+
slug: entry.slug,
|
|
570
|
+
title: entry.title ?? null,
|
|
571
|
+
subtitle: entry.subtitle ?? null,
|
|
572
|
+
nextLabel: entry.nextLabel ?? null,
|
|
573
|
+
submitLabel: entry.submitLabel ?? null,
|
|
574
|
+
successMessage: entry.successMessage ?? null,
|
|
575
|
+
locale: entry.locale ?? null,
|
|
576
|
+
steps: (entry.definition?.steps ?? []).map((step) => ({
|
|
577
|
+
...step,
|
|
578
|
+
fields: (step.fields ?? []).map(({ hubspot: _hubspot, ...fld }) => fld)
|
|
579
|
+
}))
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function groupByObject(definition, values) {
|
|
583
|
+
const groups = {};
|
|
584
|
+
for (const step of definition.steps ?? []) {
|
|
585
|
+
for (const fld of step.fields ?? []) {
|
|
586
|
+
const value = values[fld.name];
|
|
587
|
+
if (value === void 0) continue;
|
|
588
|
+
const mapping = fld.hubspot ?? {};
|
|
589
|
+
const object = mapping.object?.trim() || "contact";
|
|
590
|
+
const property = mapping.property?.trim() || fld.name;
|
|
591
|
+
(groups[object] ??= {})[property] = value;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return groups;
|
|
595
|
+
}
|
|
596
|
+
function mappingProblems(definition, portalProperties) {
|
|
597
|
+
const problems = [];
|
|
598
|
+
for (const step of definition.steps ?? []) {
|
|
599
|
+
for (const fld of step.fields ?? []) {
|
|
600
|
+
const mapping = fld.hubspot ?? {};
|
|
601
|
+
if (!mapping.property) continue;
|
|
602
|
+
const options = fld.options;
|
|
603
|
+
const problem = checkMapping(portalProperties, {
|
|
604
|
+
object: mapping.object?.trim() || "contact",
|
|
605
|
+
property: mapping.property,
|
|
606
|
+
values: options?.map((o) => o.value ?? "").filter(Boolean)
|
|
607
|
+
});
|
|
608
|
+
if (problem) problems.push({ ...problem, fieldId: fld.id });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return problems;
|
|
612
|
+
}
|
|
613
|
+
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
614
|
+
function findEmail(values) {
|
|
615
|
+
const candidates = [values.email, ...Object.values(values)];
|
|
616
|
+
for (const v of candidates) {
|
|
617
|
+
if (typeof v === "string" && EMAIL_RE.test(v.trim())) return v.trim().toLowerCase();
|
|
618
|
+
}
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
const FREEMAIL = /* @__PURE__ */ new Set([
|
|
622
|
+
"gmail.com",
|
|
623
|
+
"googlemail.com",
|
|
624
|
+
"yahoo.com",
|
|
625
|
+
"yahoo.fr",
|
|
626
|
+
"hotmail.com",
|
|
627
|
+
"hotmail.fr",
|
|
628
|
+
"outlook.com",
|
|
629
|
+
"outlook.fr",
|
|
630
|
+
"live.com",
|
|
631
|
+
"live.fr",
|
|
632
|
+
"msn.com",
|
|
633
|
+
"icloud.com",
|
|
634
|
+
"me.com",
|
|
635
|
+
"aol.com",
|
|
636
|
+
"free.fr",
|
|
637
|
+
"orange.fr",
|
|
638
|
+
"wanadoo.fr",
|
|
639
|
+
"sfr.fr",
|
|
640
|
+
"laposte.net",
|
|
641
|
+
"gmx.com",
|
|
642
|
+
"proton.me",
|
|
643
|
+
"protonmail.com",
|
|
644
|
+
"yopmail.com"
|
|
645
|
+
]);
|
|
646
|
+
function corporateDomain(email) {
|
|
647
|
+
const domain = email?.split("@")[1]?.toLowerCase();
|
|
648
|
+
return domain && !FREEMAIL.has(domain) ? domain : void 0;
|
|
649
|
+
}
|
|
650
|
+
const SUBMISSION_UID = "plugin::hubspot.submission";
|
|
651
|
+
const MAX_FIELDS = 40;
|
|
652
|
+
const MAX_VALUE_LEN = 5e3;
|
|
653
|
+
function sanitizeRawValues(raw) {
|
|
654
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
655
|
+
const entries = Object.entries(raw);
|
|
656
|
+
if (entries.length > MAX_FIELDS) return null;
|
|
657
|
+
const values = {};
|
|
658
|
+
for (const [key, value] of entries) {
|
|
659
|
+
if (typeof value === "string") values[key] = value.slice(0, MAX_VALUE_LEN);
|
|
660
|
+
else if (typeof value === "boolean" || typeof value === "number") values[key] = value;
|
|
661
|
+
}
|
|
662
|
+
return values;
|
|
663
|
+
}
|
|
664
|
+
const HS_BASE$1 = "https://api.hubapi.com";
|
|
665
|
+
const HS_COMPANIES = `${HS_BASE$1}/crm/v3/objects/companies`;
|
|
666
|
+
const HS_NOTES = `${HS_BASE$1}/crm/v3/objects/notes`;
|
|
667
|
+
const ASSOC_NOTE_TO_CONTACT = 202;
|
|
668
|
+
const ASSOC_NOTE_TO_COMPANY = 190;
|
|
669
|
+
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
670
|
+
async function hsJson(apiKey, url, init) {
|
|
671
|
+
const res = await fetch(url, {
|
|
672
|
+
method: init.method,
|
|
673
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
674
|
+
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
675
|
+
});
|
|
676
|
+
const body = await res.json().catch(() => ({}));
|
|
677
|
+
if (!res.ok) throw new Error(body.message || `HubSpot ${res.status}`);
|
|
678
|
+
return body;
|
|
679
|
+
}
|
|
680
|
+
function summaryLines(definition, values) {
|
|
681
|
+
const lines = [];
|
|
682
|
+
for (const step of definition.steps ?? []) {
|
|
683
|
+
for (const fld of step.fields ?? []) {
|
|
684
|
+
const value = values[fld.name];
|
|
685
|
+
if (value === void 0) continue;
|
|
686
|
+
const options = fld.options;
|
|
687
|
+
const label = options?.find((o) => o.value === String(value))?.label;
|
|
688
|
+
lines.push(
|
|
689
|
+
`<strong>${escapeHtml(fld.label ?? fld.name)}</strong> : ${escapeHtml(String(label ?? value))}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return lines;
|
|
694
|
+
}
|
|
695
|
+
function createFormsService(strapi, _opts = {}) {
|
|
696
|
+
const formsConfig = () => ({
|
|
697
|
+
companyFromDomain: true,
|
|
698
|
+
timelineNote: true,
|
|
699
|
+
...strapi.plugin("hubspot").config("forms", {})
|
|
700
|
+
});
|
|
701
|
+
async function partitionBySchema(apiKey, groups) {
|
|
702
|
+
let schema;
|
|
703
|
+
try {
|
|
704
|
+
schema = await loadSchema(
|
|
705
|
+
strapi,
|
|
706
|
+
apiKey,
|
|
707
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
708
|
+
);
|
|
709
|
+
} catch {
|
|
710
|
+
return { accepted: groups, rejected: [] };
|
|
711
|
+
}
|
|
712
|
+
const accepted = {};
|
|
713
|
+
const rejected = [];
|
|
714
|
+
for (const [object, props] of Object.entries(groups)) {
|
|
715
|
+
for (const [property, value] of Object.entries(props)) {
|
|
716
|
+
const problem = checkMapping(schema.properties, {
|
|
717
|
+
object,
|
|
718
|
+
property,
|
|
719
|
+
values: String(value).split(";").map((v) => v.trim()).filter(Boolean)
|
|
720
|
+
});
|
|
721
|
+
if (problem) rejected.push(problem);
|
|
722
|
+
else (accepted[object] ??= {})[property] = value;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return { accepted, rejected };
|
|
726
|
+
}
|
|
727
|
+
async function resolveCompanyByDomain(apiKey, domain, props) {
|
|
728
|
+
const found = await hsJson(apiKey, `${HS_COMPANIES}/search`, {
|
|
729
|
+
method: "POST",
|
|
730
|
+
body: {
|
|
731
|
+
filterGroups: [{ filters: [{ propertyName: "domain", operator: "EQ", value: domain }] }],
|
|
732
|
+
properties: ["domain"],
|
|
733
|
+
limit: 1
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
const stringProps = { domain };
|
|
737
|
+
for (const [key, value] of Object.entries(props)) stringProps[key] = String(value);
|
|
738
|
+
const existingId = found.results?.[0]?.id;
|
|
739
|
+
if (existingId) {
|
|
740
|
+
await hsJson(apiKey, `${HS_COMPANIES}/${existingId}`, {
|
|
741
|
+
method: "PATCH",
|
|
742
|
+
body: { properties: stringProps }
|
|
743
|
+
});
|
|
744
|
+
return existingId;
|
|
745
|
+
}
|
|
746
|
+
const created = await hsJson(apiKey, HS_COMPANIES, {
|
|
747
|
+
method: "POST",
|
|
748
|
+
body: { properties: stringProps }
|
|
749
|
+
});
|
|
750
|
+
return created.id;
|
|
751
|
+
}
|
|
752
|
+
async function createLeadNote(apiKey, opts) {
|
|
753
|
+
const lines = [];
|
|
754
|
+
const title = opts.form.title || opts.form.name;
|
|
755
|
+
lines.push(`<strong>Nouveau lead${title ? ` — ${escapeHtml(String(title))}` : ""}</strong>`);
|
|
756
|
+
if (opts.meta.pageUrl) lines.push(`Page : ${escapeHtml(String(opts.meta.pageUrl))}`);
|
|
757
|
+
if (opts.meta.originLabel) {
|
|
758
|
+
const path = opts.meta.originPath ? ` (${escapeHtml(String(opts.meta.originPath))})` : "";
|
|
759
|
+
lines.push(`<strong>Sujet</strong> : ${escapeHtml(String(opts.meta.originLabel))}${path}`);
|
|
760
|
+
}
|
|
761
|
+
lines.push("", ...summaryLines(opts.form.definition, opts.values));
|
|
762
|
+
if (opts.rejected.length) {
|
|
763
|
+
lines.push("", "<strong>⚠ Champs non enregistrés (mapping à corriger)</strong>");
|
|
764
|
+
for (const r of opts.rejected) {
|
|
765
|
+
lines.push(`${escapeHtml(r.object)}.${escapeHtml(r.property)} — ${escapeHtml(r.code)}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const associations = [
|
|
769
|
+
{
|
|
770
|
+
to: { id: opts.contactId },
|
|
771
|
+
types: [
|
|
772
|
+
{ associationCategory: "HUBSPOT_DEFINED", associationTypeId: ASSOC_NOTE_TO_CONTACT }
|
|
773
|
+
]
|
|
774
|
+
}
|
|
775
|
+
];
|
|
776
|
+
if (opts.companyId) {
|
|
777
|
+
associations.push({
|
|
778
|
+
to: { id: opts.companyId },
|
|
779
|
+
types: [
|
|
780
|
+
{ associationCategory: "HUBSPOT_DEFINED", associationTypeId: ASSOC_NOTE_TO_COMPANY }
|
|
781
|
+
]
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
await hsJson(apiKey, HS_NOTES, {
|
|
785
|
+
method: "POST",
|
|
786
|
+
body: {
|
|
787
|
+
properties: { hs_timestamp: (/* @__PURE__ */ new Date()).toISOString(), hs_note_body: lines.join("<br>") },
|
|
788
|
+
associations
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
async function submit(form, rawValues, meta = {}) {
|
|
793
|
+
const resolution = resolveSubmission(form.definition, rawValues);
|
|
794
|
+
if (resolution.missingRequired.length) {
|
|
795
|
+
return { ok: false, missingRequired: resolution.missingRequired };
|
|
796
|
+
}
|
|
797
|
+
const email = findEmail(resolution.values);
|
|
798
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
799
|
+
const config2 = formsConfig();
|
|
800
|
+
let hubspotSynced = false;
|
|
801
|
+
let contactId;
|
|
802
|
+
let companyId;
|
|
803
|
+
let rejected = [];
|
|
804
|
+
if (email && apiKey) {
|
|
805
|
+
const groups = groupByObject(form.definition, resolution.values);
|
|
806
|
+
const partition = await partitionBySchema(apiKey, groups);
|
|
807
|
+
rejected = partition.rejected;
|
|
808
|
+
const result = await strapi.plugin("hubspot").service("submit").upsert({
|
|
809
|
+
object: "contact",
|
|
810
|
+
idProperty: "email",
|
|
811
|
+
properties: { ...partition.accepted.contact ?? {}, email }
|
|
812
|
+
});
|
|
813
|
+
if (result.ok) {
|
|
814
|
+
hubspotSynced = true;
|
|
815
|
+
contactId = result.id;
|
|
816
|
+
}
|
|
817
|
+
const domain = corporateDomain(email);
|
|
818
|
+
if (contactId && domain && config2.companyFromDomain) {
|
|
819
|
+
try {
|
|
820
|
+
companyId = await resolveCompanyByDomain(
|
|
821
|
+
apiKey,
|
|
822
|
+
domain,
|
|
823
|
+
partition.accepted.company ?? {}
|
|
824
|
+
);
|
|
825
|
+
if (companyId) {
|
|
826
|
+
await hsJson(
|
|
827
|
+
apiKey,
|
|
828
|
+
`${HS_BASE$1}/crm/v4/objects/contacts/${contactId}/associations/default/companies/${companyId}`,
|
|
829
|
+
{ method: "PUT" }
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
} catch (err) {
|
|
833
|
+
strapi.log.warn(`[hubspot] company sync failed (contact OK) — ${err.message}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (contactId && config2.timelineNote) {
|
|
837
|
+
try {
|
|
838
|
+
await createLeadNote(apiKey, {
|
|
839
|
+
contactId,
|
|
840
|
+
companyId,
|
|
841
|
+
form,
|
|
842
|
+
meta,
|
|
843
|
+
values: resolution.values,
|
|
844
|
+
rejected
|
|
845
|
+
});
|
|
846
|
+
} catch (err) {
|
|
847
|
+
strapi.log.warn(`[hubspot] lead note failed (contact OK) — ${err.message}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
await strapi.documents(SUBMISSION_UID).create({
|
|
852
|
+
data: {
|
|
853
|
+
form: form.slug,
|
|
854
|
+
formTitle: form.title || form.name,
|
|
855
|
+
email,
|
|
856
|
+
values: resolution.values,
|
|
857
|
+
meta,
|
|
858
|
+
locale: form.locale ?? null,
|
|
859
|
+
hubspotSynced,
|
|
860
|
+
contactId,
|
|
861
|
+
companyId,
|
|
862
|
+
rejected
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
return { ok: true, hubspotSynced };
|
|
866
|
+
}
|
|
867
|
+
return { submit };
|
|
868
|
+
}
|
|
869
|
+
let counter = 0;
|
|
870
|
+
const makeId = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter += 1).toString(36)}`;
|
|
871
|
+
const slugifyName = (label) => label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
872
|
+
const FIELD_DEFAULTS = {
|
|
873
|
+
label: "label",
|
|
874
|
+
name: "name",
|
|
875
|
+
object: "hsObject",
|
|
876
|
+
property: "hsProperty",
|
|
877
|
+
type: "type",
|
|
878
|
+
placeholder: "placeholder",
|
|
879
|
+
required: "required",
|
|
880
|
+
icon: "icon",
|
|
881
|
+
options: "options",
|
|
882
|
+
width: "width",
|
|
883
|
+
helpText: "helpText",
|
|
884
|
+
class: "class",
|
|
885
|
+
persist: "persist"
|
|
886
|
+
};
|
|
887
|
+
const STEP_DEFAULTS = {
|
|
888
|
+
title: "title",
|
|
889
|
+
description: "description",
|
|
890
|
+
class: "class"
|
|
891
|
+
};
|
|
892
|
+
function convertLegacyForm(entry, map = {}) {
|
|
893
|
+
const stepMap = { ...STEP_DEFAULTS, ...map.step };
|
|
894
|
+
const fieldMap = { ...FIELD_DEFAULTS, ...map.field };
|
|
895
|
+
const rawSteps = entry[map.steps ?? "steps"] ?? [];
|
|
896
|
+
const steps = rawSteps.map((rawStep) => {
|
|
897
|
+
const rawFields = rawStep[map.fields ?? "fields"] ?? [];
|
|
898
|
+
return {
|
|
899
|
+
id: makeId("stp"),
|
|
900
|
+
title: rawStep[stepMap.title] ?? void 0,
|
|
901
|
+
description: rawStep[stepMap.description] ?? void 0,
|
|
902
|
+
class: rawStep[stepMap.class] ?? void 0,
|
|
903
|
+
fields: rawFields.map((raw) => {
|
|
904
|
+
const label = raw[fieldMap.label] ?? "";
|
|
905
|
+
const object = raw[fieldMap.object] ?? void 0;
|
|
906
|
+
const property = raw[fieldMap.property] ?? void 0;
|
|
907
|
+
const options = raw[fieldMap.options] ?? [];
|
|
908
|
+
return {
|
|
909
|
+
id: makeId("fld"),
|
|
910
|
+
label,
|
|
911
|
+
name: (raw[fieldMap.name] || slugifyName(label)) ?? "",
|
|
912
|
+
type: raw[fieldMap.type] ?? "text",
|
|
913
|
+
placeholder: raw[fieldMap.placeholder] ?? void 0,
|
|
914
|
+
required: Boolean(raw[fieldMap.required]),
|
|
915
|
+
icon: raw[fieldMap.icon] ?? void 0,
|
|
916
|
+
width: raw[fieldMap.width] ?? void 0,
|
|
917
|
+
helpText: raw[fieldMap.helpText] ?? void 0,
|
|
918
|
+
class: raw[fieldMap.class] ?? void 0,
|
|
919
|
+
persist: raw[fieldMap.persist] ?? void 0,
|
|
920
|
+
...options.length ? { options: options.map((o) => ({ value: o.value ?? "", label: o.label ?? void 0 })) } : {},
|
|
921
|
+
...object || property ? { hubspot: { object, property } } : {}
|
|
922
|
+
};
|
|
923
|
+
})
|
|
924
|
+
};
|
|
925
|
+
});
|
|
926
|
+
return {
|
|
927
|
+
name: entry.name ?? "",
|
|
928
|
+
slug: entry.slug ?? "",
|
|
929
|
+
title: entry.title ?? null,
|
|
930
|
+
subtitle: entry.subtitle ?? null,
|
|
931
|
+
nextLabel: entry.nextLabel ?? null,
|
|
932
|
+
submitLabel: entry.submitLabel ?? null,
|
|
933
|
+
successMessage: entry.successMessage ?? null,
|
|
934
|
+
definition: { version: 1, steps }
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
const FORM_UID = "plugin::hubspot.form";
|
|
938
|
+
const EMPTY_DEFINITION = { version: 1, steps: [] };
|
|
939
|
+
const WRITABLE = [
|
|
940
|
+
"name",
|
|
941
|
+
"title",
|
|
942
|
+
"subtitle",
|
|
943
|
+
"nextLabel",
|
|
944
|
+
"submitLabel",
|
|
945
|
+
"successMessage"
|
|
946
|
+
];
|
|
947
|
+
const slugify = (value) => value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "form";
|
|
948
|
+
function createFormsAdminController(strapi) {
|
|
949
|
+
const documents = () => strapi.documents(FORM_UID);
|
|
950
|
+
async function checkMappings(definition) {
|
|
951
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
952
|
+
if (!apiKey) return null;
|
|
953
|
+
try {
|
|
954
|
+
const schema = await loadSchema(
|
|
955
|
+
strapi,
|
|
956
|
+
apiKey,
|
|
957
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
958
|
+
);
|
|
959
|
+
return mappingProblems(definition, schema.properties);
|
|
960
|
+
} catch {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function pickWritable(body) {
|
|
965
|
+
const data = {};
|
|
966
|
+
for (const key of WRITABLE) {
|
|
967
|
+
if (key in body) data[key] = body[key];
|
|
968
|
+
}
|
|
969
|
+
return data;
|
|
970
|
+
}
|
|
971
|
+
function requireValidDefinition(ctx, definition) {
|
|
972
|
+
const def = definition ?? EMPTY_DEFINITION;
|
|
973
|
+
const errors2 = validateDefinition(def);
|
|
974
|
+
if (errors2.length) {
|
|
975
|
+
ctx.status = 400;
|
|
976
|
+
ctx.body = { errors: errors2 };
|
|
977
|
+
throw Object.assign(new Error("invalid definition"), { handled: true });
|
|
978
|
+
}
|
|
979
|
+
return def;
|
|
980
|
+
}
|
|
981
|
+
const handled = (err) => err?.handled === true;
|
|
982
|
+
return {
|
|
983
|
+
async list(ctx) {
|
|
984
|
+
const locale = ctx.query.locale;
|
|
985
|
+
const drafts = await documents().findMany({
|
|
986
|
+
locale,
|
|
987
|
+
status: "draft",
|
|
988
|
+
sort: "updatedAt:desc"
|
|
989
|
+
});
|
|
990
|
+
const published = await documents().findMany({
|
|
991
|
+
locale,
|
|
992
|
+
status: "published",
|
|
993
|
+
fields: ["slug"]
|
|
994
|
+
});
|
|
995
|
+
const publishedIds = new Set(published.map((e) => e.documentId));
|
|
996
|
+
ctx.body = {
|
|
997
|
+
forms: drafts.map((entry) => ({
|
|
998
|
+
documentId: entry.documentId,
|
|
999
|
+
name: entry.name,
|
|
1000
|
+
slug: entry.slug,
|
|
1001
|
+
updatedAt: entry.updatedAt,
|
|
1002
|
+
published: publishedIds.has(entry.documentId),
|
|
1003
|
+
steps: (entry.definition?.steps ?? []).length,
|
|
1004
|
+
fields: (entry.definition?.steps ?? []).reduce(
|
|
1005
|
+
(n, step) => n + (step.fields?.length ?? 0),
|
|
1006
|
+
0
|
|
1007
|
+
)
|
|
1008
|
+
}))
|
|
1009
|
+
};
|
|
1010
|
+
},
|
|
1011
|
+
async findOne(ctx) {
|
|
1012
|
+
const entry = await documents().findOne({
|
|
1013
|
+
documentId: ctx.params.documentId,
|
|
1014
|
+
locale: ctx.query.locale,
|
|
1015
|
+
status: "draft"
|
|
1016
|
+
});
|
|
1017
|
+
if (!entry) ctx.throw(404, "Form not found");
|
|
1018
|
+
const published = await documents().findOne({
|
|
1019
|
+
documentId: ctx.params.documentId,
|
|
1020
|
+
locale: ctx.query.locale,
|
|
1021
|
+
status: "published"
|
|
1022
|
+
});
|
|
1023
|
+
ctx.body = { form: entry, published: Boolean(published) };
|
|
1024
|
+
},
|
|
1025
|
+
async create(ctx) {
|
|
1026
|
+
const body = ctx.request.body ?? {};
|
|
1027
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
1028
|
+
if (!name) ctx.throw(400, "A form needs a name");
|
|
1029
|
+
const base = slugify(name);
|
|
1030
|
+
let slug = base;
|
|
1031
|
+
for (let i = 2; ; i += 1) {
|
|
1032
|
+
const clash = await documents().findFirst({ filters: { slug } });
|
|
1033
|
+
if (!clash) break;
|
|
1034
|
+
slug = `${base}-${i}`;
|
|
1035
|
+
}
|
|
1036
|
+
let definition;
|
|
1037
|
+
try {
|
|
1038
|
+
definition = requireValidDefinition(ctx, body.definition);
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
if (handled(err)) return;
|
|
1041
|
+
throw err;
|
|
1042
|
+
}
|
|
1043
|
+
const entry = await documents().create({
|
|
1044
|
+
locale: ctx.query.locale,
|
|
1045
|
+
data: { ...pickWritable(body), name, slug, definition }
|
|
1046
|
+
});
|
|
1047
|
+
ctx.body = { form: entry };
|
|
1048
|
+
},
|
|
1049
|
+
async update(ctx) {
|
|
1050
|
+
const body = ctx.request.body ?? {};
|
|
1051
|
+
let definition;
|
|
1052
|
+
if ("definition" in body) {
|
|
1053
|
+
try {
|
|
1054
|
+
definition = requireValidDefinition(ctx, body.definition);
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
if (handled(err)) return;
|
|
1057
|
+
throw err;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const entry = await documents().update({
|
|
1061
|
+
documentId: ctx.params.documentId,
|
|
1062
|
+
locale: ctx.query.locale,
|
|
1063
|
+
data: { ...pickWritable(body), ...definition ? { definition } : {} }
|
|
1064
|
+
});
|
|
1065
|
+
if (!entry) ctx.throw(404, "Form not found");
|
|
1066
|
+
const problems = definition ? await checkMappings(definition) : null;
|
|
1067
|
+
ctx.body = { form: entry, problems: problems ?? [] };
|
|
1068
|
+
},
|
|
1069
|
+
async publish(ctx) {
|
|
1070
|
+
const entry = await documents().findOne({
|
|
1071
|
+
documentId: ctx.params.documentId,
|
|
1072
|
+
locale: ctx.query.locale,
|
|
1073
|
+
status: "draft"
|
|
1074
|
+
});
|
|
1075
|
+
if (!entry) ctx.throw(404, "Form not found");
|
|
1076
|
+
const structural = validateDefinition(entry.definition ?? EMPTY_DEFINITION);
|
|
1077
|
+
const mappings = await checkMappings(entry.definition ?? EMPTY_DEFINITION) ?? [];
|
|
1078
|
+
if (structural.length || mappings.length) {
|
|
1079
|
+
ctx.status = 400;
|
|
1080
|
+
ctx.body = { errors: structural, problems: mappings };
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
await documents().publish({
|
|
1084
|
+
documentId: ctx.params.documentId,
|
|
1085
|
+
locale: ctx.query.locale
|
|
1086
|
+
});
|
|
1087
|
+
ctx.body = { ok: true };
|
|
1088
|
+
},
|
|
1089
|
+
async unpublish(ctx) {
|
|
1090
|
+
await documents().unpublish({
|
|
1091
|
+
documentId: ctx.params.documentId,
|
|
1092
|
+
locale: ctx.query.locale
|
|
1093
|
+
});
|
|
1094
|
+
ctx.body = { ok: true };
|
|
1095
|
+
},
|
|
1096
|
+
/** Entries of the configured legacy content type, offered for import. */
|
|
1097
|
+
async listSources(ctx) {
|
|
1098
|
+
const config2 = strapi.plugin("hubspot").config("forms", {}).import;
|
|
1099
|
+
if (!config2?.uid) {
|
|
1100
|
+
ctx.body = { configured: false, sources: [] };
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
const entries = await strapi.documents(config2.uid).findMany({
|
|
1105
|
+
fields: ["name", "slug"],
|
|
1106
|
+
sort: "name:asc"
|
|
1107
|
+
});
|
|
1108
|
+
ctx.body = {
|
|
1109
|
+
configured: true,
|
|
1110
|
+
sources: entries.map((e) => ({
|
|
1111
|
+
documentId: e.documentId,
|
|
1112
|
+
name: e.name ?? e.slug ?? e.documentId,
|
|
1113
|
+
slug: e.slug ?? ""
|
|
1114
|
+
}))
|
|
1115
|
+
};
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
strapi.log.warn(`[hubspot] import sources unavailable — ${err.message}`);
|
|
1118
|
+
ctx.body = { configured: true, sources: [] };
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
/**
|
|
1122
|
+
* Converts one legacy entry (every locale of it) into a plugin form
|
|
1123
|
+
* draft. Same slug: re-importing overwrites the draft, never touches the
|
|
1124
|
+
* published version, and never modifies the source.
|
|
1125
|
+
*/
|
|
1126
|
+
async runImport(ctx) {
|
|
1127
|
+
const config2 = strapi.plugin("hubspot").config("forms", {}).import;
|
|
1128
|
+
if (!config2?.uid) ctx.throw(400, "No import source configured (forms.import.uid)");
|
|
1129
|
+
const sourceId = ctx.request.body?.documentId;
|
|
1130
|
+
if (typeof sourceId !== "string" || !sourceId) ctx.throw(400, "documentId is required");
|
|
1131
|
+
const { uid, ...map } = config2;
|
|
1132
|
+
const populate = {
|
|
1133
|
+
[map.steps ?? "steps"]: {
|
|
1134
|
+
populate: {
|
|
1135
|
+
[map.fields ?? "fields"]: { populate: [map.field?.options ?? "options"] }
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
let locales = [void 0];
|
|
1140
|
+
try {
|
|
1141
|
+
const found = await strapi.plugin("i18n").service("locales").find();
|
|
1142
|
+
if (Array.isArray(found) && found.length) {
|
|
1143
|
+
locales = [...found].sort((a, b) => Number(b.isDefault ?? false) - Number(a.isDefault ?? false)).map((l) => l.code);
|
|
1144
|
+
}
|
|
1145
|
+
} catch {
|
|
1146
|
+
}
|
|
1147
|
+
let targetId = null;
|
|
1148
|
+
let imported = 0;
|
|
1149
|
+
for (const locale of locales) {
|
|
1150
|
+
const source = await strapi.documents(uid).findOne({
|
|
1151
|
+
documentId: sourceId,
|
|
1152
|
+
locale,
|
|
1153
|
+
populate
|
|
1154
|
+
});
|
|
1155
|
+
if (!source) continue;
|
|
1156
|
+
const converted = convertLegacyForm(source, map);
|
|
1157
|
+
if (!targetId) {
|
|
1158
|
+
const existing = await documents().findFirst({
|
|
1159
|
+
filters: { slug: converted.slug }
|
|
1160
|
+
});
|
|
1161
|
+
targetId = existing?.documentId ?? null;
|
|
1162
|
+
}
|
|
1163
|
+
const data = { ...converted };
|
|
1164
|
+
if (targetId) {
|
|
1165
|
+
delete data.slug;
|
|
1166
|
+
await documents().update({ documentId: targetId, locale, data });
|
|
1167
|
+
} else {
|
|
1168
|
+
const created = await documents().create({
|
|
1169
|
+
locale,
|
|
1170
|
+
data
|
|
1171
|
+
});
|
|
1172
|
+
targetId = created.documentId;
|
|
1173
|
+
}
|
|
1174
|
+
imported += 1;
|
|
1175
|
+
}
|
|
1176
|
+
if (!targetId) ctx.throw(404, "Source entry not found");
|
|
1177
|
+
ctx.body = { documentId: targetId, locales: imported };
|
|
1178
|
+
},
|
|
1179
|
+
async remove(ctx) {
|
|
1180
|
+
await documents().delete({
|
|
1181
|
+
documentId: ctx.params.documentId
|
|
1182
|
+
// No locale: deleting a form deletes all its locales.
|
|
1183
|
+
});
|
|
1184
|
+
ctx.body = { ok: true };
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
381
1188
|
const HS_BASE = "https://api.hubapi.com";
|
|
382
1189
|
const FAILURE_UID = "plugin::hubspot.failure";
|
|
383
1190
|
const RETRY_DELAYS_MS = [500, 2e3];
|
|
@@ -514,13 +1321,19 @@ function createSubmitService(strapi, { sleep = sleepFor } = {}) {
|
|
|
514
1321
|
return { upsert, retryFailures };
|
|
515
1322
|
}
|
|
516
1323
|
const SETTINGS_ACTION = "plugin::hubspot.settings";
|
|
1324
|
+
const FORMS_ACTION = "plugin::hubspot.forms";
|
|
517
1325
|
const config = {
|
|
518
1326
|
default: {
|
|
519
1327
|
apiKey: "",
|
|
520
1328
|
// Objects whose properties are offered. Names from the standard set, or
|
|
521
1329
|
// `{ name, path }` for a custom object type.
|
|
522
1330
|
objects: ["contact", "company"],
|
|
523
|
-
validate: []
|
|
1331
|
+
validate: [],
|
|
1332
|
+
// Submission pipeline of the built forms. Both default to true:
|
|
1333
|
+
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
1334
|
+
// domain) and associates it to the contact;
|
|
1335
|
+
// - timelineNote: a note recaps the submission on the contact's timeline.
|
|
1336
|
+
forms: { companyFromDomain: true, timelineNote: true }
|
|
524
1337
|
},
|
|
525
1338
|
validator(cfg) {
|
|
526
1339
|
if (cfg.validate && !Array.isArray(cfg.validate)) {
|
|
@@ -581,6 +1394,42 @@ const controllers = {
|
|
|
581
1394
|
ctx.body = await strapi.plugin("hubspot").service("submit").retryFailures();
|
|
582
1395
|
}
|
|
583
1396
|
}),
|
|
1397
|
+
formsAdmin: ({ strapi }) => createFormsAdminController(strapi),
|
|
1398
|
+
forms: ({ strapi }) => ({
|
|
1399
|
+
/** Published form for the host frontend — CRM mapping stripped. */
|
|
1400
|
+
async findOne(ctx) {
|
|
1401
|
+
const entry = await strapi.documents(FORM_UID).findFirst({
|
|
1402
|
+
filters: { slug: ctx.params.slug },
|
|
1403
|
+
status: "published",
|
|
1404
|
+
locale: ctx.query.locale
|
|
1405
|
+
});
|
|
1406
|
+
if (!entry?.definition) ctx.throw(404, "Form not found");
|
|
1407
|
+
ctx.body = publicForm(entry);
|
|
1408
|
+
},
|
|
1409
|
+
async submit(ctx) {
|
|
1410
|
+
const entry = await strapi.documents(FORM_UID).findFirst({
|
|
1411
|
+
filters: { slug: ctx.params.slug },
|
|
1412
|
+
status: "published",
|
|
1413
|
+
locale: ctx.query.locale
|
|
1414
|
+
});
|
|
1415
|
+
if (!entry?.definition) ctx.throw(404, "Form not found");
|
|
1416
|
+
const values = sanitizeRawValues(ctx.request.body?.values);
|
|
1417
|
+
if (!values || !Object.keys(values).length) ctx.throw(422, "Invalid submission");
|
|
1418
|
+
const rawMeta = ctx.request.body?.meta ?? {};
|
|
1419
|
+
const meta = {};
|
|
1420
|
+
for (const key of ["pagePath", "pageUrl", "originPath", "originLabel", "source"]) {
|
|
1421
|
+
const v = rawMeta[key];
|
|
1422
|
+
if (typeof v === "string" && v) meta[key] = v.slice(0, 500);
|
|
1423
|
+
}
|
|
1424
|
+
const outcome = await strapi.plugin("hubspot").service("forms").submit(entry, values, meta);
|
|
1425
|
+
if (!outcome.ok) {
|
|
1426
|
+
ctx.status = 422;
|
|
1427
|
+
ctx.body = { ok: false, missingRequired: outcome.missingRequired ?? [] };
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
ctx.body = { ok: true, hubspotSynced: outcome.hubspotSynced };
|
|
1431
|
+
}
|
|
1432
|
+
}),
|
|
584
1433
|
settings: ({ strapi }) => ({
|
|
585
1434
|
async get(ctx) {
|
|
586
1435
|
ctx.body = await publicSettings(strapi);
|
|
@@ -619,7 +1468,26 @@ const routes = {
|
|
|
619
1468
|
adminRoute("POST", "/failures/retry", "failures.retry", [SETTINGS_ACTION]),
|
|
620
1469
|
adminRoute("GET", "/settings", "settings.get", [SETTINGS_ACTION]),
|
|
621
1470
|
adminRoute("PUT", "/settings", "settings.update", [SETTINGS_ACTION]),
|
|
622
|
-
adminRoute("DELETE", "/settings", "settings.reset", [SETTINGS_ACTION])
|
|
1471
|
+
adminRoute("DELETE", "/settings", "settings.reset", [SETTINGS_ACTION]),
|
|
1472
|
+
adminRoute("GET", "/builder/forms", "formsAdmin.list", [FORMS_ACTION]),
|
|
1473
|
+
adminRoute("POST", "/builder/forms", "formsAdmin.create", [FORMS_ACTION]),
|
|
1474
|
+
adminRoute("GET", "/builder/forms/:documentId", "formsAdmin.findOne", [FORMS_ACTION]),
|
|
1475
|
+
adminRoute("PUT", "/builder/forms/:documentId", "formsAdmin.update", [FORMS_ACTION]),
|
|
1476
|
+
adminRoute("POST", "/builder/forms/:documentId/publish", "formsAdmin.publish", [FORMS_ACTION]),
|
|
1477
|
+
adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
|
|
1478
|
+
adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
|
|
1479
|
+
adminRoute("GET", "/builder/import/sources", "formsAdmin.listSources", [FORMS_ACTION]),
|
|
1480
|
+
adminRoute("POST", "/builder/import", "formsAdmin.runImport", [FORMS_ACTION])
|
|
1481
|
+
]
|
|
1482
|
+
},
|
|
1483
|
+
// Public form delivery + submission, under /api/hubspot/…. Like any
|
|
1484
|
+
// content-api route, they must be granted to the Public role in
|
|
1485
|
+
// Settings → Users & Permissions before the frontend can call them.
|
|
1486
|
+
"content-api": {
|
|
1487
|
+
type: "content-api",
|
|
1488
|
+
routes: [
|
|
1489
|
+
{ method: "GET", path: "/forms/:slug", handler: "forms.findOne", config: { policies: [] } },
|
|
1490
|
+
{ method: "POST", path: "/forms/:slug/submit", handler: "forms.submit", config: { policies: [] } }
|
|
623
1491
|
]
|
|
624
1492
|
}
|
|
625
1493
|
};
|
|
@@ -629,7 +1497,8 @@ const index = {
|
|
|
629
1497
|
controllers,
|
|
630
1498
|
routes,
|
|
631
1499
|
services: {
|
|
632
|
-
submit: ({ strapi }) => createSubmitService(strapi)
|
|
1500
|
+
submit: ({ strapi }) => createSubmitService(strapi),
|
|
1501
|
+
forms: ({ strapi }) => createFormsService(strapi)
|
|
633
1502
|
},
|
|
634
1503
|
register({ strapi }) {
|
|
635
1504
|
strapi.customFields.register({
|
|
@@ -650,6 +1519,12 @@ const index = {
|
|
|
650
1519
|
displayName: "Access the HubSpot settings",
|
|
651
1520
|
uid: "settings",
|
|
652
1521
|
pluginName: "hubspot"
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
section: "plugins",
|
|
1525
|
+
displayName: "Use the HubSpot form builder",
|
|
1526
|
+
uid: "forms",
|
|
1527
|
+
pluginName: "hubspot"
|
|
653
1528
|
}
|
|
654
1529
|
]);
|
|
655
1530
|
const targets = strapi.plugin("hubspot").config("validate", []);
|