strapi-plugin-hubspot 0.2.0 → 0.3.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/README.md CHANGED
@@ -44,8 +44,10 @@ Société · Nombre d'employés (numberofemployees)
44
44
  ```
45
45
 
46
46
  **The list narrows to the object you picked.** Point the field at the sibling
47
- that holds the object and choosing *Contact* leaves only contact properties
48
- the prefix disappears, since it is no longer telling you anything:
47
+ that holds the object from the Content-Type Builder, under *HubSpot → Object
48
+ field* and choosing *Contact* leaves only contact properties. The prefix
49
+ disappears, since it is no longer telling you anything. In `schema.json` that
50
+ setting reads:
49
51
 
50
52
  ```json
51
53
  {
@@ -69,7 +71,25 @@ entry before it is written, at any depth — steps, repeatable components, dynam
69
71
  zones. An invalid mapping is refused with a message that says which property and
70
72
  why:
71
73
 
72
- > Mapping HubSpot invalide« name » existe sur l'objet Société, pas sur Contact
74
+ > Invalid HubSpot mapping"name" exists on company, not on contact
75
+
76
+ It catches four things:
77
+
78
+ | Code | Case |
79
+ |---|---|
80
+ | `unknown` | The property doesn't exist in the portal |
81
+ | `wrong-object` | It exists, but on another object |
82
+ | `whitespace` | It exists, but the stored value has surrounding spaces |
83
+ | `bad-option` | The field offers a choice the enumeration doesn't accept |
84
+
85
+ That last one closes a real hole: HubSpot refuses a value outside an
86
+ enumeration exactly as hard as it refuses an unknown property, so a select whose
87
+ choices drifted from the CRM fails at send time with nothing to show for it.
88
+ Point `optionsField` at the repeatable holding the choices (default `options`,
89
+ each `{ value?, label? }`) and they are checked too.
90
+
91
+ Each problem is also carried as a structured code in the error's `details`, so a
92
+ host app can localize the message instead of parsing the sentence.
73
93
 
74
94
  ### A settings screen
75
95
 
@@ -111,6 +131,7 @@ export default ({ env }) => ({
111
131
  uid: "api::form.form",
112
132
  objectField: "hsObject", // holds "contact" | "company"
113
133
  propertyField: "hsProperty", // holds the property name
134
+ optionsField: "options", // optional — the field's choices
114
135
  },
115
136
  ],
116
137
  },
@@ -144,8 +165,16 @@ Create a **private app** in HubSpot with a read scope per object you list:
144
165
 
145
166
  An object the token can't read is **skipped, not fatal**: the picker keeps
146
167
  working for the others and explains which one is missing a scope. `oauth` is
147
- worth adding too — it exposes the portal id, which turns on the *view in HubSpot*
148
- links.
168
+ worth adding too — it exposes the portal id *and the portal's UI host*, which
169
+ turn on the *view in HubSpot* links.
170
+
171
+ ### Regions
172
+
173
+ The REST API is global: `api.hubapi.com` routes by token, whatever the portal's
174
+ hosting region. The **web app is not** — an EU-hosted portal lives on
175
+ `app-eu1.hubspot.com`. Deep links are built from the `uiDomain` HubSpot reports
176
+ for your portal rather than a hardcoded host, so they point at the right region
177
+ without any configuration.
149
178
 
150
179
  The key is resolved in this order, first match wins:
151
180
 
@@ -2,7 +2,7 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Field, Flex, Loader, Combobox, ComboboxOption, Link } from "@strapi/design-system";
4
4
  import { useFetchClient, useField } from "@strapi/strapi/admin";
5
- import { P as PLUGIN_ID } from "./index-DIwNOVQ0.mjs";
5
+ import { P as PLUGIN_ID } from "./index-lx7um1Yx.mjs";
6
6
  const LABELS = {
7
7
  contact: "Contact",
8
8
  company: "Société",
@@ -68,7 +68,7 @@ const HubspotPropertyInput = React.forwardRef(
68
68
  }
69
69
  return hint;
70
70
  };
