strapi-plugin-hubspot 0.1.0 → 0.2.1

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
@@ -35,8 +35,7 @@ This plugin makes all three impossible to save.
35
35
  ### A property picker instead of a text field
36
36
 
37
37
  Replace your property field's type with the `hubspot.property` custom field.
38
- Editors then search the real, writable properties of your portal, each labelled
39
- with the object it belongs to:
38
+ Editors then search the real, writable properties of your portal:
40
39
 
41
40
  ```
42
41
  Contact · Rôle (hs_role)
@@ -44,9 +43,24 @@ Contact · Prénom (firstname)
44
43
  Société · Nombre d'employés (numberofemployees)
45
44
  ```
46
45
 
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:
49
+
50
+ ```json
51
+ {
52
+ "hsProperty": {
53
+ "type": "customField",
54
+ "customField": "plugin::hubspot.property",
55
+ "options": { "objectField": "hsObject" }
56
+ }
57
+ }
58
+ ```
59
+
47
60
  Read-only properties — the ones HubSpot computes and always refuses to
48
61
  accept — are filtered out, so the list only ever offers things that will
49
- actually work.
62
+ actually work. When the portal id is readable, each selected property gets a
63
+ **Voir dans HubSpot** link straight to its settings page.
50
64
 
51
65
  ### Validation on save
52
66
 
@@ -64,6 +78,11 @@ never returned to the browser: the UI only receives whether a key exists, where
64
78
  it comes from, and its last four characters. A "Test connection" button
65
79
  round-trips to HubSpot and reports how many properties it can read.
66
80
 
81
+ > **On storage:** the key is kept in Strapi's core store, which is **not
82
+ > encrypted at rest** — it is readable by anyone with database access. It never
83
+ > reaches the browser, but treat it like any other secret in your database. Use
84
+ > `HUBSPOT_API_KEY` if your deployment already manages secrets properly.
85
+
67
86
  ## Install
68
87
 
69
88
  ```bash
@@ -80,6 +99,12 @@ export default ({ env }) => ({
80
99
  // Optional — the key can also be set from Settings → HubSpot.
81
100
  apiKey: env("HUBSPOT_API_KEY", ""),
82
101
 
102
+ // Optional — objects whose properties are offered.
103
+ // Defaults to ["contact", "company"]. Standard names: contact, company,
104
+ // deal, ticket, product, line_item, quote. A custom object is either its
105
+ // type id, or { name, path } when the two differ.
106
+ objects: ["contact", "company", "deal"],
107
+
83
108
  // Optional — entries whose mappings are validated on save.
84
109
  validate: [
85
110
  {
@@ -94,13 +119,15 @@ export default ({ env }) => ({
94
119
  ```
95
120
 
96
121
  Then point your property field at the custom field, in the component or content
97
- type that holds it:
122
+ type that holds it. `options.objectField` names the sibling holding the object —
123
+ omit it and the picker simply lists every object's properties:
98
124
 
99
125
  ```json
100
126
  {
101
127
  "hsProperty": {
102
128
  "type": "customField",
103
- "customField": "plugin::hubspot.property"
129
+ "customField": "plugin::hubspot.property",
130
+ "options": { "objectField": "hsObject" }
104
131
  }
105
132
  }
106
133
  ```
@@ -109,10 +136,24 @@ Restart Strapi and hard-refresh the admin.
109
136
 
110
137
  ### The token
111
138
 
112
- Create a **private app** in HubSpot with these scopes:
139
+ Create a **private app** in HubSpot with a read scope per object you list:
113
140
 
114
141
  - `crm.schemas.contacts.read`
115
142
  - `crm.schemas.companies.read`
143
+ - `crm.schemas.deals.read`, `crm.schemas.custom.read`… as needed
144
+
145
+ An object the token can't read is **skipped, not fatal**: the picker keeps
146
+ working for the others and explains which one is missing a scope. `oauth` is
147
+ worth adding too — it exposes the portal id *and the portal's UI host*, which
148
+ turn on the *view in HubSpot* links.
149
+
150
+ ### Regions
151
+
152
+ The REST API is global: `api.hubapi.com` routes by token, whatever the portal's
153
+ hosting region. The **web app is not** — an EU-hosted portal lives on
154
+ `app-eu1.hubspot.com`. Deep links are built from the `uiDomain` HubSpot reports
155
+ for your portal rather than a hardcoded host, so they point at the right region
156
+ without any configuration.
116
157
 
117
158
  The key is resolved in this order, first match wins:
118
159
 
@@ -140,6 +181,8 @@ This one never blocks work:
140
181
  | No API key configured | The field falls back to a plain text input, with a note explaining why |
141
182
  | HubSpot unreachable | Saving proceeds; validation is skipped and a warning is logged |
142
183
  | Property staged in HubSpot but not created yet | The picker accepts a typed value (`creatable`) |
184
+ | An object's scope is missing | That object is skipped; the others still work |
185
+ | Portal id unreadable | Deep links are omitted; everything else is unaffected |
143
186
  | Plugin uninstalled | Values remain as strings — nothing to undo |
144
187
 
145
188
  The property schema is cached for 10 minutes and de-duplicated across concurrent
@@ -152,7 +195,7 @@ All routes require an authenticated admin.
152
195
 
153
196
  | Method | Path | Purpose |
154
197
  |---|---|---|
155
- | `GET` | `/hubspot/properties` | Writable properties of both objects. `?refresh=1` bypasses the cache |
198
+ | `GET` | `/hubspot/properties` | Writable properties, readable objects, unreachable ones and the portal id. `?refresh=1` bypasses the cache |
156
199
  | `GET` | `/hubspot/settings` | Whether a key exists, its source and hint — never the key |
