strapi-plugin-hubspot 0.16.5 → 0.16.8

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 +27 -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 +293 -43
  22. package/dist/server/index.mjs +293 -43
  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 +35 -7
  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
@@ -1599,6 +1599,48 @@ function missingMappedFields(definition, existingNames) {
1599
1599
  const existing = new Set(existingNames);
1600
1600
  return mappedContactFields(definition).filter((field) => !existing.has(field.property));
1601
1601
  }
1602
+ const OBJECT_TYPE_BY_NAME = {
1603
+ contact: "0-1",
1604
+ company: "0-2"
1605
+ };
1606
+ function collectedHubspotKeys(definition) {
1607
+ const keys = /* @__PURE__ */ new Set();
1608
+ const add = (object, property) => {
1609
+ const typeId = OBJECT_TYPE_BY_NAME[object] || object;
1610
+ if (property) keys.add(`${typeId}:${property}`);
1611
+ };
1612
+ for (const step of definition.steps ?? []) {
1613
+ for (const fld of step.fields ?? []) {
1614
+ if (fld.type === "company") {
1615
+ const map = fld.companyMap ?? {};
1616
+ for (const slot of Object.values(map)) {
1617
+ if (slot?.property?.trim()) add(slot.object?.trim() || "company", slot.property.trim());
1618
+ }
1619
+ continue;
1620
+ }
1621
+ const mapping = fld.hubspot ?? {};
1622
+ const object = mapping.object?.trim() || "contact";
1623
+ const property = mapping.property?.trim() || fld.name;
1624
+ if (property) add(object, property);
1625
+ }
1626
+ }
1627
+ return keys;
1628
+ }
1629
+ function missingRequiredHubspotFields(definition, required) {
1630
+ const collected = collectedHubspotKeys(definition);
1631
+ const missing = [];
1632
+ for (const field of required) {
1633
+ if (!field.name) continue;
1634
+ const objectTypeId = field.objectTypeId?.trim() || "0-1";
1635
+ if (collected.has(`${objectTypeId}:${field.name}`)) continue;
1636
+ missing.push({
1637
+ name: field.name,
1638
+ objectTypeId,
1639
+ object: objectTypeId === "0-2" ? "company" : "contact"
1640
+ });
1641
+ }
1642
+ return missing;
1643
+ }
1602
1644
  function hsFieldType(type, options) {
1603
1645
  switch (type) {
1604
1646
  case "email":
@@ -1649,6 +1691,74 @@ function appendFieldGroups(existing, extra) {
1649
1691
  }))
1650
1692
  ];
1651
1693
  }