71
- const crmLink = schema?.portalId && value && schema.properties.some((p) => p.name === value) ? `https://app.hubspot.com/property-settings/${schema.portalId}/properties?search=${encodeURIComponent(value)}` : null;
71
+ const crmLink = schema?.portalId && value && schema.properties.some((p) => p.name === value) ? `https://${schema.uiDomain || "app.hubspot.com"}/property-settings/${schema.portalId}/properties?search=${encodeURIComponent(value)}` : null;
72
72
  return /* @__PURE__ */ jsxs(Field.Root, { name, error, hint: describe(), required, children: [
73
73
  /* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
74
74
  !schema ? /* @__PURE__ */ jsx(Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsx(Loader, { small: true, children: "Chargement des propriétés HubSpot…" }) }) : schema.configured ? /* @__PURE__ */ jsx(
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const React = require("react");
5
5
  const designSystem = require("@strapi/design-system");
6
6
  const admin = require("@strapi/strapi/admin");
7
- const index = require("./index-sOi9wC_y.js");
7
+ const index = require("./index-Bq1tS5h6.js");
8
8
  function _interopNamespace(e) {
9
9
  if (e && e.__esModule) return e;
10
10
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -88,7 +88,7 @@ const HubspotPropertyInput = React__namespace.forwardRef(
88
88
  }
89
89
  return hint;
90
90
  };
91
- const crmLink = schema?.portalId && value && schema.properties.some((p) => p.name === value) ? `https://app.hubspot.com/property-settings/${schema.portalId}/properties?search=${encodeURIComponent(value)}` : null;
91
+ const crmLink = schema?.portalId && value && schema.properties.some((p) => p.name === value) ? `https://${schema.uiDomain || "app.hubspot.com"}/property-settings/${schema.portalId}/properties?search=${encodeURIComponent(value)}` : null;
92
92
  return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name, error, hint: describe(), required, children: [
93
93
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }),
94
94
  !schema ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: "Chargement des propriétés HubSpot…" }) }) : schema.configured ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -2,7 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Box, Loader, Flex, Typography, Badge, Field, Button } from "@strapi/design-system";
4
4
  import { useFetchClient } from "@strapi/strapi/admin";
