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 +50 -7
- package/dist/_chunks/{HubspotPropertyInput-B0HpXoqz.js → HubspotPropertyInput-BVFRtt49.js} +47 -25
- package/dist/_chunks/HubspotPropertyInput-C7Wcw8ee.mjs +106 -0
- package/dist/_chunks/{Settings-Byupubny.mjs → Settings-B5w7LAsH.mjs} +1 -1
- package/dist/_chunks/{Settings-BgRpG_a6.js → Settings-TGB0MOHr.js} +1 -1
- package/dist/_chunks/{index-CER4BKjS.mjs → index-BpZtcsPM.mjs} +2 -2
- package/dist/_chunks/{index-DVRLoCa1.js → index-DgsAHPui.js} +2 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/components/HubspotPropertyInput.d.ts +14 -10
- package/dist/admin/src/objectLabels.d.ts +1 -0
- package/dist/server/index.js +110 -30
- package/dist/server/index.mjs +110 -30
- package/dist/server/src/index.d.ts +1 -0
- package/dist/server/src/properties.d.ts +43 -7
- package/package.json +2 -2
- package/dist/_chunks/HubspotPropertyInput-CTZYZS_7.mjs +0 -84
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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 [
|
|
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)
|
|
40
|
-
setConfigured(data.configured);
|
|
41
|
-
setProperties(data.properties ?? []);
|
|
53
|
+
if (!cancelled) setSchema(data);
|
|
42
54
|
}).catch((err) => {
|
|
43
55
|
if (cancelled) return;
|
|
44
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
33
|
+
Component: async () => (await Promise.resolve().then(() => require("./Settings-TGB0MOHr.js"))).default
|
|
34
34
|
}
|
|
35
35
|
]
|
|
36
36
|
);
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
|
@@ -9,26 +9,30 @@ interface InputProps {
|
|
|
9
9
|
type: string;
|
|
10
10
|
};
|
|
11
11
|
}) => void;
|
|
12
|
-
attribute?:
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
31
|
-
*
|
|
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;
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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
|
|
12
|
-
const res = await fetch(`${HS_BASE}
|
|
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
|
|
28
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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.
|
|
108
|
+
const onOther = properties.find((p) => p.name === trimmed);
|
|
56
109
|
if (onOther) {
|
|
57
|
-
return `« ${trimmed} » existe sur l'objet ${onOther.object
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
|
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
|
|
264
|
+
let schema;
|
|
189
265
|
try {
|
|
190
|
-
|
|
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(" ; ")}`
|
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
|
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
|
|
11
|
-
const res = await fetch(`${HS_BASE}
|
|
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
|
|
27
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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.
|
|
107
|
+
const onOther = properties.find((p) => p.name === trimmed);
|
|
55
108
|
if (onOther) {
|
|
56
|
-
return `« ${trimmed} » existe sur l'objet ${onOther.object
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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:
|
|
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
|
|
263
|
+
let schema;
|
|
188
264
|
try {
|
|
189
|
-
|
|
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(" ; ")}`
|
|
@@ -1,23 +1,59 @@
|
|
|
1
1
|
import type { Core } from "@strapi/strapi";
|
|
2
|
-
export
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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<
|
|
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:
|
|
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
|
|
4
|
-
"description": "HubSpot integration for Strapi — pick CRM properties from a searchable list
|
|
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
|
-
};
|