157
200
  | `PUT` | `/hubspot/settings` | Save a key (`{ apiKey }`) |
158
201
  | `DELETE` | `/hubspot/settings` | Remove the stored key |
@@ -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-DVRLoCa1.js");
7
+ const index = require("./index-DgsAHPui.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" } });
@@ -23,54 +23,75 @@ function _interopNamespace(e) {
23
23
  return Object.freeze(n);
24
24
  }
25
25
  const React__namespace = /* @__PURE__ */ _interopNamespace(React);
26
- const OBJECT_LABEL = {
26
+ const LABELS = {
27
27
  contact: "Contact",
28
- company: "Société"
28
+ company: "Société",
29
+ deal: "Transaction",
30
+ ticket: "Ticket",
31
+ product: "Produit",
32
+ line_item: "Ligne de commande",
33
+ quote: "Devis"
29
34
  };
35
+ function objectLabel(object) {
36
+ return LABELS[object] ?? object;
37
+ }
30
38
  const HubspotPropertyInput = React__namespace.forwardRef(
31
- ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
39
+ ({ name, value, onChange, attribute, disabled, error, required, label, hint, labelAction }, ref) => {
32
40
  const { get } = admin.useFetchClient();
33
- const [properties, setProperties] = React__namespace.useState([]);
34
- const [configured, setConfigured] = React__namespace.useState(null);
41
+ const [schema, setSchema] = React__namespace.useState(null);
35
42
  const [loadError, setLoadError] = React__namespace.useState("");
43
+ const objectFieldName = attribute?.options?.objectField || "hsObject";
44
+ const siblingPath = React__namespace.useMemo(
45
+ () => name.replace(/[^.]+$/, objectFieldName),
46
+ [name, objectFieldName]
47
+ );
48
+ const siblingField = admin.useField(siblingPath);
49
+ const selectedObject = siblingField?.value;
36
50
  React__namespace.useEffect(() => {
37
51
  let cancelled = false;
38
52
  get(`/${index.PLUGIN_ID}/properties`).then(({ data }) => {
39
- if (cancelled) return;
40
- setConfigured(data.configured);
41
- setProperties(data.properties ?? []);
53
+ if (!cancelled) setSchema(data);
42
54
  }).catch((err) => {
43
55
  if (cancelled) return;
44
- setConfigured(false);
45
- setLoadError(
46
- err?.response?.data?.error?.message ?? "Propriétés HubSpot indisponibles"
47
- );
56
+ setSchema({ configured: false, properties: [], objects: [], unavailable: [] });
57
+ setLoadError(err?.response?.data?.error?.message ?? "Propriétés HubSpot indisponibles");
48
58
  });
49
59
  return () => {
50
60
  cancelled = true;
51
61
  };
52
62
  }, [get]);
53
63
  const emit = (next) => onChange({ target: { name, value: next ?? "", type: "string" } });
64
+ const options = React__namespace.useMemo(() => {
65
+ const all = schema?.properties ?? [];
66
+ const scoped = selectedObject ? all.filter((p) => p.object === selectedObject) : all;
67
+ const shown = scoped.map((p) => ({
68
+ value: p.name,
69
+ // The object prefix is redundant once filtered, and noisy.
70
+ label: selectedObject ? `${p.label} (${p.name})` : `${objectLabel(p.object)} · ${p.label} (${p.name})`
71
+ }));
72
+ if (value && !scoped.some((p) => p.name === value)) {
73
+ const elsewhere = all.find((p) => p.name === value);
74
+ shown.unshift({
75
+ value,
76
+ label: elsewhere ? `${value} — appartient à ${objectLabel(elsewhere.object)}` : `${value} — inconnue du portail`
77
+ });
78
+ }
79
+ return shown;
80
+ }, [schema, selectedObject, value]);
54
81
  const describe = () => {
55
82
  if (loadError) return loadError;
56
- if (configured === false) {
83
+ if (schema && !schema.configured) {
57
84
  return "Aucune clé API HubSpot — saisie libre. Renseignez-la dans Réglages → HubSpot.";
58
85
  }
86
+ if (selectedObject && schema?.unavailable.some((u) => u.object === selectedObject)) {
87
+ return `Propriétés de « ${objectLabel(selectedObject)} » illisibles — portée manquante sur le jeton.`;
88
+ }
59
89
  return hint;
60
90
  };
61
- const options = React__namespace.useMemo(() => {
62
- const known = properties.map((p) => ({
63
- value: p.name,
64
- label: `${OBJECT_LABEL[p.object]} · ${p.label} (${p.name})`
65
- }));
66
- if (value && !properties.some((p) => p.name === value)) {
67
- known.unshift({ value, label: `${value} — inconnue du portail` });
68
- }
69
- return known;
70
- }, [properties, value]);
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;
71
92
  return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name, error, hint: describe(), required, children: [
72
93
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }),
73
- configured === null ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: "Chargement des propriétés HubSpot…" }) }) : configured ? /* @__PURE__ */ jsxRuntime.jsx(
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(
74
95
  designSystem.Combobox,
75
96
  {
76
97
  ref,
@@ -93,6 +114,7 @@ const HubspotPropertyInput = React__namespace.forwardRef(
93
114
  placeholder: "ex. hs_role, numberofemployees"
94
115
  }
95
116
  ),
117
+ crmLink ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { paddingTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Link, { href: crmLink, isExternal: true, children: "Voir dans HubSpot" }) }) : null,
96
118
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
97
119
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
98
120
  ] });
