strapi-plugin-hubspot 0.1.0 → 0.2.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 +42 -7
- package/dist/_chunks/{HubspotPropertyInput-B0HpXoqz.js → HubspotPropertyInput-BcdqfAfv.js} +47 -25
- package/dist/_chunks/HubspotPropertyInput-D0gijgli.mjs +106 -0
- package/dist/_chunks/{Settings-BgRpG_a6.js → Settings-DedA0z6w.js} +1 -1
- package/dist/_chunks/{Settings-Byupubny.mjs → Settings-DuYUpD8L.mjs} +1 -1
- package/dist/_chunks/{index-CER4BKjS.mjs → index-DIwNOVQ0.mjs} +2 -2
- package/dist/_chunks/{index-DVRLoCa1.js → index-sOi9wC_y.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 +106 -30
- package/dist/server/index.mjs +106 -30
- package/dist/server/src/index.d.ts +1 -0
- package/dist/server/src/properties.d.ts +36 -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,16 @@ 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, which turns on the *view in HubSpot*
|
|
148
|
+
links.
|
|
116
149
|
|
|
117
150
|
The key is resolved in this order, first match wins:
|
|
118
151
|
|
|
@@ -140,6 +173,8 @@ This one never blocks work:
|
|
|
140
173
|
| No API key configured | The field falls back to a plain text input, with a note explaining why |
|
|
141
174
|
| HubSpot unreachable | Saving proceeds; validation is skipped and a warning is logged |
|
|
142
175
|
| Property staged in HubSpot but not created yet | The picker accepts a typed value (`creatable`) |
|
|
176
|
+
| An object's scope is missing | That object is skipped; the others still work |
|
|
177
|
+
| Portal id unreadable | Deep links are omitted; everything else is unaffected |
|
|
143
178
|
| Plugin uninstalled | Values remain as strings — nothing to undo |
|
|
144
179
|
|
|
145
180
|
The property schema is cached for 10 minutes and de-duplicated across concurrent
|
|
@@ -152,7 +187,7 @@ All routes require an authenticated admin.
|
|
|
152
187
|
|
|
153
188
|
| Method | Path | Purpose |
|
|
154
189
|
|---|---|---|
|
|
155
|
-
| `GET` | `/hubspot/properties` | Writable properties
|
|
190
|
+
| `GET` | `/hubspot/properties` | Writable properties, readable objects, unreachable ones and the portal id. `?refresh=1` bypasses the cache |
|
|
156
191
|
| `GET` | `/hubspot/settings` | Whether a key exists, its source and hint — never the key |
|
|
157
192
|
| `PUT` | `/hubspot/settings` | Save a key (`{ apiKey }`) |
|
|
158
193
|
| `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-sOi9wC_y.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://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-DIwNOVQ0.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://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
|
+
};
|
|
@@ -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-sOi9wC_y.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" } });
|
|
@@ -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-DIwNOVQ0.mjs";
|
|
6
6
|
const SOURCE_LABEL = {
|
|
7
7
|
settings: "saisie ici",
|
|
8
8
|
config: "config/plugins.ts",
|
|
@@ -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-D0gijgli.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-DuYUpD8L.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-BcdqfAfv.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-DedA0z6w.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,91 @@
|
|
|
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 fetchPortalId(apiKey) {
|
|
43
|
+
try {
|
|
44
|
+
const { portalId } = await hsGet(apiKey, "/account-info/v3/details");
|
|
45
|
+
return portalId;
|
|
46
|
+
} catch {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
51
|
+
if (!force && cache && Date.now() - cache.at < TTL_MS) return cache.schema;
|
|
29
52
|
if (!force && inFlight) return inFlight;
|
|
30
53
|
inFlight = (async () => {
|
|
31
54
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
const settled = await Promise.all(
|
|
56
|
+
objects.map(async (object) => {
|
|
57
|
+
try {
|
|
58
|
+
return { object, properties: await fetchObject(apiKey, object) };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return { object, error: err.message };
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
const properties = [];
|
|
65
|
+
const available = [];
|
|
66
|
+
const unavailable = [];
|
|
67
|
+
for (const entry of settled) {
|
|
68
|
+
if ("error" in entry && entry.error) {
|
|
69
|
+
unavailable.push({ object: entry.object.name, reason: entry.error });
|
|
70
|
+
strapi.log.warn(`[hubspot] ${entry.object.name} illisible — ${entry.error}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
available.push(entry.object.name);
|
|
74
|
+
properties.push(...entry.properties ?? []);
|
|
75
|
+
}
|
|
76
|
+
if (!available.length) {
|
|
77
|
+
throw new Error(unavailable[0]?.reason ?? "Aucun objet lisible");
|
|
78
|
+
}
|
|
79
|
+
properties.sort(
|
|
37
80
|
(a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
|
|
38
81
|
);
|
|
39
|
-
|
|
40
|
-
|
|
82
|
+
const schema = {
|
|
83
|
+
properties,
|
|
84
|
+
objects: available,
|
|
85
|
+
unavailable,
|
|
86
|
+
portalId: await fetchPortalId(apiKey)
|
|
87
|
+
};
|
|
88
|
+
cache = { at: Date.now(), schema };
|
|
89
|
+
return schema;
|
|
41
90
|
} finally {
|
|
42
91
|
inFlight = null;
|
|
43
92
|
}
|
|
@@ -52,12 +101,28 @@ function checkProperty(properties, object, name) {
|
|
|
52
101
|
if (!trimmed) return null;
|
|
53
102
|
const match = properties.find((p) => p.object === object && p.name === trimmed);
|
|
54
103
|
if (match) return trimmed === name ? null : `« ${name} » contient un espace superflu`;
|
|
55
|
-
const onOther = properties.find((p) => p.
|
|
104
|
+
const onOther = properties.find((p) => p.name === trimmed);
|
|
56
105
|
if (onOther) {
|
|
57
|
-
return `« ${trimmed} » existe sur l'objet ${onOther.object
|
|
106
|
+
return `« ${trimmed} » existe sur l'objet ${onOther.object}, pas sur ${object}`;
|
|
58
107
|
}
|
|
59
108
|
return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
|
|
60
109
|
}
|
|
110
|
+
function resolveObjects(configured) {
|
|
111
|
+
if (!Array.isArray(configured) || !configured.length) {
|
|
112
|
+
return STANDARD_OBJECTS.filter((o) => o.name === "contact" || o.name === "company");
|
|
113
|
+
}
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const entry of configured) {
|
|
116
|
+
if (typeof entry === "string") {
|
|
117
|
+
const known = STANDARD_OBJECTS.find((o) => o.name === entry || o.path === entry);
|
|
118
|
+
out.push(known ?? { name: entry, path: entry });
|
|
119
|
+
} else if (entry && typeof entry === "object" && "name" in entry) {
|
|
120
|
+
const e = entry;
|
|
121
|
+
out.push({ name: e.name, path: e.path || e.name });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
61
126
|
const ENV_VAR = "HUBSPOT_API_KEY";
|
|
62
127
|
const store = (strapi) => strapi.store({ type: "plugin", name: "hubspot" });
|
|
63
128
|
async function getStoredSettings(strapi) {
|
|
@@ -87,6 +152,9 @@ async function publicSettings(strapi) {
|
|
|
87
152
|
const config = {
|
|
88
153
|
default: {
|
|
89
154
|
apiKey: "",
|
|
155
|
+
// Objects whose properties are offered. Names from the standard set, or
|
|
156
|
+
// `{ name, path }` for a custom object type.
|
|
157
|
+
objects: ["contact", "company"],
|
|
90
158
|
validate: []
|
|
91
159
|
},
|
|
92
160
|
validator(cfg) {
|
|
@@ -100,14 +168,17 @@ const controllers = {
|
|
|
100
168
|
async list(ctx) {
|
|
101
169
|
const { apiKey } = await resolveApiKey(strapi);
|
|
102
170
|
if (!apiKey) {
|
|
103
|
-
ctx.body = { configured: false, properties: [] };
|
|
171
|
+
ctx.body = { configured: false, properties: [], objects: [], unavailable: [] };
|
|
104
172
|
return;
|
|
105
173
|
}
|
|
106
174
|
try {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
175
|
+
const schema = await loadSchema(
|
|
176
|
+
strapi,
|
|
177
|
+
apiKey,
|
|
178
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", [])),
|
|
179
|
+
{ force: ctx.query.refresh === "1" }
|
|
180
|
+
);
|
|
181
|
+
ctx.body = { configured: true, ...schema };
|
|
111
182
|
} catch (err) {
|
|
112
183
|
strapi.log.error(`[hubspot] ${err.message}`);
|
|
113
184
|
ctx.throw(502, "Impossible de joindre HubSpot — vérifiez la clé API.");
|
|
@@ -155,8 +226,9 @@ function collectMappings(node, target, found = []) {
|
|
|
155
226
|
const obj = node;
|
|
156
227
|
const property = obj[target.propertyField];
|
|
157
228
|
if (typeof property === "string" && property.trim()) {
|
|
229
|
+
const object = obj[target.objectField];
|
|
158
230
|
found.push({
|
|
159
|
-
object:
|
|
231
|
+
object: typeof object === "string" && object ? object : "contact",
|
|
160
232
|
property
|
|
161
233
|
});
|
|
162
234
|
}
|
|
@@ -185,14 +257,18 @@ const index = {
|
|
|
185
257
|
if (!mappings.length) return next();
|
|
186
258
|
const { apiKey } = await resolveApiKey(strapi);
|
|
187
259
|
if (!apiKey) return next();
|
|
188
|
-
let
|
|
260
|
+
let schema;
|
|
189
261
|
try {
|
|
190
|
-
|
|
262
|
+
schema = await loadSchema(
|
|
263
|
+
strapi,
|
|
264
|
+
apiKey,
|
|
265
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
266
|
+
);
|
|
191
267
|
} catch {
|
|
192
268
|
strapi.log.warn("[hubspot] schéma indisponible — validation ignorée");
|
|
193
269
|
return next();
|
|
194
270
|
}
|
|
195
|
-
const problems = mappings.map((m) => checkProperty(properties, m.object, m.property)).filter((reason) => Boolean(reason));
|
|
271
|
+
const problems = mappings.map((m) => checkProperty(schema.properties, m.object, m.property)).filter((reason) => Boolean(reason));
|
|
196
272
|
if (problems.length) {
|
|
197
273
|
throw new utils.errors.ValidationError(
|
|
198
274
|
`Mapping HubSpot invalide — ${[...new Set(problems)].join(" ; ")}`
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,42 +1,91 @@
|
|
|
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 fetchPortalId(apiKey) {
|
|
42
|
+
try {
|
|
43
|
+
const { portalId } = await hsGet(apiKey, "/account-info/v3/details");
|
|
44
|
+
return portalId;
|
|
45
|
+
} catch {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function loadSchema(strapi, apiKey, objects, { force = false } = {}) {
|
|
50
|
+
if (!force && cache && Date.now() - cache.at < TTL_MS) return cache.schema;
|
|
28
51
|
if (!force && inFlight) return inFlight;
|
|
29
52
|
inFlight = (async () => {
|
|
30
53
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
const settled = await Promise.all(
|
|
55
|
+
objects.map(async (object) => {
|
|
56
|
+
try {
|
|
57
|
+
return { object, properties: await fetchObject(apiKey, object) };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return { object, error: err.message };
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
const properties = [];
|
|
64
|
+
const available = [];
|
|
65
|
+
const unavailable = [];
|
|
66
|
+
for (const entry of settled) {
|
|
67
|
+
if ("error" in entry && entry.error) {
|
|
68
|
+
unavailable.push({ object: entry.object.name, reason: entry.error });
|
|
69
|
+
strapi.log.warn(`[hubspot] ${entry.object.name} illisible — ${entry.error}`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
available.push(entry.object.name);
|
|
73
|
+
properties.push(...entry.properties ?? []);
|
|
74
|
+
}
|
|
75
|
+
if (!available.length) {
|
|
76
|
+
throw new Error(unavailable[0]?.reason ?? "Aucun objet lisible");
|
|
77
|
+
}
|
|
78
|
+
properties.sort(
|
|
36
79
|
(a, b) => a.object.localeCompare(b.object) || a.label.localeCompare(b.label)
|
|
37
80
|
);
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
const schema = {
|
|
82
|
+
properties,
|
|
83
|
+
objects: available,
|
|
84
|
+
unavailable,
|
|
85
|
+
portalId: await fetchPortalId(apiKey)
|
|
86
|
+
};
|
|
87
|
+
cache = { at: Date.now(), schema };
|
|
88
|
+
return schema;
|
|
40
89
|
} finally {
|
|
41
90
|
inFlight = null;
|
|
42
91
|
}
|
|
@@ -51,12 +100,28 @@ function checkProperty(properties, object, name) {
|
|
|
51
100
|
if (!trimmed) return null;
|
|
52
101
|
const match = properties.find((p) => p.object === object && p.name === trimmed);
|
|
53
102
|
if (match) return trimmed === name ? null : `« ${name} » contient un espace superflu`;
|
|
54
|
-
const onOther = properties.find((p) => p.
|
|
103
|
+
const onOther = properties.find((p) => p.name === trimmed);
|
|
55
104
|
if (onOther) {
|
|
56
|
-
return `« ${trimmed} » existe sur l'objet ${onOther.object
|
|
105
|
+
return `« ${trimmed} » existe sur l'objet ${onOther.object}, pas sur ${object}`;
|
|
57
106
|
}
|
|
58
107
|
return `« ${trimmed} » n'existe pas dans ce portail HubSpot`;
|
|
59
108
|
}
|
|
109
|
+
function resolveObjects(configured) {
|
|
110
|
+
if (!Array.isArray(configured) || !configured.length) {
|
|
111
|
+
return STANDARD_OBJECTS.filter((o) => o.name === "contact" || o.name === "company");
|
|
112
|
+
}
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const entry of configured) {
|
|
115
|
+
if (typeof entry === "string") {
|
|
116
|
+
const known = STANDARD_OBJECTS.find((o) => o.name === entry || o.path === entry);
|
|
117
|
+
out.push(known ?? { name: entry, path: entry });
|
|
118
|
+
} else if (entry && typeof entry === "object" && "name" in entry) {
|
|
119
|
+
const e = entry;
|
|
120
|
+
out.push({ name: e.name, path: e.path || e.name });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
60
125
|
const ENV_VAR = "HUBSPOT_API_KEY";
|
|
61
126
|
const store = (strapi) => strapi.store({ type: "plugin", name: "hubspot" });
|
|
62
127
|
async function getStoredSettings(strapi) {
|
|
@@ -86,6 +151,9 @@ async function publicSettings(strapi) {
|
|
|
86
151
|
const config = {
|
|
87
152
|
default: {
|
|
88
153
|
apiKey: "",
|
|
154
|
+
// Objects whose properties are offered. Names from the standard set, or
|
|
155
|
+
// `{ name, path }` for a custom object type.
|
|
156
|
+
objects: ["contact", "company"],
|
|
89
157
|
validate: []
|
|
90
158
|
},
|
|
91
159
|
validator(cfg) {
|
|
@@ -99,14 +167,17 @@ const controllers = {
|
|
|
99
167
|
async list(ctx) {
|
|
100
168
|
const { apiKey } = await resolveApiKey(strapi);
|
|
101
169
|
if (!apiKey) {
|
|
102
|
-
ctx.body = { configured: false, properties: [] };
|
|
170
|
+
ctx.body = { configured: false, properties: [], objects: [], unavailable: [] };
|
|
103
171
|
return;
|
|
104
172
|
}
|
|
105
173
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
174
|
+
const schema = await loadSchema(
|
|
175
|
+
strapi,
|
|
176
|
+
apiKey,
|
|
177
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", [])),
|
|
178
|
+
{ force: ctx.query.refresh === "1" }
|
|
179
|
+
);
|
|
180
|
+
ctx.body = { configured: true, ...schema };
|
|
110
181
|
} catch (err) {
|
|
111
182
|
strapi.log.error(`[hubspot] ${err.message}`);
|
|
112
183
|
ctx.throw(502, "Impossible de joindre HubSpot — vérifiez la clé API.");
|
|
@@ -154,8 +225,9 @@ function collectMappings(node, target, found = []) {
|
|
|
154
225
|
const obj = node;
|
|
155
226
|
const property = obj[target.propertyField];
|
|
156
227
|
if (typeof property === "string" && property.trim()) {
|
|
228
|
+
const object = obj[target.objectField];
|
|
157
229
|
found.push({
|
|
158
|
-
object:
|
|
230
|
+
object: typeof object === "string" && object ? object : "contact",
|
|
159
231
|
property
|
|
160
232
|
});
|
|
161
233
|
}
|
|
@@ -184,14 +256,18 @@ const index = {
|
|
|
184
256
|
if (!mappings.length) return next();
|
|
185
257
|
const { apiKey } = await resolveApiKey(strapi);
|
|
186
258
|
if (!apiKey) return next();
|
|
187
|
-
let
|
|
259
|
+
let schema;
|
|
188
260
|
try {
|
|
189
|
-
|
|
261
|
+
schema = await loadSchema(
|
|
262
|
+
strapi,
|
|
263
|
+
apiKey,
|
|
264
|
+
resolveObjects(strapi.plugin("hubspot").config("objects", []))
|
|
265
|
+
);
|
|
190
266
|
} catch {
|
|
191
267
|
strapi.log.warn("[hubspot] schéma indisponible — validation ignorée");
|
|
192
268
|
return next();
|
|
193
269
|
}
|
|
194
|
-
const problems = mappings.map((m) => checkProperty(properties, m.object, m.property)).filter((reason) => Boolean(reason));
|
|
270
|
+
const problems = mappings.map((m) => checkProperty(schema.properties, m.object, m.property)).filter((reason) => Boolean(reason));
|
|
195
271
|
if (problems.length) {
|
|
196
272
|
throw new errors.ValidationError(
|
|
197
273
|
`Mapping HubSpot invalide — ${[...new Set(problems)].join(" ; ")}`
|
|
@@ -1,23 +1,52 @@
|
|
|
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
|
+
/**
|
|
34
|
+
* Writable properties of every configured object, cached and de-duplicated
|
|
35
|
+
* across concurrent calls.
|
|
36
|
+
*
|
|
37
|
+
* An object the token can't read (missing scope) is reported in `unavailable`
|
|
38
|
+
* instead of failing the whole call: a portal that only granted contact scopes
|
|
39
|
+
* must still get a working contact picker.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadSchema(strapi: Core.Strapi, apiKey: string, objects: HsObjectDef[], { force }?: {
|
|
15
42
|
force?: boolean;
|
|
16
|
-
}): Promise<
|
|
17
|
-
/** Drop the cache — used after the API key changes. */
|
|
43
|
+
}): Promise<Schema>;
|
|
44
|
+
/** Drop the cache — used after the API key or the object list changes. */
|
|
18
45
|
export declare function clearCache(): void;
|
|
19
46
|
/**
|
|
20
47
|
* Why a property can't be written, or `null` when it's fine. Returning a reason
|
|
21
48
|
* rather than a boolean lets the caller tell an editor what to fix.
|
|
22
49
|
*/
|
|
23
|
-
export declare function checkProperty(properties: HsProperty[], object:
|
|
50
|
+
export declare function checkProperty(properties: HsProperty[], object: string, name: string): string | null;
|
|
51
|
+
/** Resolve the configured object list, accepting names or full definitions. */
|
|
52
|
+
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.
|
|
4
|
-
"description": "HubSpot integration for Strapi — pick CRM properties from a searchable list
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
-
};
|