strapi-plugin-hubspot 0.8.0 → 0.10.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/CHANGELOG.md +36 -0
- package/README.md +28 -0
- package/dist/_chunks/{Forms-77Mornxt.mjs → Forms-BGdNbq-k.mjs} +348 -33
- package/dist/_chunks/{Forms-B8kkPY0c.js → Forms-BVkE1C46.js} +345 -30
- package/dist/_chunks/{HubspotFormInput-BVknnlmR.mjs → HubspotFormInput-QJldybwT.mjs} +1 -1
- package/dist/_chunks/{HubspotFormInput-kx3LlQYF.js → HubspotFormInput-ddrrD-GJ.js} +1 -1
- package/dist/_chunks/{HubspotObjectInput-B-Y1PZ6c.mjs → HubspotObjectInput-B_W9dpvD.mjs} +3 -3
- package/dist/_chunks/{HubspotObjectInput-BOO1PvOj.js → HubspotObjectInput-DZ4H-vju.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-awfD1opH.js → HubspotPropertyInput-BJHUy5Zr.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-Drxn_Ddt.mjs → HubspotPropertyInput-CiVxcnEt.mjs} +3 -3
- package/dist/_chunks/{Settings-DrujhMxS.mjs → Settings-B0NyQtsh.mjs} +2 -2
- package/dist/_chunks/{Settings-BT1NhvxO.js → Settings-DaFRUzjG.js} +2 -2
- package/dist/_chunks/{en-BYKKi1LK.mjs → en-D1sHFQdN.mjs} +48 -1
- package/dist/_chunks/{en-B8adhJhO.js → en-zJv-SuNb.js} +48 -1
- package/dist/_chunks/{fr-DzgArgi3.mjs → fr-E3zTW7pq.mjs} +48 -1
- package/dist/_chunks/{fr-DnKtGXaB.js → fr-fh0uH6Rp.js} +48 -1
- package/dist/_chunks/{index-LrCSAHoH.mjs → index-BpapRdHX.mjs} +6 -6
- package/dist/_chunks/{index-P7p8ASb6.js → index-rgjWxSn4.js} +6 -6
- package/dist/_chunks/{objectLabels-yNrnTOBo.mjs → objectLabels-DQYwGDr3.mjs} +1 -1
- package/dist/_chunks/{objectLabels-Dn-zriuZ.js → objectLabels-mBjwR5EE.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-BottzdRR.js → useHubspotSchema-CJEiHbjz.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-DccG2KbZ.mjs → useHubspotSchema-DX4JTPFt.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/builder/types.d.ts +28 -1
- package/dist/admin/src/pages/Forms.d.ts +5 -1
- package/dist/admin/src/pages/Submissions.d.ts +8 -0
- package/dist/server/index.js +353 -14
- package/dist/server/index.mjs +353 -14
- package/dist/server/src/__tests__/importHubspot.test.d.ts +1 -0
- package/dist/server/src/__tests__/submissions.test.d.ts +1 -0
- package/dist/server/src/conditions.d.ts +3 -1
- package/dist/server/src/formsAdmin.d.ts +13 -0
- package/dist/server/src/importHubspot.d.ts +85 -0
- package/dist/server/src/index.d.ts +28 -1
- package/dist/server/src/submissions.d.ts +28 -0
- package/dist/shared/types.d.ts +15 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 — 2026-08-29
|
|
4
|
+
|
|
5
|
+
### Added — HubSpot-parity condition operators
|
|
6
|
+
- `in` / `notIn` ("is any of" / "is none of"): one rule matching several
|
|
7
|
+
values — a multi-select over the referenced field's options in the editor
|
|
8
|
+
(comma-separated input when the field has none). Closes the gap that
|
|
9
|
+
previously required stacking OR'd `eq` rules.
|
|
10
|
+
- `notContains`, `startsWith`, `endsWith` — case-insensitive, like `contains`.
|
|
11
|
+
- Structural validation flags an `in`/`notIn` rule with no values, like the
|
|
12
|
+
other comparators; switching operator clears the stale value shape.
|
|
13
|
+
|
|
14
|
+
Frontends mirroring the engine client-side must add the five operators (the
|
|
15
|
+
typed `Operator` union in `strapi-plugin-hubspot/types` is the reference —
|
|
16
|
+
the server remains the authority at submission either way).
|
|
17
|
+
|
|
18
|
+
## 0.9.0 — 2026-08-29
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Import from HubSpot**: forms built in HubSpot (marketing v3 Forms API,
|
|
22
|
+
`forms` read scope) can be translated into builder drafts — fields, options,
|
|
23
|
+
two-per-row layouts and dependent-field conditions carried over, with a
|
|
24
|
+
**CRM mapping that is valid on arrival** since a HubSpot form field is a CRM
|
|
25
|
+
property. What the builder can't express (file uploads, hidden fields,
|
|
26
|
+
content blocks, GDPR consent, exotic operators) is skipped and **reported**
|
|
27
|
+
after the import instead of silently dropped. Re-importing overwrites the
|
|
28
|
+
draft; the HubSpot original is never touched.
|
|
29
|
+
- **Submissions browser**: a Submissions page in the admin (button on the
|
|
30
|
+
forms list + per-form counts in the table) — newest first, filterable by
|
|
31
|
+
form, sync state at a glance, full answers in a modal, and a per-form
|
|
32
|
+
**CSV export** whose columns follow the field order of the form (historical
|
|
33
|
+
keys appended, never dropped).
|
|
34
|
+
- Admin routes: `GET/POST /hubspot/builder/import/hubspot`,
|
|
35
|
+
`GET /hubspot/builder/submissions`, `GET /hubspot/builder/submissions/export`
|
|
36
|
+
(all RBAC-gated by `plugin::hubspot.forms`); the forms list now carries a
|
|
37
|
+
`submissions` count per row.
|
|
38
|
+
|
|
3
39
|
## 0.8.0 — 2026-08-29
|
|
4
40
|
|
|
5
41
|
### Improved (builder UX pass, from a live session)
|
package/README.md
CHANGED
|
@@ -66,6 +66,16 @@ Every submission is stored in **HubSpot form submissions** (Content Manager),
|
|
|
66
66
|
synced or not, with the CRM ids when the sync succeeded — the source of truth
|
|
67
67
|
lives in your database, not in HubSpot's availability.
|
|
68
68
|
|
|
69
|
+
### Browsing submissions
|
|
70
|
+
|
|
71
|
+
The **Submissions** button on the forms list (also reachable from a form's
|
|
72
|
+
count in the table) opens the submissions browser: every stored answer,
|
|
73
|
+
newest first, filterable by form, with the sync state at a glance and the
|
|
74
|
+
full answer set one click away. **Export CSV** downloads one form's whole
|
|
75
|
+
history — columns follow the form's field order, and keys found only in older
|
|
76
|
+
submissions (a renamed or removed field) are appended rather than dropped.
|
|
77
|
+
Deleting stays in the Content Manager, where the type remains visible.
|
|
78
|
+
|
|
69
79
|
Typed payloads for your frontend:
|
|
70
80
|
|
|
71
81
|
```ts
|
|
@@ -99,6 +109,24 @@ never modified, re-importing overwrites the draft only, and unpublishing rolls
|
|
|
99
109
|
a migrated form back. Migrate form by form; the validation middleware keeps
|
|
100
110
|
protecting the ones that stay.
|
|
101
111
|
|
|
112
|
+
### Importing forms from HubSpot
|
|
113
|
+
|
|
114
|
+
Forms built **in HubSpot itself** can be imported too — give the private app
|
|
115
|
+
token the `forms` read scope and the list page offers every regular form of
|
|
116
|
+
the portal. The translation carries over what the builder can express:
|
|
117
|
+
|
|
118
|
+
- fields, labels, placeholders, help texts, required flags and options —
|
|
119
|
+
and since a HubSpot form field *is* a CRM property, the imported form
|
|
120
|
+
arrives with a **mapping that is already valid** for the portal;
|
|
121
|
+
- two-fields-per-row layouts become two half-width fields;
|
|
122
|
+
- dependent fields become `visibleIf` conditions (value lists are expanded
|
|
123
|
+
into OR'd/AND'd rules).
|
|
124
|
+
|
|
125
|
+
Everything else — file uploads, hidden fields, content blocks, GDPR consent
|
|
126
|
+
blocks, an operator with no equivalent — is **skipped and reported** after the
|
|
127
|
+
import, so nothing is silently half-migrated. The HubSpot original is never
|
|
128
|
+
modified; re-importing overwrites the draft, never the published version.
|
|
129
|
+
|
|
102
130
|
### i18n and publishing
|
|
103
131
|
|
|
104
132
|
Forms are draft & publish, localized like your content: `name` and `slug` are
|
|
@@ -1,15 +1,38 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useParams, useSearchParams, Link, useNavigate, Routes, Route } from "react-router-dom";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { u as useIntl } from "./index-CEh8vkxY.mjs";
|
|
5
|
-
import { Typography, Flex, Box, SingleSelect, SingleSelectOption, TextInput, IconButton, Button, Field, Checkbox, Divider, Combobox, ComboboxOption, Loader, Badge, Textarea, Table, Thead, Tr, Th, Tbody, Td } from "@strapi/design-system";
|
|
6
|
-
import { Trash, Plus, ArrowClockwise, ArrowLeft, ArrowUp, ArrowDown, Duplicate } from "@strapi/icons";
|
|
5
|
+
import { Typography, Flex, Box, SingleSelect, SingleSelectOption, MultiSelect, MultiSelectOption, TextInput, IconButton, Button, Field, Checkbox, Divider, Combobox, ComboboxOption, Loader, Badge, Textarea, Table, Thead, Tr, Th, Tbody, Td, Modal } from "@strapi/design-system";
|
|
6
|
+
import { Trash, Plus, ArrowClockwise, ArrowLeft, ArrowUp, ArrowDown, List, Duplicate, Download } from "@strapi/icons";
|
|
7
7
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
8
|
-
import { g as getTranslation, P as PLUGIN_ID } from "./index-
|
|
9
|
-
import { o as objectLabel } from "./objectLabels-
|
|
10
|
-
import { u as useHubspotSchema } from "./useHubspotSchema-
|
|
11
|
-
const OPERATORS = [
|
|
12
|
-
|
|
8
|
+
import { g as getTranslation, P as PLUGIN_ID } from "./index-BpapRdHX.mjs";
|
|
9
|
+
import { o as objectLabel } from "./objectLabels-DQYwGDr3.mjs";
|
|
10
|
+
import { u as useHubspotSchema } from "./useHubspotSchema-DX4JTPFt.mjs";
|
|
11
|
+
const OPERATORS = [
|
|
12
|
+
"eq",
|
|
13
|
+
"neq",
|
|
14
|
+
"in",
|
|
15
|
+
"notIn",
|
|
16
|
+
"contains",
|
|
17
|
+
"notContains",
|
|
18
|
+
"startsWith",
|
|
19
|
+
"endsWith",
|
|
20
|
+
"empty",
|
|
21
|
+
"notEmpty",
|
|
22
|
+
"gt",
|
|
23
|
+
"lt"
|
|
24
|
+
];
|
|
25
|
+
const NEEDS_VALUE = /* @__PURE__ */ new Set([
|
|
26
|
+
"eq",
|
|
27
|
+
"neq",
|
|
28
|
+
"contains",
|
|
29
|
+
"notContains",
|
|
30
|
+
"startsWith",
|
|
31
|
+
"endsWith",
|
|
32
|
+
"gt",
|
|
33
|
+
"lt"
|
|
34
|
+
]);
|
|
35
|
+
const NEEDS_VALUES = /* @__PURE__ */ new Set(["in", "notIn"]);
|
|
13
36
|
const ConditionEditor = ({ condition, candidates, onChange }) => {
|
|
14
37
|
const { formatMessage } = useIntl();
|
|
15
38
|
const t = (id, defaultMessage) => formatMessage({ id: getTranslation(id), defaultMessage });
|
|
@@ -20,7 +43,12 @@ const ConditionEditor = ({ condition, candidates, onChange }) => {
|
|
|
20
43
|
empty: t("condition.op.empty", "is empty"),
|
|
21
44
|
notEmpty: t("condition.op.notEmpty", "is filled"),
|
|
22
45
|
gt: t("condition.op.gt", "is greater than"),
|
|
23
|
-
lt: t("condition.op.lt", "is less than")
|
|
46
|
+
lt: t("condition.op.lt", "is less than"),
|
|
47
|
+
in: t("condition.op.in", "is any of"),
|
|
48
|
+
notIn: t("condition.op.notIn", "is none of"),
|
|
49
|
+
notContains: t("condition.op.notContains", "doesn't contain"),
|
|
50
|
+
startsWith: t("condition.op.startsWith", "starts with"),
|
|
51
|
+
endsWith: t("condition.op.endsWith", "ends with")
|
|
24
52
|
};
|
|
25
53
|
const rules = condition?.rules ?? [];
|
|
26
54
|
const logic = condition?.logic ?? "and";
|
|
@@ -54,10 +82,31 @@ const ConditionEditor = ({ condition, candidates, onChange }) => {
|
|
|
54
82
|
{
|
|
55
83
|
"aria-label": t("condition.operator", "Operator"),
|
|
56
84
|
value: rule.operator,
|
|
57
|
-
onChange: (v) => updateRule(index, { operator: v }),
|
|
85
|
+
onChange: (v) => updateRule(index, { operator: v, value: "", values: [] }),
|
|
58
86
|
children: OPERATORS.map((op) => /* @__PURE__ */ jsx(SingleSelectOption, { value: op, children: opLabel[op] }, op))
|
|
59
87
|
}
|
|
60
88
|
) }),
|
|
89
|
+
NEEDS_VALUES.has(rule.operator) && /* @__PURE__ */ jsx(Box, { flex: "1", minWidth: "140px", children: targetOptions.length ? /* @__PURE__ */ jsx(
|
|
90
|
+
MultiSelect,
|
|
91
|
+
{
|
|
92
|
+
"aria-label": t("condition.values", "Values"),
|
|
93
|
+
value: rule.values ?? [],
|
|
94
|
+
withTags: true,
|
|
95
|
+
onChange: (values) => updateRule(index, { values }),
|
|
96
|
+
children: targetOptions.map((o) => /* @__PURE__ */ jsx(MultiSelectOption, { value: o.value, children: o.label || o.value }, o.value))
|
|
97
|
+
}
|
|
98
|
+
) : /* @__PURE__ */ jsx(
|
|
99
|
+
TextInput,
|
|
100
|
+
{
|
|
101
|
+
"aria-label": t("condition.values", "Values"),
|
|
102
|
+
placeholder: t("condition.values-placeholder", "value1, value2…"),
|
|
103
|
+
defaultValue: (rule.values ?? []).join(", "),
|
|
104
|
+
onBlur: (e) => updateRule(index, {
|
|
105
|
+
values: e.target.value.split(",").map((v) => v.trim()).filter(Boolean)
|
|
106
|
+
})
|
|
107
|
+
},
|
|
108
|
+
`${index}-${rule.operator}`
|
|
109
|
+
) }),
|
|
61
110
|
NEEDS_VALUE.has(rule.operator) && /* @__PURE__ */ jsx(Box, { flex: "1", minWidth: "110px", children: targetOptions.length ? /* @__PURE__ */ jsx(
|
|
62
111
|
SingleSelect,
|
|
63
112
|
{
|
|
@@ -910,6 +959,12 @@ const FormsList = () => {
|
|
|
910
959
|
const [error, setError] = React.useState(null);
|
|
911
960
|
const [sources, setSources] = React.useState([]);
|
|
912
961
|
const [sourceId, setSourceId] = React.useState("");
|
|
962
|
+
const [hsForms, setHsForms] = React.useState([]);
|
|
963
|
+
const [hsFormId, setHsFormId] = React.useState("");
|
|
964
|
+
const [hsError, setHsError] = React.useState(null);
|
|
965
|
+
const [report, setReport] = React.useState(
|
|
966
|
+
null
|
|
967
|
+
);
|
|
913
968
|
const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
|
|
914
969
|
const load = React.useCallback(async () => {
|
|
915
970
|
const { data } = await get(`/${PLUGIN_ID}/builder/forms`);
|
|
@@ -920,6 +975,17 @@ const FormsList = () => {
|
|
|
920
975
|
get(
|
|
921
976
|
`/${PLUGIN_ID}/builder/import/sources`
|
|
922
977
|
).then(({ data }) => setSources(data.sources ?? [])).catch(() => setSources([]));
|
|
978
|
+
get(`/${PLUGIN_ID}/builder/import/hubspot`).then(({ data }) => setHsForms(data.configured ? data.forms ?? [] : [])).catch(
|
|
979
|
+
() => (
|
|
980
|
+
// Reachable settings but no portal list: almost always the missing scope.
|
|
981
|
+
setHsError(
|
|
982
|
+
t(
|
|
983
|
+
"forms.hubspot-list-error",
|
|
984
|
+
"Could not list the portal's forms — the token needs the `forms` read scope."
|
|
985
|
+
)
|
|
986
|
+
)
|
|
987
|
+
)
|
|
988
|
+
);
|
|
923
989
|
}, [load]);
|
|
924
990
|
const runImport = async () => {
|
|
925
991
|
if (!sourceId) return;
|
|
@@ -935,6 +1001,48 @@ const FormsList = () => {
|
|
|
935
1001
|
setBusy(false);
|
|
936
1002
|
}
|
|
937
1003
|
};
|
|
1004
|
+
const runHubspotImport = async () => {
|
|
1005
|
+
if (!hsFormId) return;
|
|
1006
|
+
setBusy(true);
|
|
1007
|
+
setError(null);
|
|
1008
|
+
setReport(null);
|
|
1009
|
+
try {
|
|
1010
|
+
const { data } = await post(
|
|
1011
|
+
`/${PLUGIN_ID}/builder/import/hubspot`,
|
|
1012
|
+
{ formId: hsFormId }
|
|
1013
|
+
);
|
|
1014
|
+
if (data.skipped.length) {
|
|
1015
|
+
setReport(data);
|
|
1016
|
+
setBusy(false);
|
|
1017
|
+
await load();
|
|
1018
|
+
} else {
|
|
1019
|
+
navigate(data.documentId);
|
|
1020
|
+
}
|
|
1021
|
+
} catch {
|
|
1022
|
+
setError(t("forms.import-error", "Could not import the form."));
|
|
1023
|
+
setBusy(false);
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
const skippedLine = (item) => {
|
|
1027
|
+
const label = item.label ?? "";
|
|
1028
|
+
const detail = item.detail ?? "";
|
|
1029
|
+
switch (item.code) {
|
|
1030
|
+
case "field-type":
|
|
1031
|
+
return t("forms.skipped-field-type", '"{label}" skipped — unsupported type ({detail})', { label, detail });
|
|
1032
|
+
case "hidden-field":
|
|
1033
|
+
return t("forms.skipped-hidden", '"{label}" skipped — hidden fields are not supported yet', { label });
|
|
1034
|
+
case "object":
|
|
1035
|
+
return t("forms.skipped-object", '"{label}" skipped — unmapped object ({detail})', { label, detail });
|
|
1036
|
+
case "duplicate":
|
|
1037
|
+
return t("forms.skipped-duplicate", '"{label}" skipped — the name "{detail}" is already used', { label, detail });
|
|
1038
|
+
case "condition":
|
|
1039
|
+
return t("forms.skipped-condition", 'Condition on "{label}" dropped — no equivalent operator ({detail})', { label, detail });
|
|
1040
|
+
case "rich-text":
|
|
1041
|
+
return t("forms.skipped-rich-text", "A content block was skipped — the builder has no rich-text element");
|
|
1042
|
+
case "legal-consent":
|
|
1043
|
+
return t("forms.skipped-consent", "The GDPR consent block was skipped — rebuild it as a field if needed");
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
938
1046
|
const create = async () => {
|
|
939
1047
|
if (!name.trim()) return;
|
|
940
1048
|
setBusy(true);
|
|
@@ -987,6 +1095,7 @@ const FormsList = () => {
|
|
|
987
1095
|
) })
|
|
988
1096
|
] }),
|
|
989
1097
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
1098
|
+
/* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(List, {}), onClick: () => navigate("submissions"), children: t("forms.submissions", "Submissions") }),
|
|
990
1099
|
/* @__PURE__ */ jsx(
|
|
991
1100
|
TextInput,
|
|
992
1101
|
{
|
|
@@ -1001,38 +1110,74 @@ const FormsList = () => {
|
|
|
1001
1110
|
] })
|
|
1002
1111
|
] }),
|
|
1003
1112
|
error && /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }) }),
|
|
1004
|
-
sources.length > 0 && /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, marginBottom: 4, children: /* @__PURE__ */ jsxs(Flex, {
|
|
1113
|
+
(sources.length > 0 || hsForms.length > 0 || hsError) && /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, marginBottom: 4, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
|
|
1114
|
+
hsForms.length > 0 && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "flex-end", justifyContent: "space-between", children: [
|
|
1115
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1116
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("forms.import-hubspot-title", "Import from your HubSpot portal") }),
|
|
1117
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", tag: "p", children: t(
|
|
1118
|
+
"forms.import-hubspot-hint",
|
|
1119
|
+
"Translates a form built in HubSpot into a draft here, CRM mapping included. What the builder can't express is skipped and reported. Re-importing overwrites the draft."
|
|
1120
|
+
) })
|
|
1121
|
+
] }),
|
|
1122
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
1123
|
+
/* @__PURE__ */ jsx(
|
|
1124
|
+
SingleSelect,
|
|
1125
|
+
{
|
|
1126
|
+
"aria-label": t("forms.import-hubspot-source", "HubSpot form to import"),
|
|
1127
|
+
placeholder: t("forms.import-hubspot-source", "HubSpot form to import"),
|
|
1128
|
+
value: hsFormId,
|
|
1129
|
+
onChange: (v) => setHsFormId(String(v)),
|
|
1130
|
+
children: hsForms.map((f) => /* @__PURE__ */ jsx(SingleSelectOption, { value: f.id, children: f.name }, f.id))
|
|
1131
|
+
}
|
|
1132
|
+
),
|
|
1133
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: runHubspotImport, disabled: busy || !hsFormId, children: t("forms.import", "Import") })
|
|
1134
|
+
] })
|
|
1135
|
+
] }),
|
|
1136
|
+
hsError && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "warning700", children: hsError }),
|
|
1137
|
+
sources.length > 0 && /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "flex-end", justifyContent: "space-between", children: [
|
|
1138
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1139
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("forms.import-title", "Import an existing form") }),
|
|
1140
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", tag: "p", children: t(
|
|
1141
|
+
"forms.import-hint",
|
|
1142
|
+
"Converts a legacy content-type form (all locales) into a draft here — same slug, source untouched. Re-importing overwrites the draft."
|
|
1143
|
+
) })
|
|
1144
|
+
] }),
|
|
1145
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
1146
|
+
/* @__PURE__ */ jsx(
|
|
1147
|
+
SingleSelect,
|
|
1148
|
+
{
|
|
1149
|
+
"aria-label": t("forms.import-source", "Form to import"),
|
|
1150
|
+
placeholder: t("forms.import-source", "Form to import"),
|
|
1151
|
+
value: sourceId,
|
|
1152
|
+
onChange: (v) => setSourceId(String(v)),
|
|
1153
|
+
children: sources.map((s) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: s.documentId, children: [
|
|
1154
|
+
s.name,
|
|
1155
|
+
" ",
|
|
1156
|
+
s.slug ? `(${s.slug})` : ""
|
|
1157
|
+
] }, s.documentId))
|
|
1158
|
+
}
|
|
1159
|
+
),
|
|
1160
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: runImport, disabled: busy || !sourceId, children: t("forms.import", "Import") })
|
|
1161
|
+
] })
|
|
1162
|
+
] })
|
|
1163
|
+
] }) }),
|
|
1164
|
+
report && /* @__PURE__ */ jsx(Box, { background: "warning100", hasRadius: true, padding: 4, marginBottom: 4, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-start", gap: 4, children: [
|
|
1005
1165
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1006
|
-
/* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("forms.
|
|
1007
|
-
/* @__PURE__ */ jsx(
|
|
1008
|
-
"forms.import-hint",
|
|
1009
|
-
"Converts a legacy content-type form (all locales) into a draft here — same slug, source untouched. Re-importing overwrites the draft."
|
|
1010
|
-
) })
|
|
1166
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", textColor: "warning700", children: t("forms.report-title", "Imported with gaps") }),
|
|
1167
|
+
/* @__PURE__ */ jsx(Box, { tag: "ul", paddingTop: 2, style: { listStyle: "disc", paddingLeft: 16 }, children: report.skipped.map((item, i) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "warning700", children: skippedLine(item) }) }, i)) })
|
|
1011
1168
|
] }),
|
|
1012
|
-
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
1013
|
-
/* @__PURE__ */ jsx(
|
|
1014
|
-
|
|
1015
|
-
{
|
|
1016
|
-
"aria-label": t("forms.import-source", "Form to import"),
|
|
1017
|
-
placeholder: t("forms.import-source", "Form to import"),
|
|
1018
|
-
value: sourceId,
|
|
1019
|
-
onChange: (v) => setSourceId(String(v)),
|
|
1020
|
-
children: sources.map((s) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: s.documentId, children: [
|
|
1021
|
-
s.name,
|
|
1022
|
-
" ",
|
|
1023
|
-
s.slug ? `(${s.slug})` : ""
|
|
1024
|
-
] }, s.documentId))
|
|
1025
|
-
}
|
|
1026
|
-
),
|
|
1027
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: runImport, disabled: busy || !sourceId, children: t("forms.import", "Import") })
|
|
1169
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, shrink: 0, children: [
|
|
1170
|
+
/* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: () => setReport(null), children: t("forms.report-dismiss", "Dismiss") }),
|
|
1171
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => navigate(report.documentId), children: t("forms.report-open", "Open the form") })
|
|
1028
1172
|
] })
|
|
1029
1173
|
] }) }),
|
|
1030
|
-
!rows ? /* @__PURE__ */ jsx(Loader, { children: t("forms.loading", "Loading…") }) : rows.length === 0 ? /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 8, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.empty", "No form yet — name one above to open the builder.") }) }) : /* @__PURE__ */ jsxs(Table, { colCount:
|
|
1174
|
+
!rows ? /* @__PURE__ */ jsx(Loader, { children: t("forms.loading", "Loading…") }) : rows.length === 0 ? /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 8, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.empty", "No form yet — name one above to open the builder.") }) }) : /* @__PURE__ */ jsxs(Table, { colCount: 7, rowCount: rows.length, children: [
|
|
1031
1175
|
/* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
|
|
1032
1176
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-name", "Name") }) }),
|
|
1033
1177
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-slug", "Slug") }) }),
|
|
1034
1178
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-structure", "Structure") }) }),
|
|
1035
1179
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-status", "Status") }) }),
|
|
1180
|
+
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-submissions", "Submissions") }) }),
|
|
1036
1181
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-updated", "Updated") }) }),
|
|
1037
1182
|
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-actions", "Actions") }) })
|
|
1038
1183
|
] }) }),
|
|
@@ -1049,6 +1194,15 @@ const FormsList = () => {
|
|
|
1049
1194
|
fields: row.fields
|
|
1050
1195
|
}) }) }),
|
|
1051
1196
|
/* @__PURE__ */ jsx(Td, { children: row.published ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: t("forms.published", "Published") }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: t("forms.draft", "Draft") }) }),
|
|
1197
|
+
/* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: row.submissions > 0 ? /* @__PURE__ */ jsx(
|
|
1198
|
+
Button,
|
|
1199
|
+
{
|
|
1200
|
+
variant: "ghost",
|
|
1201
|
+
startIcon: /* @__PURE__ */ jsx(List, {}),
|
|
1202
|
+
onClick: () => navigate(`submissions?form=${encodeURIComponent(row.slug)}`),
|
|
1203
|
+
children: row.submissions
|
|
1204
|
+
}
|
|
1205
|
+
) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "—" }) }),
|
|
1052
1206
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.updatedAt ? new Date(row.updatedAt).toLocaleString() : "—" }) }),
|
|
1053
1207
|
/* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
|
|
1054
1208
|
/* @__PURE__ */ jsx(
|
|
@@ -1077,8 +1231,169 @@ const FormsList = () => {
|
|
|
1077
1231
|
] })
|
|
1078
1232
|
] });
|
|
1079
1233
|
};
|
|
1234
|
+
const PAGE_SIZE = 20;
|
|
1235
|
+
const preview = (values, max = 3) => {
|
|
1236
|
+
const parts = Object.entries(values ?? {}).slice(0, max).map(([k, v]) => `${k}: ${String(v)}`);
|
|
1237
|
+
const more = Object.keys(values ?? {}).length - max;
|
|
1238
|
+
return parts.join(" · ") + (more > 0 ? ` · +${more}` : "");
|
|
1239
|
+
};
|
|
1240
|
+
const Submissions = () => {
|
|
1241
|
+
const { formatMessage } = useIntl();
|
|
1242
|
+
const { get } = useFetchClient();
|
|
1243
|
+
const navigate = useNavigate();
|
|
1244
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
1245
|
+
const form = searchParams.get("form") ?? "";
|
|
1246
|
+
const [forms, setForms] = React.useState([]);
|
|
1247
|
+
const [rows, setRows] = React.useState(null);
|
|
1248
|
+
const [total, setTotal] = React.useState(0);
|
|
1249
|
+
const [page, setPage] = React.useState(1);
|
|
1250
|
+
const [selected, setSelected] = React.useState(null);
|
|
1251
|
+
const [error, setError] = React.useState(null);
|
|
1252
|
+
const [exporting, setExporting] = React.useState(false);
|
|
1253
|
+
const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
|
|
1254
|
+
React.useEffect(() => {
|
|
1255
|
+
get(`/${PLUGIN_ID}/builder/forms`).then(({ data }) => setForms(data.forms)).catch(() => setForms([]));
|
|
1256
|
+
}, [get]);
|
|
1257
|
+
React.useEffect(() => {
|
|
1258
|
+
setRows(null);
|
|
1259
|
+
const query = new URLSearchParams({ page: String(page), pageSize: String(PAGE_SIZE) });
|
|
1260
|
+
if (form) query.set("form", form);
|
|
1261
|
+
get(
|
|
1262
|
+
`/${PLUGIN_ID}/builder/submissions?${query}`
|
|
1263
|
+
).then(({ data }) => {
|
|
1264
|
+
setRows(data.submissions);
|
|
1265
|
+
setTotal(data.total);
|
|
1266
|
+
}).catch(() => setError(t("submissions.load-error", "Could not load the submissions.")));
|
|
1267
|
+
}, [get, form, page]);
|
|
1268
|
+
const changeForm = (slug) => {
|
|
1269
|
+
setPage(1);
|
|
1270
|
+
setSearchParams(slug ? { form: slug } : {});
|
|
1271
|
+
};
|
|
1272
|
+
const exportCsv = async () => {
|
|
1273
|
+
if (!form) return;
|
|
1274
|
+
setExporting(true);
|
|
1275
|
+
setError(null);
|
|
1276
|
+
try {
|
|
1277
|
+
const { data } = await get(
|
|
1278
|
+
`/${PLUGIN_ID}/builder/submissions/export?form=${encodeURIComponent(form)}`
|
|
1279
|
+
);
|
|
1280
|
+
const url = URL.createObjectURL(new Blob([data.csv], { type: "text/csv;charset=utf-8" }));
|
|
1281
|
+
const link = document.createElement("a");
|
|
1282
|
+
link.href = url;
|
|
1283
|
+
link.download = data.filename;
|
|
1284
|
+
link.click();
|
|
1285
|
+
URL.revokeObjectURL(url);
|
|
1286
|
+
} catch {
|
|
1287
|
+
setError(t("submissions.export-error", "Could not export the submissions."));
|
|
1288
|
+
} finally {
|
|
1289
|
+
setExporting(false);
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
1293
|
+
return /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
|
|
1294
|
+
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", paddingBottom: 6, children: [
|
|
1295
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1296
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
1297
|
+
/* @__PURE__ */ jsx(IconButton, { label: t("submissions.back", "Back to the forms"), onClick: () => navigate(".."), children: /* @__PURE__ */ jsx(ArrowLeft, {}) }),
|
|
1298
|
+
/* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: t("submissions.title", "Submissions") })
|
|
1299
|
+
] }),
|
|
1300
|
+
/* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", tag: "p", children: t(
|
|
1301
|
+
"submissions.subtitle",
|
|
1302
|
+
"Every answer received by the built forms — stored whatever the CRM's mood."
|
|
1303
|
+
) })
|
|
1304
|
+
] }),
|
|
1305
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
1306
|
+
/* @__PURE__ */ jsx(
|
|
1307
|
+
SingleSelect,
|
|
1308
|
+
{
|
|
1309
|
+
"aria-label": t("submissions.filter", "Filter by form"),
|
|
1310
|
+
placeholder: t("submissions.all-forms", "All forms"),
|
|
1311
|
+
value: form,
|
|
1312
|
+
onChange: (v) => changeForm(String(v)),
|
|
1313
|
+
onClear: () => changeForm(""),
|
|
1314
|
+
children: forms.map((f) => /* @__PURE__ */ jsx(SingleSelectOption, { value: f.slug, children: f.name }, f.slug))
|
|
1315
|
+
}
|
|
1316
|
+
),
|
|
1317
|
+
/* @__PURE__ */ jsx(
|
|
1318
|
+
Button,
|
|
1319
|
+
{
|
|
1320
|
+
variant: "secondary",
|
|
1321
|
+
startIcon: /* @__PURE__ */ jsx(Download, {}),
|
|
1322
|
+
onClick: exportCsv,
|
|
1323
|
+
disabled: !form || exporting,
|
|
1324
|
+
title: form ? void 0 : t("submissions.export-pick", "Pick a form to export"),
|
|
1325
|
+
children: t("submissions.export", "Export CSV")
|
|
1326
|
+
}
|
|
1327
|
+
)
|
|
1328
|
+
] })
|
|
1329
|
+
] }),
|
|
1330
|
+
error && /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }) }),
|
|
1331
|
+
!rows ? /* @__PURE__ */ jsx(Loader, { children: t("submissions.loading", "Loading…") }) : rows.length === 0 ? /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 8, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: form ? t("submissions.empty-form", "No submission for this form yet.") : t("submissions.empty", "No submission yet.") }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1332
|
+
/* @__PURE__ */ jsxs(Table, { colCount: 5, rowCount: rows.length, children: [
|
|
1333
|
+
/* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
|
|
1334
|
+
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-date", "Date") }) }),
|
|
1335
|
+
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-form", "Form") }) }),
|
|
1336
|
+
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-email", "Email") }) }),
|
|
1337
|
+
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-answers", "Answers") }) }),
|
|
1338
|
+
/* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("submissions.col-crm", "CRM") }) })
|
|
1339
|
+
] }) }),
|
|
1340
|
+
/* @__PURE__ */ jsx(Tbody, { children: rows.map((row) => /* @__PURE__ */ jsxs(
|
|
1341
|
+
Tr,
|
|
1342
|
+
{
|
|
1343
|
+
onClick: () => setSelected(row),
|
|
1344
|
+
style: { cursor: "pointer" },
|
|
1345
|
+
children: [
|
|
1346
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.createdAt ? new Date(row.createdAt).toLocaleString() : "—" }) }),
|
|
1347
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: row.formTitle || row.form }) }),
|
|
1348
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: row.email || "—" }) }),
|
|
1349
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", ellipsis: true, children: preview(row.values) }) }),
|
|
1350
|
+
/* @__PURE__ */ jsx(Td, { children: row.hubspotSynced ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: t("submissions.synced", "Synced") }) : /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("submissions.not-synced", "Not synced") }) })
|
|
1351
|
+
]
|
|
1352
|
+
},
|
|
1353
|
+
row.documentId
|
|
1354
|
+
)) })
|
|
1355
|
+
] }),
|
|
1356
|
+
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", paddingTop: 4, children: [
|
|
1357
|
+
/* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("submissions.count", "{total, plural, one {# submission} other {# submissions}}", {
|
|
1358
|
+
total
|
|
1359
|
+
}) }),
|
|
1360
|
+
pages > 1 && /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
1361
|
+
/* @__PURE__ */ jsx(Button, { variant: "tertiary", disabled: page <= 1, onClick: () => setPage(page - 1), children: t("submissions.prev", "Previous") }),
|
|
1362
|
+
/* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("submissions.page", "{page} / {pages}", { page, pages }) }),
|
|
1363
|
+
/* @__PURE__ */ jsx(
|
|
1364
|
+
Button,
|
|
1365
|
+
{
|
|
1366
|
+
variant: "tertiary",
|
|
1367
|
+
disabled: page >= pages,
|
|
1368
|
+
onClick: () => setPage(page + 1),
|
|
1369
|
+
children: t("submissions.next", "Next")
|
|
1370
|
+
}
|
|
1371
|
+
)
|
|
1372
|
+
] })
|
|
1373
|
+
] })
|
|
1374
|
+
] }),
|
|
1375
|
+
/* @__PURE__ */ jsx(Modal.Root, { open: Boolean(selected), onOpenChange: (open) => !open && setSelected(null), children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
|
|
1376
|
+
/* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsxs(Modal.Title, { children: [
|
|
1377
|
+
selected?.formTitle || selected?.form,
|
|
1378
|
+
" ",
|
|
1379
|
+
selected?.createdAt ? `— ${new Date(selected.createdAt).toLocaleString()}` : ""
|
|
1380
|
+
] }) }),
|
|
1381
|
+
/* @__PURE__ */ jsx(Modal.Body, { children: selected && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
|
|
1382
|
+
Object.entries(selected.values ?? {}).map(([key, value]) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
1383
|
+
/* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "p", children: key }),
|
|
1384
|
+
/* @__PURE__ */ jsx(Typography, { tag: "p", children: String(value) })
|
|
1385
|
+
] }, key)),
|
|
1386
|
+
selected.meta?.pagePath && /* @__PURE__ */ jsxs(Box, { children: [
|
|
1387
|
+
/* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "p", children: t("submissions.meta-page", "Submitted from") }),
|
|
1388
|
+
/* @__PURE__ */ jsx(Typography, { tag: "p", children: selected.meta.pagePath })
|
|
1389
|
+
] })
|
|
1390
|
+
] }) })
|
|
1391
|
+
] }) })
|
|
1392
|
+
] });
|
|
1393
|
+
};
|
|
1080
1394
|
const Forms = () => /* @__PURE__ */ jsxs(Routes, { children: [
|
|
1081
1395
|
/* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(FormsList, {}) }),
|
|
1396
|
+
/* @__PURE__ */ jsx(Route, { path: "submissions", element: /* @__PURE__ */ jsx(Submissions, {}) }),
|
|
1082
1397
|
/* @__PURE__ */ jsx(Route, { path: ":documentId", element: /* @__PURE__ */ jsx(FormEditor, {}) })
|
|
1083
1398
|
] });
|
|
1084
1399
|
export {
|