strapi-plugin-hubspot 0.7.0 → 0.8.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/_chunks/{Forms-CKA6ukB6.mjs → Forms-77Mornxt.mjs} +114 -18
  3. package/dist/_chunks/{Forms-HMZbTk_B.js → Forms-B8kkPY0c.js} +113 -17
  4. package/dist/_chunks/{HubspotFormInput-CKYRcNNU.mjs → HubspotFormInput-BVknnlmR.mjs} +1 -1
  5. package/dist/_chunks/{HubspotFormInput-BequMb12.js → HubspotFormInput-kx3LlQYF.js} +1 -1
  6. package/dist/_chunks/{HubspotObjectInput-V3uhGK8k.mjs → HubspotObjectInput-B-Y1PZ6c.mjs} +3 -3
  7. package/dist/_chunks/{HubspotObjectInput-D-FGwsyS.js → HubspotObjectInput-BOO1PvOj.js} +3 -3
  8. package/dist/_chunks/{HubspotPropertyInput-DrBsx9jQ.mjs → HubspotPropertyInput-Drxn_Ddt.mjs} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-Dn1QvOcu.js → HubspotPropertyInput-awfD1opH.js} +3 -3
  10. package/dist/_chunks/{Settings-Cgp7M1Cv.js → Settings-BT1NhvxO.js} +2 -2
  11. package/dist/_chunks/{Settings-BjkNbyna.mjs → Settings-DrujhMxS.mjs} +2 -2
  12. package/dist/_chunks/{en-D5Ch9YFo.js → en-B8adhJhO.js} +10 -3
  13. package/dist/_chunks/{en-Cun5LoUP.mjs → en-BYKKi1LK.mjs} +10 -3
  14. package/dist/_chunks/{fr-CqlhHltZ.js → fr-DnKtGXaB.js} +10 -3
  15. package/dist/_chunks/{fr-ClOWflse.mjs → fr-DzgArgi3.mjs} +10 -3
  16. package/dist/_chunks/{index-FKsiOEPB.mjs → index-LrCSAHoH.mjs} +6 -6
  17. package/dist/_chunks/{index-D2Q34BuH.js → index-P7p8ASb6.js} +6 -6
  18. package/dist/_chunks/{objectLabels-LAQ0M6-a.js → objectLabels-Dn-zriuZ.js} +1 -1
  19. package/dist/_chunks/{objectLabels-DBKevJle.mjs → objectLabels-yNrnTOBo.mjs} +1 -1
  20. package/dist/_chunks/{useHubspotSchema-C08qBAyy.js → useHubspotSchema-BottzdRR.js} +1 -1
  21. package/dist/_chunks/{useHubspotSchema-BC0Od-Wl.mjs → useHubspotSchema-DccG2KbZ.mjs} +1 -1
  22. package/dist/admin/index.js +1 -1
  23. package/dist/admin/index.mjs +1 -1
  24. package/dist/admin/src/builder/types.d.ts +1 -0
  25. package/dist/server/index.js +67 -8
  26. package/dist/server/index.mjs +67 -8
  27. package/dist/server/src/content-types.d.ts +3 -0
  28. package/dist/server/src/forms.d.ts +2 -0
  29. package/dist/server/src/formsAdmin.d.ts +6 -0
  30. package/dist/server/src/importLegacy.d.ts +1 -0
  31. package/dist/server/src/index.d.ts +4 -0
  32. package/dist/shared/types.d.ts +2 -0
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 — 2026-08-29
4
+
5
+ ### Improved (builder UX pass, from a live session)
6
+ - **The whole step card now selects the step** — the click target was the tiny
7
+ "Step N" text, which read as "step titles can't be edited". Inner controls
8
+ stop propagation, so field selection and the action buttons are unaffected.
9
+ - Field panel gains the imported-but-invisible settings: **icon**, **answer
10
+ memory (persist)** and **CSS classes**; the step and form panels gain **CSS
11
+ classes** too.
12
+ - **Form-level `class`** now exists end to end: content-type attribute, save,
13
+ public API, legacy import (it used to be silently dropped on migration).
14
+ - **Duplicate** action on the forms list — every locale of the draft is
15
+ copied under a fresh slug.
16
+ - Leaving the editor with unsaved changes now warns before the tab closes.
17
+ - Cosmetics: the actions column no longer shows a raw translation key, and the
18
+ structure column pluralizes properly.
19
+
3
20
  ## 0.7.0 — 2026-08-29
4
21
 
5
22
  ### Added
@@ -3,11 +3,11 @@ import { useParams, useSearchParams, Link, useNavigate, Routes, Route } from "re
3
3
  import * as React from "react";
4
4
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
5
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 } from "@strapi/icons";
6
+ import { Trash, Plus, ArrowClockwise, ArrowLeft, ArrowUp, ArrowDown, Duplicate } from "@strapi/icons";
7
7
  import { useFetchClient } from "@strapi/strapi/admin";
8
- import { g as getTranslation, P as PLUGIN_ID } from "./index-FKsiOEPB.mjs";
9
- import { o as objectLabel } from "./objectLabels-DBKevJle.mjs";
10
- import { u as useHubspotSchema } from "./useHubspotSchema-BC0Od-Wl.mjs";
8
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-LrCSAHoH.mjs";
9
+ import { o as objectLabel } from "./objectLabels-yNrnTOBo.mjs";
10
+ import { u as useHubspotSchema } from "./useHubspotSchema-DccG2KbZ.mjs";
11
11
  const OPERATORS = ["eq", "neq", "contains", "empty", "notEmpty", "gt", "lt"];
12
12
  const NEEDS_VALUE = /* @__PURE__ */ new Set(["eq", "neq", "contains", "gt", "lt"]);
13
13
  const ConditionEditor = ({ condition, candidates, onChange }) => {
@@ -224,6 +224,43 @@ const FieldPanel = ({ field, candidates, problem, onChange }) => {
224
224
  children: t("panel.required", "Required")
225
225
  }
226
226
  ),
227
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, children: [
228
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
229
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.icon", "Icon") }),
230
+ /* @__PURE__ */ jsx(
231
+ TextInput,
232
+ {
233
+ value: field.icon ?? "",
234
+ placeholder: "i-lucide-building-2",
235
+ onChange: (e) => onChange({ icon: e.target.value })
236
+ }
237
+ )
238
+ ] }) }),
239
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
240
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.persist", "Answer memory") }),
241
+ /* @__PURE__ */ jsxs(
242
+ SingleSelect,
243
+ {
244
+ value: field.persist ?? "session",
245
+ onChange: (v) => onChange({ persist: v }),
246
+ children: [
247
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "session", children: t("panel.persist-session", "This visit only") }),
248
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "days30", children: t("panel.persist-days30", "30 days (non-identifying fields only)") })
249
+ ]
250
+ }
251
+ )
252
+ ] }) })
253
+ ] }),
254
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
255
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.class", "CSS classes") }),
256
+ /* @__PURE__ */ jsx(
257
+ TextInput,
258
+ {
259
+ value: field.class ?? "",
260
+ onChange: (e) => onChange({ class: e.target.value })
261
+ }
262
+ )
263
+ ] }),
227
264
  isChoice && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
