strapi-plugin-hubspot 0.3.0 → 0.4.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 +121 -2
- package/dist/_chunks/HubspotObjectInput-DAq_W1rA.js +91 -0
- package/dist/_chunks/HubspotObjectInput-G-A_pL2j.mjs +73 -0
- package/dist/_chunks/HubspotPropertyInput-B-Jg0x68.js +186 -0
- package/dist/_chunks/HubspotPropertyInput-Cv77QiS6.mjs +168 -0
- package/dist/_chunks/Settings-58edRL5D.mjs +1293 -0
- package/dist/_chunks/Settings-CryiUgSe.js +1311 -0
- package/dist/_chunks/en-CnQbjHs-.js +81 -0
- package/dist/_chunks/en-DHQZuRsj.mjs +81 -0
- package/dist/_chunks/fr-Bv8zsNX5.js +81 -0
- package/dist/_chunks/fr-Dxgil7RP.mjs +81 -0
- package/dist/_chunks/index-BudHTwSw.mjs +137 -0
- package/dist/_chunks/index-Tlw1p93d.js +136 -0
- package/dist/_chunks/objectLabels-CJTKZ4vT.mjs +425 -0
- package/dist/_chunks/objectLabels-GRmYGCIn.js +442 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/components/AuditSection.d.ts +5 -0
- package/dist/admin/src/components/FailuresSection.d.ts +11 -0
- package/dist/admin/src/components/HubspotObjectInput.d.ts +28 -0
- package/dist/admin/src/components/HubspotPropertyInput.d.ts +1 -0
- package/dist/admin/src/getTranslation.d.ts +3 -0
- package/dist/admin/src/index.d.ts +6 -0
- package/dist/admin/src/objectLabels.d.ts +2 -1
- package/dist/server/index.js +417 -69
- package/dist/server/index.mjs +417 -69
- package/dist/server/src/__tests__/audit.test.d.ts +1 -0
- package/dist/server/src/__tests__/checkMapping.test.d.ts +1 -0
- package/dist/server/src/__tests__/loadSchema.test.d.ts +1 -0
- package/dist/server/src/__tests__/submit.test.d.ts +1 -0
- package/dist/server/src/__tests__/validation.test.d.ts +1 -0
- package/dist/server/src/audit.d.ts +60 -0
- package/dist/server/src/content-types.d.ts +56 -0
- package/dist/server/src/index.d.ts +104 -28
- package/dist/server/src/properties.d.ts +7 -2
- package/dist/server/src/submit.d.ts +35 -0
- package/dist/server/src/validation.d.ts +37 -0
- package/package.json +5 -2
- package/dist/_chunks/HubspotPropertyInput-C3KbDHYP.mjs +0 -106
- package/dist/_chunks/HubspotPropertyInput-C9PpYYgj.js +0 -124
- package/dist/_chunks/Settings-DGJ_mmlw.mjs +0 -140
- package/dist/_chunks/Settings-Lj49S5fO.js +0 -158
- package/dist/_chunks/index-Bq1tS5h6.js +0 -71
- 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,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-GRmYGCIn.js");
|
|
6
|
+
const designSystem = require("@strapi/design-system");
|
|
7
|
+
const admin = require("@strapi/strapi/admin");
|
|
8
|
+
const index = require("./index-Tlw1p93d.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,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-CJTKZ4vT.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-BudHTwSw.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,186 @@
|
|
|
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-GRmYGCIn.js");
|
|
6
|
+
const designSystem = require("@strapi/design-system");
|
|
7
|
+
const admin = require("@strapi/strapi/admin");
|
|
8
|
+
const index = require("./index-Tlw1p93d.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 HubspotPropertyInput = React__namespace.forwardRef(
|
|
28
|
+
({ name, value, onChange, attribute, 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
|
+
const [loadError, setLoadError] = React__namespace.useState("");
|
|
34
|
+
const objectFieldName = attribute?.options?.objectField || "hsObject";
|
|
35
|
+
const siblingPath = React__namespace.useMemo(
|
|
36
|
+
() => name.replace(/[^.]+$/, objectFieldName),
|
|
37
|
+
[name, objectFieldName]
|
|
38
|
+
);
|
|
39
|
+
const siblingField = admin.useField(siblingPath);
|
|
40
|
+
const selectedObject = siblingField?.value;
|
|
41
|
+
const optionsFieldName = attribute?.options?.optionsField || "options";
|
|
42
|
+
const optionsPath = React__namespace.useMemo(
|
|
43
|
+
() => name.replace(/[^.]+$/, optionsFieldName),
|
|
44
|
+
[name, optionsFieldName]
|
|
45
|
+
);
|
|
46
|
+
const optionsField = admin.useField(optionsPath);
|
|
47
|
+
const [imported, setImported] = React__namespace.useState(false);
|
|
48
|
+
React__namespace.useEffect(() => {
|
|
49
|
+
let cancelled = false;
|
|
50
|
+
get(`/${index.PLUGIN_ID}/properties`).then(({ data }) => {
|
|
51
|
+
if (!cancelled) setSchema(data);
|
|
52
|
+
}).catch((err) => {
|
|
53
|
+
if (cancelled) return;
|
|
54
|
+
setSchema({ configured: false, properties: [], objects: [], unavailable: [] });
|
|
55
|
+
setLoadError(
|
|
56
|
+
err?.response?.data?.error?.message ?? formatMessage({
|
|
57
|
+
id: index.getTranslation("picker.load-error"),
|
|
58
|
+
defaultMessage: "HubSpot properties unavailable"
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
cancelled = true;
|
|
64
|
+
};
|
|
65
|
+
}, [get]);
|
|
66
|
+
const emit = (next) => onChange({ target: { name, value: next ?? "", type: "string" } });
|
|
67
|
+
const options = React__namespace.useMemo(() => {
|
|
68
|
+
const all = schema?.properties ?? [];
|
|
69
|
+
const scoped = selectedObject ? all.filter((p) => p.object === selectedObject) : all;
|
|
70
|
+
const sorted = selectedObject ? [...scoped].sort(
|
|
71
|
+
(a, b) => (a.group ?? "").localeCompare(b.group ?? "") || a.label.localeCompare(b.label)
|
|
72
|
+
) : scoped;
|
|
73
|
+
const shown = sorted.map((p) => ({
|
|
74
|
+
value: p.name,
|
|
75
|
+
label: selectedObject ? `${p.group ? `${p.group} · ` : ""}${p.label} (${p.name})` : `${objectLabels.objectLabel(intl, p.object)} · ${p.label} (${p.name})`
|
|
76
|
+
}));
|
|
77
|
+
if (value && !scoped.some((p) => p.name === value)) {
|
|
78
|
+
const elsewhere = all.find((p) => p.name === value);
|
|
79
|
+
shown.unshift({
|
|
80
|
+
value,
|
|
81
|
+
label: elsewhere ? formatMessage(
|
|
82
|
+
{ id: index.getTranslation("picker.belongs-to"), defaultMessage: "{property} — belongs to {object}" },
|
|
83
|
+
{ property: value, object: objectLabels.objectLabel(intl, elsewhere.object) }
|
|
84
|
+
) : formatMessage(
|
|
85
|
+
{ id: index.getTranslation("picker.unknown"), defaultMessage: "{property} — unknown to the portal" },
|
|
86
|
+
{ property: value }
|
|
87
|
+
)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return shown;
|
|
91
|
+
}, [schema, selectedObject, value, intl, formatMessage]);
|
|
92
|
+
const describe = () => {
|
|
93
|
+
if (loadError) return loadError;
|
|
94
|
+
if (schema && !schema.configured) {
|
|
95
|
+
return formatMessage({
|
|
96
|
+
id: index.getTranslation("picker.not-configured"),
|
|
97
|
+
defaultMessage: "No HubSpot API key — free text. Set it in Settings → HubSpot."
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (selectedObject && schema?.unavailable.some((u) => u.object === selectedObject)) {
|
|
101
|
+
return formatMessage(
|
|
102
|
+
{
|
|
103
|
+
id: index.getTranslation("picker.object-unavailable"),
|
|
104
|
+
defaultMessage: "Properties of “{object}” unreadable — missing scope on the token."
|
|
105
|
+
},
|
|
106
|
+
{ object: objectLabels.objectLabel(intl, selectedObject) }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return hint;
|
|
110
|
+
};
|
|
111
|
+
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;
|
|
112
|
+
const selectedProperty = value && schema ? schema.properties.find(
|
|
113
|
+
(p) => p.name === value && (!selectedObject || p.object === selectedObject)
|
|
114
|
+
) : void 0;
|
|
115
|
+
const importable = selectedProperty?.type === "enumeration" && (selectedProperty.options?.length ?? 0) > 0;
|
|
116
|
+
React__namespace.useEffect(() => setImported(false), [value]);
|
|
117
|
+
const importOptions = () => {
|
|
118
|
+
if (!selectedProperty || !optionsField) return;
|
|
119
|
+
const rows = selectedProperty.options.map((o, i) => ({
|
|
120
|
+
__temp_key__: `a${i}`,
|
|
121
|
+
value: o.value,
|
|
122
|
+
label: o.label ?? o.value
|
|
123
|
+
}));
|
|
124
|
+
optionsField.onChange({
|
|
125
|
+
target: { name: optionsPath, value: rows, type: "json" }
|
|
126
|
+
});
|
|
127
|
+
setImported(true);
|
|
128
|
+
};
|
|
129
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name, error, hint: describe(), required, children: [
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }),
|
|
131
|
+
!schema ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: formatMessage({
|
|
132
|
+
id: index.getTranslation("picker.loading"),
|
|
133
|
+
defaultMessage: "Loading HubSpot properties…"
|
|
134
|
+
}) }) }) : schema.configured ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
135
|
+
designSystem.Combobox,
|
|
136
|
+
{
|
|
137
|
+
ref,
|
|
138
|
+
value: value || "",
|
|
139
|
+
onChange: emit,
|
|
140
|
+
onClear: () => emit(""),
|
|
141
|
+
disabled,
|
|
142
|
+
placeholder: formatMessage({
|
|
143
|
+
id: index.getTranslation("picker.placeholder"),
|
|
144
|
+
defaultMessage: "Search a property…"
|
|
145
|
+
}),
|
|
146
|
+
creatable: true,
|
|
147
|
+
onCreateOption: emit,
|
|
148
|
+
children: options.map((o) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: o.value, children: o.label }, o.value))
|
|
149
|
+
}
|
|
150
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
151
|
+
designSystem.Field.Input,
|
|
152
|
+
{
|
|
153
|
+
ref,
|
|
154
|
+
value: value || "",
|
|
155
|
+
onChange: (e) => emit(e.target.value),
|
|
156
|
+
disabled,
|
|
157
|
+
placeholder: formatMessage({
|
|
158
|
+
id: index.getTranslation("picker.free-placeholder"),
|
|
159
|
+
defaultMessage: "e.g. hs_role, numberofemployees"
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
),
|
|
163
|
+
crmLink || importable ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { paddingTop: 1, gap: 3, alignItems: "center", wrap: "wrap", children: [
|
|
164
|
+
importable ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", size: "S", onClick: importOptions, disabled, children: formatMessage(
|
|
165
|
+
{
|
|
166
|
+
id: index.getTranslation("picker.import"),
|
|
167
|
+
defaultMessage: "Import the {count} options from HubSpot"
|
|
168
|
+
},
|
|
169
|
+
{ count: selectedProperty?.options.length ?? 0 }
|
|
170
|
+
) }) : null,
|
|
171
|
+
imported ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "success600", children: formatMessage({
|
|
172
|
+
id: index.getTranslation("picker.imported"),
|
|
173
|
+
defaultMessage: "Options imported — save to keep them."
|
|
174
|
+
}) }) : null,
|
|
175
|
+
crmLink ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Link, { href: crmLink, isExternal: true, children: formatMessage({
|
|
176
|
+
id: index.getTranslation("picker.view"),
|
|
177
|
+
defaultMessage: "View in HubSpot"
|
|
178
|
+
}) }) : null
|
|
179
|
+
] }) : null,
|
|
180
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
|
|
181
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
|
|
182
|
+
] });
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
HubspotPropertyInput.displayName = "HubspotPropertyInput";
|
|
186
|
+
exports.default = HubspotPropertyInput;
|