strapi-plugin-hubspot 0.2.1 → 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 +145 -5
- 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 +445 -69
- package/dist/server/index.mjs +445 -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 -22
- package/dist/server/src/properties.d.ts +41 -5
- 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-BVFRtt49.js +0 -124
- package/dist/_chunks/HubspotPropertyInput-C7Wcw8ee.mjs +0 -106
- package/dist/_chunks/Settings-B5w7LAsH.mjs +0 -140
- package/dist/_chunks/Settings-TGB0MOHr.js +0 -158
- package/dist/_chunks/index-BpZtcsPM.mjs +0 -42
- package/dist/_chunks/index-DgsAHPui.js +0 -41
package/README.md
CHANGED
|
@@ -44,8 +44,10 @@ Société · Nombre d'employés (numberofemployees)
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
**The list narrows to the object you picked.** Point the field at the sibling
|
|
47
|
-
that holds the object
|
|
48
|
-
|
|
47
|
+
that holds the object — from the Content-Type Builder, under *HubSpot → Object
|
|
48
|
+
field* — and choosing *Contact* leaves only contact properties. The prefix
|
|
49
|
+
disappears, since it is no longer telling you anything. In `schema.json` that
|
|
50
|
+
setting reads:
|
|
49
51
|
|
|
50
52
|
```json
|
|
51
53
|
{
|
|
@@ -57,11 +59,46 @@ the prefix disappears, since it is no longer telling you anything:
|
|
|
57
59
|
}
|
|
58
60
|
```
|
|
59
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
|
+
|
|
60
71
|
Read-only properties — the ones HubSpot computes and always refuses to
|
|
61
72
|
accept — are filtered out, so the list only ever offers things that will
|
|
62
73
|
actually work. When the portal id is readable, each selected property gets a
|
|
63
74
|
**Voir dans HubSpot** link straight to its settings page.
|
|
64
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
|
+
|
|
65
102
|
### Validation on save
|
|
66
103
|
|
|
67
104
|
Point the plugin at the content types that carry mappings and it walks each
|
|
@@ -69,7 +106,82 @@ entry before it is written, at any depth — steps, repeatable components, dynam
|
|
|
69
106
|
zones. An invalid mapping is refused with a message that says which property and
|
|
70
107
|
why:
|
|
71
108
|
|
|
72
|
-
>
|
|
109
|
+
> Invalid HubSpot mapping — "name" exists on company, not on contact
|
|
110
|
+
|
|
111
|
+
It catches four things:
|
|
112
|
+
|
|
113
|
+
| Code | Case |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `unknown` | The property doesn't exist in the portal |
|
|
116
|
+
| `wrong-object` | It exists, but on another object |
|
|
117
|
+
| `whitespace` | It exists, but the stored value has surrounding spaces |
|
|
118
|
+
| `bad-option` | The field offers a choice the enumeration doesn't accept |
|
|
119
|
+
|
|
120
|
+
That last one closes a real hole: HubSpot refuses a value outside an
|
|
121
|
+
enumeration exactly as hard as it refuses an unknown property, so a select whose
|
|
122
|
+
choices drifted from the CRM fails at send time with nothing to show for it.
|
|
123
|
+
Point `optionsField` at the repeatable holding the choices (default `options`,
|
|
124
|
+
each `{ value?, label? }`) and they are checked too.
|
|
125
|
+
|
|
126
|
+
Each problem is also carried as a structured code in the error's `details`, so a
|
|
127
|
+
host app can localize the message instead of parsing the sentence.
|
|
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).
|
|
73
185
|
|
|
74
186
|
### A settings screen
|
|
75
187
|
|
|
@@ -78,6 +190,11 @@ never returned to the browser: the UI only receives whether a key exists, where
|
|
|
78
190
|
it comes from, and its last four characters. A "Test connection" button
|
|
79
191
|
round-trips to HubSpot and reports how many properties it can read.
|
|
80
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
|
+
|
|
81
198
|
> **On storage:** the key is kept in Strapi's core store, which is **not
|
|
82
199
|
> encrypted at rest** — it is readable by anyone with database access. It never
|
|
83
200
|
> reaches the browser, but treat it like any other secret in your database. Use
|
|
@@ -111,6 +228,9 @@ export default ({ env }) => ({
|
|
|
111
228
|
uid: "api::form.form",
|
|
112
229
|
objectField: "hsObject", // holds "contact" | "company"
|
|
113
230
|
propertyField: "hsProperty", // holds the property name
|
|
231
|
+
optionsField: "options", // optional — the field's choices
|
|
232
|
+
strict: true, // optional — false lets `unknown`
|
|
233
|
+
// properties through with a warning
|
|
114
234
|
},
|
|
115
235
|
],
|
|
116
236
|
},
|
|
@@ -180,7 +300,7 @@ This one never blocks work:
|
|
|
180
300
|
|---|---|
|
|
181
301
|
| No API key configured | The field falls back to a plain text input, with a note explaining why |
|
|
182
302
|
| HubSpot unreachable | Saving proceeds; validation is skipped and a warning is logged |
|
|
183
|
-
| 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 |
|
|
184
304
|
| An object's scope is missing | That object is skipped; the others still work |
|
|
185
305
|
| Portal id unreadable | Deep links are omitted; everything else is unaffected |
|
|
186
306
|
| Plugin uninstalled | Values remain as strings — nothing to undo |
|
|
@@ -191,15 +311,35 @@ key drops the cache immediately.
|
|
|
191
311
|
|
|
192
312
|
## Admin API
|
|
193
313
|
|
|
194
|
-
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.
|
|
195
316
|
|
|
196
317
|
| Method | Path | Purpose |
|
|
197
318
|
|---|---|---|
|
|
198
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 |
|
|
199
323
|
| `GET` | `/hubspot/settings` | Whether a key exists, its source and hint — never the key |
|
|
200
324
|
| `PUT` | `/hubspot/settings` | Save a key (`{ apiKey }`) |
|
|
201
325
|
| `DELETE` | `/hubspot/settings` | Remove the stored key |
|
|
202
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
|
+
|
|
203
343
|
## Compatibility
|
|
204
344
|
|
|
205
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;
|