228
265
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("panel.options", "Choices") }),
229
266
  (field.options ?? []).map((option, index) => /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
@@ -370,6 +407,14 @@ const FormEditor = () => {
370
407
  const [feedback, setFeedback] = React.useState(null);
371
408
  const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
372
409
  const query = locale ? `?locale=${locale}` : "";
410
+ React.useEffect(() => {
411
+ if (!dirty) return void 0;
412
+ const warn = (event) => {
413
+ event.preventDefault();
414
+ };
415
+ window.addEventListener("beforeunload", warn);
416
+ return () => window.removeEventListener("beforeunload", warn);
417
+ }, [dirty]);
373
418
  React.useEffect(() => {
374
419
  get("/i18n/locales").then(({ data }) => Array.isArray(data) && setLocales(data)).catch(() => setLocales([]));
375
420
  }, [get]);
@@ -462,6 +507,7 @@ const FormEditor = () => {
462
507
  nextLabel: entry.nextLabel,
463
508
  submitLabel: entry.submitLabel,
464
509
  successMessage: entry.successMessage,
510
+ class: entry.class,
465
511
  definition
466
512
  }
467
513
  );
@@ -567,6 +613,8 @@ const FormEditor = () => {
567
613
  hasRadius: true,
568
614
  shadow: "tableShadow",
569
615
  padding: 4,
616
+ onClick: () => setSelection({ kind: "step", stepId: step.id }),
617
+ style: { cursor: "pointer" },
570
618
  borderColor: selection.kind === "step" && selection.stepId === step.id ? "primary600" : void 0,
571
619
  borderStyle: selection.kind === "step" && selection.stepId === step.id ? "solid" : void 0,
572
620
  borderWidth: selection.kind === "step" && selection.stepId === step.id ? "1px" : void 0,
@@ -589,7 +637,7 @@ const FormEditor = () => {
589
637
  ]
590
638
  }
591
639
  ),
592
- /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
640
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, onClick: (e) => e.stopPropagation(), children: [
593
641
  /* @__PURE__ */ jsx(
594
642
  IconButton,
595
643
  {
@@ -631,7 +679,10 @@ const FormEditor = () => {
631
679
  background: isSelected ? "primary100" : "neutral100",
632
680
  hasRadius: true,
633
681
  padding: 3,
634
- onClick: () => setSelection({ kind: "field", fieldId: field.id }),
682
+ onClick: (e) => {
683
+ e.stopPropagation();
684
+ setSelection({ kind: "field", fieldId: field.id });
685
+ },
635
686
  style: { cursor: "pointer" },
636
687
  children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
637
688
  /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
@@ -689,7 +740,8 @@ const FormEditor = () => {
689
740
  {
690
741
  variant: "tertiary",
691
742
  startIcon: /* @__PURE__ */ jsx(Plus, {}),
692
- onClick: () => {
743
+ onClick: (e) => {
744
+ e.stopPropagation();
693
745
  const field = newField(t("editor.new-field", "New field"));
694
746
  patchStep(step.id, { fields: [...step.fields, field] });
695
747
  setSelection({ kind: "field", fieldId: field.id });
@@ -746,6 +798,16 @@ const FormEditor = () => {
746
798
  }
747
799
  )
748
800
  ] }),
801
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
802
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.class", "CSS classes") }),
803
+ /* @__PURE__ */ jsx(
804
+ TextInput,
805
+ {
806
+ value: selectedStep.class ?? "",
807
+ onChange: (e) => patchStep(selectedStep.id, { class: e.target.value })
808
+ }
809
+ )
810
+ ] }),
749
811
  /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("editor.step-condition", "Show this step only when…") }),
750
812
  /* @__PURE__ */ jsx(
751
813
  ConditionEditor,
@@ -819,6 +881,16 @@ const FormEditor = () => {
819
881
  }
820
882
  )
821
883
  ] }),
884
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
885
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.class", "CSS classes") }),
886
+ /* @__PURE__ */ jsx(
887
+ TextInput,
888
+ {
889
+ value: entry.class ?? "",
890
+ onChange: (e) => patchEntry({ class: e.target.value })
891
+ }
892
+ )
893
+ ] }),
822
894
  /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
823
895
  "editor.form-hint",
824
896
  "Select a step or a field to edit it. The public endpoint serves this form at /api/hubspot/forms/{slug}.",
@@ -878,6 +950,19 @@ const FormsList = () => {
878
950
  setBusy(false);
879
951
  }
880
952
  };
953
+ const duplicate = async (row) => {
954
+ setBusy(true);
955
+ setError(null);
956
+ try {
957
+ const { data } = await post(
958
+ `/${PLUGIN_ID}/builder/forms/${row.documentId}/duplicate`
959
+ );
960
+ navigate(data.documentId);
961
+ } catch {
962
+ setError(t("forms.duplicate-error", "Could not duplicate the form."));
963
+ setBusy(false);
964
+ }
965
+ };
881
966
  const remove = async (row) => {
882
967
  if (!window.confirm(t("forms.delete-confirm", 'Delete "{name}" and all its locales?', { name: row.name }))) {
883
968
  return;
@@ -949,7 +1034,7 @@ const FormsList = () => {
949
1034
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-structure", "Structure") }) }),
950
1035
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-status", "Status") }) }),
951
1036
  /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-updated", "Updated") }) }),
952
- /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-actions", "") }) })
1037
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-actions", "Actions") }) })
953
1038
  ] }) }),
954
1039
  /* @__PURE__ */ jsx(Tbody, { children: rows.map((row) => /* @__PURE__ */ jsxs(
955
1040
  Tr,
@@ -959,21 +1044,32 @@ const FormsList = () => {
959
1044
  children: [
960
1045
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: row.name }) }),
961
1046
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.slug }) }),
962
- /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.structure", "{steps} steps · {fields} fields", {
1047
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.structure", "{steps, plural, one {# step} other {# steps}} · {fields, plural, one {# field} other {# fields}}", {
963
1048
  steps: row.steps,
964
1049
  fields: row.fields
965
1050
  }) }) }),
966
1051
  /* @__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") }) }),
967
1052
  /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.updatedAt ? new Date(row.updatedAt).toLocaleString() : "—" }) }),
968
- /* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
969
- IconButton,
970
- {
971
- label: t("forms.delete", "Delete"),
972
- onClick: () => remove(row),
973
- disabled: busy,
974
- children: /* @__PURE__ */ jsx(Trash, {})
975
- }
976
- ) })
1053
+ /* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
1054
+ /* @__PURE__ */ jsx(
1055
+ IconButton,
1056
+ {
1057
+ label: t("forms.duplicate", "Duplicate"),
1058
+ onClick: () => duplicate(row),
1059
+ disabled: busy,
1060
+ children: /* @__PURE__ */ jsx(Duplicate, {})
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ jsx(
1064
+ IconButton,
1065
+ {
1066
+ label: t("forms.delete", "Delete"),
1067
+ onClick: () => remove(row),
1068
+ disabled: busy,
1069
+ children: /* @__PURE__ */ jsx(Trash, {})
1070
+ }
1071
+ )
1072
+ ] }) })
977
1073
  ]
978
1074
  },
979
1075
  row.documentId
@@ -7,9 +7,9 @@ const index = require("./index-DkTxsEqL.js");
7
7
  const designSystem = require("@strapi/design-system");
8
8
  const icons = require("@strapi/icons");
9
9
  const admin = require("@strapi/strapi/admin");