1694
+ function conditionToHubspot(condition, parentId) {
1695
+ const rules = (condition.rules ?? []).filter((rule) => rule.field === parentId);
1696
+ if (!rules.length || rules.length !== (condition.rules ?? []).length) return null;
1697
+ const op = rules[0]?.operator;
1698
+ if (rules.some((rule) => rule.operator !== op)) return null;
1699
+ const values = rules.map((rule) => rule.value).filter((value) => Boolean(value));
1700
+ switch (op) {
1701
+ case "eq":
1702
+ return { operator: values.length > 1 ? "SET_ANY" : "EQ", values };
1703
+ case "neq":
1704
+ return { operator: values.length > 1 ? "NOT_SET_ANY" : "NEQ", values };
1705
+ case "contains":
1706
+ return { operator: "CONTAINS", values };
1707
+ case "gt":
1708
+ return { operator: "GT", values };
1709
+ case "lt":
1710
+ return { operator: "LT", values };
1711
+ case "notEmpty":
1712
+ return { operator: "SET", values: [] };
1713
+ case "empty":
1714
+ return { operator: "NOT_SET", values: [] };
1715
+ default:
1716
+ return null;
1717
+ }
1718
+ }
1719
+ function applyVisibleIfToFieldGroups(groups, definition) {
1720
+ const byId = /* @__PURE__ */ new Map();
1721
+ for (const step of definition.steps ?? []) {
1722
+ for (const fld of step.fields ?? []) {
1723
+ const mapping = fld.hubspot ?? {};
1724
+ byId.set(fld.id, { name: fld.name, property: mapping.property?.trim() || fld.name });
1725
+ }
1726
+ }
1727
+ const moves = /* @__PURE__ */ new Map();
1728
+ for (const step of definition.steps ?? []) {
1729
+ for (const fld of step.fields ?? []) {
1730
+ const visibleIf = fld.visibleIf;
1731
+ const parentId = visibleIf?.rules?.[0]?.field;
1732
+ if (!visibleIf || !parentId) continue;
1733
+ const converted = conditionToHubspot(visibleIf, parentId);
1734
+ const parent = byId.get(parentId);
1735
+ const mapping = fld.hubspot ?? {};
1736
+ const property = mapping.property?.trim() || fld.name;
1737
+ if (!converted || !parent) continue;
1738
+ moves.set(property, { parent: parent.property, ...converted });
1739
+ }
1740
+ }
1741
+ if (!moves.size) return groups;
1742
+ const cloned = JSON.parse(JSON.stringify(groups));
1743
+ const flat = [];
1744
+ for (const group of cloned) {
1745
+ (group.fields ?? []).forEach((field, index2) => flat.push({ group, index: index2, field }));
1746
+ }
1747
+ for (const [property, move] of moves) {
1748
+ const child = flat.find((item) => item.field.name === property);
1749
+ const parent = flat.find((item) => item.field.name === move.parent);
1750
+ if (!child || !parent || child === parent) continue;
1751
+ parent.field.dependentFields = [
1752
+ ...parent.field.dependentFields ?? [],
1753
+ {
1754
+ dependentCondition: { operator: move.operator, values: move.values },
1755
+ field: child.field
1756
+ }
1757
+ ];
1758
+ child.group.fields?.splice(child.index, 1);
1759
+ }
1760
+ return cloned;
1761
+ }
1652
1762
  const HS_BASE$2 = "https://api.hubapi.com";
