strapi-plugin-hubspot 0.3.0 → 0.4.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.
Files changed (44) hide show
  1. package/README.md +121 -2
  2. package/dist/_chunks/HubspotObjectInput-DiYaEyE7.mjs +73 -0
  3. package/dist/_chunks/HubspotObjectInput-Dqgz6Epv.js +91 -0
  4. package/dist/_chunks/HubspotPropertyInput-BUbQk1CX.mjs +174 -0
  5. package/dist/_chunks/HubspotPropertyInput-YKXA_-Sk.js +192 -0
  6. package/dist/_chunks/Settings-B_lg_AaC.mjs +1293 -0
  7. package/dist/_chunks/Settings-D34hp54b.js +1311 -0
  8. package/dist/_chunks/en-CnQbjHs-.js +81 -0
  9. package/dist/_chunks/en-DHQZuRsj.mjs +81 -0
  10. package/dist/_chunks/fr-Bv8zsNX5.js +81 -0
  11. package/dist/_chunks/fr-Dxgil7RP.mjs +81 -0
  12. package/dist/_chunks/index-CDpmcrIu.js +136 -0
  13. package/dist/_chunks/index-ymm9UsG8.mjs +137 -0
  14. package/dist/_chunks/objectLabels-CORLL0o7.js +442 -0
  15. package/dist/_chunks/objectLabels-DKr0yCaU.mjs +425 -0
  16. package/dist/admin/index.js +1 -1
  17. package/dist/admin/index.mjs +1 -1
  18. package/dist/admin/src/components/AuditSection.d.ts +5 -0
  19. package/dist/admin/src/components/FailuresSection.d.ts +11 -0
  20. package/dist/admin/src/components/HubspotObjectInput.d.ts +28 -0
  21. package/dist/admin/src/components/HubspotPropertyInput.d.ts +1 -0
  22. package/dist/admin/src/getTranslation.d.ts +3 -0
  23. package/dist/admin/src/index.d.ts +6 -0
  24. package/dist/admin/src/objectLabels.d.ts +2 -1
  25. package/dist/server/index.js +417 -69
  26. package/dist/server/index.mjs +417 -69
  27. package/dist/server/src/__tests__/audit.test.d.ts +1 -0
  28. package/dist/server/src/__tests__/checkMapping.test.d.ts +1 -0
  29. package/dist/server/src/__tests__/loadSchema.test.d.ts +1 -0
  30. package/dist/server/src/__tests__/submit.test.d.ts +1 -0
  31. package/dist/server/src/__tests__/validation.test.d.ts +1 -0
  32. package/dist/server/src/audit.d.ts +60 -0
  33. package/dist/server/src/content-types.d.ts +56 -0
  34. package/dist/server/src/index.d.ts +104 -28
  35. package/dist/server/src/properties.d.ts +7 -2
  36. package/dist/server/src/submit.d.ts +35 -0
  37. package/dist/server/src/validation.d.ts +37 -0
  38. package/package.json +5 -2
  39. package/dist/_chunks/HubspotPropertyInput-C3KbDHYP.mjs +0 -106
  40. package/dist/_chunks/HubspotPropertyInput-C9PpYYgj.js +0 -124
  41. package/dist/_chunks/Settings-DGJ_mmlw.mjs +0 -140
  42. package/dist/_chunks/Settings-Lj49S5fO.js +0 -158
  43. package/dist/_chunks/index-Bq1tS5h6.js +0 -71
  44. package/dist/_chunks/index-lx7um1Yx.mjs +0 -72
package/README.md CHANGED
@@ -59,11 +59,46 @@ setting reads:
59
59
  }