5
- import { P as PLUGIN_ID } from "./index-DIwNOVQ0.mjs";
5
+ import { P as PLUGIN_ID } from "./index-lx7um1Yx.mjs";
6
6
  const SOURCE_LABEL = {
7
7
  settings: "saisie ici",
8
8
  config: "config/plugins.ts",
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const React = require("react");
5
5
  const designSystem = require("@strapi/design-system");
6
6
  const admin = require("@strapi/strapi/admin");
7
- const index = require("./index-sOi9wC_y.js");
7
+ const index = require("./index-Bq1tS5h6.js");
8
8
  function _interopNamespace(e) {
9
9
  if (e && e.__esModule) return e;
10
10
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ const PLUGIN_ID = "hubspot";
3
+ const index = {
4
+ register(app) {
5
+ app.customFields.register({
6
+ name: "property",
7
+ pluginId: PLUGIN_ID,
8
+ type: "string",
9
+ intlLabel: {
10
+ id: `${PLUGIN_ID}.property.label`,
11
+ defaultMessage: "Propriété HubSpot"
12
+ },
13
+ intlDescription: {
14
+ id: `${PLUGIN_ID}.property.description`,
15
+ defaultMessage: "Choisie dans les propriétés réelles du portail"
16
+ },
17
+ components: {
18
+ Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-C9PpYYgj.js"))
19
+ },
20
+ // Exposed in the Content-Type Builder. Without this, `objectField` could
21
+ // only be set by hand-editing schema.json — which is not how anyone
22
+ // installing from npm adds a field, so the filtering would be unreachable.
23
+ options: {
24
+ base: [
25
+ {
26
+ sectionTitle: {
27
+ id: `${PLUGIN_ID}.options.section`,
28
+ defaultMessage: "HubSpot"
29
+ },
30
+ items: [
31
+ {
32
+ name: "options.objectField",
33
+ type: "text",
34
+ intlLabel: {
35
+ id: `${PLUGIN_ID}.options.objectField.label`,
36
+ defaultMessage: "Object field"
37
+ },
38
+ description: {
39
+ id: `${PLUGIN_ID}.options.objectField.description`,
40
+ defaultMessage: "Name of the sibling field holding the HubSpot object (e.g. hsObject). Leave empty to list every object's properties."
41
+ },
42
+ placeholder: {
43
+ id: `${PLUGIN_ID}.options.objectField.placeholder`,
44
+ defaultMessage: "hsObject"
45
+ }
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }
51
+ });
52
+ app.createSettingSection(
53
+ {
54
+ id: PLUGIN_ID,
55
+ intlLabel: { id: `${PLUGIN_ID}.settings.section`, defaultMessage: "HubSpot" }
56
+ },
57
+ [
58
+ {
59
+ intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
60
+ id: `${PLUGIN_ID}-settings`,
61
+ to: `/settings/${PLUGIN_ID}`,
62
+ permissions: [],
63
+ Component: async () => (await Promise.resolve().then(() => require("./Settings-Lj49S5fO.js"))).default
64
+ }
65
+ ]
66
+ );
67
+ app.registerPlugin({ id: PLUGIN_ID, name: PLUGIN_ID });
68
+ }
69
+ };
70
+ exports.PLUGIN_ID = PLUGIN_ID;
71
+ exports.index = index;
@@ -0,0 +1,72 @@
1
+ const PLUGIN_ID = "hubspot";
2
+ const index = {
3
+ register(app) {
4
+ app.customFields.register({
5
+ name: "property",
6
+ pluginId: PLUGIN_ID,
7
+ type: "string",
8
+ intlLabel: {
9
+ id: `${PLUGIN_ID}.property.label`,
10
+ defaultMessage: "Propriété HubSpot"
11
+ },
12
+ intlDescription: {
13
+ id: `${PLUGIN_ID}.property.description`,
14
+ defaultMessage: "Choisie dans les propriétés réelles du portail"
15
+ },
16
+ components: {
17
+ Input: async () => import("./HubspotPropertyInput-C3KbDHYP.mjs")
18
+ },
19
+ // Exposed in the Content-Type Builder. Without this, `objectField` could
20
+ // only be set by hand-editing schema.json — which is not how anyone
21
+ // installing from npm adds a field, so the filtering would be unreachable.
22
+ options: {
23
+ base: [
24
+ {
25
+ sectionTitle: {
26
+ id: `${PLUGIN_ID}.options.section`,
27
+ defaultMessage: "HubSpot"
28
+ },
29
+ items: [
30
+ {
31
+ name: "options.objectField",
32
+ type: "text",
33
+ intlLabel: {
34
+ id: `${PLUGIN_ID}.options.objectField.label`,
35
+ defaultMessage: "Object field"
36
+ },
37
+ description: {
38
+ id: `${PLUGIN_ID}.options.objectField.description`,
39
+ defaultMessage: "Name of the sibling field holding the HubSpot object (e.g. hsObject). Leave empty to list every object's properties."
40
+ },
41
+ placeholder: {
42
+ id: `${PLUGIN_ID}.options.objectField.placeholder`,
43
+ defaultMessage: "hsObject"
44
+ }
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
50
+ });
51
+ app.createSettingSection(
52
+ {
53
+ id: PLUGIN_ID,
54
+ intlLabel: { id: `${PLUGIN_ID}.settings.section`, defaultMessage: "HubSpot" }
55
+ },
56
+ [
57
+ {
58
+ intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
59
+ id: `${PLUGIN_ID}-settings`,
60
+ to: `/settings/${PLUGIN_ID}`,
61
+ permissions: [],
62
+ Component: async () => (await import("./Settings-DGJ_mmlw.mjs")).default
63
+ }
64
+ ]
65
+ );
66
+ app.registerPlugin({ id: PLUGIN_ID, name: PLUGIN_ID });
67
+ }
68
+ };
69
+ export {
70
+ PLUGIN_ID as P,
71
+ index as i
72
+ };
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-sOi9wC_y.js");
2
+ const index = require("../_chunks/index-Bq1tS5h6.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-DIwNOVQ0.mjs";
1
+ import { i } from "../_chunks/index-lx7um1Yx.mjs";
2
2
  export {
3
3
  i as default
4
4
  };
@@ -39,12 +39,14 @@ async function fetchObject(apiKey, object) {
39
39
  group: p.groupName
40
40
  }));
41
41
  }
42
- async function fetchPortalId(apiKey) {
42
+ async function fetchAccount(apiKey) {
43
43
  try {
44
- const { portalId } = await hsGet(apiKey, "/account-info/v3/details");
45
- return portalId;
44
+ return await hsGet(
45
+ apiKey,
46
+ "/account-info/v3/details"
47
+ );
46
48
  } catch {
47
- return void 0;
49
+ return {};
48
50
  }
49
51
  }
50
52
  async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
@@ -67,23 +69,25 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
67
69
  for (const entry of settled) {
68
70
  if ("error" in entry && entry.error) {
69
71
  unavailable.push({ object: entry.object.name, reason: entry.error });
70
- strapi.log.warn(`[hubspot] ${entry.object.name} illisible — ${entry.error}`);
72
+ strapi.log.warn(`[hubspot] ${entry.object.name} unreadable — ${entry.error}`);
71
73
  continue;
72
74
  }
73
75
  available.push(entry.object.name);
74
76
  properties.push(...entry.properties ?? []);
75
77
  }
76
78
  if (!available.length) {
77
- throw new Error(unavailable[0]?.reason ?? "Aucun objet lisible");
79
+ throw new Error(unavailable[0]?.reason ?? "No readable object");
78
80
  }
79
81
  properties.sort(
80
82
  (a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
81
83
  );
84
+ const account = await fetchAccount(apiKey);
82
85
  const schema = {
83
86
  properties,
84
87
  objects: available,
85
88
  unavailable,
86
- portalId: await fetchPortalId(apiKey)
89
+ portalId: account.portalId,
90
+ uiDomain: account.uiDomain
87
91
  };
88
92
  cache = { at: Date.now(), schema };
89
93
  return schema;
@@ -96,16 +100,35 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
96
100
  function clearCache() {
97
101
  cache = null;
98
102
  }
99
- function checkProperty(properties, object, name) {
100
- const trimmed = name.trim();
103
+ function checkMapping(properties, mapping) {
104
+ const { object, property } = mapping;
105
+ const trimmed = property.trim();
101
106
  if (!trimmed) return null;
102
107
  const match = properties.find((p) => p.object === object && p.name === trimmed);
103
- if (match) return trimmed === name ? null : `« ${name} » contient un espace superflu`;
104
- const onOther = properties.find((p) => p.name === trimmed);
105
- if (onOther) {
106
- return `« ${trimmed} » existe sur l'objet ${onOther.object}, pas sur ${object}`;
108
+ if (!match) {
109
+ const elsewhere = properties.find((p) => p.name === trimmed);
110
+ return elsewhere ? { code: "wrong-object", property: trimmed, object, actualObject: elsewhere.object } : { code: "unknown", property: trimmed, object };
111
+ }
112
+ if (trimmed !== property) return { code: "whitespace", property, object };
113
+ if (match.type === "enumeration" && match.options.length && mapping.values?.length) {
114
+ const stray = mapping.values.filter((v) => v && !match.options.includes(v));
115
+ if (stray.length) {
116
+ return { code: "bad-option", property: trimmed, object, values: stray };
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ function describeProblem(problem) {
122
+ switch (problem.code) {
123
+ case "unknown":
124
+ return `"${problem.property}" does not exist on ${problem.object} in this HubSpot portal`;
125
+ case "wrong-object":
126
+ return `"${problem.property}" exists on ${problem.actualObject}, not on ${problem.object}`;
127
+ case "whitespace":
128
+ return `"${problem.property}" has surrounding whitespace`;
129
+ case "bad-option":
130
+ return `"${problem.property}" does not accept ${problem.values.map((v) => `"${v}"`).join(", ")}`;
107
131
  }
108
- return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
109
132
  }
110
133
  function resolveObjects(configured) {
111
134
  if (!Array.isArray(configured) || !configured.length) {
@@ -181,7 +204,7 @@ const controllers = {
181
204
  ctx.body = { configured: true, ...schema };
182
205
  } catch (err) {
183
206
  strapi.log.error(`[hubspot] ${err.message}`);
184
- ctx.throw(502, "Impossible de joindre HubSpot — vérifiez la clé API.");
207
+ ctx.throw(502, "Cannot reach HubSpot — check the API key.");
185
208
  }
186
209
  }
187
210
  }),
@@ -227,9 +250,16 @@ function collectMappings(node, target, found = []) {
227
250
  const property = obj[target.propertyField];
228
251
  if (typeof property === "string" && property.trim()) {
229
252
  const object = obj[target.objectField];
253
+ const rawOptions = obj[target.optionsField || "options"];
254
+ const values = Array.isArray(rawOptions) ? rawOptions.map((o) => {
255
+ const opt = o ?? {};
256
+ const v = typeof opt.value === "string" && opt.value.trim() ? opt.value : opt.label;
257
+ return typeof v === "string" ? v.trim() : "";
258
+ }).filter(Boolean) : void 0;
230
259
  found.push({
231
260
  object: typeof object === "string" && object ? object : "contact",
232
- property
261
+ property,
262
+ values
233
263
  });
234
264
  }
235
265
  for (const value of Object.values(obj)) collectMappings(value, target, found);
@@ -265,13 +295,15 @@ const index = {
265
295
  resolveObjects(strapi.plugin("hubspot").config("objects", []))
266
296
  );
267
297
  } catch {
268
- strapi.log.warn("[hubspot] schéma indisponible — validation ignorée");
298
+ strapi.log.warn("[hubspot] schema unavailable — validation skipped");
269
299
  return next();
270
300
  }
271
- const problems = mappings.map((m) => checkProperty(schema.properties, m.object, m.property)).filter((reason) => Boolean(reason));
301
+ const problems = mappings.map((m) => checkMapping(schema.properties, m)).filter((p) => Boolean(p));
272
302
  if (problems.length) {
303
+ const sentences = [...new Set(problems.map(describeProblem))];
273
304
  throw new utils.errors.ValidationError(
274
- `Mapping HubSpot invalide — ${[...new Set(problems)].join(" ; ")}`
305
+ `Invalid HubSpot mapping — ${sentences.join("; ")}`,
306
+ { problems }
275
307
  );
276
308
  }
277
309
  return next();
@@ -38,12 +38,14 @@ async function fetchObject(apiKey, object) {
38
38
  group: p.groupName
39
39
  }));
40
40
  }
41
- async function fetchPortalId(apiKey) {
41
+ async function fetchAccount(apiKey) {
42
42
  try {
43
- const { portalId } = await hsGet(apiKey, "/account-info/v3/details");
44
- return portalId;
43
+ return await hsGet(
44
+ apiKey,
45
+ "/account-info/v3/details"
46
+ );
45
47
  } catch {
46
- return void 0;
48
+ return {};
47
49
  }
48
50
  }
49
51
  async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
@@ -66,23 +68,25 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
66
68
  for (const entry of settled) {
67
69
  if ("error" in entry && entry.error) {
68
70
  unavailable.push({ object: entry.object.name, reason: entry.error });
69
- strapi.log.warn(`[hubspot] ${entry.object.name} illisible — ${entry.error}`);
71
+ strapi.log.warn(`[hubspot] ${entry.object.name} unreadable — ${entry.error}`);
70
72
  continue;
71
73
  }
72
74
  available.push(entry.object.name);
73
75
  properties.push(...entry.properties ?? []);
74
76
  }
75
77
  if (!available.length) {
76
- throw new Error(unavailable[0]?.reason ?? "Aucun objet lisible");
78
+ throw new Error(unavailable[0]?.reason ?? "No readable object");
77
79
  }
78
80
  properties.sort(
79
81
  (a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
80
82
  );
83
+ const account = await fetchAccount(apiKey);
81
84
  const schema = {
82
85
  properties,
83
86
  objects: available,
84
87
  unavailable,
85
- portalId: await fetchPortalId(apiKey)
88
+ portalId: account.portalId,
89
+ uiDomain: account.uiDomain
86
90
  };
87
91
  cache = { at: Date.now(), schema };
88
92
  return schema;
@@ -95,16 +99,35 @@ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
95
99
  function clearCache() {
96
100
  cache = null;
97
101
  }
98
- function checkProperty(properties, object, name) {
99
- const trimmed = name.trim();
102
+ function checkMapping(properties, mapping) {
103
+ const { object, property } = mapping;
104
+ const trimmed = property.trim();
100
105
  if (!trimmed) return null;
101
106
  const match = properties.find((p) => p.object === object && p.name === trimmed);
102
- if (match) return trimmed === name ? null : `« ${name} » contient un espace superflu`;
103
- const onOther = properties.find((p) => p.name === trimmed);
104
- if (onOther) {
105
- return `« ${trimmed} » existe sur l'objet ${onOther.object}, pas sur ${object}`;
107
+ if (!match) {
108
+ const elsewhere = properties.find((p) => p.name === trimmed);
109
+ return elsewhere ? { code: "wrong-object", property: trimmed, object, actualObject: elsewhere.object } : { code: "unknown", property: trimmed, object };
110
+ }
111
+ if (trimmed !== property) return { code: "whitespace", property, object };
112
+ if (match.type === "enumeration" && match.options.length && mapping.values?.length) {
113
+ const stray = mapping.values.filter((v) => v && !match.options.includes(v));
114
+ if (stray.length) {
115
+ return { code: "bad-option", property: trimmed, object, values: stray };
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+ function describeProblem(problem) {
121
+ switch (problem.code) {
122
+ case "unknown":
123
+ return `"${problem.property}" does not exist on ${problem.object} in this HubSpot portal`;
124
+ case "wrong-object":
125
+ return `"${problem.property}" exists on ${problem.actualObject}, not on ${problem.object}`;
126
+ case "whitespace":
127
+ return `"${problem.property}" has surrounding whitespace`;
128
+ case "bad-option":
129
+ return `"${problem.property}" does not accept ${problem.values.map((v) => `"${v}"`).join(", ")}`;
106
130
  }
107
- return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
108
131
  }
109
132
  function resolveObjects(configured) {
110
133
  if (!Array.isArray(configured) || !configured.length) {
@@ -180,7 +203,7 @@ const controllers = {
180
203
  ctx.body = { configured: true, ...schema };
181
204
  } catch (err) {
182
205
  strapi.log.error(`[hubspot] ${err.message}`);
183
- ctx.throw(502, "Impossible de joindre HubSpot — vérifiez la clé API.");
206
+ ctx.throw(502, "Cannot reach HubSpot — check the API key.");
184
207
  }
185
208
  }
186
209
  }),
@@ -226,9 +249,16 @@ function collectMappings(node, target, found = []) {
226
249
  const property = obj[target.propertyField];
227
250
  if (typeof property === "string" && property.trim()) {
228
251
  const object = obj[target.objectField];
252
+ const rawOptions = obj[target.optionsField || "options"];
253
+ const values = Array.isArray(rawOptions) ? rawOptions.map((o) => {
254
+ const opt = o ?? {};
255
+ const v = typeof opt.value === "string" && opt.value.trim() ? opt.value : opt.label;
256
+ return typeof v === "string" ? v.trim() : "";
257
+ }).filter(Boolean) : void 0;
229
258
  found.push({
230
259
  object: typeof object === "string" && object ? object : "contact",
231
- property
260
+ property,
261
+ values
232
262
  });
233
263
  }
234
264
  for (const value of Object.values(obj)) collectMappings(value, target, found);
@@ -264,13 +294,15 @@ const index = {
264
294
  resolveObjects(strapi.plugin("hubspot").config("objects", []))
265
295
  );
266
296
  } catch {
267
- strapi.log.warn("[hubspot] schéma indisponible — validation ignorée");
297
+ strapi.log.warn("[hubspot] schema unavailable — validation skipped");
268
298
  return next();
269
299
  }
270
- const problems = mappings.map((m) => checkProperty(schema.properties, m.object, m.property)).filter((reason) => Boolean(reason));
300
+ const problems = mappings.map((m) => checkMapping(schema.properties, m)).filter((p) => Boolean(p));
271
301
  if (problems.length) {
302
+ const sentences = [...new Set(problems.map(describeProblem))];
272
303
  throw new errors.ValidationError(
273
- `Mapping HubSpot invalide — ${[...new Set(problems)].join(" ; ")}`
304
+ `Invalid HubSpot mapping — ${sentences.join("; ")}`,
305
+ { problems }
274
306
  );
275
307
  }
276
308
  return next();
@@ -18,6 +18,12 @@ interface ValidateTarget {
18
18
  uid: string;
19
19
  objectField: string;
20
20
  propertyField: string;
21
+ /**
22
+ * Repeatable holding the values a field can submit (default `options`), each
23
+ * `{ value?, label? }`. Checked against enumeration properties: a select whose
24
+ * choices drifted from HubSpot fails exactly like an unknown property.
25
+ */
26
+ optionsField?: string;
21
27
  }
22
28
  declare const _default: {
23
29
  config: {
@@ -29,6 +29,13 @@ export interface Schema {
29
29
  }[];
30
30
  /** Enables deep links into the CRM; derived from the token, never configured. */
31
31
  portalId?: number;
32
+ /**
33
+ * Region-specific UI host for this portal (`app-eu1.hubspot.com`…). The REST
34
+ * API is global — `api.hubapi.com` routes by token — but the web app is not,
35
+ * so a link built on `app.hubspot.com` lands on the wrong host for any portal
36
+ * hosted outside NA.
37
+ */
38
+ uiDomain?: string;
32
39
  }
33
40
  /**
34
41
  * Writable properties of every configured object, cached and de-duplicated
@@ -43,10 +50,41 @@ export declare function loadSchema(strapi: Core.Strapi, apiKey: string, objects:
43
50
  }): Promise<Schema>;
44
51
  /** Drop the cache — used after the API key or the object list changes. */
45
52
  export declare function clearCache(): void;
53
+ /** A mapping the portal would reject, and why. */
54
+ export type Problem = {
55
+ code: "unknown";
56
+ property: string;
57
+ object: string;
58
+ } | {
59
+ code: "wrong-object";
60
+ property: string;
61
+ object: string;
62
+ actualObject: string;
63
+ } | {
64
+ code: "whitespace";
65
+ property: string;
66
+ object: string;
67
+ } | {
68
+ code: "bad-option";
69
+ property: string;
70
+ object: string;
71
+ values: string[];
72
+ };
73
+ export interface Mapping {
74
+ object: string;
75
+ property: string;
76
+ /** Values the field can submit — checked against enumeration properties. */
77
+ values?: string[];
78
+ }
46
79
  /**
47
- * Why a property can't be written, or `null` when it's fine. Returning a reason
48
- * rather than a boolean lets the caller tell an editor what to fix.
80
+ * Why a mapping can't be written, or `null` when it's fine.
81
+ *
82
+ * Returns a structured code rather than a sentence: the message is rendered by
83
+ * the caller, so the wording stays in one place and stays translatable. The
84
+ * codes are also carried in the ValidationError details, for anyone localizing.
49
85
  */
50
- export declare function checkProperty(properties: HsProperty[], object: string, name: string): string | null;
86
+ export declare function checkMapping(properties: HsProperty[], mapping: Mapping): Problem | null;
87
+ /** English rendering of a problem — the package's language. */
88
+ export declare function describeProblem(problem: Problem): string;
51
89
  /** Resolve the configured object list, accepting names or full definitions. */
52
90
  export declare function resolveObjects(configured: unknown): HsObjectDef[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-hubspot",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "HubSpot integration for Strapi — pick CRM properties from a searchable list filtered by object, and catch bad mappings before they reach the API.",
5
5
  "keywords": [
6
6
  "strapi",
@@ -1,42 +0,0 @@
1
- const PLUGIN_ID = "hubspot";
2
- const index = {
3
- register(app) {
4
- app.customFields.register({
5
- name: "property",
6
- pluginId: PLUGIN_ID,
7
- type: "string",
8
- intlLabel: {
9
- id: `${PLUGIN_ID}.property.label`,
10
- defaultMessage: "Propriété HubSpot"
11
- },
12
- intlDescription: {
13
- id: `${PLUGIN_ID}.property.description`,
14
- defaultMessage: "Choisie dans les propriétés réelles du portail"
15
- },
16
- components: {
17
- Input: async () => import("./HubspotPropertyInput-D0gijgli.mjs")
18
- },
19
- options: {}
20
- });
21
- app.createSettingSection(
22
- {
23
- id: PLUGIN_ID,
24
- intlLabel: { id: `${PLUGIN_ID}.settings.section`, defaultMessage: "HubSpot" }
25
- },
26
- [
27
- {
28
- intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
29
- id: `${PLUGIN_ID}-settings`,
30
- to: `/settings/${PLUGIN_ID}`,
31
- permissions: [],
32
- Component: async () => (await import("./Settings-DuYUpD8L.mjs")).default
33
- }
34
- ]
35
- );
36
- app.registerPlugin({ id: PLUGIN_ID, name: PLUGIN_ID });
37
- }
38
- };
39
- export {
40
- PLUGIN_ID as P,
41
- index as i
42
- };
@@ -1,41 +0,0 @@
1
- "use strict";
2
- const PLUGIN_ID = "hubspot";
3
- const index = {
4
- register(app) {
5
- app.customFields.register({
6
- name: "property",
7
- pluginId: PLUGIN_ID,
8
- type: "string",
9
- intlLabel: {
10
- id: `${PLUGIN_ID}.property.label`,
11
- defaultMessage: "Propriété HubSpot"
12
- },
13
- intlDescription: {
14
- id: `${PLUGIN_ID}.property.description`,
15
- defaultMessage: "Choisie dans les propriétés réelles du portail"
16
- },
17
- components: {
18
- Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-BcdqfAfv.js"))
19
- },
20
- options: {}
21
- });
22
- app.createSettingSection(
23
- {
24
- id: PLUGIN_ID,
25
- intlLabel: { id: `${PLUGIN_ID}.settings.section`, defaultMessage: "HubSpot" }
26
- },
27
- [
28
- {
29
- intlLabel: { id: `${PLUGIN_ID}.settings.link`, defaultMessage: "Configuration" },
30
- id: `${PLUGIN_ID}-settings`,
31
- to: `/settings/${PLUGIN_ID}`,
32
- permissions: [],
33
- Component: async () => (await Promise.resolve().then(() => require("./Settings-DedA0z6w.js"))).default
34
- }
35
- ]
36
- );
37
- app.registerPlugin({ id: PLUGIN_ID, name: PLUGIN_ID });
38
- }
39
- };
40
- exports.PLUGIN_ID = PLUGIN_ID;
41
- exports.index = index;