strapi-plugin-hubspot 0.16.7 → 0.16.9

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/_chunks/{Forms-DyV6KyiO.mjs → Forms-CUNHDvhI.mjs} +52 -6
  3. package/dist/_chunks/{Forms-uUN4XjhW.js → Forms-CylJ0nD3.js} +52 -6
  4. package/dist/_chunks/{HubspotFormInput-BkWat_wu.js → HubspotFormInput-D2H2u9fZ.js} +1 -1
  5. package/dist/_chunks/{HubspotFormInput-DEXPX-6j.mjs → HubspotFormInput-Dppgr_rd.mjs} +1 -1
  6. package/dist/_chunks/{HubspotObjectInput-CFDYW-TM.js → HubspotObjectInput-BjRCaqXr.js} +3 -3
  7. package/dist/_chunks/{HubspotObjectInput-DL8fYK2x.mjs → HubspotObjectInput-CWFddNNw.mjs} +3 -3
  8. package/dist/_chunks/{HubspotPropertyInput-CFMkbQeT.js → HubspotPropertyInput-BlDarkcj.js} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-WU5FMJj3.mjs → HubspotPropertyInput-TGG9ozTa.mjs} +3 -3
  10. package/dist/_chunks/{Settings-B_hhdpHa.js → Settings-DOx7YSqA.js} +2 -2
  11. package/dist/_chunks/{Settings-CiXEps_C.mjs → Settings-tct49GYU.mjs} +2 -2
  12. package/dist/_chunks/{index-BeBn7bJa.mjs → index-Au3W85GX.mjs} +5 -5
  13. package/dist/_chunks/{index-CG_WQzSa.js → index-DhGVA3ZL.js} +5 -5
  14. package/dist/_chunks/{objectLabels-CS_0QCNz.mjs → objectLabels-3rnJ0WUc.mjs} +1 -1
  15. package/dist/_chunks/{objectLabels-BGq-Ypgn.js → objectLabels-Bx7x8fFO.js} +1 -1
  16. package/dist/_chunks/{useHubspotSchema-CP7LRdzO.mjs → useHubspotSchema-BlVXfwju.mjs} +1 -1
  17. package/dist/_chunks/{useHubspotSchema-BZP2qkAu.js → useHubspotSchema-DrGeidXC.js} +1 -1
  18. package/dist/admin/index.js +1 -1
  19. package/dist/admin/index.mjs +1 -1
  20. package/dist/admin/src/builder/types.d.ts +2 -1
  21. package/dist/server/index.js +243 -41
  22. package/dist/server/index.mjs +243 -41
  23. package/dist/server/src/conditions.d.ts +1 -0
  24. package/dist/server/src/formSync.d.ts +20 -0
  25. package/dist/server/src/formsAdmin.d.ts +1 -0
  26. package/dist/server/src/hsforms.d.ts +14 -1
  27. package/dist/server/src/importHubspot.d.ts +2 -1
  28. package/dist/server/src/index.d.ts +1 -0
  29. package/package.json +1 -1
@@ -1600,6 +1600,48 @@ function missingMappedFields(definition, existingNames) {
1600
1600
  const existing = new Set(existingNames);
1601
1601
  return mappedContactFields(definition).filter((field) => !existing.has(field.property));
1602
1602
  }