1653
1763
  const OBJECT_BY_TYPE_ID = {
1654
1764
  "0-1": "contact",
@@ -1730,7 +1840,7 @@ function convertField(raw, ctx) {
1730
1840
  helpText: raw.description || void 0,
1731
1841
  ...options.length ? { options } : {},
1732
1842
  hubspot: { object, property: name },
1733
- ...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
1843
+ ...ctx.parentCondition ? { visibleIf: ctx.parentCondition, visibleIfSource: "hubspot" } : {}
1734
1844
  };
1735
1845
  const out = [field];
1736
1846
  for (const dependent of raw.dependentFields ?? []) {
@@ -1753,6 +1863,12 @@ function convertHubspotForm(raw) {
1753
1863
  const usedNames = /* @__PURE__ */ new Set();
1754
1864
  const fields = [];
1755
1865
  for (const group of raw.fieldGroups ?? []) {
1866
+ if (group.groupType === "progressive" || group.groupType === "queued") {
1867
+ skipped.push({
1868
+ code: "progressive",
1869
+ detail: group.groupType
1870
+ });
1871
+ }
1756
1872
  if (group.richText && !(group.fields ?? []).length) {
1757
1873
  skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
1758
1874
  continue;
@@ -1824,7 +1940,10 @@ function mergeHubspotImport(current, incoming) {
1824
1940
  existing.helpText = next.helpText;
1825
1941
  if (next.options) existing.options = next.options;
1826
1942
  else delete existing.options;
1827
- if (!existing.visibleIf && next.visibleIf) existing.visibleIf = next.visibleIf;
1943
+ if (next.visibleIf && (existing.visibleIfSource === "hubspot" || !existing.visibleIf)) {
1944
+ existing.visibleIf = next.visibleIf;
1945
+ if (existing.visibleIfSource !== "editor") existing.visibleIfSource = "hubspot";
1946
+ }
1828
1947
  updated.push(key);
1829
1948
  continue;
1830
1949
  }
@@ -1922,10 +2041,6 @@ function formsSubmitUrl(portalId, formGuid, region = "eu1") {
1922
2041
  function toHsFields(props) {
1923
2042
  return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
1924
2043
  }
1925
- function filterToFormFields(fields, allowed) {
1926
- if (!allowed) return fields;
1927
- return fields.filter((field) => allowed.has(field.name));
1928
- }
1929
2044
  function formFieldNames(raw) {
1930
2045
  const names = [];
1931
2046
  for (const group of raw.fieldGroups ?? []) {
@@ -1935,20 +2050,42 @@ function formFieldNames(raw) {
1935
2050
  }
1936
2051
  return names;
1937
2052
  }
1938
- function formRequiredNames(raw) {
1939
- const names = [];
2053
+ const OBJECT_BAG = {
2054
+ "0-1": "contact",
2055
+ "0-2": "company"
2056
+ };
2057
+ function formShapeFields(raw) {
2058
+ const fields = [];
1940
2059
  for (const group of raw.fieldGroups ?? []) {
1941
2060
  for (const field of group.fields ?? []) {
1942
- if (field.name && field.required) names.push(field.name);
2061
+ if (!field.name) continue;
2062
+ fields.push({
2063
+ name: field.name,
2064
+ objectTypeId: field.objectTypeId?.trim() || "0-1",
2065
+ required: Boolean(field.required)
2066
+ });
1943
2067
  }
1944
2068
  }
1945
- return names;
2069
+ return fields;
2070
+ }
2071
+ function fieldsForHubspotForm(bags, shape, extra = {}) {
2072
+ const contact = { ...bags.contact ?? {}, ...extra };
2073
+ if (!shape?.fields.length) {
2074
+ return toHsFields(contact).map((field) => ({ ...field, objectTypeId: "0-1" }));
2075
+ }
2076
+ const out = [];
2077
+ for (const field of shape.fields) {
2078
+ const bagName = OBJECT_BAG[field.objectTypeId] || "contact";
2079
+ const value = bags[bagName]?.[field.name] ?? contact[field.name];
2080
+ if (!value) continue;
2081
+ out.push({ name: field.name, value, objectTypeId: field.objectTypeId || "0-1" });
2082
+ }
2083
+ return out;
1946
2084
  }
1947
2085
  function parseFormShape(raw) {
1948
2086
  const legal = raw.legalConsentOptions;
1949
- const hasLegalConsent = Boolean(
1950
- legal && typeof legal === "object" && legal.type && legal.type !== "none"
1951
- );
2087
+ const legalType = typeof legal?.type === "string" ? legal.type : void 0;
2088
+ const hasLegalConsent = Boolean(legalType && legalType !== "none");
1952
2089
  const communications = [];
1953
2090
  for (const box of legal?.communicationsCheckboxes ?? []) {
1954
2091
  const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
@@ -1958,12 +2095,21 @@ function parseFormShape(raw) {
1958
2095
  text: String(box.label || box.text || "").trim() || "I agree to receive communications."
1959
2096
  });
1960
2097
  }
2098
+ const subscriptionTypeIds = (legal?.subscriptionTypeIds ?? []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0);
1961
2099
  const consentToProcessText = legal?.consentToProcessText?.trim();
2100
+ const privacyText = legal?.privacyText?.trim();
2101
+ const lawfulRaw = String(legal?.lawfulBasis || "").toUpperCase();
2102
+ const fields = formShapeFields(raw);
1962
2103
  return {
1963
- names: new Set(formFieldNames(raw)),
1964
- required: new Set(formRequiredNames(raw)),
2104
+ names: new Set(fields.map((field) => field.name)),
2105
+ required: new Set(fields.filter((field) => field.required).map((field) => field.name)),
2106
+ fields,
1965
2107
  hasLegalConsent,
1966
- captcha: raw.configuration?.captchaEnabled === true,
2108
+ ...legalType ? { legalType } : {},
2109
+ lawfulBasis: lawfulRaw.includes("CUSTOMER") ? "CUSTOMER" : "LEAD",
2110
+ subscriptionTypeIds,
2111
+ ...privacyText ? { privacyText } : {},
2112
+ captcha: raw.configuration?.recaptchaEnabled === true || raw.configuration?.captchaEnabled === true,
1967
2113
  ...consentToProcessText ? { consentToProcessText } : {},
1968
2114
  communications
1969
2115
  };
@@ -1975,9 +2121,22 @@ function canRetryEmailOnly(required) {
1975
2121
  function buildLegalConsent(shape, given, fallbackText) {
1976
2122
  if (!given) return void 0;
1977
2123
  if (shape && !shape.hasLegalConsent) return void 0;
2124
+ const fallback = fallbackText?.trim() || DEFAULT_CONSENT_TEXT;
2125
+ if (shape?.legalType === "legitimate_interest") {
2126
+ const subscriptionTypeId = shape.subscriptionTypeIds[0];
2127
+ if (!subscriptionTypeId) return void 0;
2128
+ return {
2129
+ legitimateInterest: {
2130
+ value: true,
2131
+ subscriptionTypeId,
2132
+ legalBasis: shape.lawfulBasis || "LEAD",
2133
+ text: shape.privacyText?.trim() || fallback
2134
+ }
2135
+ };
2136
+ }
1978
2137
  const consent = {
1979
2138
  consentToProcess: true,
1980
- text: shape?.consentToProcessText?.trim() || fallbackText?.trim() || DEFAULT_CONSENT_TEXT
2139
+ text: shape?.consentToProcessText?.trim() || fallback
1981
2140
  };
1982
2141
  if (shape?.communications.length) {
1983
2142
  consent.communications = shape.communications.map((item) => ({
@@ -2105,13 +2264,62 @@ async function submitMarketingForm(input) {
2105
2264
  async function syncFieldsToHubspotForm(apiKey, formGuid, definition) {
2106
2265
  const raw = await fetchHubspotForm(apiKey, formGuid);
2107
2266
  const extra = missingMappedFields(definition, formFieldNames(raw));
2108
- if (!extra.length) return [];
2109
- await patchHubspotForm(apiKey, formGuid, {
2110
- fieldGroups: appendFieldGroups(raw.fieldGroups ?? [], extra)
2111
- });
2267
+ const fieldGroups = applyVisibleIfToFieldGroups(
2268
+ appendFieldGroups(raw.fieldGroups ?? [], extra),
2269
+ definition
2270
+ );
2271
+ if (!extra.length && JSON.stringify(fieldGroups) === JSON.stringify(appendFieldGroups(raw.fieldGroups ?? [], []))) {
2272
+ return [];
2273
+ }
2274
+ await patchHubspotForm(apiKey, formGuid, { fieldGroups });
2112
2275
  clearFormShapeCache(formGuid);
2113
2276
  return extra.map((field) => field.property);
2114
2277
  }
2278
+ async function createMarketingForm(apiKey, name, definition) {
2279
+ const fields = mappedContactFields(definition).map((field) => toHsFormField(field));
2280
+ if (!fields.length) {
2281
+ throw new Error("Add at least one mapped contact field before creating a HubSpot form");
2282
+ }
2283
+ const res = await fetch("https://api.hubapi.com/marketing/v3/forms", {
2284
+ method: "POST",
2285
+ headers: {
2286
+ Authorization: `Bearer ${apiKey}`,
2287
+ "Content-Type": "application/json"
2288
+ },
2289
+ body: JSON.stringify({
2290
+ name,
2291
+ formType: "hubspot",
2292
+ fieldGroups: applyVisibleIfToFieldGroups(
2293
+ fields.map((field) => ({
2294
+ groupType: "default_group",
2295
+ richTextType: "text",
2296
+ fields: [field]
2297
+ })),
2298
+ definition
2299
+ ),
2300
+ configuration: {
2301
+ language: "fr",
2302
+ recaptchaEnabled: false,
2303
+ createNewContactForNewEmail: true,
2304
+ prePopulateKnownValues: true,
2305
+ notifyContactOwner: false,
2306
+ notifyRecipients: [],
2307
+ editable: true,
2308
+ archivable: true,
2309
+ cloneable: true,
2310
+ allowLinkToResetKnownValues: false,
2311
+ postSubmitAction: { type: "thank_you", value: "Merci." }
2312
+ },
2313
+ displayOptions: { submitButtonText: "Envoyer", style: { submitButtonText: "Envoyer" } },
2314
+ legalConsentOptions: { type: "none" }
2315
+ })
2316
+ });
2317
+ const body = await res.json().catch(() => ({}));
2318
+ if (!res.ok || !body.id) {
2319
+ throw new Error(body.message || `HubSpot ${res.status}`);
2320
+ }
2321
+ return body.id;
2322
+ }
2115
2323
  function publicForm(entry) {
2116
2324
  return {
2117
2325
  slug: entry.slug,
@@ -2125,7 +2333,9 @@ function publicForm(entry) {
2125
2333
  steps: (entry.definition?.steps ?? []).map((step) => ({
2126
2334
  ...step,
2127
2335
  // The CRM mappings are the server's business — never the browser's.
2128
- fields: (step.fields ?? []).map(({ hubspot: _hubspot, companyMap: _companyMap, ...fld }) => fld)
2336
+ fields: (step.fields ?? []).map(
2337
+ ({ hubspot: _hubspot, companyMap: _companyMap, visibleIfSource: _source, ...fld }) => fld
2338
+ )
2129
2339
  }))
2130
2340
  };
2131
2341
  }
@@ -2499,12 +2709,16 @@ function createFormsService(strapi, _opts = {}) {
2499
2709
  if (tryFormsApi && portalId && formGuid) {
2500
2710
  try {
2501
2711
  const shape = await loadFormShape(apiKey, formGuid);
2502
- const fields = filterToFormFields(
2503
- toHsFields({ ...contactProps, email }),
2504
- shape?.names ?? null
2712
+ const fields = fieldsForHubspotForm(
2713
+ {
2714
+ contact: contactProps,
2715
+ company: stringProps(partition.accepted.company ?? {})
2716
+ },
2717
+ shape,
2718
+ { email }
2505
2719
  );
2506
2720
  if (fields.length && !fields.some((f) => f.name === "email")) {
2507
- fields.unshift({ name: "email", value: email });
2721
+ fields.unshift({ name: "email", value: email, objectTypeId: "0-1" });
2508
2722
  }
2509
2723
  sentFieldNames = fields.map((f) => f.name);
2510
2724
  const submitted = await submitMarketingForm({
@@ -2512,7 +2726,7 @@ function createFormsService(strapi, _opts = {}) {
2512
2726
  portalId,
2513
2727
  formGuid,
2514
2728
  region,
2515
- fields: fields.length ? fields : [{ name: "email", value: email }],
2729
+ fields: fields.length ? fields : [{ name: "email", value: email, objectTypeId: "0-1" }],
2516
2730
  context: {
2517
2731
  hutk: sanitizeHutk(meta.hutk),
2518
2732
  pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
@@ -2865,6 +3079,22 @@ function createFormsAdminController(strapi) {
2865
3079
  ctx.body = { errors: structural, problems: mappings };
2866
3080
  return;
2867
3081
  }
3082
+ const { apiKey } = await resolveApiKey(strapi);
3083
+ const account = await resolveAccount(strapi);
3084
+ const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
3085
+ const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
3086
+ if (apiKey && formGuid) {
3087
+ const shape = await loadFormShape(apiKey, formGuid);
3088
+ const missingHubspot = shape ? missingRequiredHubspotFields(
3089
+ entry.definition ?? EMPTY_DEFINITION,
3090
+ [...shape.fields.filter((field) => field.required)]
3091
+ ) : [];
3092
+ if (missingHubspot.length) {
3093
+ ctx.status = 400;
3094
+ ctx.body = { errors: structural, problems: mappings, missingHubspot };
3095
+ return;
3096
+ }
3097
+ }
2868
3098
  await documents().publish({
2869
3099
  documentId: ctx.params.documentId,
2870
3100
  locale: ctx.query.locale
@@ -2872,22 +3102,16 @@ function createFormsAdminController(strapi) {
2872
3102
  const policy = await resolvePolicy(strapi);
2873
3103
  let addedFields = [];
2874
3104
  let syncWarning;
2875
- if (policy.syncFieldsOnPublish) {
2876
- const { apiKey } = await resolveApiKey(strapi);
2877
- const account = await resolveAccount(strapi);
2878
- const ownGuid = typeof entry.hubspotFormId === "string" ? entry.hubspotFormId.trim() : "";
2879
- const formGuid = isFormGuid(ownGuid) ? ownGuid : isFormGuid(account.defaultFormId) ? account.defaultFormId.trim() : "";
2880
- if (apiKey && formGuid) {
2881
- try {
2882
- addedFields = await syncFieldsToHubspotForm(
2883
- apiKey,
2884
- formGuid,
2885
- entry.definition ?? EMPTY_DEFINITION
2886
- );
2887
- } catch (err) {
2888
- syncWarning = err.message;
2889
- strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
2890
- }
3105
+ if (policy.syncFieldsOnPublish && apiKey && formGuid) {
3106
+ try {
3107
+ addedFields = await syncFieldsToHubspotForm(
3108
+ apiKey,
3109
+ formGuid,
3110
+ entry.definition ?? EMPTY_DEFINITION
3111
+ );
3112
+ } catch (err) {
3113
+ syncWarning = err.message;
3114
+ strapi.log.warn(`[hubspot] field sync on publish failed — ${syncWarning}`);
2891
3115
  }
2892
3116
  }
2893
3117
  ctx.body = {
@@ -2896,6 +3120,31 @@ function createFormsAdminController(strapi) {
2896
3120
  ...syncWarning ? { syncWarning } : {}
2897
3121
  };
2898
3122
  },
3123
+ async createInHubspot(ctx) {
3124
+ const entry = await documents().findOne({
3125
+ documentId: ctx.params.documentId,
3126
+ locale: ctx.query.locale,
3127
+ status: "draft"
3128
+ });
3129
+ if (!entry) ctx.throw(404, "Form not found");
3130
+ const { apiKey } = await resolveApiKey(strapi);
3131
+ if (!apiKey) ctx.throw(400, "HubSpot API key is missing");
3132
+ try {
3133
+ const id = await createMarketingForm(
3134
+ apiKey,
3135
+ entry.name || entry.slug || "Formulaire",
3136
+ entry.definition ?? EMPTY_DEFINITION
3137
+ );
3138
+ const updated = await documents().update({
3139
+ documentId: ctx.params.documentId,
3140
+ locale: ctx.query.locale,
3141
+ data: { hubspotFormId: id }
3142
+ });
3143
+ ctx.body = { form: updated, hubspotFormId: id };
3144
+ } catch (err) {
3145
+ ctx.throw(400, err.message);
3146
+ }
3147
+ },
2899
3148
  async unpublish(ctx) {
2900
3149
  await documents().unpublish({
2901
3150
  documentId: ctx.params.documentId,
@@ -3662,6 +3911,7 @@ const routes = {
3662
3911
  adminRoute("GET", "/builder/forms/:documentId", "formsAdmin.findOne", [FORMS_ACTION]),
3663
3912
  adminRoute("PUT", "/builder/forms/:documentId", "formsAdmin.update", [FORMS_ACTION]),
3664
3913
  adminRoute("POST", "/builder/forms/:documentId/publish", "formsAdmin.publish", [FORMS_ACTION]),
3914
+ adminRoute("POST", "/builder/forms/:documentId/create-hubspot", "formsAdmin.createInHubspot", [FORMS_ACTION]),
3665
3915
  adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
3666
3916
  adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
3667
3917
  adminRoute("POST", "/builder/forms/:documentId/duplicate", "formsAdmin.duplicate", [FORMS_ACTION]),
@@ -22,6 +22,7 @@ export interface FormFieldDef {
22
22
  type: string;
23
23
  required?: boolean;
24
24
  visibleIf?: Condition | null;
25
+ visibleIfSource?: "hubspot" | "editor" | null;
25
26
  [key: string]: unknown;
26
27
  }
27
28
  export interface FormStepDef {
@@ -26,6 +26,21 @@ export interface MappedFormField {
26
26
  export declare function leftoverContactProps(contact: Record<string, string>, sentFieldNames: Iterable<string>): Record<string, string>;
27
27
  export declare function mappedContactFields(definition: FormDefinition): MappedFormField[];
28
28
  export declare function missingMappedFields(definition: FormDefinition, existingNames: Iterable<string>): MappedFormField[];
29
+ export interface MissingHubspotRequired {
30
+ name: string;
31
+ objectTypeId: string;
32
+ object: string;
33
+ }
34
+ /** Properties the Strapi form can actually send (`objectTypeId:property`). */
35
+ export declare function collectedHubspotKeys(definition: FormDefinition): Set<string>;
36
+ /**
37
+ * Required marketing-form fields the Strapi form never collects. HubSpot
38
+ * rejects the whole conversion when one of these is absent.
39
+ */
40
+ export declare function missingRequiredHubspotFields(definition: FormDefinition, required: {
41
+ name: string;
42
+ objectTypeId?: string;
43
+ }[]): MissingHubspotRequired[];
29
44
  export declare function hsFieldType(type: string, options?: unknown[]): string;
30
45
  export declare function toHsFormField(field: MappedFormField): Record<string, unknown>;
31
46
  export interface RawFieldGroupLike {
@@ -35,3 +50,8 @@ export interface RawFieldGroupLike {
35
50
  fields?: unknown[];
36
51
  }
37
52
  export declare function appendFieldGroups(existing: RawFieldGroupLike[], extra: MappedFormField[]): Record<string, unknown>[];
53
+ /**
54
+ * Nest Strapi `visibleIf` fields under their HubSpot parent as dependentFields.
55
+ * Conditions edited in Strapi are included; unsupported multi-parent rules stay flat.
56
+ */
57
+ export declare function applyVisibleIfToFieldGroups(groups: Record<string, unknown>[], definition: FormDefinition): Record<string, unknown>[];
@@ -26,6 +26,7 @@ export declare function createFormsAdminController(strapi: Core.Strapi): {
26
26
  create(ctx: Ctx): Promise<void>;
27
27
  update(ctx: Ctx): Promise<void>;
28
28
  publish(ctx: Ctx): Promise<void>;
29
+ createInHubspot(ctx: Ctx): Promise<void>;
29
30
  unpublish(ctx: Ctx): Promise<void>;
30
31
  /**
31
32
  * Feeds the `hubspot.form-picker` custom field: every form of the builder
@@ -23,13 +23,9 @@ export declare function toHsFields(props: Record<string, string>): {
23
23
  name: string;
24
24
  value: string;
25
25
  }[];
26
- export declare function filterToFormFields(fields: {
26
+ export declare function filterToFormFields<T extends {
27
27
  name: string;
28
- value: string;
29
- }[], allowed: Set<string> | null): {
30
- name: string;
31
- value: string;
32
- }[];
28
+ }>(fields: T[], allowed: Set<string> | null): T[];
33
29
  export declare function formFieldNames(raw: {
34
30
  fieldGroups?: {
35
31
  fields?: {
@@ -50,26 +46,56 @@ export interface CommunicationConsent {
50
46
  subscriptionTypeId: number;
51
47
  text: string;
52
48
  }
49
+ export interface FormShapeField {
50
+ name: string;
51
+ objectTypeId: string;
52
+ required: boolean;
53
+ }
53
54
  export interface FormShape {
54
55
  names: Set<string>;
55
56
  required: Set<string>;
57
+ fields: FormShapeField[];
56
58
  hasLegalConsent: boolean;
59
+ legalType?: string;
60
+ lawfulBasis?: "LEAD" | "CUSTOMER";
61
+ subscriptionTypeIds: number[];
62
+ privacyText?: string;
57
63
  captcha: boolean;
58
64
  consentToProcessText?: string;
59
65
  communications: CommunicationConsent[];
60
66
  }
67
+ export declare function formShapeFields(raw: {
68
+ fieldGroups?: {
69
+ fields?: {
70
+ name?: string;
71
+ required?: boolean;
72
+ objectTypeId?: string;
73
+ }[];
74
+ }[];
75
+ }): FormShapeField[];
76
+ /** Values HubSpot declared on the marketing form, with the right objectTypeId. */
77
+ export declare function fieldsForHubspotForm(bags: Record<string, Record<string, string>>, shape: FormShape | null | undefined, extra?: Record<string, string>): {
78
+ name: string;
79
+ value: string;
80
+ objectTypeId: string;
81
+ }[];
61
82
  export declare function parseFormShape(raw: {
62
83
  fieldGroups?: {
63
84
  fields?: {
64
85
  name?: string;
65
86
  required?: boolean;
87
+ objectTypeId?: string;
66
88
  }[];
67
89
  }[];
68
90
  configuration?: {
69
91
  captchaEnabled?: boolean;
92
+ recaptchaEnabled?: boolean;
70
93
  };
71
94
  legalConsentOptions?: {
72
95
  type?: string;
96
+ lawfulBasis?: string;
97
+ privacyText?: string;
98
+ subscriptionTypeIds?: (number | string)[];
73
99
  consentToProcessText?: string;
74
100
  communicationsCheckboxes?: {
75
101
  label?: string;
@@ -81,7 +107,7 @@ export declare function parseFormShape(raw: {
81
107
  }): FormShape;
82
108
  /** Email-only retry is only safe when HubSpot doesn't require other fields. */
83
109
  export declare function canRetryEmailOnly(required: Iterable<string> | undefined): boolean;
84
- export declare function buildLegalConsent(shape: Pick<FormShape, "hasLegalConsent" | "communications" | "consentToProcessText"> | null | undefined, given: boolean, fallbackText?: string): Record<string, unknown> | undefined;
110
+ export declare function buildLegalConsent(shape: Pick<FormShape, "hasLegalConsent" | "legalType" | "lawfulBasis" | "subscriptionTypeIds" | "privacyText" | "communications" | "consentToProcessText"> | null | undefined, given: boolean, fallbackText?: string): Record<string, unknown> | undefined;
85
111
  export interface FormSubmitContext {
86
112
  hutk?: string;
87
113
  pageUri?: string;
@@ -127,3 +153,5 @@ export declare function submitMarketingForm(input: FormSubmitInput): Promise<{
127
153
  * that already exist. Returns the property names that were added.
128
154
  */
129
155
  export declare function syncFieldsToHubspotForm(apiKey: string, formGuid: string, definition: FormDefinition): Promise<string[]>;
156
+ /** Create a marketing form from the Strapi definition and return its GUID. */
157
+ export declare function createMarketingForm(apiKey: string, name: string, definition: FormDefinition): Promise<string>;
@@ -21,7 +21,7 @@ export interface HubspotFormSummary {
21
21
  }
22
22
  /** Something the converter could not express in the builder's model. */
23
23
  export interface SkippedItem {
24
- code: "field-type" | "hidden-field" | "object" | "duplicate" | "condition" | "rich-text" | "legal-consent";
24
+ code: "field-type" | "hidden-field" | "object" | "duplicate" | "condition" | "rich-text" | "legal-consent" | "progressive";
25
25
  /** Field label or name, when the item is a field. */
26
26
  label?: string;
27
27
  /** Raw detail (the fieldType, the operator…) for the report. */
@@ -58,6 +58,7 @@ interface RawField {
58
58
  dependentFields?: RawDependent[];
59
59
  }
60
60
  interface RawFieldGroup {
61
+ groupType?: string;
61
62
  richText?: string;
62
63
  fields?: RawField[];
63
64
  }
@@ -274,6 +274,7 @@ declare const _default: {
274
274
  create(ctx: import("./formsAdmin").Ctx): Promise<void>;
275
275
  update(ctx: import("./formsAdmin").Ctx): Promise<void>;
276
276
  publish(ctx: import("./formsAdmin").Ctx): Promise<void>;
277
+ createInHubspot(ctx: import("./formsAdmin").Ctx): Promise<void>;
277
278
  unpublish(ctx: import("./formsAdmin").Ctx): Promise<void>;
278
279
  options(ctx: import("./formsAdmin").Ctx): Promise<void>;
279
280
  listSources(ctx: import("./formsAdmin").Ctx): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-hubspot",
3
- "version": "0.16.5",
3
+ "version": "0.16.8",
4
4
  "description": "The form builder for HubSpot in Strapi — multi-step forms with conditional fields mapped to real CRM properties, a public submit pipeline with retries, and save-time mapping validation.",
5
5
  "keywords": [
6
6
  "strapi",