10
- const index$1 = require("./index-D2Q34BuH.js");
11
- const objectLabels = require("./objectLabels-LAQ0M6-a.js");
12
- const useHubspotSchema = require("./useHubspotSchema-C08qBAyy.js");
10
+ const index$1 = require("./index-P7p8ASb6.js");
11
+ const objectLabels = require("./objectLabels-Dn-zriuZ.js");
12
+ const useHubspotSchema = require("./useHubspotSchema-BottzdRR.js");
13
13
  function _interopNamespace(e) {
14
14
  if (e && e.__esModule) return e;
15
15
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -244,6 +244,43 @@ const FieldPanel = ({ field, candidates, problem, onChange }) => {
244
244
  children: t("panel.required", "Required")
245
245
  }
246
246
  ),
247
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, children: [
248
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { flex: "1", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
249
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("panel.icon", "Icon") }),
250
+ /* @__PURE__ */ jsxRuntime.jsx(
251
+ designSystem.TextInput,
252
+ {
253
+ value: field.icon ?? "",
254
+ placeholder: "i-lucide-building-2",
255
+ onChange: (e) => onChange({ icon: e.target.value })
256
+ }
257
+ )
258
+ ] }) }),
259
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { flex: "1", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
260
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("panel.persist", "Answer memory") }),
261
+ /* @__PURE__ */ jsxRuntime.jsxs(
262
+ designSystem.SingleSelect,
263
+ {
264
+ value: field.persist ?? "session",
265
+ onChange: (v) => onChange({ persist: v }),
266
+ children: [
267
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "session", children: t("panel.persist-session", "This visit only") }),
268
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "days30", children: t("panel.persist-days30", "30 days (non-identifying fields only)") })
269
+ ]
270
+ }
271
+ )
272
+ ] }) })
273
+ ] }),
274
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
275
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("panel.class", "CSS classes") }),
276
+ /* @__PURE__ */ jsxRuntime.jsx(
277
+ designSystem.TextInput,
278
+ {
279
+ value: field.class ?? "",
280
+ onChange: (e) => onChange({ class: e.target.value })
281
+ }
282
+ )
283
+ ] }),
247
284
  isChoice && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
248
285
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: t("panel.options", "Choices") }),
249
286
  (field.options ?? []).map((option, index2) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
@@ -390,6 +427,14 @@ const FormEditor = () => {
390
427
  const [feedback, setFeedback] = React__namespace.useState(null);
391
428
  const t = (id, defaultMessage, values) => formatMessage({ id: index$1.getTranslation(id), defaultMessage }, values);
392
429
  const query = locale ? `?locale=${locale}` : "";
430
+ React__namespace.useEffect(() => {
431
+ if (!dirty) return void 0;
432
+ const warn = (event) => {
433
+ event.preventDefault();
434
+ };
435
+ window.addEventListener("beforeunload", warn);
436
+ return () => window.removeEventListener("beforeunload", warn);
437
+ }, [dirty]);
393
438
  React__namespace.useEffect(() => {
394
439
  get("/i18n/locales").then(({ data }) => Array.isArray(data) && setLocales(data)).catch(() => setLocales([]));
395
440
  }, [get]);
@@ -482,6 +527,7 @@ const FormEditor = () => {
482
527
  nextLabel: entry.nextLabel,
483
528
  submitLabel: entry.submitLabel,
484
529
  successMessage: entry.successMessage,
530
+ class: entry.class,
485
531
  definition
486
532
  }
487
533
  );
@@ -587,6 +633,8 @@ const FormEditor = () => {
587
633
  hasRadius: true,
588
634
  shadow: "tableShadow",
589
635
  padding: 4,
636
+ onClick: () => setSelection({ kind: "step", stepId: step.id }),
637
+ style: { cursor: "pointer" },
590
638
  borderColor: selection.kind === "step" && selection.stepId === step.id ? "primary600" : void 0,
591
639
  borderStyle: selection.kind === "step" && selection.stepId === step.id ? "solid" : void 0,
592
640
  borderWidth: selection.kind === "step" && selection.stepId === step.id ? "1px" : void 0,
@@ -609,7 +657,7 @@ const FormEditor = () => {
609
657
  ]
610
658
  }
611
659
  ),
612
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, children: [
660
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, onClick: (e) => e.stopPropagation(), children: [
613
661
  /* @__PURE__ */ jsxRuntime.jsx(
614
662
  designSystem.IconButton,
615
663
  {
@@ -651,7 +699,10 @@ const FormEditor = () => {
651
699
  background: isSelected ? "primary100" : "neutral100",
652
700
  hasRadius: true,
653
701
  padding: 3,
654
- onClick: () => setSelection({ kind: "field", fieldId: field.id }),
702
+ onClick: (e) => {
703
+ e.stopPropagation();
704
+ setSelection({ kind: "field", fieldId: field.id });
705
+ },
655
706
  style: { cursor: "pointer" },
656
707
  children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
657
708
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
@@ -709,7 +760,8 @@ const FormEditor = () => {
709
760
  {
710
761
  variant: "tertiary",
711
762
  startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
712
- onClick: () => {
763
+ onClick: (e) => {
764
+ e.stopPropagation();
713
765
  const field = newField(t("editor.new-field", "New field"));
714
766
  patchStep(step.id, { fields: [...step.fields, field] });
715
767
  setSelection({ kind: "field", fieldId: field.id });
@@ -766,6 +818,16 @@ const FormEditor = () => {
766
818
  }
767
819
  )
768
820
  ] }),
821
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
822
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("panel.class", "CSS classes") }),
823
+ /* @__PURE__ */ jsxRuntime.jsx(
824
+ designSystem.TextInput,
825
+ {
826
+ value: selectedStep.class ?? "",
827
+ onChange: (e) => patchStep(selectedStep.id, { class: e.target.value })
828
+ }
829
+ )
830
+ ] }),
769
831
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: t("editor.step-condition", "Show this step only when…") }),
770
832
  /* @__PURE__ */ jsxRuntime.jsx(
771
833
  ConditionEditor,
@@ -839,6 +901,16 @@ const FormEditor = () => {
839
901
  }
840
902
  )
841
903
  ] }),
904
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
905
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: t("panel.class", "CSS classes") }),
906
+ /* @__PURE__ */ jsxRuntime.jsx(
907
+ designSystem.TextInput,
908
+ {
909
+ value: entry.class ?? "",
910
+ onChange: (e) => patchEntry({ class: e.target.value })
911
+ }
912
+ )
913
+ ] }),
842
914
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: t(
843
915
  "editor.form-hint",
844
916
  "Select a step or a field to edit it. The public endpoint serves this form at /api/hubspot/forms/{slug}.",
@@ -898,6 +970,19 @@ const FormsList = () => {
898
970
  setBusy(false);
899
971
  }
900
972
  };
973
+ const duplicate = async (row) => {
974
+ setBusy(true);
975
+ setError(null);
976
+ try {
977
+ const { data } = await post(
978
+ `/${index$1.PLUGIN_ID}/builder/forms/${row.documentId}/duplicate`
979
+ );
980
+ navigate(data.documentId);
981
+ } catch {
982
+ setError(t("forms.duplicate-error", "Could not duplicate the form."));
983
+ setBusy(false);
984
+ }
985
+ };
901
986
  const remove = async (row) => {
902
987
  if (!window.confirm(t("forms.delete-confirm", 'Delete "{name}" and all its locales?', { name: row.name }))) {
903
988
  return;
@@ -969,7 +1054,7 @@ const FormsList = () => {
969
1054
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: t("forms.col-structure", "Structure") }) }),
970
1055
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: t("forms.col-status", "Status") }) }),
971
1056
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: t("forms.col-updated", "Updated") }) }),
972
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: t("forms.col-actions", "") }) })
1057
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", children: t("forms.col-actions", "Actions") }) })
973
1058
  ] }) }),
