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