strapi-plugin-hubspot 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Lefizelier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # Strapi HubSpot
2
+
3
+ **Stop typing HubSpot property names from memory.** This plugin turns any CRM
4
+ property field in your content types into a searchable picker fed by your actual
5
+ portal, and refuses a bad mapping at save time instead of letting it fail
6
+ silently three weeks later.
7
+
8
+ ## The problem it solves
9
+
10
+ If you build lead forms in Strapi and push them to HubSpot, somewhere in your
11
+ schema there is a field where an editor types a property name — `hs_role`,
12
+ `numberofemployees`, `jobtitle`. It is a free-text field, and nothing checks it.
13
+
14
+ That matters more than it looks, because of how the HubSpot API behaves:
15
+
16
+ > **A single unknown property makes HubSpot reject the entire upsert.**
17
+
18
+ So one typo doesn't cost you one answer. It costs you the whole lead. The
19
+ submission is accepted by your site, stored in Strapi, and never reaches the
20
+ CRM — with no error anyone will notice until someone asks why the pipeline is
21
+ empty.
22
+
23
+ The three ways this happens are all invisible in a text input:
24
+
25
+ | What you typed | What's wrong |
26
+ |---|---|
27
+ | `hs_rôle` | Doesn't exist — a typo, an autocorrect, a copy-paste |
28
+ | `role ` | Exists, but with a trailing space |
29
+ | `name` | Exists, but on **Company** — you mapped it to Contact |
30
+
31
+ This plugin makes all three impossible to save.
32
+
33
+ ## What you get
34
+
35
+ ### A property picker instead of a text field
36
+
37
+ Replace your property field's type with the `hubspot.property` custom field.
38
+ Editors then search the real, writable properties of your portal, each labelled
39
+ with the object it belongs to:
40
+
41
+ ```
42
+ Contact · Rôle (hs_role)
43
+ Contact · Prénom (firstname)
44
+ Société · Nombre d'employés (numberofemployees)
45
+ ```
46
+
47
+ Read-only properties — the ones HubSpot computes and always refuses to
48
+ accept — are filtered out, so the list only ever offers things that will
49
+ actually work.
50
+
51
+ ### Validation on save
52
+
53
+ Point the plugin at the content types that carry mappings and it walks each
54
+ entry before it is written, at any depth — steps, repeatable components, dynamic
55
+ zones. An invalid mapping is refused with a message that says which property and
56
+ why:
57
+
58
+ > Mapping HubSpot invalide — « name » existe sur l'objet Société, pas sur Contact
59
+
60
+ ### A settings screen
61
+
62
+ **Settings → HubSpot** holds the private app token. It is stored server-side and
63
+ never returned to the browser: the UI only receives whether a key exists, where
64
+ it comes from, and its last four characters. A "Test connection" button
65
+ round-trips to HubSpot and reports how many properties it can read.
66
+
67
+ ## Install
68
+
69
+ ```bash
70
+ npm install strapi-plugin-hubspot
71
+ ```
72
+
73
+ Enable it in `config/plugins.ts`:
74
+
75
+ ```ts
76
+ export default ({ env }) => ({
77
+ hubspot: {
78
+ enabled: true,
79
+ config: {
80
+ // Optional — the key can also be set from Settings → HubSpot.
81
+ apiKey: env("HUBSPOT_API_KEY", ""),
82
+
83
+ // Optional — entries whose mappings are validated on save.
84
+ validate: [
85
+ {
86
+ uid: "api::form.form",
87
+ objectField: "hsObject", // holds "contact" | "company"
88
+ propertyField: "hsProperty", // holds the property name
89
+ },
90
+ ],
91
+ },
92
+ },
93
+ });
94
+ ```
95
+
96
+ Then point your property field at the custom field, in the component or content
97
+ type that holds it:
98
+
99
+ ```json
100
+ {
101
+ "hsProperty": {
102
+ "type": "customField",
103
+ "customField": "plugin::hubspot.property"
104
+ }
105
+ }
106
+ ```
107
+
108
+ Restart Strapi and hard-refresh the admin.
109
+
110
+ ### The token
111
+
112
+ Create a **private app** in HubSpot with these scopes:
113
+
114
+ - `crm.schemas.contacts.read`
115
+ - `crm.schemas.companies.read`
116
+
117
+ The key is resolved in this order, first match wins:
118
+
119
+ 1. saved from **Settings → HubSpot**
120
+ 2. `config.apiKey` in `config/plugins.ts`
121
+ 3. the `HUBSPOT_API_KEY` environment variable
122
+
123
+ ## Migrating an existing field
124
+
125
+ The custom field is backed by a plain `string`. Switching an existing text field
126
+ to it needs **no migration**: every value already saved stays valid and
127
+ selectable, and uninstalling the plugin leaves readable data behind.
128
+
129
+ A stored value your portal doesn't recognise — typed before you installed this,
130
+ or since deleted in HubSpot — stays selected and is flagged *inconnue du portail*
131
+ rather than being silently dropped on the next save.
132
+
133
+ ## How it degrades
134
+
135
+ A plugin that sits between your editors and their content has to fail quietly.
136
+ This one never blocks work:
137
+
138
+ | Situation | Behaviour |
139
+ |---|---|
140
+ | No API key configured | The field falls back to a plain text input, with a note explaining why |
141
+ | HubSpot unreachable | Saving proceeds; validation is skipped and a warning is logged |
142
+ | Property staged in HubSpot but not created yet | The picker accepts a typed value (`creatable`) |
143
+ | Plugin uninstalled | Values remain as strings — nothing to undo |
144
+
145
+ The property schema is cached for 10 minutes and de-duplicated across concurrent
146
+ requests, so a busy Content Manager doesn't hammer the HubSpot API. Saving a new
147
+ key drops the cache immediately.
148
+
149
+ ## Admin API
150
+
151
+ All routes require an authenticated admin.
152
+
153
+ | Method | Path | Purpose |
154
+ |---|---|---|
155
+ | `GET` | `/hubspot/properties` | Writable properties of both objects. `?refresh=1` bypasses the cache |
156
+ | `GET` | `/hubspot/settings` | Whether a key exists, its source and hint — never the key |
157
+ | `PUT` | `/hubspot/settings` | Save a key (`{ apiKey }`) |
158
+ | `DELETE` | `/hubspot/settings` | Remove the stored key |
159
+
160
+ ## Compatibility
161
+
162
+ Strapi **v5**. Node **>= 18**.
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const designSystem = require("@strapi/design-system");
6
+ const admin = require("@strapi/strapi/admin");
7
+ const index = require("./index-DVRLoCa1.js");
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
11
+ if (e) {
12
+ for (const k in e) {
13
+ if (k !== "default") {
14
+ const d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: () => e[k]
18
+ });
19
+ }
20
+ }
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+ const React__namespace = /* @__PURE__ */ _interopNamespace(React);
26
+ const OBJECT_LABEL = {
27
+ contact: "Contact",
28
+ company: "Société"
29
+ };
30
+ const HubspotPropertyInput = React__namespace.forwardRef(
31
+ ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
32
+ const { get } = admin.useFetchClient();
33
+ const [properties, setProperties] = React__namespace.useState([]);
34
+ const [configured, setConfigured] = React__namespace.useState(null);
35
+ const [loadError, setLoadError] = React__namespace.useState("");
36
+ React__namespace.useEffect(() => {
37
+ let cancelled = false;
38
+ get(`/${index.PLUGIN_ID}/properties`).then(({ data }) => {
39
+ if (cancelled) return;
40
+ setConfigured(data.configured);
41
+ setProperties(data.properties ?? []);
42
+ }).catch((err) => {
43
+ if (cancelled) return;
44
+ setConfigured(false);
45
+ setLoadError(
46
+ err?.response?.data?.error?.message ?? "Propriétés HubSpot indisponibles"
47
+ );
48
+ });
49
+ return () => {
50
+ cancelled = true;
51
+ };
52
+ }, [get]);
53
+ const emit = (next) => onChange({ target: { name, value: next ?? "", type: "string" } });
54
+ const describe = () => {
55
+ if (loadError) return loadError;
56
+ if (configured === false) {
57
+ return "Aucune clé API HubSpot — saisie libre. Renseignez-la dans Réglages → HubSpot.";
58
+ }
59
+ return hint;
60
+ };
61
+ const options = React__namespace.useMemo(() => {
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]);
71
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name, error, hint: describe(), required, children: [
72
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { action: labelAction, children: label }),
73
+ configured === null ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { paddingTop: 2, paddingBottom: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: "Chargement des propriétés HubSpot…" }) }) : configured ? /* @__PURE__ */ jsxRuntime.jsx(
74
+ designSystem.Combobox,
75
+ {
76
+ ref,
77
+ value: value || "",
78
+ onChange: emit,
79
+ onClear: () => emit(""),
80
+ disabled,
81
+ placeholder: "Rechercher une propriété…",
82
+ creatable: true,
83
+ onCreateOption: emit,
84
+ children: options.map((o) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: o.value, children: o.label }, o.value))
85
+ }
86
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
87
+ designSystem.Field.Input,
88
+ {
89
+ ref,
90
+ value: value || "",
91
+ onChange: (e) => emit(e.target.value),
92
+ disabled,
93
+ placeholder: "ex. hs_role, numberofemployees"
94
+ }
95
+ ),
96
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
97
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
98
+ ] });
99
+ }
100
+ );
101
+ HubspotPropertyInput.displayName = "HubspotPropertyInput";
102
+ exports.default = HubspotPropertyInput;
@@ -0,0 +1,84 @@
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
+ };
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const designSystem = require("@strapi/design-system");
6
+ const admin = require("@strapi/strapi/admin");
7
+ const index = require("./index-DVRLoCa1.js");
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
11
+ if (e) {
12
+ for (const k in e) {
13
+ if (k !== "default") {
14
+ const d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: () => e[k]
18
+ });
19
+ }
20
+ }
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+ const React__namespace = /* @__PURE__ */ _interopNamespace(React);
26
+ const SOURCE_LABEL = {
27
+ settings: "saisie ici",
28
+ config: "config/plugins.ts",
29
+ env: "variable d'environnement"
30
+ };
31
+ const HubspotSettings = () => {
32
+ const { get, put, del } = admin.useFetchClient();
33
+ const [settings, setSettings] = React__namespace.useState(null);
34
+ const [apiKey, setApiKey] = React__namespace.useState("");
35
+ const [busy, setBusy] = React__namespace.useState(false);
36
+ const [feedback, setFeedback] = React__namespace.useState(
37
+ null
38
+ );
39
+ const load = React__namespace.useCallback(async () => {
40
+ const { data } = await get(`/${index.PLUGIN_ID}/settings`);
41
+ setSettings(data);
42
+ }, [get]);
43
+ React__namespace.useEffect(() => {
44
+ load().catch(() => setFeedback({ tone: "danger", text: "Réglages illisibles." }));
45
+ }, [load]);
46
+ const save = async () => {
47
+ setBusy(true);
48
+ setFeedback(null);
49
+ try {
50
+ const { data } = await put(`/${index.PLUGIN_ID}/settings`, { apiKey });
51
+ setSettings(data);
52
+ setApiKey("");
53
+ setFeedback({ tone: "success", text: "Clé enregistrée." });
54
+ } catch {
55
+ setFeedback({ tone: "danger", text: "Enregistrement impossible." });
56
+ } finally {
57
+ setBusy(false);
58
+ }
59
+ };
60
+ const remove = async () => {
61
+ setBusy(true);
62
+ setFeedback(null);
63
+ try {
64
+ const { data } = await del(`/${index.PLUGIN_ID}/settings`);
65
+ setSettings(data);
66
+ setApiKey("");
67
+ setFeedback({ tone: "success", text: "Clé supprimée." });
68
+ } catch {
69
+ setFeedback({ tone: "danger", text: "Suppression impossible." });
70
+ } finally {
71
+ setBusy(false);
72
+ }
73
+ };
74
+ const test = async () => {
75
+ setBusy(true);
76
+ setFeedback(null);
77
+ try {
78
+ const { data } = await get(
79
+ `/${index.PLUGIN_ID}/properties?refresh=1`
80
+ );
81
+ setFeedback(
82
+ data.configured ? { tone: "success", text: `Connexion établie — ${data.properties.length} propriétés lisibles.` } : { tone: "danger", text: "Aucune clé configurée." }
83
+ );
84
+ } catch {
85
+ setFeedback({ tone: "danger", text: "HubSpot injoignable — clé invalide ou révoquée ?" });
86
+ } finally {
87
+ setBusy(false);
88
+ }
89
+ };
90
+ if (!settings) {
91
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: "Chargement…" }) });
92
+ }
93
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 8, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
94
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "flex-start", gap: 2, children: [
95
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", children: "HubSpot" }),
96
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "epsilon", textColor: "neutral600", children: "Le jeton d'application privée utilisé pour lire les propriétés du portail et valider les mappings de formulaires." })
97
+ ] }),
98
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
99
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { active: settings.configured, children: settings.configured ? `Configurée ${settings.hint}` : "Non configurée" }),
100
+ settings.keySource ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
101
+ "source : ",
102
+ SOURCE_LABEL[settings.keySource]
103
+ ] }) : null
104
+ ] }),
105
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { maxWidth: "32rem", children: /* @__PURE__ */ jsxRuntime.jsxs(
106
+ designSystem.Field.Root,
107
+ {
108
+ name: "apiKey",
109
+ hint: "Saisir une clé ici remplace celles venant de config/plugins.ts et de HUBSPOT_API_KEY.",
110
+ children: [
111
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Jeton d'application privée" }),
112
+ /* @__PURE__ */ jsxRuntime.jsx(
113
+ designSystem.Field.Input,
114
+ {
115
+ type: "password",
116
+ value: apiKey,
117
+ autoComplete: "off",
118
+ placeholder: settings.configured ? "•••••••• (laisser vide pour conserver)" : "pat-eu1-…",
119
+ onChange: (e) => setApiKey(e.target.value)
120
+ }
121
+ ),
122
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
123
+ ]
124
+ }
125
+ ) }),
126
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
127
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: save, loading: busy, disabled: !apiKey.trim(), children: "Enregistrer" }),
128
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", onClick: test, loading: busy, disabled: !settings.configured, children: "Tester la connexion" }),
129
+ /* @__PURE__ */ jsxRuntime.jsx(
130
+ designSystem.Button,
131
+ {
132
+ variant: "danger-light",
133
+ onClick: remove,
134
+ loading: busy,
135
+ disabled: settings.keySource !== "settings",
136
+ children: "Supprimer"
137
+ }
138
+ )
139
+ ] }),
140
+ feedback ? /* @__PURE__ */ jsxRuntime.jsx(
141
+ designSystem.Typography,
142
+ {
143
+ variant: "pi",
144
+ textColor: feedback.tone === "success" ? "success600" : "danger600",
145
+ children: feedback.text
146
+ }
147
+ ) : null,
148
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
149
+ "Le jeton a besoin des portées ",
150
+ /* @__PURE__ */ jsxRuntime.jsx("b", { children: "crm.schemas.contacts.read" }),
151
+ " et",
152
+ " ",
153
+ /* @__PURE__ */ jsxRuntime.jsx("b", { children: "crm.schemas.companies.read" }),
154
+ " pour lister les propriétés. Il n'est jamais renvoyé au navigateur : seuls son existence, sa provenance et ses quatre derniers caractères le sont."
155
+ ] }) })
156
+ ] }) });
157
+ };
158
+ exports.default = HubspotSettings;
@@ -0,0 +1,140 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Box, Loader, Flex, Typography, Badge, Field, Button } from "@strapi/design-system";
4
+ import { useFetchClient } from "@strapi/strapi/admin";
5
+ import { P as PLUGIN_ID } from "./index-CER4BKjS.mjs";
6
+ const SOURCE_LABEL = {
7
+ settings: "saisie ici",
8
+ config: "config/plugins.ts",
9
+ env: "variable d'environnement"
10
+ };
11
+ const HubspotSettings = () => {
12
+ const { get, put, del } = useFetchClient();
13
+ const [settings, setSettings] = React.useState(null);
14
+ const [apiKey, setApiKey] = React.useState("");
15
+ const [busy, setBusy] = React.useState(false);
16
+ const [feedback, setFeedback] = React.useState(
17
+ null
18
+ );
19
+ const load = React.useCallback(async () => {
20
+ const { data } = await get(`/${PLUGIN_ID}/settings`);
21
+ setSettings(data);
22
+ }, [get]);
23
+ React.useEffect(() => {
24
+ load().catch(() => setFeedback({ tone: "danger", text: "Réglages illisibles." }));
25
+ }, [load]);
26
+ const save = async () => {
27
+ setBusy(true);
28
+ setFeedback(null);
29
+ try {
30
+ const { data } = await put(`/${PLUGIN_ID}/settings`, { apiKey });
31
+ setSettings(data);
32
+ setApiKey("");
33
+ setFeedback({ tone: "success", text: "Clé enregistrée." });
34
+ } catch {
35
+ setFeedback({ tone: "danger", text: "Enregistrement impossible." });
36
+ } finally {
37
+ setBusy(false);
38
+ }
39
+ };
40
+ const remove = async () => {
41
+ setBusy(true);
42
+ setFeedback(null);
43
+ try {
44
+ const { data } = await del(`/${PLUGIN_ID}/settings`);
45
+ setSettings(data);
46
+ setApiKey("");
47
+ setFeedback({ tone: "success", text: "Clé supprimée." });
48
+ } catch {
49
+ setFeedback({ tone: "danger", text: "Suppression impossible." });
50
+ } finally {
51
+ setBusy(false);
52
+ }
53
+ };
54
+ const test = async () => {
55
+ setBusy(true);
56
+ setFeedback(null);
57
+ try {
58
+ const { data } = await get(
59
+ `/${PLUGIN_ID}/properties?refresh=1`
60
+ );
61
+ setFeedback(
62
+ data.configured ? { tone: "success", text: `Connexion établie — ${data.properties.length} propriétés lisibles.` } : { tone: "danger", text: "Aucune clé configurée." }
63
+ );
64
+ } catch {
65
+ setFeedback({ tone: "danger", text: "HubSpot injoignable — clé invalide ou révoquée ?" });
66
+ } finally {
67
+ setBusy(false);
68
+ }
69
+ };
70
+ if (!settings) {
71
+ return /* @__PURE__ */ jsx(Box, { padding: 8, children: /* @__PURE__ */ jsx(Loader, { small: true, children: "Chargement…" }) });
72
+ }
73
+ return /* @__PURE__ */ jsx(Box, { padding: 8, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
74
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 2, children: [
75
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", children: "HubSpot" }),
76
+ /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: "Le jeton d'application privée utilisé pour lire les propriétés du portail et valider les mappings de formulaires." })
77
+ ] }),
78
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
79
+ /* @__PURE__ */ jsx(Badge, { active: settings.configured, children: settings.configured ? `Configurée ${settings.hint}` : "Non configurée" }),
80
+ settings.keySource ? /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
81
+ "source : ",
82
+ SOURCE_LABEL[settings.keySource]
83
+ ] }) : null
84
+ ] }),
85
+ /* @__PURE__ */ jsx(Box, { maxWidth: "32rem", children: /* @__PURE__ */ jsxs(
86
+ Field.Root,
87
+ {
88
+ name: "apiKey",
89
+ hint: "Saisir une clé ici remplace celles venant de config/plugins.ts et de HUBSPOT_API_KEY.",
90
+ children: [
91
+ /* @__PURE__ */ jsx(Field.Label, { children: "Jeton d'application privée" }),
92
+ /* @__PURE__ */ jsx(
93
+ Field.Input,
94
+ {
95
+ type: "password",
96
+ value: apiKey,
97
+ autoComplete: "off",
98
+ placeholder: settings.configured ? "•••••••• (laisser vide pour conserver)" : "pat-eu1-…",
99
+ onChange: (e) => setApiKey(e.target.value)
100
+ }
101
+ ),
102
+ /* @__PURE__ */ jsx(Field.Hint, {})
103
+ ]
104
+ }
105
+ ) }),
106
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
107
+ /* @__PURE__ */ jsx(Button, { onClick: save, loading: busy, disabled: !apiKey.trim(), children: "Enregistrer" }),
108
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: test, loading: busy, disabled: !settings.configured, children: "Tester la connexion" }),
109
+ /* @__PURE__ */ jsx(
110
+ Button,
111
+ {
112
+ variant: "danger-light",
113
+ onClick: remove,
114
+ loading: busy,
115
+ disabled: settings.keySource !== "settings",
116
+ children: "Supprimer"
117
+ }
118
+ )
119
+ ] }),
120
+ feedback ? /* @__PURE__ */ jsx(
121
+ Typography,
122
+ {
123
+ variant: "pi",
124
+ textColor: feedback.tone === "success" ? "success600" : "danger600",
125
+ children: feedback.text
126
+ }
127
+ ) : null,
128
+ /* @__PURE__ */ jsx(Box, { paddingTop: 4, children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
129
+ "Le jeton a besoin des portées ",
130
+ /* @__PURE__ */ jsx("b", { children: "crm.schemas.contacts.read" }),
131
+ " et",
132
+ " ",
133
+ /* @__PURE__ */ jsx("b", { children: "crm.schemas.companies.read" }),
134
+ " pour lister les propriétés. Il n'est jamais renvoyé au navigateur : seuls son existence, sa provenance et ses quatre derniers caractères le sont."
135
+ ] }) })
136
+ ] }) });
137
+ };
138
+ export {
139
+ HubspotSettings as default
140
+ };