60
60
  ```
61
61
 
62
+ Once filtered, the list is ordered and prefixed by the property's **HubSpot
63
+ group** — the portal's own way of organising hundreds of properties:
64
+
65
+ ```
66
+ Informations de contact · Prénom (firstname)
67
+ Informations de contact · Rôle (hs_role)
68
+ Historique · Source (hs_analytics_source)
69
+ ```
70
+
62
71
  Read-only properties — the ones HubSpot computes and always refuses to
63
72
  accept — are filtered out, so the list only ever offers things that will
64
73
  actually work. When the portal id is readable, each selected property gets a
65
74
  **Voir dans HubSpot** link straight to its settings page.
66
75
 
76
+ ### An object picker to feed it
77
+
78
+ The sibling object field doesn't have to be a hand-typed enum: the
79
+ `hubspot.object` custom field is a select over the portal's configured
80
+ objects. An object the token can't read stays selectable, flagged with its
81
+ missing scope.
82
+
83
+ ```json
84
+ {
85
+ "hsObject": {
86
+ "type": "customField",
87
+ "customField": "plugin::hubspot.object"
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### One-click option import
93
+
94
+ For an **enumeration** property, the picker shows an *Import the N options
95
+ from HubSpot* button that fills the sibling repeatable (default `options`,
96
+ entries `{ value, label }` — the same shape the validation reads) with the
97
+ enumeration's real choices. The `bad-option` check detects a select that
98
+ drifted from the CRM; this is what fixes it. Point `optionsField` (in the
99
+ field's HubSpot options, next to `objectField`) at the repeatable if it isn't
100
+ called `options`.
101
+
67
102
  ### Validation on save
68
103
 
69
104
  Point the plugin at the content types that carry mappings and it walks each
@@ -91,6 +126,63 @@ each `{ value?, label? }`) and they are checked too.
91
126
  Each problem is also carried as a structured code in the error's `details`, so a
92
127
  host app can localize the message instead of parsing the sentence.
93
128
 
129
+ **`unknown` is the one code a legitimate workflow can produce** — a property
130
+ staged in HubSpot but not created yet. Set `strict: false` on a validate target
131
+ and those pass with a warning in the logs instead of blocking the save. The
132
+ other three codes always block, whatever `strict` says: no workflow produces a
133
+ wrong object, a trailing space, or a drifted option on purpose.
134
+
135
+ ### An audit of existing content
136
+
137
+ Save-time validation only protects entries as they are written. A property
138
+ deleted in HubSpot afterwards leaves invalid mappings dormant in content nobody
139
+ re-saves — until a submission silently fails. **Settings → HubSpot → Mapping
140
+ audit** walks every entry of the validated content types (drafts, every locale,
141
+ at any depth) against a freshly fetched schema, and lists each entry the portal
142
+ would reject today, linking straight to it in the Content Manager.
143
+
144
+ The audit reports `unknown` properties even on `strict: false` targets: strict
145
+ only decides whether a save is blocked, not whether the mapping would reach the
146
+ CRM.
147
+
148
+ ### A sending service
149
+
150
+ The host app doesn't have to talk to HubSpot itself — the plugin exposes the
151
+ upsert, with everything above applied on the way out:
152
+
153
+ ```ts
154
+ const result = await strapi.plugin("hubspot").service("submit").upsert({
155
+ object: "contact",
156
+ idProperty: "email", // find-or-create key
157
+ properties: {
158
+ email: "jane@acme.com",
159
+ firstname: "Jane",
160
+ hs_role: "dev;designer", // multi-select values use HubSpot's `;` separator
161
+ },
162
+ });
163
+ // { ok: true, id: "…" }
164
+ // { ok: false, problems: [{ code: "unknown", … }] } — refused before sending
165
+ // { ok: false, queued: true, error: "…" } — parked for replay
166
+ ```
167
+
168
+ The payload is validated against the portal schema **before** it is sent —
169
+ the same checks as on save, values coerced to strings, empty ones dropped.
170
+ Then:
171
+
172
+ - a **permanent refusal** (4xx) comes back as `{ ok: false, error }` with
173
+ HubSpot's message — retrying can't fix a wrong payload, so nothing is queued;
174
+ - a **transient failure** (429, 5xx, network) is retried twice with backoff,
175
+ then parked in **Content Manager → HubSpot failed submissions** and reported
176
+ as `{ ok: false, queued: true }`.
177
+
178
+ `service("submit").retryFailures()` replays the queue, oldest first — from a
179
+ cron in the host app, or the **Replay now** button in Settings → HubSpot. A
180
+ replay that succeeds removes the row; one that keeps failing stays, with its
181
+ error and attempt count, until it works or an admin deletes it.
182
+
183
+ Sending needs write scopes on the token: `crm.objects.contacts.write` (and
184
+ its equivalent per object you send to).
185
+
94
186
  ### A settings screen
95
187
 
96
188
  **Settings → HubSpot** holds the private app token. It is stored server-side and
@@ -98,6 +190,11 @@ never returned to the browser: the UI only receives whether a key exists, where
98
190
  it comes from, and its last four characters. A "Test connection" button
99
191
  round-trips to HubSpot and reports how many properties it can read.
100
192
 
193
+ Access is gated by a dedicated RBAC permission — **Settings →
194
+ Roles → Plugins → Hubspot**. A role without it neither sees the settings link
195
+ nor can call the settings routes; reading properties stays open to every
196
+ authenticated admin, since the picker needs it in the Content Manager.
197
+
101
198
  > **On storage:** the key is kept in Strapi's core store, which is **not
102
199
  > encrypted at rest** — it is readable by anyone with database access. It never
103
200
  > reaches the browser, but treat it like any other secret in your database. Use
@@ -132,6 +229,8 @@ export default ({ env }) => ({
132
229
  objectField: "hsObject", // holds "contact" | "company"
133
230
  propertyField: "hsProperty", // holds the property name
134
231
  optionsField: "options", // optional — the field's choices
232
+ strict: true, // optional — false lets `unknown`
233
+ // properties through with a warning
135
234
  },
136
235
  ],
137
236
  },
@@ -201,7 +300,7 @@ This one never blocks work:
201
300
  |---|---|
202
301
  | No API key configured | The field falls back to a plain text input, with a note explaining why |
203
302
  | HubSpot unreachable | Saving proceeds; validation is skipped and a warning is logged |
204
- | Property staged in HubSpot but not created yet | The picker accepts a typed value (`creatable`) |
303
+ | Property staged in HubSpot but not created yet | The picker accepts a typed value (`creatable`); set `strict: false` on the validate target so the save passes too |
205
304
  | An object's scope is missing | That object is skipped; the others still work |
206
305
  | Portal id unreadable | Deep links are omitted; everything else is unaffected |
207
306
  | Plugin uninstalled | Values remain as strings — nothing to undo |
@@ -212,15 +311,35 @@ key drops the cache immediately.
212
311
 
213
312
  ## Admin API
214
313
 
215
- All routes require an authenticated admin.
314
+ All routes require an authenticated admin; the settings routes additionally
315
+ require the `plugin::hubspot.settings` RBAC permission.
216
316
 
217
317
  | Method | Path | Purpose |
218
318
  |---|---|---|
219
319
  | `GET` | `/hubspot/properties` | Writable properties, readable objects, unreachable ones and the portal id. `?refresh=1` bypasses the cache |
320
+ | `GET` | `/hubspot/audit` | Scans every entry of the validated content types and returns the invalid mappings, per entry |
321
+ | `GET` | `/hubspot/failures` | Number of parked submissions |
322
+ | `POST` | `/hubspot/failures/retry` | Replays the parked submissions and reports the outcome |
220
323
  | `GET` | `/hubspot/settings` | Whether a key exists, its source and hint — never the key |
221
324
  | `PUT` | `/hubspot/settings` | Save a key (`{ apiKey }`) |
222
325
  | `DELETE` | `/hubspot/settings` | Remove the stored key |
223
326
 
327
+ ## i18n
328
+
329
+ The admin UI ships in English and French, keyed on the Strapi admin locale.
330
+ Server-side error messages stay in English; the structured codes in the
331
+ error's `details` are the localization hook for host apps.
332
+
333
+ ## Tests
334
+
335
+ ```bash
336
+ npm test
337
+ ```
338
+
339
+ Vitest over the server logic: mapping checks, deep collection through dynamic
340
+ zones and repeatables, schema loading (cache, concurrent de-duplication,
341
+ missing scopes) and the strict/non-strict save middleware.
342
+
224
343
  ## Compatibility
225
344
 
226
345
  Strapi **v5**. Node **>= 18**.
@@ -0,0 +1,73 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { u as useIntl, o as objectLabel } from "./objectLabels-DKr0yCaU.mjs";
4
+ import { Field, Flex, Loader, SingleSelect, SingleSelectOption } from "@strapi/design-system";
5
+ import { useFetchClient } from "@strapi/strapi/admin";
6
+ import { P as PLUGIN_ID, g as getTranslation } from "./index-ymm9UsG8.mjs";
7
+ const HubspotObjectInput = React.forwardRef(
8
+ ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
9
+ const intl = useIntl();
10
+ const { formatMessage } = intl;
11
+ const { get } = useFetchClient();
12
+ const [schema, setSchema] = React.useState(null);
13
+ React.useEffect(() => {
14
+ let cancelled = false;
15
+ get(`/${PLUGIN_ID}/properties`).then(({ data }) => {
16
+ if (!cancelled) setSchema(data);
17
+ }).catch(() => {
18
+ if (!cancelled) setSchema({ configured: false, objects: [], unavailable: [] });
19
+ });
20
+ return () => {
21
+ cancelled = true;
22
+ };
23
+ }, [get]);
24
+ const emit = (next) => onChange({ target: { name, value: String(next ?? ""), type: "string" } });
25
+ const known = [
26
+ ...schema?.objects ?? [],
27
+ ...(schema?.unavailable ?? []).map((u) => u.object)
28
+ ];
29
+ const options = value && !known.includes(value) ? [value, ...known] : known;
30
+ return /* @__PURE__ */ jsxs(Field.Root, { name, error, hint, required, children: [
31
+ /* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
32
+ !schema ? /* @__PURE__ */ jsx(Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsx(Loader, { small: true, children: formatMessage({
33
+ id: getTranslation("object.loading"),
34
+ defaultMessage: "Loading HubSpot objects…"
35
+ }) }) }) : schema.configured ? /* @__PURE__ */ jsx(
36
+ SingleSelect,
37
+ {
38
+ ref,
39
+ value: value || "",
40
+ onChange: emit,
41
+ onClear: () => emit(""),
42
+ disabled,
43
+ placeholder: formatMessage({
44
+ id: getTranslation("object.placeholder"),
45
+ defaultMessage: "Choose an object…"
46
+ }),
47
+ children: options.map((object) => /* @__PURE__ */ jsx(SingleSelectOption, { value: object, children: schema.unavailable.some((u) => u.object === object) ? formatMessage(
48
+ {
49
+ id: getTranslation("object.no-scope"),
50
+ defaultMessage: "{object} — missing scope on the token"
51
+ },
52
+ { object: objectLabel(intl, object) }
53
+ ) : objectLabel(intl, object) }, object))
54
+ }
55
+ ) : /* @__PURE__ */ jsx(
56
+ Field.Input,
57
+ {
58
+ ref,
59
+ value: value || "",
60
+ onChange: (e) => emit(e.target.value),
61
+ disabled,
62
+ placeholder: "contact"
63
+ }
64
+ ),
65
+ /* @__PURE__ */ jsx(Field.Hint, {}),
66
+ /* @__PURE__ */ jsx(Field.Error, {})
67
+ ] });
68
+ }
69
+ );
70
+ HubspotObjectInput.displayName = "HubspotObjectInput";
71
+ export {
72
+ HubspotObjectInput as default
73
+ };
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const objectLabels = require("./objectLabels-CORLL0o7.js");
6
+ const designSystem = require("@strapi/design-system");
7
+ const admin = require("@strapi/strapi/admin");
8
+ const index = require("./index-CDpmcrIu.js");
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
12
+ if (e) {
13
+ for (const k in e) {
14
+ if (k !== "default") {
15
+ const d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: () => e[k]
19
+ });
20
+ }
21
+ }
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+ const React__namespace = /* @__PURE__ */ _interopNamespace(React);
27
+ const HubspotObjectInput = React__namespace.forwardRef(
28
+ ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
29
+ const intl = objectLabels.useIntl();
30
+ const { formatMessage } = intl;
31
+ const { get } = admin.useFetchClient();
32
+ const [schema, setSchema] = React__namespace.useState(null);
33
+ React__namespace.useEffect(() => {
34
+ let cancelled = false;
35
+ get(`/${index.PLUGIN_ID}/properties`).then(({ data }) => {
36
+ if (!cancelled) setSchema(data);
37
+ }).catch(() => {
38
+ if (!cancelled) setSchema({ configured: false, objects: [], unavailable: [] });
39
+ });
40
+ return () => {
41
+ cancelled = true;
42
+ };
43
+ }, [get]);
44
+ const emit = (next) => onChange({ target: { name, value: String(next ?? ""), type: "string" } });
45
+ const known = [
46
+ ...schema?.objects ?? [],
47
+ ...(schema?.unavailable ?? []).map((u) => u.object)
48
+ ];
49
+ const options = value && !known.includes(value) ? [value, ...known] : known;
50
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name, error, hint, required, children: [
51
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }),
52
+ !schema ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: formatMessage({
53
+ id: index.getTranslation("object.loading"),
54
+ defaultMessage: "Loading HubSpot objects…"
55
+ }) }) }) : schema.configured ? /* @__PURE__ */ jsxRuntime.jsx(
56
+ designSystem.SingleSelect,
57
+ {
58
+ ref,
59
+ value: value || "",
60
+ onChange: emit,
61
+ onClear: () => emit(""),
62
+ disabled,
63
+ placeholder: formatMessage({
64
+ id: index.getTranslation("object.placeholder"),
65
+ defaultMessage: "Choose an object…"
66
+ }),
67
+ children: options.map((object) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: object, children: schema.unavailable.some((u) => u.object === object) ? formatMessage(
68
+ {
69
+ id: index.getTranslation("object.no-scope"),
70
+ defaultMessage: "{object} — missing scope on the token"
71
+ },
72
+ { object: objectLabels.objectLabel(intl, object) }
73
+ ) : objectLabels.objectLabel(intl, object) }, object))
74
+ }
75
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
76
+ designSystem.Field.Input,
77
+ {
78
+ ref,
79
+ value: value || "",
80
+ onChange: (e) => emit(e.target.value),
81
+ disabled,
82
+ placeholder: "contact"
83
+ }
84
+ ),
85
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
86
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
87
+ ] });
88
+ }
89
+ );
90
+ HubspotObjectInput.displayName = "HubspotObjectInput";
91
+ exports.default = HubspotObjectInput;
@@ -0,0 +1,174 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { u as useIntl, o as objectLabel } from "./objectLabels-DKr0yCaU.mjs";
4
+ import { Field, Flex, Loader, Combobox, ComboboxOption, Button, Typography, Link } from "@strapi/design-system";
5
+ import { useFetchClient, useField } from "@strapi/strapi/admin";
6
+ import { P as PLUGIN_ID, g as getTranslation } from "./index-ymm9UsG8.mjs";
7
+ const oneLine = {
8
+ display: "block",
9
+ whiteSpace: "nowrap",
10
+ overflow: "hidden",
11
+ textOverflow: "ellipsis"
12
+ };
13
+ const HubspotPropertyInput = React.forwardRef(
14
+ ({ name, value, onChange, attribute, disabled, error, required, label, hint, labelAction }, ref) => {
15
+ const intl = useIntl();
16
+ const { formatMessage } = intl;
17
+ const { get } = useFetchClient();
18
+ const [schema, setSchema] = React.useState(null);
19
+ const [loadError, setLoadError] = React.useState("");
20
+ const objectFieldName = attribute?.options?.objectField || "hsObject";
21
+ const siblingPath = React.useMemo(
22
+ () => name.replace(/[^.]+$/, objectFieldName),
23
+ [name, objectFieldName]
24
+ );
25
+ const siblingField = useField(siblingPath);
26
+ const selectedObject = siblingField?.value;
27
+ const optionsFieldName = attribute?.options?.optionsField || "options";
28
+ const optionsPath = React.useMemo(
29
+ () => name.replace(/[^.]+$/, optionsFieldName),
30
+ [name, optionsFieldName]
31
+ );
32
+ const optionsField = useField(optionsPath);
33
+ const [imported, setImported] = React.useState(false);
34
+ React.useEffect(() => {
35
+ let cancelled = false;
36
+ get(`/${PLUGIN_ID}/properties`).then(({ data }) => {
37
+ if (!cancelled) setSchema(data);
38
+ }).catch((err) => {
39
+ if (cancelled) return;
40
+ setSchema({ configured: false, properties: [], objects: [], unavailable: [] });
41
+ setLoadError(
42
+ err?.response?.data?.error?.message ?? formatMessage({
43
+ id: getTranslation("picker.load-error"),
44
+ defaultMessage: "HubSpot properties unavailable"
45
+ })
46
+ );
47
+ });
48
+ return () => {
49
+ cancelled = true;
50
+ };
51
+ }, [get]);
52
+ const emit = (next) => onChange({ target: { name, value: next ?? "", type: "string" } });
53
+ const options = React.useMemo(() => {
54
+ const all = schema?.properties ?? [];
55
+ const scoped = selectedObject ? all.filter((p) => p.object === selectedObject) : all;
56
+ const sorted = selectedObject ? [...scoped].sort(
57
+ (a, b) => (a.group ?? "").localeCompare(b.group ?? "") || a.label.localeCompare(b.label)
58
+ ) : scoped;
59
+ const shown = sorted.map((p) => ({
60
+ value: p.name,
61
+ label: selectedObject ? `${p.group ? `${p.group} · ` : ""}${p.label} (${p.name})` : `${objectLabel(intl, p.object)} · ${p.label} (${p.name})`
62
+ }));
63
+ if (value && !scoped.some((p) => p.name === value)) {
64
+ const elsewhere = all.find((p) => p.name === value);
65
+ shown.unshift({
66
+ value,
67
+ label: elsewhere ? formatMessage(
68
+ { id: getTranslation("picker.belongs-to"), defaultMessage: "{property} — belongs to {object}" },
69
+ { property: value, object: objectLabel(intl, elsewhere.object) }
70
+ ) : formatMessage(
71
+ { id: getTranslation("picker.unknown"), defaultMessage: "{property} — unknown to the portal" },
72
+ { property: value }
73
+ )
74
+ });
75
+ }
76
+ return shown;
77
+ }, [schema, selectedObject, value, intl, formatMessage]);
78
+ const describe = () => {
79
+ if (loadError) return loadError;
80
+ if (schema && !schema.configured) {
81
+ return formatMessage({
82
+ id: getTranslation("picker.not-configured"),
83
+ defaultMessage: "No HubSpot API key — free text. Set it in Settings → HubSpot."
84
+ });
85
+ }
86
+ if (selectedObject && schema?.unavailable.some((u) => u.object === selectedObject)) {
87
+ return formatMessage(
88
+ {
89
+ id: getTranslation("picker.object-unavailable"),
90
+ defaultMessage: "Properties of “{object}” unreadable — missing scope on the token."
91
+ },
92
+ { object: objectLabel(intl, selectedObject) }
93
+ );
94
+ }
95
+ return hint;
96
+ };
97
+ 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;
98
+ const selectedProperty = value && schema ? schema.properties.find(
99
+ (p) => p.name === value && (!selectedObject || p.object === selectedObject)
100
+ ) : void 0;
101
+ const importable = selectedProperty?.type === "enumeration" && (selectedProperty.options?.length ?? 0) > 0;
102
+ React.useEffect(() => setImported(false), [value]);
103
+ const importOptions = () => {
104
+ if (!selectedProperty || !optionsField) return;
105
+ const rows = selectedProperty.options.map((o, i) => ({
106
+ __temp_key__: `a${i}`,
107
+ value: o.value,
108
+ label: o.label ?? o.value
109
+ }));
110
+ optionsField.onChange({
111
+ target: { name: optionsPath, value: rows, type: "json" }
112
+ });
113
+ setImported(true);
114
+ };
115
+ return /* @__PURE__ */ jsxs(Field.Root, { name, error, hint: describe(), required, children: [
116
+ /* @__PURE__ */ jsx(Field.Label, { action: labelAction, children: label }),
117
+ !schema ? /* @__PURE__ */ jsx(Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsx(Loader, { small: true, children: formatMessage({
118
+ id: getTranslation("picker.loading"),
119
+ defaultMessage: "Loading HubSpot properties…"
120
+ }) }) }) : schema.configured ? /* @__PURE__ */ jsx(
121
+ Combobox,
122
+ {
123
+ ref,
124
+ value: value || "",
125
+ onChange: emit,
126
+ onClear: () => emit(""),
127
+ disabled,
128
+ placeholder: formatMessage({
129
+ id: getTranslation("picker.placeholder"),
130
+ defaultMessage: "Search a property…"
131
+ }),
132
+ creatable: true,
133
+ onCreateOption: emit,
134
+ children: options.map((o) => /* @__PURE__ */ jsx(ComboboxOption, { value: o.value, textValue: o.label, children: /* @__PURE__ */ jsx("span", { title: o.label, style: oneLine, children: o.label }) }, o.value))
135
+ }
136
+ ) : /* @__PURE__ */ jsx(
137
+ Field.Input,
138
+ {
139
+ ref,
140
+ value: value || "",
141
+ onChange: (e) => emit(e.target.value),
142
+ disabled,
143
+ placeholder: formatMessage({
144
+ id: getTranslation("picker.free-placeholder"),
145
+ defaultMessage: "e.g. hs_role, numberofemployees"
146
+ })
147
+ }
148
+ ),
149
+ crmLink || importable ? /* @__PURE__ */ jsxs(Flex, { paddingTop: 1, gap: 3, alignItems: "center", wrap: "wrap", children: [
150
+ importable ? /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", onClick: importOptions, disabled, children: formatMessage(
151
+ {
152
+ id: getTranslation("picker.import"),
153
+ defaultMessage: "Import the {count} options from HubSpot"
154
+ },
155
+ { count: selectedProperty?.options.length ?? 0 }
156
+ ) }) : null,
157
+ imported ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "success600", children: formatMessage({
158
+ id: getTranslation("picker.imported"),
159
+ defaultMessage: "Options imported — save to keep them."
160
+ }) }) : null,
161
+ crmLink ? /* @__PURE__ */ jsx(Link, { href: crmLink, isExternal: true, children: formatMessage({
162
+ id: getTranslation("picker.view"),
163
+ defaultMessage: "View in HubSpot"
164
+ }) }) : null
165
+ ] }) : null,
166
+ /* @__PURE__ */ jsx(Field.Hint, {}),
167
+ /* @__PURE__ */ jsx(Field.Error, {})
168
+ ] });
169
+ }
170
+ );
171
+ HubspotPropertyInput.displayName = "HubspotPropertyInput";
172
+ export {
173
+ HubspotPropertyInput as default
174
+ };