1603
+ const OBJECT_TYPE_BY_NAME = {
1604
+ contact: "0-1",
1605
+ company: "0-2"
1606
+ };
1607
+ function collectedHubspotKeys(definition) {
1608
+ const keys = /* @__PURE__ */ new Set();
1609
+ const add = (object, property) => {
1610
+ const typeId = OBJECT_TYPE_BY_NAME[object] || object;
1611
+ if (property) keys.add(`${typeId}:${property}`);
1612
+ };
1613
+ for (const step of definition.steps ?? []) {
1614
+ for (const fld of step.fields ?? []) {
1615
+ if (fld.type === "company") {
1616
+ const map = fld.companyMap ?? {};
1617
+ for (const slot of Object.values(map)) {
1618
+ if (slot?.property?.trim()) add(slot.object?.trim() || "company", slot.property.trim());
1619
+ }
1620
+ continue;
1621
+ }
1622
+ const mapping = fld.hubspot ?? {};
1623
+ const object = mapping.object?.trim() || "contact";
1624
+ const property = mapping.property?.trim() || fld.name;
1625
+ if (property) add(object, property);
1626
+ }
1627
+ }
1628
+ return keys;
1629
+ }
1630
+ function missingRequiredHubspotFields(definition, required) {
1631
+ const collected = collectedHubspotKeys(definition);
1632
+ const missing = [];
1633
+ for (const field of required) {
1634
+ if (!field.name) continue;
1635
+ const objectTypeId = field.objectTypeId?.trim() || "0-1";
1636
+ if (collected.has(`${objectTypeId}:${field.name}`)) continue;
1637
+ missing.push({
1638
+ name: field.name,
1639
+ objectTypeId,
1640
+ object: objectTypeId === "0-2" ? "company" : "contact"
1641
+ });
1642
+ }
1643
+ return missing;
1644
+ }
1603
1645
  function hsFieldType(type, options) {
1604
1646
  switch (type) {
1605
1647
  case "email":
@@ -1650,6 +1692,74 @@ function appendFieldGroups(existing, extra) {
1650
1692
  }))
1651
1693
  ];
1652
1694
  }
1695
+ function conditionToHubspot(condition, parentId) {
1696
+ const rules = (condition.rules ?? []).filter((rule) => rule.field === parentId);
1697
+ if (!rules.length || rules.length !== (condition.rules ?? []).length) return null;
1698
+ const op = rules[0]?.operator;
1699
+ if (rules.some((rule) => rule.operator !== op)) return null;
1700
+ const values = rules.map((rule) => rule.value).filter((value) => Boolean(value));
1701
+ switch (op) {
1702
+ case "eq":
1703
+ return { operator: values.length > 1 ? "SET_ANY" : "EQ", values };
1704
+ case "neq":
1705
+ return { operator: values.length > 1 ? "NOT_SET_ANY" : "NEQ", values };
1706
+ case "contains":
1707
+ return { operator: "CONTAINS", values };
1708
+ case "gt":
1709
+ return { operator: "GT", values };
1710
+ case "lt":
1711
+ return { operator: "LT", values };
1712
+ case "notEmpty":
1713
+ return { operator: "SET", values: [] };
1714
+ case "empty":
1715
+ return { operator: "NOT_SET", values: [] };
1716
+ default:
1717
+ return null;
1718
+ }
1719
+ }
1720
+ function applyVisibleIfToFieldGroups(groups, definition) {
1721
+ const byId = /* @__PURE__ */ new Map();
1722
+ for (const step of definition.steps ?? []) {
1723
+ for (const fld of step.fields ?? []) {
1724
+ const mapping = fld.hubspot ?? {};
1725
+ byId.set(fld.id, { name: fld.name, property: mapping.property?.trim() || fld.name });
1726
+ }
1727
+ }
1728
+ const moves = /* @__PURE__ */ new Map();
1729
+ for (const step of definition.steps ?? []) {
1730
+ for (const fld of step.fields ?? []) {
1731
+ const visibleIf = fld.visibleIf;
1732
+ const parentId = visibleIf?.rules?.[0]?.field;
1733
+ if (!visibleIf || !parentId) continue;
1734
+ const converted = conditionToHubspot(visibleIf, parentId);
1735
+ const parent = byId.get(parentId);
1736
+ const mapping = fld.hubspot ?? {};
1737
+ const property = mapping.property?.trim() || fld.name;
1738
+ if (!converted || !parent) continue;
1739
+ moves.set(property, { parent: parent.property, ...converted });
1740
+ }
1741
+ }
1742
+ if (!moves.size) return groups;
1743
+ const cloned = JSON.parse(JSON.stringify(groups));
1744
+ const flat = [];
1745
+ for (const group of cloned) {
1746
+ (group.fields ?? []).forEach((field, index2) => flat.push({ group, index: index2, field }));
1747
+ }
1748
+ for (const [property, move] of moves) {
1749
+ const child = flat.find((item) => item.field.name === property);
1750
+ const parent = flat.find((item) => item.field.name === move.parent);
1751
+ if (!child || !parent || child === parent) continue;
1752
+ parent.field.dependentFields = [
1753
+ ...parent.field.dependentFields ?? [],
1754
+ {
1755
+ dependentCondition: { operator: move.operator, values: move.values },
1756
+ field: child.field
1757
+ }
1758
+ ];
1759
+ child.group.fields?.splice(child.index, 1);
1760
+ }
1761
+ return cloned;
1762
+ }
1653
1763
  const HS_BASE$2 = "https://api.hubapi.com";