@@ -0,0 +1,106 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Field, Flex, Loader, Combobox, ComboboxOption, Link } from "@strapi/design-system";
4
+ import { useFetchClient, useField } from "@strapi/strapi/admin";
5
+ import { P as PLUGIN_ID } from "./index-BpZtcsPM.mjs";
6
+ const LABELS = {
7
+ contact: "Contact",
8
+ company: "Société",
9
+ deal: "Transaction",
10
+ ticket: "Ticket",
11
+ product: "Produit",
12
+ line_item: "Ligne de commande",
13
+ quote: "Devis"
14
+ };
15
+ function objectLabel(object) {
16
+ return LABELS[object] ?? object;
17
+ }
18
+ const HubspotPropertyInput = React.forwardRef(
19
+ ({ name, value, onChange, attribute, disabled, error, required, label, hint, labelAction }, ref) => {
20
+ const { get } = useFetchClient();
21
+ const [schema, setSchema] = React.useState(null);
22
+ const [loadError, setLoadError] = React.useState("");
23
+ const objectFieldName = attribute?.options?.objectField || "hsObject";
24
+ const siblingPath = React.useMemo(
25
+ () => name.replace(/[^.]+$/, objectFieldName),
26
+ [name, objectFieldName]
27
+ );
28
+ const siblingField = useField(siblingPath);
29
+ const selectedObject = siblingField?.value;
30
+ React.useEffect(() => {
31
+ let cancelled = false;
32
+ get(`/${PLUGIN_ID}/properties`).then(({ data }) => {
33
+ if (!cancelled) setSchema(data);
34
+ }).catch((err) => {
35
+ if (cancelled) return;
36
+ setSchema({ configured: false, properties: [], objects: [], unavailable: [] });
37
+ setLoadError(err?.response?.data?.error?.message ?? "Propriétés HubSpot indisponibles");
38
+ });
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ }, [get]);
43
+ const emit = (next) => onChange({ target: { name, value: next ?? "", type: "string" } });
44
+ const options = React.useMemo(() => {
45
+ const all = schema?.properties ?? [];
46
+ const scoped = selectedObject ? all.filter((p) => p.object === selectedObject) : all;
47
+ const shown = scoped.map((p) => ({
48
+ value: p.name,
49
+ // The object prefix is redundant once filtered, and noisy.
50
+ label: selectedObject ? `${p.label} (${p.name})` : `${objectLabel(p.object)} · ${p.label} (${p.name})`
51
+ }));
52
+ if (value && !scoped.some((p) => p.name === value)) {
53
+ const elsewhere = all.find((p) => p.name === value);
54
+ shown.unshift({
55
+ value,
56
+ label: elsewhere ? `${value} — appartient à ${objectLabel(elsewhere.object)}` : `${value} — inconnue du portail`
57
+ });
58
+ }
59
+ return shown;
60
+ }, [schema, selectedObject, value]);
61
+ const describe = () => {
62
+ if (loadError) return loadError;
63
+ if (schema && !schema.configured) {
64
+ return "Aucune clé API HubSpot — saisie libre. Renseignez-la dans Réglages → HubSpot.";
65
+ }
66
+ if (selectedObject && schema?.unavailable.some((u) => u.object === selectedObject)) {
67
+ return `Propriétés de « ${objectLabel(selectedObject)} » illisibles — portée manquante sur le jeton.`;
68
+ }
69
+ return hint;
70
+ };
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
+ return /* @__PURE__ */ jsxs(Field.Root, { name, error, hint: describe(), required, children: [
73
+ /* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
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(
75
+ Combobox,
76
+ {
77
+ ref,
78
+ value: value || "",
79
+ onChange: emit,
80
+ onClear: () => emit(""),
81
+ disabled,
82
+ placeholder: "Rechercher une propriété…",
83
+ creatable: true,
84
+ onCreateOption: emit,
85
+ children: options.map((o) => /* @__PURE__ */ jsx(ComboboxOption, { value: o.value, children: o.label }, o.value))
86
+ }
87
+ ) : /* @__PURE__ */ jsx(
88
+ Field.Input,
89
+ {
90
+ ref,
91
+ value: value || "",
92
+ onChange: (e) => emit(e.target.value),
93
+ disabled,
94
+ placeholder: "ex. hs_role, numberofemployees"
95
+ }
96
+ ),
97
+ crmLink ? /* @__PURE__ */ jsx(Flex, { paddingTop: 1, children: /* @__PURE__ */ jsx(Link, { href: crmLink, isExternal: true, children: "Voir dans HubSpot" }) }) : null,
98
+ /* @__PURE__ */ jsx(Field.Hint, {}),
99
+ /* @__PURE__ */ jsx(Field.Error, {})
100
+ ] });
101
+ }
102
+ );
103
+ HubspotPropertyInput.displayName = "HubspotPropertyInput";
104
+ export {
105
+ HubspotPropertyInput as default
106
+ };
@@ -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-CER4BKjS.mjs";
5
+ import { P as PLUGIN_ID } from "./index-BpZtcsPM.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-DVRLoCa1.js");
7
+ const index = require("./index-DgsAHPui.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" } });
@@ -14,7 +14,7 @@ const index = {
14
14
  defaultMessage: "Choisie dans les propriétés réelles du portail"
15
15
  },
16
16
  components: {
17
- Input: async () => import("./HubspotPropertyInput-CTZYZS_7.mjs")
17
+ Input: async () => import("./HubspotPropertyInput-C7Wcw8ee.mjs")
18
18
  },
19
19
  options: {}
20
20
  });
@@ -29,7 +29,7 @@ const index = {
29
29
  id: `${PLUGIN_ID}-settings`,
30
30
  to: `/settings/${PLUGIN_ID}`,
31
31
  permissions: [],
32
- Component: async () => (await import("./Settings-Byupubny.mjs")).default
32
+ Component: async () => (await import("./Settings-B5w7LAsH.mjs")).default
33
33
  }