974
1059
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
975
1060
  designSystem.Tr,
@@ -979,21 +1064,32 @@ const FormsList = () => {
979
1064
  children: [
980
1065
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: "semiBold", children: row.name }) }),
981
1066
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: row.slug }) }),
982
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: t("forms.structure", "{steps} steps · {fields} fields", {
1067
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: t("forms.structure", "{steps, plural, one {# step} other {# steps}} · {fields, plural, one {# field} other {# fields}}", {
983
1068
  steps: row.steps,
984
1069
  fields: row.fields
985
1070
  }) }) }),
986
1071
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: row.published ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "success100", textColor: "success700", children: t("forms.published", "Published") }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "secondary100", textColor: "secondary700", children: t("forms.draft", "Draft") }) }),
987
1072
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: row.updatedAt ? new Date(row.updatedAt).toLocaleString() : "—" }) }),
988
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsx(
989
- designSystem.IconButton,
990
- {
991
- label: t("forms.delete", "Delete"),
992
- onClick: () => remove(row),
993
- disabled: busy,
994
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
995
- }
996
- ) })
1073
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, children: [
1074
+ /* @__PURE__ */ jsxRuntime.jsx(
1075
+ designSystem.IconButton,
1076
+ {
1077
+ label: t("forms.duplicate", "Duplicate"),
1078
+ onClick: () => duplicate(row),
1079
+ disabled: busy,
1080
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Duplicate, {})
1081
+ }
1082
+ ),
1083
+ /* @__PURE__ */ jsxRuntime.jsx(
1084
+ designSystem.IconButton,
1085
+ {
1086
+ label: t("forms.delete", "Delete"),
1087
+ onClick: () => remove(row),
1088
+ disabled: busy,
1089
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
1090
+ }
1091
+ )
1092
+ ] }) })
997
1093
  ]
998
1094
  },
999
1095
  row.documentId
@@ -3,7 +3,7 @@ import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, Combobox, ComboboxOption } from "@strapi/design-system";
5
5
  import { useFetchClient } from "@strapi/strapi/admin";
