strapi-plugin-hubspot 0.8.0 → 0.9.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 +21 -0
- package/README.md +28 -0
- package/dist/_chunks/{Forms-77Mornxt.mjs → Forms-BsKys-Hc.mjs} +295 -29
- package/dist/_chunks/{Forms-B8kkPY0c.js → Forms-JuzNfsbL.js} +292 -26
- package/dist/_chunks/{HubspotFormInput-BVknnlmR.mjs → HubspotFormInput-CIvHS5rp.mjs} +1 -1
- package/dist/_chunks/{HubspotFormInput-kx3LlQYF.js → HubspotFormInput-ph9pe6xY.js} +1 -1
- package/dist/_chunks/{HubspotObjectInput-B-Y1PZ6c.mjs → HubspotObjectInput-BX1qYD2c.mjs} +3 -3
- package/dist/_chunks/{HubspotObjectInput-BOO1PvOj.js → HubspotObjectInput-dAd-lMdD.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-awfD1opH.js → HubspotPropertyInput-BM3ZaY8f.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-Drxn_Ddt.mjs → HubspotPropertyInput-C2Z6472q.mjs} +3 -3
- package/dist/_chunks/{Settings-BT1NhvxO.js → Settings-Cdcjy9DA.js} +2 -2
- package/dist/_chunks/{Settings-DrujhMxS.mjs → Settings-DnwGetcc.mjs} +2 -2
- package/dist/_chunks/{en-BYKKi1LK.mjs → en-CcQWnXra.mjs} +41 -1
- package/dist/_chunks/{en-B8adhJhO.js → en-Cqfair98.js} +41 -1
- package/dist/_chunks/{fr-DzgArgi3.mjs → fr-CBRZ9q8U.mjs} +41 -1
- package/dist/_chunks/{fr-DnKtGXaB.js → fr-D117-3Ta.js} +41 -1
- package/dist/_chunks/{index-P7p8ASb6.js → index-BbRtXnAL.js} +6 -6
- package/dist/_chunks/{index-LrCSAHoH.mjs → index-LTaVoyWJ.mjs} +6 -6
- package/dist/_chunks/{objectLabels-yNrnTOBo.mjs → objectLabels-DE2xGq7f.mjs} +1 -1
- package/dist/_chunks/{objectLabels-Dn-zriuZ.js → objectLabels-DU0Cagt_.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-DccG2KbZ.mjs → useHubspotSchema-BSKow0X3.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-BottzdRR.js → useHubspotSchema-DzBMAxv7.js} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/builder/types.d.ts +25 -0
- 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 +329 -13
- package/dist/server/index.mjs +329 -13
- 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/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/package.json +2 -2
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const utils = require("@strapi/utils");
|
|
3
|
-
const HS_BASE$
|
|
3
|
+
const HS_BASE$3 = "https://api.hubapi.com";
|
|
4
4
|
const TTL_MS = 10 * 60 * 1e3;
|
|
5
5
|
const STANDARD_OBJECTS = [
|
|
6
6
|
{ name: "contact", path: "contacts" },
|
|
@@ -13,8 +13,8 @@ const STANDARD_OBJECTS = [
|
|
|
13
13
|
];
|
|
14
14
|
let cache = null;
|
|
15
15
|
let inFlight = null;
|
|
16
|
-
async function hsGet(apiKey, path) {
|
|
17
|
-
const res = await fetch(`${HS_BASE$
|
|
16
|
+
async function hsGet$1(apiKey, path) {
|
|
17
|
+
const res = await fetch(`${HS_BASE$3}${path}`, {
|
|
18
18
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
19
19
|
});
|
|
20
20
|
if (!res.ok) {
|
|
@@ -27,7 +27,7 @@ async function hsGet(apiKey, path) {
|
|
|
27
27
|
}
|
|
28
28
|
async function fetchGroups(apiKey, path) {
|
|
29
29
|
try {
|
|
30
|
-
const res = await hsGet(
|
|
30
|
+
const res = await hsGet$1(
|
|
31
31
|
apiKey,
|
|
32
32
|
`/crm/v3/properties/${path}/groups`
|
|
33
33
|
);
|
|
@@ -38,7 +38,7 @@ async function fetchGroups(apiKey, path) {
|
|
|
38
38
|
}
|
|
39
39
|
async function fetchObject(apiKey, object) {
|
|
40
40
|
const [res, groups] = await Promise.all([
|
|
41
|
-
hsGet(apiKey, `/crm/v3/properties/${object.path}`),
|
|
41
|
+
hsGet$1(apiKey, `/crm/v3/properties/${object.path}`),
|
|
42
42
|
fetchGroups(apiKey, object.path)
|
|
43
43
|
]);
|
|
44
44
|
return (res.results ?? []).filter((p) => !p.modificationMetadata?.readOnlyValue).map((p) => ({
|
|
@@ -52,7 +52,7 @@ async function fetchObject(apiKey, object) {
|
|
|
52
52
|
}
|
|
53
53
|
async function fetchAccount(apiKey) {
|
|
54
54
|
try {
|
|
55
|
-
return await hsGet(
|
|
55
|
+
return await hsGet$1(
|
|
56
56
|
apiKey,
|
|
57
57
|
"/account-info/v3/details"
|
|
58
58
|
);
|
|
@@ -664,9 +664,9 @@ function sanitizeRawValues(raw) {
|
|
|
664
664
|
}
|
|
665
665
|
return values;
|
|
666
666
|
}
|
|
667
|
-
const HS_BASE$
|
|
668
|
-
const HS_COMPANIES = `${HS_BASE$
|
|
669
|
-
const HS_NOTES = `${HS_BASE$
|
|
667
|
+
const HS_BASE$2 = "https://api.hubapi.com";
|
|
668
|
+
const HS_COMPANIES = `${HS_BASE$2}/crm/v3/objects/companies`;
|
|
669
|
+
const HS_NOTES = `${HS_BASE$2}/crm/v3/objects/notes`;
|
|
670
670
|
const ASSOC_NOTE_TO_CONTACT = 202;
|
|
671
671
|
const ASSOC_NOTE_TO_COMPANY = 190;
|
|
672
672
|
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -828,7 +828,7 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
828
828
|
if (companyId) {
|
|
829
829
|
await hsJson(
|
|
830
830
|
apiKey,
|
|
831
|
-
`${HS_BASE$
|
|
831
|
+
`${HS_BASE$2}/crm/v4/objects/contacts/${contactId}/associations/default/companies/${companyId}`,
|
|
832
832
|
{ method: "PUT" }
|
|
833
833
|
);
|
|
834
834
|
}
|
|
@@ -869,6 +869,168 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
869
869
|
}
|
|
870
870
|
return { submit };
|
|
871
871
|
}
|
|
872
|
+
const HS_BASE$1 = "https://api.hubapi.com";
|
|
873
|
+
const OBJECT_BY_TYPE_ID = {
|
|
874
|
+
"0-1": "contact",
|
|
875
|
+
"0-2": "company"
|
|
876
|
+
};
|
|
877
|
+
const TYPE_MAP = {
|
|
878
|
+
single_line_text: "text",
|
|
879
|
+
multi_line_text: "textarea",
|
|
880
|
+
email: "email",
|
|
881
|
+
phone: "tel",
|
|
882
|
+
mobile_phone: "tel",
|
|
883
|
+
number: "number",
|
|
884
|
+
dropdown: "select",
|
|
885
|
+
select: "select",
|
|
886
|
+
radio: "radio",
|
|
887
|
+
multiple_checkboxes: "checkbox",
|
|
888
|
+
single_checkbox: "checkbox",
|
|
889
|
+
booleancheckbox: "checkbox"
|
|
890
|
+
};
|
|
891
|
+
let counter$1 = 0;
|
|
892
|
+
const makeId$1 = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter$1 += 1).toString(36)}`;
|
|
893
|
+
function convertCondition(parentFieldId, raw) {
|
|
894
|
+
const operator = (raw.operator ?? "").toUpperCase();
|
|
895
|
+
const values = (raw.values ?? []).map((v) => String(v)).filter((v) => v !== "");
|
|
896
|
+
const spread = (op, logic) => values.length ? { logic, rules: values.map((value) => ({ field: parentFieldId, operator: op, value })) } : null;
|
|
897
|
+
switch (operator) {
|
|
898
|
+
case "EQ":
|
|
899
|
+
case "SET_ANY":
|
|
900
|
+
return spread("eq", "or");
|
|
901
|
+
case "NEQ":
|
|
902
|
+
case "NOT_SET_ANY":
|
|
903
|
+
return spread("neq", "and");
|
|
904
|
+
case "CONTAINS":
|
|
905
|
+
return spread("contains", "or");
|
|
906
|
+
case "GT":
|
|
907
|
+
return spread("gt", "or");
|
|
908
|
+
case "LT":
|
|
909
|
+
return spread("lt", "or");
|
|
910
|
+
case "SET":
|
|
911
|
+
case "NOT_EMPTY":
|
|
912
|
+
return { logic: "and", rules: [{ field: parentFieldId, operator: "notEmpty" }] };
|
|
913
|
+
case "NOT_SET":
|
|
914
|
+
case "EMPTY":
|
|
915
|
+
return { logic: "and", rules: [{ field: parentFieldId, operator: "empty" }] };
|
|
916
|
+
default:
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function convertField(raw, ctx) {
|
|
921
|
+
const label = raw.label || raw.name || "";
|
|
922
|
+
const name = raw.name ?? "";
|
|
923
|
+
if (raw.hidden) {
|
|
924
|
+
ctx.skipped.push({ code: "hidden-field", label });
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
927
|
+
const object = OBJECT_BY_TYPE_ID[raw.objectTypeId ?? "0-1"];
|
|
928
|
+
if (!object) {
|
|
929
|
+
ctx.skipped.push({ code: "object", label, detail: raw.objectTypeId });
|
|
930
|
+
return [];
|
|
931
|
+
}
|
|
932
|
+
const type = TYPE_MAP[raw.fieldType ?? ""];
|
|
933
|
+
if (!type || !name) {
|
|
934
|
+
ctx.skipped.push({ code: "field-type", label, detail: raw.fieldType });
|
|
935
|
+
return [];
|
|
936
|
+
}
|
|
937
|
+
if (ctx.usedNames.has(name)) {
|
|
938
|
+
ctx.skipped.push({ code: "duplicate", label, detail: name });
|
|
939
|
+
return [];
|
|
940
|
+
}
|
|
941
|
+
ctx.usedNames.add(name);
|
|
942
|
+
const options = (raw.options ?? []).map((o) => ({ value: String(o.value ?? ""), label: o.label || void 0 })).filter((o) => o.value !== "");
|
|
943
|
+
const field = {
|
|
944
|
+
id: makeId$1("fld"),
|
|
945
|
+
name,
|
|
946
|
+
label,
|
|
947
|
+
type,
|
|
948
|
+
required: Boolean(raw.required),
|
|
949
|
+
placeholder: raw.placeholder || void 0,
|
|
950
|
+
helpText: raw.description || void 0,
|
|
951
|
+
...options.length ? { options } : {},
|
|
952
|
+
hubspot: { object, property: name },
|
|
953
|
+
...ctx.parentCondition ? { visibleIf: ctx.parentCondition } : {}
|
|
954
|
+
};
|
|
955
|
+
const out = [field];
|
|
956
|
+
for (const dependent of raw.dependentFields ?? []) {
|
|
957
|
+
if (!dependent.field) continue;
|
|
958
|
+
const childLabel = dependent.field.label || dependent.field.name || "";
|
|
959
|
+
const condition = dependent.dependentCondition ? convertCondition(field.id, dependent.dependentCondition) : null;
|
|
960
|
+
if (!condition) {
|
|
961
|
+
ctx.skipped.push({
|
|
962
|
+
code: "condition",
|
|
963
|
+
label: childLabel,
|
|
964
|
+
detail: dependent.dependentCondition?.operator
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
out.push(...convertField(dependent.field, { ...ctx, parentCondition: condition }));
|
|
968
|
+
}
|
|
969
|
+
return out;
|
|
970
|
+
}
|
|
971
|
+
function convertHubspotForm(raw) {
|
|
972
|
+
const skipped = [];
|
|
973
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
974
|
+
const fields = [];
|
|
975
|
+
for (const group of raw.fieldGroups ?? []) {
|
|
976
|
+
if (group.richText && !(group.fields ?? []).length) {
|
|
977
|
+
skipped.push({ code: "rich-text", detail: group.richText.slice(0, 80) });
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const groupFields = (group.fields ?? []).flatMap(
|
|
981
|
+
(f) => convertField(f, { usedNames, skipped })
|
|
982
|
+
);
|
|
983
|
+
const topLevel = (group.fields ?? []).length;
|
|
984
|
+
if (topLevel === 2 && groupFields.length >= 2) {
|
|
985
|
+
groupFields[0].width = "half";
|
|
986
|
+
groupFields[1].width = "half";
|
|
987
|
+
}
|
|
988
|
+
fields.push(...groupFields);
|
|
989
|
+
}
|
|
990
|
+
if (raw.legalConsentOptions && Object.keys(raw.legalConsentOptions).length) {
|
|
991
|
+
skipped.push({ code: "legal-consent" });
|
|
992
|
+
}
|
|
993
|
+
const postSubmit = raw.configuration?.postSubmitAction;
|
|
994
|
+
return {
|
|
995
|
+
name: raw.name ?? "",
|
|
996
|
+
submitLabel: raw.displayOptions?.submitButtonText || null,
|
|
997
|
+
successMessage: postSubmit?.type === "thank_you" && postSubmit.value ? postSubmit.value : null,
|
|
998
|
+
// HubSpot forms are single-page: one step, reorganizable in the builder.
|
|
999
|
+
definition: { version: 1, steps: [{ id: makeId$1("stp"), fields }] },
|
|
1000
|
+
skipped
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
async function hsGet(apiKey, path) {
|
|
1004
|
+
const res = await fetch(`${HS_BASE$1}${path}`, {
|
|
1005
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1006
|
+
});
|
|
1007
|
+
if (!res.ok) {
|
|
1008
|
+
const body = await res.json().catch(() => ({}));
|
|
1009
|
+
throw Object.assign(new Error(body.message || `HubSpot ${res.status}`), {
|
|
1010
|
+
status: res.status
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
return await res.json();
|
|
1014
|
+
}
|
|
1015
|
+
async function listHubspotForms(apiKey) {
|
|
1016
|
+
const out = [];
|
|
1017
|
+
let after;
|
|
1018
|
+
for (let page = 0; page < 10; page += 1) {
|
|
1019
|
+
const query = after ? `&after=${encodeURIComponent(after)}` : "";
|
|
1020
|
+
const res = await hsGet(apiKey, `/marketing/v3/forms/?limit=100&formTypes=hubspot${query}`);
|
|
1021
|
+
for (const form of res.results ?? []) {
|
|
1022
|
+
if (form.id && !form.archived) {
|
|
1023
|
+
out.push({ id: form.id, name: form.name || form.id, updatedAt: form.updatedAt });
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
after = res.paging?.next?.after;
|
|
1027
|
+
if (!after) break;
|
|
1028
|
+
}
|
|
1029
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1030
|
+
}
|
|
1031
|
+
async function fetchHubspotForm(apiKey, formId) {
|
|
1032
|
+
return hsGet(apiKey, `/marketing/v3/forms/${encodeURIComponent(formId)}`);
|
|
1033
|
+
}
|
|
872
1034
|
let counter = 0;
|
|
873
1035
|
const makeId = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter += 1).toString(36)}`;
|
|
874
1036
|
const slugifyName = (label) => label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
@@ -1007,8 +1169,15 @@ function createFormsAdminController(strapi) {
|
|
|
1007
1169
|
fields: ["slug"]
|
|
1008
1170
|
});
|
|
1009
1171
|
const publishedIds = new Set(published.map((e) => e.documentId));
|
|
1172
|
+
const submissionCounts = await Promise.all(
|
|
1173
|
+
drafts.map(
|
|
1174
|
+
(entry) => strapi.documents(SUBMISSION_UID).count({
|
|
1175
|
+
filters: { form: entry.slug }
|
|
1176
|
+
})
|
|
1177
|
+
)
|
|
1178
|
+
);
|
|
1010
1179
|
ctx.body = {
|
|
1011
|
-
forms: drafts.map((entry) => ({
|
|
1180
|
+
forms: drafts.map((entry, i) => ({
|
|
1012
1181
|
documentId: entry.documentId,
|
|
1013
1182
|
name: entry.name,
|
|
1014
1183
|
slug: entry.slug,
|
|
@@ -1018,7 +1187,8 @@ function createFormsAdminController(strapi) {
|
|
|
1018
1187
|
fields: (entry.definition?.steps ?? []).reduce(
|
|
1019
1188
|
(n, step) => n + (step.fields?.length ?? 0),
|
|
1020
1189
|
0
|
|
1021
|
-
)
|
|
1190
|
+
),
|
|
1191
|
+
submissions: submissionCounts[i] ?? 0
|
|
1022
1192
|
}))
|
|
1023
1193
|
};
|
|
1024
1194
|
},
|
|
@@ -1209,6 +1379,66 @@ function createFormsAdminController(strapi) {
|
|
|
1209
1379
|
if (!targetId) ctx.throw(404, "Source entry not found");
|
|
1210
1380
|
ctx.body = { documentId: targetId, locales: imported };
|
|
1211
1381
|
},
|
|
1382
|
+
/**
|
|
1383
|
+
* Forms of the connected HubSpot portal, offered for import. Needs the
|
|
1384
|
+
* `forms` read scope on the private app token — a token without it gets
|
|
1385
|
+
* a clear message rather than an empty list.
|
|
1386
|
+
*/
|
|
1387
|
+
async listHubspotSources(ctx) {
|
|
1388
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
1389
|
+
if (!apiKey) {
|
|
1390
|
+
ctx.body = { configured: false, forms: [] };
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
try {
|
|
1394
|
+
ctx.body = { configured: true, forms: await listHubspotForms(apiKey) };
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
strapi.log.warn(`[hubspot] portal forms unavailable — ${err.message}`);
|
|
1397
|
+
ctx.throw(502, err.message || "Cannot reach HubSpot");
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
/**
|
|
1401
|
+
* Converts one portal form into a plugin form draft, in the current
|
|
1402
|
+
* locale. Slug derived from the HubSpot name: re-importing the same form
|
|
1403
|
+
* overwrites the draft — never the published version, never the portal.
|
|
1404
|
+
* What the builder can't express is skipped and returned in `skipped`.
|
|
1405
|
+
*/
|
|
1406
|
+
async runHubspotImport(ctx) {
|
|
1407
|
+
const formId = ctx.request.body?.formId;
|
|
1408
|
+
if (typeof formId !== "string" || !formId) ctx.throw(400, "formId is required");
|
|
1409
|
+
const { apiKey } = await resolveApiKey(strapi);
|
|
1410
|
+
if (!apiKey) ctx.throw(400, "No HubSpot API key configured");
|
|
1411
|
+
let converted;
|
|
1412
|
+
try {
|
|
1413
|
+
converted = convertHubspotForm(await fetchHubspotForm(apiKey, formId));
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
strapi.log.warn(`[hubspot] form import failed — ${err.message}`);
|
|
1416
|
+
ctx.throw(502, err.message || "Cannot reach HubSpot");
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const name = converted.name || "HubSpot form";
|
|
1420
|
+
const { skipped, definition, ...meta } = converted;
|
|
1421
|
+
const slug = slugify(name);
|
|
1422
|
+
const existing = await documents().findFirst({
|
|
1423
|
+
filters: { slug }
|
|
1424
|
+
});
|
|
1425
|
+
let documentId;
|
|
1426
|
+
if (existing) {
|
|
1427
|
+
await documents().update({
|
|
1428
|
+
documentId: existing.documentId,
|
|
1429
|
+
locale: ctx.query.locale,
|
|
1430
|
+
data: { ...meta, name, definition }
|
|
1431
|
+
});
|
|
1432
|
+
documentId = existing.documentId;
|
|
1433
|
+
} else {
|
|
1434
|
+
const created = await documents().create({
|
|
1435
|
+
locale: ctx.query.locale,
|
|
1436
|
+
data: { ...meta, name, slug, definition }
|
|
1437
|
+
});
|
|
1438
|
+
documentId = created.documentId;
|
|
1439
|
+
}
|
|
1440
|
+
ctx.body = { documentId, skipped };
|
|
1441
|
+
},
|
|
1212
1442
|
/**
|
|
1213
1443
|
* Copies a form (every locale of its draft) into a new draft document —
|
|
1214
1444
|
* the quickest way to A/B a variant or to start from an existing form.
|
|
@@ -1269,6 +1499,45 @@ function createFormsAdminController(strapi) {
|
|
|
1269
1499
|
}
|
|
1270
1500
|
};
|
|
1271
1501
|
}
|
|
1502
|
+
function fieldOrder(definition) {
|
|
1503
|
+
if (!definition) return [];
|
|
1504
|
+
return definition.steps.flatMap((step) => step.fields.map((field) => field.name));
|
|
1505
|
+
}
|
|
1506
|
+
const escapeCell = (value) => {
|
|
1507
|
+
if (value === null || value === void 0) return "";
|
|
1508
|
+
const text = String(value);
|
|
1509
|
+
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
1510
|
+
};
|
|
1511
|
+
function submissionsCsv(rows, knownFields) {
|
|
1512
|
+
const extra = /* @__PURE__ */ new Set();
|
|
1513
|
+
for (const row of rows) {
|
|
1514
|
+
for (const key of Object.keys(row.values ?? {})) {
|
|
1515
|
+
if (!knownFields.includes(key)) extra.add(key);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
const valueColumns = [...knownFields, ...[...extra].sort()];
|
|
1519
|
+
const header = [
|
|
1520
|
+
"submittedAt",
|
|
1521
|
+
...valueColumns,
|
|
1522
|
+
"hubspotSynced",
|
|
1523
|
+
"locale",
|
|
1524
|
+
"pagePath",
|
|
1525
|
+
"source"
|
|
1526
|
+
];
|
|
1527
|
+
const lines = [header.map(escapeCell).join(",")];
|
|
1528
|
+
for (const row of rows) {
|
|
1529
|
+
const cells = [
|
|
1530
|
+
row.createdAt ?? "",
|
|
1531
|
+
...valueColumns.map((key) => row.values?.[key]),
|
|
1532
|
+
row.hubspotSynced ? "true" : "false",
|
|
1533
|
+
row.locale ?? "",
|
|
1534
|
+
row.meta?.pagePath ?? "",
|
|
1535
|
+
row.meta?.source ?? ""
|
|
1536
|
+
];
|
|
1537
|
+
lines.push(cells.map(escapeCell).join(","));
|
|
1538
|
+
}
|
|
1539
|
+
return lines.join("\r\n");
|
|
1540
|
+
}
|
|
1272
1541
|
const HS_BASE = "https://api.hubapi.com";
|
|
1273
1542
|
const FAILURE_UID = "plugin::hubspot.failure";
|
|
1274
1543
|
const RETRY_DELAYS_MS = [500, 2e3];
|
|
@@ -1479,6 +1748,49 @@ const controllers = {
|
|
|
1479
1748
|
}
|
|
1480
1749
|
}),
|
|
1481
1750
|
formsAdmin: ({ strapi }) => createFormsAdminController(strapi),
|
|
1751
|
+
submissions: ({ strapi }) => ({
|
|
1752
|
+
/** Paged submissions, newest first, optionally narrowed to one form. */
|
|
1753
|
+
async list(ctx) {
|
|
1754
|
+
const filters = ctx.query.form ? { form: ctx.query.form } : void 0;
|
|
1755
|
+
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
1756
|
+
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 20));
|
|
1757
|
+
const [rows, total] = await Promise.all([
|
|
1758
|
+
strapi.documents(SUBMISSION_UID).findMany({
|
|
1759
|
+
filters,
|
|
1760
|
+
sort: "createdAt:desc",
|
|
1761
|
+
limit: pageSize,
|
|
1762
|
+
start: (page - 1) * pageSize
|
|
1763
|
+
}),
|
|
1764
|
+
strapi.documents(SUBMISSION_UID).count({ filters })
|
|
1765
|
+
]);
|
|
1766
|
+
ctx.body = { submissions: rows, total, page, pageSize };
|
|
1767
|
+
},
|
|
1768
|
+
/**
|
|
1769
|
+
* Whole history of one form as CSV, definition columns first. Returned as
|
|
1770
|
+
* JSON (`{ csv, filename }`): the admin fetch client speaks JSON, and the
|
|
1771
|
+
* page turns it into a download — no auth-header gymnastics.
|
|
1772
|
+
*/
|
|
1773
|
+
async export(ctx) {
|
|
1774
|
+
const slug = ctx.query.form;
|
|
1775
|
+
if (!slug) ctx.throw(400, "form is required");
|
|
1776
|
+
const [rows, entry] = await Promise.all([
|
|
1777
|
+
strapi.documents(SUBMISSION_UID).findMany({
|
|
1778
|
+
filters: { form: slug },
|
|
1779
|
+
sort: "createdAt:desc",
|
|
1780
|
+
limit: 1e4
|
|
1781
|
+
}),
|
|
1782
|
+
strapi.documents(FORM_UID).findFirst({
|
|
1783
|
+
filters: { slug },
|
|
1784
|
+
status: "draft"
|
|
1785
|
+
})
|
|
1786
|
+
]);
|
|
1787
|
+
ctx.body = {
|
|
1788
|
+
csv: submissionsCsv(rows, fieldOrder(entry?.definition)),
|
|
1789
|
+
filename: `${slug}-submissions-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`,
|
|
1790
|
+
total: rows.length
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
}),
|
|
1482
1794
|
forms: ({ strapi }) => ({
|
|
1483
1795
|
/** Published form for the host frontend — CRM mapping stripped. */
|
|
1484
1796
|
async findOne(ctx) {
|
|
@@ -1565,7 +1877,11 @@ const routes = {
|
|
|
1565
1877
|
// they pick properties — the builder itself stays gated.
|
|
1566
1878
|
adminRoute("GET", "/forms-options", "formsAdmin.options"),
|
|
1567
1879
|
adminRoute("GET", "/builder/import/sources", "formsAdmin.listSources", [FORMS_ACTION]),
|
|
1568
|
-
adminRoute("POST", "/builder/import", "formsAdmin.runImport", [FORMS_ACTION])
|
|
1880
|
+
adminRoute("POST", "/builder/import", "formsAdmin.runImport", [FORMS_ACTION]),
|
|
1881
|
+
adminRoute("GET", "/builder/import/hubspot", "formsAdmin.listHubspotSources", [FORMS_ACTION]),
|
|
1882
|
+
adminRoute("POST", "/builder/import/hubspot", "formsAdmin.runHubspotImport", [FORMS_ACTION]),
|
|
1883
|
+
adminRoute("GET", "/builder/submissions", "submissions.list", [FORMS_ACTION]),
|
|
1884
|
+
adminRoute("GET", "/builder/submissions/export", "submissions.export", [FORMS_ACTION])
|
|
1569
1885
|
]
|
|
1570
1886
|
},
|
|
1571
1887
|
// Public form delivery + submission, under /api/hubspot/…. Like any
|