34
34
  ]
35
35
  );
@@ -15,7 +15,7 @@ const index = {
15
15
  defaultMessage: "Choisie dans les propriétés réelles du portail"
16
16
  },
17
17
  components: {
18
- Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-B0HpXoqz.js"))
18
+ Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-BVFRtt49.js"))
19
19
  },
20
20
  options: {}
21
21
  });
@@ -30,7 +30,7 @@ const index = {
30
30
  id: `${PLUGIN_ID}-settings`,
31
31
  to: `/settings/${PLUGIN_ID}`,
32
32
  permissions: [],
33
- Component: async () => (await Promise.resolve().then(() => require("./Settings-BgRpG_a6.js"))).default
33
+ Component: async () => (await Promise.resolve().then(() => require("./Settings-TGB0MOHr.js"))).default
34
34
  }
35
35
  ]
36
36
  );
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-DVRLoCa1.js");
2
+ const index = require("../_chunks/index-DgsAHPui.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-CER4BKjS.mjs";
1
+ import { i } from "../_chunks/index-BpZtcsPM.mjs";
2
2
  export {
3
3
  i as default
4
4
  };
@@ -9,26 +9,30 @@ interface InputProps {
9
9
  type: string;
10
10
  };
11
11
  }) => void;
12
- attribute?: unknown;
12
+ attribute?: {
13
+ options?: {
14
+ objectField?: string;
15
+ };
16
+ };
13
17
  disabled?: boolean;
14
18
  error?: string;
15
19
  required?: boolean;
16
20
  labelAction?: React.ReactNode;
17
21
  hint?: React.ReactNode;
18
22
  label?: string;
19
- placeholder?: string;
20
23
  }
21
24
  /**
22
- * Picker over the portal's writable properties.
25
+ * Picker over the portal's writable properties, narrowed to the object the
26
+ * sibling field selects.
23
27
  *
24
- * Contact and company properties share one list, each prefixed by its object,
25
- * rather than being filtered by a sibling `hsObject` field: reading a sibling
26
- * value from inside a repeatable component nested in a dynamic zone is brittle,
27
- * and showing the object inline is arguably clearer anyway the editor sees at
28
- * a glance which record the answer lands on.
28
+ * The sibling is found by path arithmetic on our own `name`: inside a repeatable
29
+ * component in a dynamic zone it looks like
30
+ * `blocks.3.form.steps.0.fields.2.hsProperty`, so swapping the last segment
31
+ * yields the neighbour. Which segment to swap is configurable per attribute
32
+ * (`options.objectField`) so the plugin isn't tied to a field called `hsObject`.
29
33
  *
30
- * Falls back to a plain text input when no API key is configured, so the field
31
- * never blocks authoring on a fresh install.
34
+ * Before an object is chosen the full list shows, each entry prefixed by its
35
+ * object an empty picker would read as broken.
32
36
  */
33
37
  declare const HubspotPropertyInput: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
34
38
  export default HubspotPropertyInput;
@@ -0,0 +1 @@
1
+ export declare function objectLabel(object: string): string;
@@ -2,42 +2,95 @@
2
2
  const utils = require("@strapi/utils");
3
3
  const HS_BASE = "https://api.hubapi.com";
4
4
  const TTL_MS = 10 * 60 * 1e3;
5
- const PLURAL = {
6
- contact: "contacts",
7
- company: "companies"
8
- };
5
+ const STANDARD_OBJECTS = [
6
+ { name: "contact", path: "contacts" },
7
+ { name: "company", path: "companies" },
8
+ { name: "deal", path: "deals" },
9
+ { name: "ticket", path: "tickets" },
10
+ { name: "product", path: "products" },
11
+ { name: "line_item", path: "line_items" },
12
+ { name: "quote", path: "quotes" }
13
+ ];
9
14
  let cache = null;
10
15
  let inFlight = null;