6
- import { P as PLUGIN_ID, g as getTranslation } from "./index-FKsiOEPB.mjs";
6
+ import { P as PLUGIN_ID, g as getTranslation } from "./index-LrCSAHoH.mjs";
7
7
  const HubspotFormInput = React.forwardRef(
8
8
  ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
9
9
  const { formatMessage } = useIntl();
@@ -5,7 +5,7 @@ const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const admin = require("@strapi/strapi/admin");
8
- const index$1 = require("./index-D2Q34BuH.js");
8
+ const index$1 = require("./index-P7p8ASb6.js");
9
9
  function _interopNamespace(e) {
10
10
  if (e && e.__esModule) return e;
11
11
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -2,9 +2,9 @@ import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, SingleSelect, SingleSelectOption } from "@strapi/design-system";
5
- import { g as getTranslation } from "./index-FKsiOEPB.mjs";
6
- import { o as objectLabel } from "./objectLabels-DBKevJle.mjs";
7
- import { u as useHubspotSchema } from "./useHubspotSchema-BC0Od-Wl.mjs";
5
+ import { g as getTranslation } from "./index-LrCSAHoH.mjs";
6
+ import { o as objectLabel } from "./objectLabels-yNrnTOBo.mjs";
7
+ import { u as useHubspotSchema } from "./useHubspotSchema-DccG2KbZ.mjs";
8
8
  const HubspotObjectInput = React.forwardRef(
9
9
  ({ name, value, onChange, disabled, error, required, label, hint, labelAction }, ref) => {
10
10
  const intl = useIntl();
@@ -4,9 +4,9 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
- const index$1 = require("./index-D2Q34BuH.js");
8
- const objectLabels = require("./objectLabels-LAQ0M6-a.js");
9
- const useHubspotSchema = require("./useHubspotSchema-C08qBAyy.js");
7
+ const index$1 = require("./index-P7p8ASb6.js");
8
+ const objectLabels = require("./objectLabels-Dn-zriuZ.js");
9
+ const useHubspotSchema = require("./useHubspotSchema-BottzdRR.js");
10
10
  function _interopNamespace(e) {
11
11
  if (e && e.__esModule) return e;
12
12
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -4,9 +4,9 @@ import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Field, Flex, Loader, Combobox, ComboboxOption, IconButton, Button, Typography, Link } from "@strapi/design-system";
5
5
  import { ArrowClockwise } from "@strapi/icons";
6
6
  import { useField } from "@strapi/strapi/admin";
7
- import { g as getTranslation } from "./index-FKsiOEPB.mjs";
8
- import { o as objectLabel } from "./objectLabels-DBKevJle.mjs";
9
- import { u as useHubspotSchema } from "./useHubspotSchema-BC0Od-Wl.mjs";
7
+ import { g as getTranslation } from "./index-LrCSAHoH.mjs";
8
+ import { o as objectLabel } from "./objectLabels-yNrnTOBo.mjs";
9
+ import { u as useHubspotSchema } from "./useHubspotSchema-DccG2KbZ.mjs";
10
10
  const oneLine = {
11
11
  display: "block",
12
12
  whiteSpace: "nowrap",
@@ -6,9 +6,9 @@ const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const icons = require("@strapi/icons");
8
8
  const admin = require("@strapi/strapi/admin");
9
- const index$1 = require("./index-D2Q34BuH.js");
10
- const objectLabels = require("./objectLabels-LAQ0M6-a.js");
11
- const useHubspotSchema = require("./useHubspotSchema-C08qBAyy.js");
9
+ const index$1 = require("./index-P7p8ASb6.js");
10
+ const objectLabels = require("./objectLabels-Dn-zriuZ.js");
11
+ const useHubspotSchema = require("./useHubspotSchema-BottzdRR.js");
12
12
  function _interopNamespace(e) {
13
13
  if (e && e.__esModule) return e;
14
14
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -5,9 +5,9 @@ const React = require("react");
5
5
  const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const admin = require("@strapi/strapi/admin");
8
- const index$1 = require("./index-D2Q34BuH.js");
8
+ const index$1 = require("./index-P7p8ASb6.js");
9
9
  const reactRouterDom = require("react-router-dom");
10
- const objectLabels = require("./objectLabels-LAQ0M6-a.js");
10
+ const objectLabels = require("./objectLabels-Dn-zriuZ.js");
11
11
  function _interopNamespace(e) {
12
12
  if (e && e.__esModule) return e;
13
13
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -3,9 +3,9 @@ import * as React from "react";
3
3
  import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Flex, Divider, Typography, Button, Box, Link, Badge, Loader, Field } from "@strapi/design-system";
5
5
  import { useFetchClient } from "@strapi/strapi/admin";
6
- import { g as getTranslation, P as PLUGIN_ID } from "./index-FKsiOEPB.mjs";
6
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-LrCSAHoH.mjs";
7
7
  import { Link as Link$1 } from "react-router-dom";
8
- import { o as objectLabel } from "./objectLabels-DBKevJle.mjs";
8
+ import { o as objectLabel } from "./objectLabels-yNrnTOBo.mjs";
9
9
  const AuditSection = ({ configured }) => {
10
10
  const intl = useIntl();
11
11
  const { formatMessage } = intl;
@@ -94,8 +94,8 @@ const en = {
94
94
  "forms.col-structure": "Structure",
95
95
  "forms.col-status": "Status",
96
96
  "forms.col-updated": "Updated",
97
- "forms.col-actions": "",
98
- "forms.structure": "{steps} steps · {fields} fields",
97
+ "forms.col-actions": "Actions",
98
+ "forms.structure": "{steps, plural, one {# step} other {# steps}} · {fields, plural, one {# field} other {# fields}}",
99
99
  "forms.published": "Published",
100
100
  "forms.draft": "Draft",
101
101
  "forms.delete": "Delete",
@@ -199,6 +199,13 @@ const en = {
199
199
  "form-picker.placeholder": "Choose a form…",
200
200
  "form-picker.fallback": "Form slug",
201
201
  "form-picker.orphan": "{slug} — unknown form",
202
- "form-picker.draft": "{name} ({slug}) — draft, not served yet"
202
+ "form-picker.draft": "{name} ({slug}) — draft, not served yet",
203
+ "forms.duplicate": "Duplicate",
204
+ "forms.duplicate-error": "Could not duplicate the form.",
205
+ "panel.icon": "Icon",
206
+ "panel.persist": "Answer memory",
207
+ "panel.persist-session": "This visit only",
208
+ "panel.persist-days30": "30 days (non-identifying fields only)",
209
+ "panel.class": "CSS classes"
203
210
  };
204
211
  exports.default = en;
@@ -92,8 +92,8 @@ const en = {
92
92
  "forms.col-structure": "Structure",
93
93
  "forms.col-status": "Status",
94
94
  "forms.col-updated": "Updated",
95
- "forms.col-actions": "",
96
- "forms.structure": "{steps} steps · {fields} fields",
95
+ "forms.col-actions": "Actions",
96
+ "forms.structure": "{steps, plural, one {# step} other {# steps}} · {fields, plural, one {# field} other {# fields}}",
97
97
  "forms.published": "Published",
98
98
  "forms.draft": "Draft",
99
99
  "forms.delete": "Delete",
@@ -197,7 +197,14 @@ const en = {
197
197
  "form-picker.placeholder": "Choose a form…",
198
198
  "form-picker.fallback": "Form slug",
199
199
  "form-picker.orphan": "{slug} — unknown form",
200
- "form-picker.draft": "{name} ({slug}) — draft, not served yet"
200
+ "form-picker.draft": "{name} ({slug}) — draft, not served yet",
201
+ "forms.duplicate": "Duplicate",
202
+ "forms.duplicate-error": "Could not duplicate the form.",
203
+ "panel.icon": "Icon",
204
+ "panel.persist": "Answer memory",
205
+ "panel.persist-session": "This visit only",
206
+ "panel.persist-days30": "30 days (non-identifying fields only)",
207
+ "panel.class": "CSS classes"
201
208
  };
202
209
  export {
203
210
  en as default
@@ -94,8 +94,8 @@ const fr = {
94
94
  "forms.col-structure": "Structure",
95
95
  "forms.col-status": "Statut",
96
96
  "forms.col-updated": "Modifié",
97
- "forms.col-actions": "",
98
- "forms.structure": "{steps} étapes · {fields} champs",
97
+ "forms.col-actions": "Actions",
98
+ "forms.structure": "{steps, plural, one {# étape} other {# étapes}} · {fields, plural, one {# champ} other {# champs}}",
99
99
  "forms.published": "Publié",
100
100
  "forms.draft": "Brouillon",
101
101
  "forms.delete": "Supprimer",
@@ -199,6 +199,13 @@ const fr = {
199
199
  "form-picker.placeholder": "Choisir un formulaire…",
200
200
  "form-picker.fallback": "Slug du formulaire",
201
201
  "form-picker.orphan": "{slug} — formulaire inconnu",
202
- "form-picker.draft": "{name} ({slug}) — brouillon, pas encore servi"
202
+ "form-picker.draft": "{name} ({slug}) — brouillon, pas encore servi",
203
+ "forms.duplicate": "Dupliquer",
204
+ "forms.duplicate-error": "Impossible de dupliquer le formulaire.",
205
+ "panel.icon": "Icône",
206
+ "panel.persist": "Mémoire de la réponse",
207
+ "panel.persist-session": "Cette visite seulement",
208
+ "panel.persist-days30": "30 jours (champs non identifiants uniquement)",
209
+ "panel.class": "Classes CSS"
203
210
  };
204
211
  exports.default = fr;
@@ -92,8 +92,8 @@ const fr = {
92
92
  "forms.col-structure": "Structure",
93
93
  "forms.col-status": "Statut",
94
94
  "forms.col-updated": "Modifié",
95
- "forms.col-actions": "",
96
- "forms.structure": "{steps} étapes · {fields} champs",
95
+ "forms.col-actions": "Actions",
96
+ "forms.structure": "{steps, plural, one {# étape} other {# étapes}} · {fields, plural, one {# champ} other {# champs}}",
97
97
  "forms.published": "Publié",
98
98
  "forms.draft": "Brouillon",
99
99
  "forms.delete": "Supprimer",
@@ -197,7 +197,14 @@ const fr = {
197
197
  "form-picker.placeholder": "Choisir un formulaire…",
198
198
  "form-picker.fallback": "Slug du formulaire",
199
199
  "form-picker.orphan": "{slug} — formulaire inconnu",
200
- "form-picker.draft": "{name} ({slug}) — brouillon, pas encore servi"
200
+ "form-picker.draft": "{name} ({slug}) — brouillon, pas encore servi",
201
+ "forms.duplicate": "Dupliquer",
202
+ "forms.duplicate-error": "Impossible de dupliquer le formulaire.",
203
+ "panel.icon": "Icône",
204
+ "panel.persist": "Mémoire de la réponse",
205
+ "panel.persist-session": "Cette visite seulement",
206
+ "panel.persist-days30": "30 jours (champs non identifiants uniquement)",
207
+ "panel.class": "Classes CSS"
201
208
  };
202
209
  export {
203
210
  fr as default
@@ -25,7 +25,7 @@ const index = {
25
25
  icon: PaperPlane,
26
26
  intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
27
27
  permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
28
- Component: async () => (await import("./Forms-CKA6ukB6.mjs")).default
28
+ Component: async () => (await import("./Forms-77Mornxt.mjs")).default
29
29
  });
30
30
  app.customFields.register({
31
31
  name: "property",
@@ -40,7 +40,7 @@ const index = {
40
40
  defaultMessage: "Picked from the portal's real properties"
41
41
  },
42
42
  components: {
43
- Input: async () => import("./HubspotPropertyInput-DrBsx9jQ.mjs")
43
+ Input: async () => import("./HubspotPropertyInput-Drxn_Ddt.mjs")
44
44
  },
45
45
  // Exposed in the Content-Type Builder. Without this, `objectField` could
46
46
  // only be set by hand-editing schema.json — which is not how anyone
@@ -103,7 +103,7 @@ const index = {
103
103
  defaultMessage: "Picked from the portal's configured objects"
104
104
  },
105
105
  components: {
106
- Input: async () => import("./HubspotObjectInput-V3uhGK8k.mjs")
106
+ Input: async () => import("./HubspotObjectInput-B-Y1PZ6c.mjs")
107
107
  }
108
108
  });
109
109
  app.customFields.register({
@@ -119,7 +119,7 @@ const index = {
119
119
  defaultMessage: "A form built in the HubSpot form builder, stored by slug"
120
120
  },
121
121
  components: {
122
- Input: async () => import("./HubspotFormInput-CKYRcNNU.mjs")
122
+ Input: async () => import("./HubspotFormInput-BVknnlmR.mjs")
123
123
  }
124
124
  });
125
125
  app.createSettingSection(
@@ -135,7 +135,7 @@ const index = {
135
135
  // Mirrors the RBAC action registered server-side: a role without it
136
136
  // doesn't see the link at all — the routes refuse it anyway.
137
137
  permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
138
- Component: async () => (await import("./Settings-BjkNbyna.mjs")).default
138
+ Component: async () => (await import("./Settings-DrujhMxS.mjs")).default
139
139
  }
140
140
  ]
141
141
  );
@@ -145,7 +145,7 @@ const index = {
145
145
  return Promise.all(
146
146
  locales.map(async (locale) => {
147
147
  try {
148
- const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-Cun5LoUP.mjs"), "./translations/fr.json": () => import("./fr-ClOWflse.mjs") }), `./translations/${locale}.json`, 3);
148
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-BYKKi1LK.mjs"), "./translations/fr.json": () => import("./fr-DzgArgi3.mjs") }), `./translations/${locale}.json`, 3);
149
149
  return { data: prefixPluginTranslations(data), locale };
150
150
  } catch {
151
151
  return { data: {}, locale };
@@ -26,7 +26,7 @@ const index = {
26
26
  icon: icons.PaperPlane,
27
27
  intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
28
28
  permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
29
- Component: async () => (await Promise.resolve().then(() => require("./Forms-HMZbTk_B.js"))).default
29
+ Component: async () => (await Promise.resolve().then(() => require("./Forms-B8kkPY0c.js"))).default
30
30
  });
31
31
  app.customFields.register({
32
32
  name: "property",
@@ -41,7 +41,7 @@ const index = {
41
41
  defaultMessage: "Picked from the portal's real properties"
42
42
  },
43
43
  components: {
44
- Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-Dn1QvOcu.js"))
44
+ Input: async () => Promise.resolve().then(() => require("./HubspotPropertyInput-awfD1opH.js"))
45
45
  },
46
46
  // Exposed in the Content-Type Builder. Without this, `objectField` could
47
47
  // only be set by hand-editing schema.json — which is not how anyone
@@ -104,7 +104,7 @@ const index = {
104
104
  defaultMessage: "Picked from the portal's configured objects"
105
105
  },
106
106
  components: {
107
- Input: async () => Promise.resolve().then(() => require("./HubspotObjectInput-D-FGwsyS.js"))
107
+ Input: async () => Promise.resolve().then(() => require("./HubspotObjectInput-BOO1PvOj.js"))
108
108
  }
109
109
  });
110
110
  app.customFields.register({
@@ -120,7 +120,7 @@ const index = {
120
120
  defaultMessage: "A form built in the HubSpot form builder, stored by slug"
121
121
  },
122
122
  components: {
123
- Input: async () => Promise.resolve().then(() => require("./HubspotFormInput-BequMb12.js"))
123
+ Input: async () => Promise.resolve().then(() => require("./HubspotFormInput-kx3LlQYF.js"))
124
124
  }
125
125
  });
126
126
  app.createSettingSection(
@@ -136,7 +136,7 @@ const index = {
136
136
  // Mirrors the RBAC action registered server-side: a role without it
137
137
  // doesn't see the link at all — the routes refuse it anyway.
138
138
  permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
139
- Component: async () => (await Promise.resolve().then(() => require("./Settings-Cgp7M1Cv.js"))).default
139
+ Component: async () => (await Promise.resolve().then(() => require("./Settings-BT1NhvxO.js"))).default
140
140
  }
141
141
  ]
142
142
  );
@@ -146,7 +146,7 @@ const index = {
146
146
  return Promise.all(
147
147
  locales.map(async (locale) => {
148
148
  try {
149
- const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-D5Ch9YFo.js")), "./translations/fr.json": () => Promise.resolve().then(() => require("./fr-CqlhHltZ.js")) }), `./translations/${locale}.json`, 3);
149
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-B8adhJhO.js")), "./translations/fr.json": () => Promise.resolve().then(() => require("./fr-DnKtGXaB.js")) }), `./translations/${locale}.json`, 3);
150
150
  return { data: prefixPluginTranslations(data), locale };
151
151
  } catch {
152
152
  return { data: {}, locale };
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const index = require("./index-D2Q34BuH.js");
2
+ const index = require("./index-P7p8ASb6.js");
3
3
  const STANDARD = /* @__PURE__ */ new Set([
4
4
  "contact",
5
5
  "company",
@@ -1,4 +1,4 @@
1
- import { g as getTranslation } from "./index-FKsiOEPB.mjs";
1
+ import { g as getTranslation } from "./index-LrCSAHoH.mjs";
2
2
  const STANDARD = /* @__PURE__ */ new Set([
3
3
  "contact",
4
4
  "company",
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  const React = require("react");
3
3
  const admin = require("@strapi/strapi/admin");
4
- const index = require("./index-D2Q34BuH.js");
4
+ const index = require("./index-P7p8ASb6.js");
5
5
  function _interopNamespace(e) {
6
6
  if (e && e.__esModule) return e;
7
7
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { useFetchClient } from "@strapi/strapi/admin";
3
- import { P as PLUGIN_ID } from "./index-FKsiOEPB.mjs";
3
+ import { P as PLUGIN_ID } from "./index-LrCSAHoH.mjs";
4
4
  let cached = null;
5
5
  let inFlight = null;
6
6
  const listeners = /* @__PURE__ */ new Set();
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-D2Q34BuH.js");
2
+ const index = require("../_chunks/index-P7p8ASb6.js");
3
3
  require("@strapi/icons");
4
4
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-FKsiOEPB.mjs";
1
+ import { i } from "../_chunks/index-LrCSAHoH.mjs";
2
2
  import "@strapi/icons";
3
3
  export {
4
4
  i as default
@@ -55,6 +55,7 @@ export interface FormEntryDto {
55
55
  nextLabel?: string | null;
56
56
  submitLabel?: string | null;
57
57
  successMessage?: string | null;
58
+ class?: string | null;
58
59
  locale?: string | null;
59
60
  updatedAt?: string;
60
61
  definition: FormDefinition;
@@ -397,6 +397,7 @@ const contentTypes = {
397
397
  nextLabel: localizedString(),
398
398
  submitLabel: localizedString(),
399
399
  successMessage: localizedString("text"),
400
+ class: { type: "string" },
400
401
  definition: {
401
402
  type: "json",
402
403
  required: true,
@@ -573,6 +574,7 @@ function publicForm(entry) {
573
574
  nextLabel: entry.nextLabel ?? null,
574
575
  submitLabel: entry.submitLabel ?? null,
575
576
  successMessage: entry.successMessage ?? null,
577
+ class: entry.class ?? null,
576
578
  locale: entry.locale ?? null,
577
579
  steps: (entry.definition?.steps ?? []).map((step) => ({
578
580
  ...step,
@@ -932,6 +934,7 @@ function convertLegacyForm(entry, map = {}) {
932
934
  nextLabel: entry.nextLabel ?? null,
933
935
  submitLabel: entry.submitLabel ?? null,
934
936
  successMessage: entry.successMessage ?? null,
937
+ class: entry.class ?? null,
935
938
  definition: { version: 1, steps }
936
939
  };
937
940
  }
@@ -943,7 +946,8 @@ const WRITABLE = [
943
946
  "subtitle",
944
947
  "nextLabel",
945
948
  "submitLabel",
946
- "successMessage"
949
+ "successMessage",
950
+ "class"
947
951
  ];
948
952
  const slugify = (value) => value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "form";
949
953
  function createFormsAdminController(strapi) {
@@ -962,6 +966,15 @@ function createFormsAdminController(strapi) {
962
966
  return null;
963
967
  }
964
968
  }
969
+ async function uniqueSlug(name) {
970
+ const base = slugify(name);
971
+ let slug = base;
972
+ for (let i = 2; ; i += 1) {
973
+ const clash = await documents().findFirst({ filters: { slug } });
974
+ if (!clash) return slug;
975
+ slug = `${base}-${i}`;
976
+ }
977
+ }
965
978
  function pickWritable(body) {
966
979
  const data = {};
967
980
  for (const key of WRITABLE) {
@@ -1027,13 +1040,7 @@ function createFormsAdminController(strapi) {
1027
1040
  const body = ctx.request.body ?? {};
1028
1041
  const name = typeof body.name === "string" ? body.name.trim() : "";
1029
1042
  if (!name) ctx.throw(400, "A form needs a name");
1030
- const base = slugify(name);
1031
- let slug = base;
1032
- for (let i = 2; ; i += 1) {
1033
- const clash = await documents().findFirst({ filters: { slug } });
1034
- if (!clash) break;
1035
- slug = `${base}-${i}`;
1036
- }
1043
+ const slug = await uniqueSlug(name);
1037
1044
  let definition;
1038
1045
  try {
1039
1046
  definition = requireValidDefinition(ctx, body.definition);
@@ -1202,6 +1209,57 @@ function createFormsAdminController(strapi) {
1202
1209
  if (!targetId) ctx.throw(404, "Source entry not found");
1203
1210
  ctx.body = { documentId: targetId, locales: imported };
1204
1211
  },
1212
+ /**
1213
+ * Copies a form (every locale of its draft) into a new draft document —
1214
+ * the quickest way to A/B a variant or to start from an existing form.
1215
+ * Internal step/field ids are kept: they only need uniqueness per form.
1216
+ */
1217
+ async duplicate(ctx) {
1218
+ const sourceId = ctx.params.documentId;
1219
+ let locales = [void 0];
1220
+ try {
1221
+ const found = await strapi.plugin("i18n").service("locales").find();
1222
+ if (Array.isArray(found) && found.length) {
1223
+ locales = [...found].sort((a, b) => Number(b.isDefault ?? false) - Number(a.isDefault ?? false)).map((l) => l.code);
1224
+ }
1225
+ } catch {
1226
+ }
1227
+ let targetId = null;
1228
+ let slug = null;
1229
+ for (const locale of locales) {
1230
+ const source = await documents().findOne({
1231
+ documentId: sourceId,
1232
+ locale,
1233
+ status: "draft"
1234
+ });
1235
+ if (!source) continue;
1236
+ const name = `${source.name} (2)`;
1237
+ slug ??= await uniqueSlug(name);
1238
+ const data = {
1239
+ name,
1240
+ slug,
1241
+ title: source.title,
1242
+ subtitle: source.subtitle,
1243
+ nextLabel: source.nextLabel,
1244
+ submitLabel: source.submitLabel,
1245
+ successMessage: source.successMessage,
1246
+ class: source.class,
1247
+ definition: source.definition
1248
+ };
1249
+ if (targetId) {
1250
+ const { slug: _slug, name: _name, ...localized } = data;
1251
+ await documents().update({ documentId: targetId, locale, data: localized });
1252
+ } else {
1253
+ const created = await documents().create({
1254
+ locale,
1255
+ data
1256
+ });
1257
+ targetId = created.documentId;
1258
+ }
1259
+ }
1260
+ if (!targetId) ctx.throw(404, "Form not found");
1261
+ ctx.body = { documentId: targetId };
1262
+ },
1205
1263
  async remove(ctx) {
1206
1264
  await documents().delete({
1207
1265
  documentId: ctx.params.documentId
@@ -1502,6 +1560,7 @@ const routes = {
1502
1560
  adminRoute("POST", "/builder/forms/:documentId/publish", "formsAdmin.publish", [FORMS_ACTION]),
1503
1561
  adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
1504
1562
  adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
1563
+ adminRoute("POST", "/builder/forms/:documentId/duplicate", "formsAdmin.duplicate", [FORMS_ACTION]),
1505
1564
  // No FORMS_ACTION: editors pick forms from the Content Manager, like
1506
1565
  // they pick properties — the builder itself stays gated.
1507
1566
  adminRoute("GET", "/forms-options", "formsAdmin.options"),
@@ -396,6 +396,7 @@ const contentTypes = {
396
396
  nextLabel: localizedString(),
397
397
  submitLabel: localizedString(),
398
398
  successMessage: localizedString("text"),
399
+ class: { type: "string" },
399
400
  definition: {
400
401
  type: "json",
401
402
  required: true,
@@ -572,6 +573,7 @@ function publicForm(entry) {
572
573
  nextLabel: entry.nextLabel ?? null,
573
574
  submitLabel: entry.submitLabel ?? null,
574
575
  successMessage: entry.successMessage ?? null,
576
+ class: entry.class ?? null,
575
577
  locale: entry.locale ?? null,
576
578
  steps: (entry.definition?.steps ?? []).map((step) => ({
577
579
  ...step,
@@ -931,6 +933,7 @@ function convertLegacyForm(entry, map = {}) {
931
933
  nextLabel: entry.nextLabel ?? null,
932
934
  submitLabel: entry.submitLabel ?? null,
933
935
  successMessage: entry.successMessage ?? null,
936
+ class: entry.class ?? null,
934
937
  definition: { version: 1, steps }
935
938
  };
936
939
  }
@@ -942,7 +945,8 @@ const WRITABLE = [
942
945
  "subtitle",
943
946
  "nextLabel",
944
947
  "submitLabel",
945
- "successMessage"
948
+ "successMessage",
949
+ "class"
946
950
  ];
947
951
  const slugify = (value) => value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "form";
948
952
  function createFormsAdminController(strapi) {
@@ -961,6 +965,15 @@ function createFormsAdminController(strapi) {
961
965
  return null;
962
966
  }
963
967
  }
968
+ async function uniqueSlug(name) {
969
+ const base = slugify(name);
970
+ let slug = base;
971
+ for (let i = 2; ; i += 1) {
972
+ const clash = await documents().findFirst({ filters: { slug } });
973
+ if (!clash) return slug;
974
+ slug = `${base}-${i}`;
975
+ }
976
+ }
964
977
  function pickWritable(body) {
965
978
  const data = {};
966
979
  for (const key of WRITABLE) {
@@ -1026,13 +1039,7 @@ function createFormsAdminController(strapi) {
1026
1039
  const body = ctx.request.body ?? {};
1027
1040
  const name = typeof body.name === "string" ? body.name.trim() : "";
1028
1041
  if (!name) ctx.throw(400, "A form needs a name");
1029
- const base = slugify(name);
1030
- let slug = base;
1031
- for (let i = 2; ; i += 1) {
1032
- const clash = await documents().findFirst({ filters: { slug } });
1033
- if (!clash) break;
1034
- slug = `${base}-${i}`;
1035
- }
1042
+ const slug = await uniqueSlug(name);
1036
1043
  let definition;
1037
1044
  try {
1038
1045
  definition = requireValidDefinition(ctx, body.definition);
@@ -1201,6 +1208,57 @@ function createFormsAdminController(strapi) {
1201
1208
  if (!targetId) ctx.throw(404, "Source entry not found");
1202
1209
  ctx.body = { documentId: targetId, locales: imported };
1203
1210
  },
1211
+ /**
1212
+ * Copies a form (every locale of its draft) into a new draft document —
1213
+ * the quickest way to A/B a variant or to start from an existing form.
1214
+ * Internal step/field ids are kept: they only need uniqueness per form.
1215
+ */
1216
+ async duplicate(ctx) {
1217
+ const sourceId = ctx.params.documentId;
1218
+ let locales = [void 0];
1219
+ try {
1220
+ const found = await strapi.plugin("i18n").service("locales").find();
1221
+ if (Array.isArray(found) && found.length) {
1222
+ locales = [...found].sort((a, b) => Number(b.isDefault ?? false) - Number(a.isDefault ?? false)).map((l) => l.code);
1223
+ }
1224
+ } catch {
1225
+ }
1226
+ let targetId = null;
1227
+ let slug = null;
1228
+ for (const locale of locales) {
1229
+ const source = await documents().findOne({
1230
+ documentId: sourceId,
1231
+ locale,
1232
+ status: "draft"
1233
+ });
1234
+ if (!source) continue;
1235
+ const name = `${source.name} (2)`;
1236
+ slug ??= await uniqueSlug(name);
1237
+ const data = {
1238
+ name,
1239
+ slug,
1240
+ title: source.title,
1241
+ subtitle: source.subtitle,
1242
+ nextLabel: source.nextLabel,
1243
+ submitLabel: source.submitLabel,
1244
+ successMessage: source.successMessage,
1245
+ class: source.class,
1246
+ definition: source.definition
1247
+ };
1248
+ if (targetId) {
1249
+ const { slug: _slug, name: _name, ...localized } = data;
1250
+ await documents().update({ documentId: targetId, locale, data: localized });
1251
+ } else {
1252
+ const created = await documents().create({
1253
+ locale,
1254
+ data
1255
+ });
1256
+ targetId = created.documentId;
1257
+ }
1258
+ }
1259
+ if (!targetId) ctx.throw(404, "Form not found");
1260
+ ctx.body = { documentId: targetId };
1261
+ },
1204
1262
  async remove(ctx) {
1205
1263
  await documents().delete({
1206
1264
  documentId: ctx.params.documentId
@@ -1501,6 +1559,7 @@ const routes = {
1501
1559
  adminRoute("POST", "/builder/forms/:documentId/publish", "formsAdmin.publish", [FORMS_ACTION]),
1502
1560
  adminRoute("POST", "/builder/forms/:documentId/unpublish", "formsAdmin.unpublish", [FORMS_ACTION]),
1503
1561
  adminRoute("DELETE", "/builder/forms/:documentId", "formsAdmin.remove", [FORMS_ACTION]),
1562
+ adminRoute("POST", "/builder/forms/:documentId/duplicate", "formsAdmin.duplicate", [FORMS_ACTION]),
1504
1563
  // No FORMS_ACTION: editors pick forms from the Content Manager, like
1505
1564
  // they pick properties — the builder itself stays gated.
1506
1565
  adminRoute("GET", "/forms-options", "formsAdmin.options"),
@@ -85,6 +85,9 @@ declare const _default: {
85
85
  };
86
86
  };
87
87
  };
88
+ class: {
89
+ type: string;
90
+ };
88
91
  definition: {
89
92
  type: string;
90
93
  required: boolean;
@@ -19,6 +19,7 @@ export interface FormEntry {
19
19
  nextLabel?: string | null;
20
20
  submitLabel?: string | null;
21
21
  successMessage?: string | null;
22
+ class?: string | null;
22
23
  locale?: string | null;
23
24
  definition: FormDefinition;
24
25
  [key: string]: unknown;
@@ -30,6 +31,7 @@ export interface PublicForm {
30
31
  nextLabel?: string | null;
31
32
  submitLabel?: string | null;
32
33
  successMessage?: string | null;
34
+ class?: string | null;
33
35
  locale?: string | null;
34
36
  steps: FormDefinition["steps"];
35
37
  }
@@ -42,5 +42,11 @@ export declare function createFormsAdminController(strapi: Core.Strapi): {
42
42
  * published version, and never modifies the source.
43
43
  */
44
44
  runImport(ctx: Ctx): Promise<void>;
45
+ /**
46
+ * Copies a form (every locale of its draft) into a new draft document —
47
+ * the quickest way to A/B a variant or to start from an existing form.
48
+ * Internal step/field ids are kept: they only need uniqueness per form.
49
+ */
50
+ duplicate(ctx: Ctx): Promise<void>;
45
51
  remove(ctx: Ctx): Promise<void>;
46
52
  };
@@ -20,6 +20,7 @@ export interface ConvertedForm {
20
20
  nextLabel?: string | null;
21
21
  submitLabel?: string | null;
22
22
  successMessage?: string | null;
23
+ class?: string | null;
23
24
  definition: FormDefinition;
24
25
  }
25
26
  export declare function convertLegacyForm(entry: Record<string, unknown>, map?: ImportMap): ConvertedForm;
@@ -96,6 +96,9 @@ declare const _default: {
96
96
  };
97
97
  };
98
98
  };
99
+ class: {
100
+ type: string;
101
+ };
99
102
  definition: {
100
103
  type: string;
101
104
  required: boolean;
@@ -253,6 +256,7 @@ declare const _default: {
253
256
  options(ctx: import("./formsAdmin").Ctx): Promise<void>;
254
257
  listSources(ctx: import("./formsAdmin").Ctx): Promise<void>;
255
258
  runImport(ctx: import("./formsAdmin").Ctx): Promise<void>;
259
+ duplicate(ctx: import("./formsAdmin").Ctx): Promise<void>;
256
260
  remove(ctx: import("./formsAdmin").Ctx): Promise<void>;
257
261
  };
258
262
  forms: ({ strapi }: {
@@ -72,6 +72,8 @@ export interface PublicForm {
72
72
  nextLabel?: string | null;
73
73
  submitLabel?: string | null;
74
74
  successMessage?: string | null;
75
+ /** Extra utility classes on the form wrapper, for the host frontend. */
76
+ class?: string | null;
75
77
  locale?: string | null;
76
78
  steps: PublicFormStep[];
77
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-hubspot",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The form builder for HubSpot in Strapi — multi-step forms with conditional fields mapped to real CRM properties, a public submit pipeline with retries, and save-time mapping validation.",
5
5
  "keywords": [
6
6
  "strapi",