1654
1764
  const OBJECT_BY_TYPE_ID = {
1655
1765
  "0-1": "contact",
@@ -1731,7 +1841,7 @@ function convertField(raw, ctx) {
1731
1841
  helpText: raw.description || void 0,
1732
1842
  ...options.length ? { options } : {},
1733
1843
  hubspot: { object, property: name },
1734
- ...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
1844
+ ...ctx.parentCondition ? { visibleIf: ctx.parentCondition, visibleIfSource: "hubspot" } : {}
1735
1845
  };
1736
1846
  const out = [field];
1737
1847
  for (const dependent of raw.dependentFields ?? []) {
@@ -1754,6 +1864,12 @@ function convertHubspotForm(raw) {
1754
1864
  const usedNames = /* @__PURE__ */ new Set();
1755
1865
  const fields = [];
1756
1866
  for (const group of raw.fieldGroups ?? []) {
1867
+ if (group.groupType === "progressive" || group.groupType === "queued") {
1868
+ skipped.push({
1869
+ code: "progressive",
1870
+ detail: group.groupType
1871
+ });
1872
+ }
1757
1873
  if (group.richText && !(group.fields ?? []).length) {
1758
1874
  skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
1759
1875
  continue;
@@ -1825,7 +1941,10 @@ function mergeHubspotImport(current, incoming) {
1825
1941
  existing.helpText = next.helpText;
1826
1942
  if (next.options) existing.options = next.options;
1827
1943
  else delete existing.options;
1828
- if (!existing.visibleIf && next.visibleIf) existing.visibleIf = next.visibleIf;
1944
+ if (next.visibleIf && (existing.visibleIfSource === "hubspot" || !existing.visibleIf)) {
1945
+ existing.visibleIf = next.visibleIf;
1946
+ if (existing.visibleIfSource !== "editor") existing.visibleIfSource = "hubspot";
1947
+ }
1829
1948
  updated.push(key);
1830
1949
  continue;
1831
1950
  }
@@ -2048,24 +2167,20 @@ async function loadFormShape(apiKey, formGuid) {
2048
2167
  return null;
2049
2168
  }
2050
2169
  }
2051
- async function findContactIdByEmail(apiKey, email) {
2052
- const res = await fetch("https://api.hubapi.com/crm/v3/objects/contacts/search", {
2053
- method: "POST",
2054
- headers: {
2055
- Authorization: `Bearer ${apiKey}`,
2056
- "Content-Type": "application/json"
2057
- },
2058
- body: JSON.stringify({
2059
- filterGroups: [
2060
- { filters: [{ propertyName: "email", operator: "EQ", value: email }] }
2061
- ],
2062
- properties: ["email"],
2063
- limit: 1
2064
- })
2065
- });
2066
- const body = await res.json().catch(() => ({}));
2067
- if (!res.ok) return void 0;
2068
- return body.results?.[0]?.id;
2170
+ async function findContactIdByEmail(apiKey, email, retryDelaysMs = [400, 1200]) {
2171
+ for (const delay of [0, ...retryDelaysMs]) {
2172
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
2173
+ const res = await fetch(
2174
+ `https://api.hubapi.com/crm/v3/objects/contacts/${encodeURIComponent(email)}?idProperty=email`,
2175
+ { headers: { Authorization: `Bearer ${apiKey}` } }
2176
+ );
2177
+ if (res.ok) {
2178
+ const body = await res.json().catch(() => ({}));
2179
+ return body.id || void 0;
2180
+ }
2181
+ if (res.status !== 404) return void 0;
2182
+ }
2183
+ return void 0;
2069
2184
  }
2070
2185
  function contextPayload(ctx) {
2071
2186
  const out = {};
@@ -2146,13 +2261,62 @@ async function submitMarketingForm(input) {
2146
2261
  async function syncFieldsToHubspotForm(apiKey, formGuid, definition) {
2147
2262
  const raw = await fetchHubspotForm(apiKey, formGuid);
2148
2263
  const extra = missingMappedFields(definition, formFieldNames(raw));
2149
- if (!extra.length) return [];
2150
- await patchHubspotForm(apiKey, formGuid, {
2151
- fieldGroups: appendFieldGroups(raw.fieldGroups ?? [], extra)
2152
- });
2264
+ const fieldGroups = applyVisibleIfToFieldGroups(
2265
+ appendFieldGroups(raw.fieldGroups ?? [], extra),
2266
+ definition
2267
+ );
2268
+ if (!extra.length && JSON.stringify(fieldGroups) === JSON.stringify(appendFieldGroups(raw.fieldGroups ?? [], []))) {
2269
+ return [];
2270
+ }
2271
+ await patchHubspotForm(apiKey, formGuid, { fieldGroups });
2153
2272
  clearFormShapeCache(formGuid);
2154
2273
  return extra.map((field) => field.property);
2155
2274
  }
2275
+ async function createMarketingForm(apiKey, name, definition) {
2276
+ const fields = mappedContactFields(definition).map((field) => toHsFormField(field));
2277
+ if (!fields.length) {
2278
+ throw new Error("Add at least one mapped contact field before creating a HubSpot form");
2279
+ }
2280
+ const res = await fetch("https://api.hubapi.com/marketing/v3/forms", {
2281
+ method: "POST",
2282
+ headers: {
2283
+ Authorization: `Bearer ${apiKey}`,
2284
+ "Content-Type": "application/json"
2285
+ },
2286
+ body: JSON.stringify({
2287
+ name,
2288
+ formType: "hubspot",
2289
+ fieldGroups: applyVisibleIfToFieldGroups(
2290
+ fields.map((field) => ({
2291
+ groupType: "default_group",
2292
+ richTextType: "text",
2293
+ fields: [field]
2294
+ })),
2295
+ definition
2296
+ ),
2297
+ configuration: {
2298
+ language: "fr",
2299
+ recaptchaEnabled: false,
2300
+ createNewContactForNewEmail: true,
2301
+ prePopulateKnownValues: true,
2302
+ notifyContactOwner: false,
2303
+ notifyRecipients: [],
2304
+ editable: true,
2305
+ archivable: true,
2306
+ cloneable: true,
2307
+ allowLinkToResetKnownValues: false,
2308
+ postSubmitAction: { type: "thank_you", value: "Merci." }
2309
+ },
2310
+ displayOptions: { submitButtonText: "Envoyer", style: { submitButtonText: "Envoyer" } },
2311
+ legalConsentOptions: { type: "none" }
2312
+ })
2313
+ });
2314
+ const body = await res.json().catch(() => ({}));
2315
+ if (!res.ok || !body.id) {
2316
+ throw new Error(body.message || `HubSpot ${res.status}`);
2317
+ }
2318
+ return body.id;
2319
+ }
2156
2320
  function publicForm(entry) {
2157
2321
  return {
2158
2322
  slug: entry.slug,
@@ -2166,7 +2330,9 @@ function publicForm(entry) {
2166
2330
  steps: (entry.definition?.steps ?? []).map((step) => ({
2167
2331
  ...step,
2168
2332
  // The CRM mappings are the server's business — never the browser's.
2169
- fields: (step.fields ?? []).map(({ hubspot: _hubspot, companyMap: _companyMap, ...fld }) => fld)
2333
+ fields: (step.fields ?? []).map(
2334
+ ({ hubspot: _hubspot, companyMap: _companyMap, visibleIfSource: _source, ...fld }) => fld
2335
+ )
2170
2336
  }))
2171
2337
  };
2172
2338
  }
@@ -2910,6 +3076,22 @@ function createFormsAdminController(strapi) {
2910
3076
  ctx.body = { errors: structural, problems: mappings };
2911
3077
  return;
2912
3078
  }
3079
+ const { apiKey } = await resolveApiKey(strapi);
3080
+ const account = await resolveAccount(strapi);
3081
+ const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
3082
+ const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
3083
+ if (apiKey && formGuid) {
3084
+ const shape = await loadFormShape(apiKey, formGuid);
3085
+ const missingHubspot = shape ? missingRequiredHubspotFields(
3086
+ entry.definition ?? EMPTY_DEFINITION,
3087
+ [...shape.fields.filter((field) => field.required)]
3088
+ ) : [];
3089
+ if (missingHubspot.length) {
3090
+ ctx.status = 400;
3091
+ ctx.body = { errors: structural, problems: mappings, missingHubspot };
3092
+ return;
3093
+ }
3094
+ }
2913
3095
  await documents().publish({
2914
3096
  documentId: ctx.params.documentId,
2915
3097
  locale: ctx.query.locale
@@ -2917,22 +3099,16 @@ function createFormsAdminController(strapi) {
2917
3099
  const policy = await resolvePolicy(strapi);
2918
3100
  let addedFields = [];
2919
3101
  let syncWarning;
2920
- if (policy.syncFieldsOnPublish) {
2921
- const { apiKey } = await resolveApiKey(strapi);
2922
- const account = await resolveAccount(strapi);
2923
- const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
2924
- const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
2925
- if (apiKey && formGuid) {
2926
- try {
2927
- addedFields = await syncFieldsToHubspotForm(
2928
- apiKey,
2929
- formGuid,
2930
- entry.definition ?? EMPTY_DEFINITION
2931
- );
2932
- } catch (err) {
2933
- syncWarning = err.message;
2934
- strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
2935
- }
3102
+ if (policy.syncFieldsOnPublish && apiKey && formGuid) {
3103
+ try {
3104
+ addedFields = await syncFieldsToHubspotForm(
3105
+ apiKey,
3106
+ formGuid,
3107
+ entry.definition ?? EMPTY_DEFINITION
3108
+ );
3109
+ } catch (err) {
3110
+ syncWarning = err.message;
3111
+ strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
2936
3112
  }
2937
3113
  }
2938
3114
  ctx.body = {
@@ -2941,6 +3117,31 @@ function createFormsAdminController(strapi) {
2941
3117
  ...syncWarning ? { syncWarning } : {}
2942
3118
  };
2943
3119
  },
3120
+ async createInHubspot(ctx) {
3121
+ const entry = await documents().findOne({
3122
+ documentId: ctx.params.documentId,
3123
+ locale: ctx.query.locale,
3124
+ status: "draft"
3125
+ });
3126
+ if (!entry) ctx.throw(404, "Form not found");
3127
+ const { apiKey } = await resolveApiKey(strapi);
3128
+ if (!apiKey) ctx.throw(400, "HubSpot API key is missing");
3129
+ try {
3130
+ const id = await createMarketingForm(
3131
+ apiKey,
3132
+ entry.name || entry.slug || "Formulaire",
3133
+ entry.definition ?? EMPTY_DEFINITION
3134
+ );
3135
+ const updated = await documents().update({
3136
+ documentId: ctx.params.documentId,
3137
+ locale: ctx.query.locale,
3138
+ data: { hubspotFormId: id }
3139
+ });
3140
+ ctx.body = { form: updated, hubspotFormId: id };
3141
+ } catch (err) {
3142
+ ctx.throw(400, err.message);
3143
+ }
3144
+ },
2944
3145
  async unpublish(ctx) {
2945
3146
  await documents().unpublish({
2946
3147
  documentId: ctx.params.documentId,
@@ -3707,6 +3908,7 @@ const routes = {
3707
3908
  adminRoute("GET", "/builder/forms/:documentId", "formsAdmin.findOne", [FORMS_ACTION]),
3708
3909
  adminRoute("PUT", "/builder/forms/:documentId", "formsAdmin.update", [FORMS_ACTION]),
3709
3910
  adminRoute("POST", "/builder/forms/:documentId/publish", "formsAdmin.publish", [FORMS_ACTION]),
3911
+ adminRoute("POST", "/builder/forms/:documentId/create-hubspot", "formsAdmin.createInHubspot", [FORMS_ACTION]),
3710
3912
  adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
3711
3913
  adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
3712
3914
  adminRoute("POST", "/builder/forms/:documentId/duplicate", "formsAdmin.duplicate", [FORMS_ACTION]),