11
- async function fetchObject(apiKey, object) {
12
- const res = await fetch(`${HS_BASE}/crm/v3/properties/${PLURAL[object]}`, {
16
+ async function hsGet(apiKey, path) {
17
+ const res = await fetch(`${HS_BASE}${path}`, {
13
18
  headers: { Authorization: `Bearer ${apiKey}` }
14
- }).then(async (r) => {
15
- if (!r.ok) throw new Error(`HubSpot ${r.status} sur ${PLURAL[object]}`);
16
- return r.json();
17
19
  });
20
+ if (!res.ok) {
21
+ const body = await res.json().catch(() => ({}));
22
+ throw Object.assign(new Error(body.message || `HubSpot ${res.status}`), {
23
+ status: res.status
24
+ });
25
+ }
26
+ return await res.json();
27
+ }
28
+ async function fetchObject(apiKey, object) {
29
+ const res = await hsGet(
30
+ apiKey,
31
+ `/crm/v3/properties/${object.path}`
32
+ );
18
33
  return (res.results ?? []).filter((p) => !p.modificationMetadata?.readOnlyValue).map((p) => ({
19
34
  name: p.name,
20
35
  label: p.label || p.name,
21
- object,
36
+ object: object.name,
22
37
  type: p.type,
23
38
  options: (p.options ?? []).map((o) => o.value),
24
39
  group: p.groupName
25
40
  }));
26
41
  }
27
- async function listProperties(strapi, apiKey, { force = false } = {}) {
28
- if (!force && cache && Date.now() - cache.at < TTL_MS) return cache.properties;
42
+ async function fetchAccount(apiKey) {
43
+ try {
44
+ return await hsGet(
45
+ apiKey,
46
+ "/account-info/v3/details"
47
+ );
48
+ } catch {
49
+ return {};
50
+ }
51
+ }
52
+ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
53
+ if (!force && cache && Date.now() - cache.at < TTL_MS) return cache.schema;
29
54
  if (!force && inFlight) return inFlight;
30
55
  inFlight = (async () => {
31
56
  try {
32
- const [contact, company] = await Promise.all([
33
- fetchObject(apiKey, "contact"),
34
- fetchObject(apiKey, "company")
35
- ]);
36
- const properties = [...contact, ...company].sort(
57
+ const settled = await Promise.all(
58
+ objects.map(async (object) => {
59
+ try {
60
+ return { object, properties: await fetchObject(apiKey, object) };
61
+ } catch (err) {
62
+ return { object, error: err.message };
63
+ }
64
+ })
65
+ );
66
+ const properties = [];
67
+ const available = [];
68
+ const unavailable = [];
69
+ for (const entry of settled) {
70
+ if ("error" in entry && entry.error) {
71
+ unavailable.push({ object: entry.object.name, reason: entry.error });
72
+ strapi.log.warn(`[hubspot] ${entry.object.name} illisible — ${entry.error}`);
73
+ continue;
74
+ }
75
+ available.push(entry.object.name);
76
+ properties.push(...entry.properties ?? []);
77
+ }
78
+ if (!available.length) {
79
+ throw new Error(unavailable[0]?.reason ?? "Aucun objet lisible");
80
+ }
81
+ properties.sort(
37
82
  (a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
38
83
  );
39
- cache = { at: Date.now(), properties };
40
- return properties;
84
+ const account = await fetchAccount(apiKey);
85
+ const schema = {
86
+ properties,
87
+ objects: available,
88
+ unavailable,
89
+ portalId: account.portalId,
90
+ uiDomain: account.uiDomain
91
+ };
92
+ cache = { at: Date.now(), schema };
93
+ return schema;
41
94
  } finally {
42
95
  inFlight = null;
43
96
  }
@@ -52,12 +105,28 @@ function checkProperty(properties, object, name) {
52
105
  if (!trimmed) return null;
53
106
  const match = properties.find((p) => p.object === object && p.name === trimmed);
54
107
  if (match) return trimmed === name ? null : `« ${name} » contient un espace superflu`;
55
- const onOther = properties.find((p) => p.object !== object && p.name === trimmed);
108
+ const onOther = properties.find((p) => p.name === trimmed);
56
109
  if (onOther) {
57
- return `« ${trimmed} » existe sur l'objet ${onOther.object === "contact" ? "Contact" : "Société"}, pas sur ${object === "contact" ? "Contact" : "Société"}`;
110
+ return `« ${trimmed} » existe sur l'objet ${onOther.object}, pas sur ${object}`;
58
111
  }
59
112
  return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
60
113
  }
114
+ function resolveObjects(configured) {
115
+ if (!Array.isArray(configured) || !configured.length) {
116
+ return STANDARD_OBJECTS.filter((o) => o.name === "contact" || o.name === "company");
117
+ }
118
+ const out = [];
119
+ for (const entry of configured) {
120
+ if (typeof entry === "string") {
121
+ const known = STANDARD_OBJECTS.find((o) => o.name === entry || o.path === entry);
122
+ out.push(known ?? { name: entry, path: entry });
123
+ } else if (entry && typeof entry === "object" && "name" in entry) {
124
+ const e = entry;
125
+ out.push({ name: e.name, path: e.path || e.name });
126
+ }
127
+ }
128
+ return out;
129
+ }
61
130
  const ENV_VAR = "HUBSPOT_API_KEY";
62
131
  const store = (strapi) => strapi.store({ type: "plugin", name: "hubspot" });
63
132
  async function getStoredSettings(strapi) {
@@ -87,6 +156,9 @@ async function publicSettings(strapi) {
87
156
  const config = {
88
157
  default: {
89
158
  apiKey: "",
159
+ // Objects whose properties are offered. Names from the standard set, or
160
+ // `{ name, path }` for a custom object type.
161
+ objects: ["contact", "company"],
90
162
  validate: []
91
163
  },
92
164
  validator(cfg) {
@@ -100,14 +172,17 @@ const controllers = {
100
172
  async list(ctx) {
101
173
  const { apiKey } = await resolveApiKey(strapi);
102
174
  if (!apiKey) {
103
- ctx.body = { configured: false, properties: [] };
175
+ ctx.body = { configured: false, properties: [], objects: [], unavailable: [] };
104
176
  return;
105
177
  }
106
178
  try {
107
- const properties = await listProperties(strapi, apiKey, {
108
- force: ctx.query.refresh === "1"
109
- });
110
- ctx.body = { configured: true, properties };
179
+ const schema = await loadSchema(
180
+ strapi,
181
+ apiKey,
182
+ resolveObjects(strapi.plugin("hubspot").config("objects", [])),
183
+ { force: ctx.query.refresh === "1" }
184
+ );
185
+ ctx.body = { configured: true, ...schema };
111
186
  } catch (err) {
112
187
  strapi.log.error(`[hubspot] ${err.message}`);
113
188
  ctx.throw(502, "Impossible de joindre HubSpot — vérifiez la clé API.");
@@ -155,8 +230,9 @@ function collectMappings(node, target, found = []) {
155
230
  const obj = node;
156
231
  const property = obj[target.propertyField];
157
232
  if (typeof property === "string" && property.trim()) {
233
+ const object = obj[target.objectField];
158
234
  found.push({
159
- object: obj[target.objectField] === "company" ? "company" : "contact",
235
+ object: typeof object === "string" && object ? object : "contact",
160
236
  property
161
237
  });
162
238
  }
@@ -185,14 +261,18 @@ const index = {
185
261
  if (!mappings.length) return next();
186
262
  const { apiKey } = await resolveApiKey(strapi);
187
263
  if (!apiKey) return next();
188
- let properties;
264
+ let schema;
189
265
  try {
190
- properties = await listProperties(strapi, apiKey);
266
+ schema = await loadSchema(
267
+ strapi,
268
+ apiKey,
269
+ resolveObjects(strapi.plugin("hubspot").config("objects", []))
270
+ );
191
271
  } catch {
192
272
  strapi.log.warn("[hubspot] schéma indisponible — validation ignorée");
193
273
  return next();
194
274
  }
195
- const problems = mappings.map((m) => checkProperty(properties, m.object, m.property)).filter((reason) => Boolean(reason));
275
+ const problems = mappings.map((m) => checkProperty(schema.properties, m.object, m.property)).filter((reason) => Boolean(reason));
196
276
  if (problems.length) {
197
277
  throw new utils.errors.ValidationError(
198
278
  `Mapping HubSpot invalide — ${[...new Set(problems)].join(" ; ")}`
@@ -1,42 +1,95 @@
1
1
  import { errors } from "@strapi/utils";
2
2
  const HS_BASE = "https://api.hubapi.com";
3
3
  const TTL_MS = 10 * 60 * 1e3;
4
- const PLURAL = {
5
- contact: "contacts",
6
- company: "companies"
7
- };
4
+ const STANDARD_OBJECTS = [
5
+ { name: "contact", path: "contacts" },
6
+ { name: "company", path: "companies" },
7
+ { name: "deal", path: "deals" },
8
+ { name: "ticket", path: "tickets" },
9
+ { name: "product", path: "products" },
10
+ { name: "line_item", path: "line_items" },
11
+ { name: "quote", path: "quotes" }
12
+ ];
8
13
  let cache = null;
9
14
  let inFlight = null;
10
- async function fetchObject(apiKey, object) {
11
- const res = await fetch(`${HS_BASE}/crm/v3/properties/${PLURAL[object]}`, {
15
+ async function hsGet(apiKey, path) {
16
+ const res = await fetch(`${HS_BASE}${path}`, {
12
17
  headers: { Authorization: `Bearer ${apiKey}` }
13
- }).then(async (r) => {
14
- if (!r.ok) throw new Error(`HubSpot ${r.status} sur ${PLURAL[object]}`);
15
- return r.json();
16
18
  });
19
+ if (!res.ok) {
20
+ const body = await res.json().catch(() => ({}));
21
+ throw Object.assign(new Error(body.message || `HubSpot ${res.status}`), {
22
+ status: res.status
23
+ });
24
+ }
25
+ return await res.json();
26
+ }
27
+ async function fetchObject(apiKey, object) {
28
+ const res = await hsGet(
29
+ apiKey,
30
+ `/crm/v3/properties/${object.path}`
31
+ );
17
32
  return (res.results ?? []).filter((p) => !p.modificationMetadata?.readOnlyValue).map((p) => ({
18
33
  name: p.name,
19
34
  label: p.label || p.name,
20
- object,
35
+ object: object.name,
21
36
  type: p.type,
22
37
  options: (p.options ?? []).map((o) => o.value),
23
38
  group: p.groupName
24
39
  }));
25
40
  }
26
- async function listProperties(strapi, apiKey, { force = false } = {}) {
27
- if (!force && cache && Date.now() - cache.at < TTL_MS) return cache.properties;
41
+ async function fetchAccount(apiKey) {
42
+ try {
43
+ return await hsGet(
44
+ apiKey,
45
+ "/account-info/v3/details"
46
+ );
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+ async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
52
+ if (!force && cache && Date.now() - cache.at < TTL_MS) return cache.schema;
28
53
  if (!force && inFlight) return inFlight;
29
54
  inFlight = (async () => {
30
55
  try {
31
- const [contact, company] = await Promise.all([
32
- fetchObject(apiKey, "contact"),
33
- fetchObject(apiKey, "company")
34
- ]);
35
- const properties = [...contact, ...company].sort(
56
+ const settled = await Promise.all(
57
+ objects.map(async (object) => {
58
+ try {
59
+ return { object, properties: await fetchObject(apiKey, object) };
60
+ } catch (err) {
61
+ return { object, error: err.message };
62
+ }
63
+ })
64
+ );
65
+ const properties = [];
66
+ const available = [];
67
+ const unavailable = [];
68
+ for (const entry of settled) {
69
+ if ("error" in entry && entry.error) {
70
+ unavailable.push({ object: entry.object.name, reason: entry.error });
71
+ strapi.log.warn(`[hubspot] ${entry.object.name} illisible — ${entry.error}`);
72
+ continue;
73
+ }
74
+ available.push(entry.object.name);
75
+ properties.push(...entry.properties ?? []);
76
+ }
77
+ if (!available.length) {
78
+ throw new Error(unavailable[0]?.reason ?? "Aucun objet lisible");
79
+ }
80
+ properties.sort(
36
81
  (a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
37
82
  );
38
- cache = { at: Date.now(), properties };
39
- return properties;
83
+ const account = await fetchAccount(apiKey);
84
+ const schema = {
85
+ properties,
86
+ objects: available,
87
+ unavailable,
88
+ portalId: account.portalId,
89
+ uiDomain: account.uiDomain
90
+ };
91
+ cache = { at: Date.now(), schema };
92
+ return schema;
40
93
  } finally {
41
94
  inFlight = null;
42
95
  }
@@ -51,12 +104,28 @@ function checkProperty(properties, object, name) {
51
104
  if (!trimmed) return null;
52
105
  const match = properties.find((p) => p.object === object && p.name === trimmed);
53
106
  if (match) return trimmed === name ? null : `« ${name} » contient un espace superflu`;
54
- const onOther = properties.find((p) => p.object !== object && p.name === trimmed);
107
+ const onOther = properties.find((p) => p.name === trimmed);
55
108
  if (onOther) {
56
- return `« ${trimmed} » existe sur l'objet ${onOther.object === "contact" ? "Contact" : "Société"}, pas sur ${object === "contact" ? "Contact" : "Société"}`;
109
+ return `« ${trimmed} » existe sur l'objet ${onOther.object}, pas sur ${object}`;
57
110
  }
58
111
  return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
59
112
  }
113
+ function resolveObjects(configured) {
114
+ if (!Array.isArray(configured) || !configured.length) {
115
+ return STANDARD_OBJECTS.filter((o) => o.name === "contact" || o.name === "company");
116
+ }
117
+ const out = [];
118
+ for (const entry of configured) {
119
+ if (typeof entry === "string") {
120
+ const known = STANDARD_OBJECTS.find((o) => o.name === entry || o.path === entry);
121
+ out.push(known ?? { name: entry, path: entry });
122
+ } else if (entry && typeof entry === "object" && "name" in entry) {
123
+ const e = entry;
124
+ out.push({ name: e.name, path: e.path || e.name });
125
+ }
126
+ }
127
+ return out;
128
+ }
60
129
  const ENV_VAR = "HUBSPOT_API_KEY";
61
130
  const store = (strapi) => strapi.store({ type: "plugin", name: "hubspot" });
62
131
  async function getStoredSettings(strapi) {
@@ -86,6 +155,9 @@ async function publicSettings(strapi) {
86
155
  const config = {
87
156
  default: {
88
157
  apiKey: "",
158
+ // Objects whose properties are offered. Names from the standard set, or
159
+ // `{ name, path }` for a custom object type.
160
+ objects: ["contact", "company"],
89
161
  validate: []
90
162
  },
91
163
  validator(cfg) {
@@ -99,14 +171,17 @@ const controllers = {
99
171
  async list(ctx) {
100
172
  const { apiKey } = await resolveApiKey(strapi);
101
173
  if (!apiKey) {
102
- ctx.body = { configured: false, properties: [] };
174
+ ctx.body = { configured: false, properties: [], objects: [], unavailable: [] };
103
175
  return;
104
176
  }
105
177
  try {
106
- const properties = await listProperties(strapi, apiKey, {
107
- force: ctx.query.refresh === "1"
108
- });
109
- ctx.body = { configured: true, properties };
178
+ const schema = await loadSchema(
179
+ strapi,
180
+ apiKey,
181
+ resolveObjects(strapi.plugin("hubspot").config("objects", [])),
182
+ { force: ctx.query.refresh === "1" }
183
+ );
184
+ ctx.body = { configured: true, ...schema };
110
185
  } catch (err) {
111
186
  strapi.log.error(`[hubspot] ${err.message}`);
112
187
  ctx.throw(502, "Impossible de joindre HubSpot — vérifiez la clé API.");
@@ -154,8 +229,9 @@ function collectMappings(node, target, found = []) {
154
229
  const obj = node;
155
230
  const property = obj[target.propertyField];
156
231
  if (typeof property === "string" && property.trim()) {
232
+ const object = obj[target.objectField];
157
233
  found.push({
158
- object: obj[target.objectField] === "company" ? "company" : "contact",
234
+ object: typeof object === "string" && object ? object : "contact",
159
235
  property
160
236
  });
161
237
  }
@@ -184,14 +260,18 @@ const index = {
184
260
  if (!mappings.length) return next();
185
261
  const { apiKey } = await resolveApiKey(strapi);
186
262
  if (!apiKey) return next();
187
- let properties;
263
+ let schema;
188
264
  try {
189
- properties = await listProperties(strapi, apiKey);
265
+ schema = await loadSchema(
266
+ strapi,
267
+ apiKey,
268
+ resolveObjects(strapi.plugin("hubspot").config("objects", []))
269
+ );
190
270
  } catch {
191
271
  strapi.log.warn("[hubspot] schéma indisponible — validation ignorée");
192
272
  return next();
193
273
  }
194
- const problems = mappings.map((m) => checkProperty(properties, m.object, m.property)).filter((reason) => Boolean(reason));
274
+ const problems = mappings.map((m) => checkProperty(schema.properties, m.object, m.property)).filter((reason) => Boolean(reason));
195
275
  if (problems.length) {
196
276
  throw new errors.ValidationError(
197
277
  `Mapping HubSpot invalide — ${[...new Set(problems)].join(" ; ")}`
@@ -23,6 +23,7 @@ declare const _default: {
23
23
  config: {
24
24
  default: {
25
25
  apiKey: string;
26
+ objects: unknown[];
26
27
  validate: ValidateTarget[];
27
28
  };
28
29
  validator(cfg: {
@@ -1,23 +1,59 @@
1
1
  import type { Core } from "@strapi/strapi";
2
- export type HsObject = "contact" | "company";
2
+ export interface HsObjectDef {
3
+ /** Value stored in the entry's object field, e.g. `contact`. */
4
+ name: string;
5
+ /** HubSpot's URL segment for that object, e.g. `contacts`. */
6
+ path: string;
7
+ }
8
+ /** Objects HubSpot ships with; anything else is declared in the plugin config. */
9
+ export declare const STANDARD_OBJECTS: HsObjectDef[];
3
10
  export interface HsProperty {
4
11
  name: string;
5
12
  label: string;
6
- object: HsObject;
13
+ /** Object this property belongs to (`HsObjectDef.name`). */
14
+ object: string;
7
15
  type?: string;
8
16
  /** Allowed values, for enumeration properties. */
9
17
  options: string[];
10
18
  /** HubSpot's own grouping, shown to help editors locate a property. */
11
19
  group?: string;
12
20
  }
13
- /** Writable properties of both objects, cached and de-duplicated across calls. */
14
- export declare function listProperties(strapi: Core.Strapi, apiKey: string, { force }?: {
21
+ export interface Schema {
22
+ properties: HsProperty[];
23
+ /** Objects actually readable with the current token. */
24
+ objects: string[];
25
+ /** Objects that were configured but refused — almost always a missing scope. */
26
+ unavailable: {
27
+ object: string;
28
+ reason: string;
29
+ }[];
30
+ /** Enables deep links into the CRM; derived from the token, never configured. */
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;
39
+ }
40
+ /**
41
+ * Writable properties of every configured object, cached and de-duplicated
42
+ * across concurrent calls.
43
+ *
44
+ * An object the token can't read (missing scope) is reported in `unavailable`
45
+ * instead of failing the whole call: a portal that only granted contact scopes
46
+ * must still get a working contact picker.
47
+ */
48
+ export declare function loadSchema(strapi: Core.Strapi, apiKey: string, objects: HsObjectDef[], { force }?: {
15
49
  force?: boolean;
16
- }): Promise<HsProperty[]>;
17
- /** Drop the cache — used after the API key changes. */
50
+ }): Promise<Schema>;
51
+ /** Drop the cache — used after the API key or the object list changes. */
18
52
  export declare function clearCache(): void;
19
53
  /**
20
54
  * Why a property can't be written, or `null` when it's fine. Returning a reason
21
55
  * rather than a boolean lets the caller tell an editor what to fix.
22
56
  */
23
- export declare function checkProperty(properties: HsProperty[], object: HsObject, name: string): string | null;
57
+ export declare function checkProperty(properties: HsProperty[], object: string, name: string): string | null;
58
+ /** Resolve the configured object list, accepting names or full definitions. */
59
+ export declare function resolveObjects(configured: unknown): HsObjectDef[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "strapi-plugin-hubspot",
3
- "version": "0.1.0",
4
- "description": "HubSpot integration for Strapi — pick CRM properties from a searchable list instead of typing them, and catch bad mappings before they reach the API.",
3
+ "version": "0.2.1",
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",
7
7
  "plugin",
@@ -1,84 +0,0 @@
1
- import { jsxs, jsx } from "react/jsx-runtime";
2
- import * as React from "react";
3
- import { Field, Flex, Loader, Combobox, ComboboxOption } from "@strapi/design-system";
4
- import { useFetchClient } from "@strapi/strapi/admin";
5
- import { P as PLUGIN_ID } from "./index-CER4BKjS.mjs";
6
- const OBJECT_LABEL = {
7
- contact: "Contact",
8
- company: "Société"
9
- };
10
- const HubspotPropertyInput = React.forwardRef(
11
- ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
12
- const { get } = useFetchClient();
13
- const [properties, setProperties] = React.useState([]);
14
- const [configured, setConfigured] = React.useState(null);
15
- const [loadError, setLoadError] = React.useState("");
16
- React.useEffect(() => {
17
- let cancelled = false;
18
- get(`/${PLUGIN_ID}/properties`).then(({ data }) => {
19
- if (cancelled) return;
20
- setConfigured(data.configured);
21
- setProperties(data.properties ?? []);
22
- }).catch((err) => {
23
- if (cancelled) return;
24
- setConfigured(false);
25
- setLoadError(
26
- err?.response?.data?.error?.message ?? "Propriétés HubSpot indisponibles"
27
- );
28
- });
29
- return () => {
30
- cancelled = true;
31
- };
32
- }, [get]);
33
- const emit = (next) => onChange({ target: { name, value: next ?? "", type: "string" } });
34
- const describe = () => {
35
- if (loadError) return loadError;
36
- if (configured === false) {
37
- return "Aucune clé API HubSpot — saisie libre. Renseignez-la dans Réglages → HubSpot.";
38
- }
39
- return hint;
40
- };
41
- const options = React.useMemo(() => {
42
- const known = properties.map((p) => ({
43
- value: p.name,
44
- label: `${OBJECT_LABEL[p.object]} · ${p.label} (${p.name})`
45
- }));
46
- if (value && !properties.some((p) => p.name === value)) {
47
- known.unshift({ value, label: `${value} — inconnue du portail` });
48
- }
49
- return known;
50
- }, [properties, value]);
51
- return /* @__PURE__ */ jsxs(Field.Root, { name, error, hint: describe(), required, children: [
52
- /* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
53
- configured === null ? /* @__PURE__ */ jsx(Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsx(Loader, { small: true, children: "Chargement des propriétés HubSpot…" }) }) : configured ? /* @__PURE__ */ jsx(
54
- Combobox,
55
- {
56
- ref,
57
- value: value || "",
58
- onChange: emit,
59
- onClear: () => emit(""),
60
- disabled,
61
- placeholder: "Rechercher une propriété…",
62
- creatable: true,
63
- onCreateOption: emit,
64
- children: options.map((o) => /* @__PURE__ */ jsx(ComboboxOption, { value: o.value, children: o.label }, o.value))
65
- }
66
- ) : /* @__PURE__ */ jsx(
67
- Field.Input,
68
- {
69
- ref,
70
- value: value || "",
71
- onChange: (e) => emit(e.target.value),
72
- disabled,
73
- placeholder: "ex. hs_role, numberofemployees"
74
- }
75
- ),
76
- /* @__PURE__ */ jsx(Field.Hint, {}),
77
- /* @__PURE__ */ jsx(Field.Error, {})
78
- ] });
79
- }
80
- );
81
- HubspotPropertyInput.displayName = "HubspotPropertyInput";
82
- export {
83
- HubspotPropertyInput as default
84
- };