strapi-plugin-hubspot 0.4.2 → 0.6.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 (54) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +116 -4
  3. package/dist/_chunks/Forms-BF4_zA-m.js +1007 -0
  4. package/dist/_chunks/Forms-CW1KFO9U.mjs +989 -0
  5. package/dist/_chunks/{HubspotObjectInput-DutelVp6.mjs → HubspotObjectInput-CpxeM2J0.mjs} +4 -16
  6. package/dist/_chunks/{HubspotObjectInput-Br2jTFkG.js → HubspotObjectInput-hjAYWtaU.js} +4 -16
  7. package/dist/_chunks/HubspotPropertyInput-C-52Lu24.mjs +227 -0
  8. package/dist/_chunks/HubspotPropertyInput-OSeIBaRC.js +245 -0
  9. package/dist/_chunks/Settings-2V7TH5CX.mjs +321 -0
  10. package/dist/_chunks/Settings-DcA9M5Tw.js +339 -0
  11. package/dist/_chunks/en-CJ9wYHgN.mjs +197 -0
  12. package/dist/_chunks/en-C_-N8cjv.js +197 -0
  13. package/dist/_chunks/fr-BHObPoUp.js +197 -0
  14. package/dist/_chunks/fr-DttDn-2Y.mjs +197 -0
  15. package/dist/_chunks/{Settings-CQsB0B2T.mjs → index-C1UJXE_o.mjs} +576 -317
  16. package/dist/_chunks/{index-BUlB1rK9.js → index-CMzg-x5i.js} +12 -4
  17. package/dist/_chunks/{Settings-CvbKVl-t.js → index-Dgbid2LK.js} +580 -322
  18. package/dist/_chunks/{index-CKoBcX1T.mjs → index-DkGOTp8p.mjs} +12 -4
  19. package/dist/_chunks/{objectLabels-dM4o67Fz.mjs → objectLabels-D9UEuOGr.mjs} +1 -1
  20. package/dist/_chunks/{objectLabels-B76t8qqg.js → objectLabels-Th7aYW-B.js} +1 -1
  21. package/dist/_chunks/useHubspotSchema-Bgrgg8Mt.js +75 -0
  22. package/dist/_chunks/useHubspotSchema-DQkQcA_V.mjs +58 -0
  23. package/dist/admin/index.js +2 -1
  24. package/dist/admin/index.mjs +2 -1
  25. package/dist/admin/src/builder/ConditionEditor.d.ts +17 -0
  26. package/dist/admin/src/builder/FieldPanel.d.ts +16 -0
  27. package/dist/admin/src/builder/types.d.ts +90 -0
  28. package/dist/admin/src/index.d.ts +1 -0
  29. package/dist/admin/src/pages/FormEditor.d.ts +9 -0
  30. package/dist/admin/src/pages/Forms.d.ts +4 -0
  31. package/dist/admin/src/pages/FormsList.d.ts +8 -0
  32. package/dist/admin/src/useHubspotSchema.d.ts +28 -0
  33. package/dist/server/index.js +880 -5
  34. package/dist/server/index.mjs +880 -5
  35. package/dist/server/src/__tests__/conditions.test.d.ts +1 -0
  36. package/dist/server/src/__tests__/forms.test.d.ts +1 -0
  37. package/dist/server/src/__tests__/formsSubmit.test.d.ts +1 -0
  38. package/dist/server/src/__tests__/importLegacy.test.d.ts +1 -0
  39. package/dist/server/src/conditions.d.ts +88 -0
  40. package/dist/server/src/content-types.d.ts +161 -9
  41. package/dist/server/src/forms.d.ts +81 -0
  42. package/dist/server/src/formsAdmin.d.ts +39 -0
  43. package/dist/server/src/importLegacy.d.ts +25 -0
  44. package/dist/server/src/index.d.ts +214 -16
  45. package/dist/shared/types.d.ts +103 -0
  46. package/dist/shared/types.js +2 -0
  47. package/dist/shared/types.mjs +2 -0
  48. package/package.json +23 -6
  49. package/dist/_chunks/HubspotPropertyInput-CK6Yj9rC.mjs +0 -177
  50. package/dist/_chunks/HubspotPropertyInput-DIPDPRXJ.js +0 -195
  51. package/dist/_chunks/en-D4ueeUcJ.mjs +0 -82
  52. package/dist/_chunks/en-PBG7-vMR.js +0 -82
  53. package/dist/_chunks/fr-DL4wdV8I.js +0 -82
  54. package/dist/_chunks/fr-DYnxVmIO.mjs +0 -82
@@ -0,0 +1,989 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { u as useIntl, o as objectLabel } from "./objectLabels-D9UEuOGr.mjs";
4
+ import { u as useParams, a as useSearchParams, L as Link, b as useNavigate, R as Routes, c as Route } from "./index-C1UJXE_o.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 } from "@strapi/icons";
7
+ import { useFetchClient } from "@strapi/strapi/admin";
8
+ import { g as getTranslation, P as PLUGIN_ID } from "./index-DkGOTp8p.mjs";
9
+ import { u as useHubspotSchema } from "./useHubspotSchema-DQkQcA_V.mjs";
10
+ const OPERATORS = ["eq", "neq", "contains", "empty", "notEmpty", "gt", "lt"];
11
+ const NEEDS_VALUE = /* @__PURE__ */ new Set(["eq", "neq", "contains", "gt", "lt"]);
12
+ const ConditionEditor = ({ condition, candidates, onChange }) => {
13
+ const { formatMessage } = useIntl();
14
+ const t = (id, defaultMessage) => formatMessage({ id: getTranslation(id), defaultMessage });
15
+ const opLabel = {
16
+ eq: t("condition.op.eq", "is"),
17
+ neq: t("condition.op.neq", "is not"),
18
+ contains: t("condition.op.contains", "contains"),
19
+ empty: t("condition.op.empty", "is empty"),
20
+ notEmpty: t("condition.op.notEmpty", "is filled"),
21
+ gt: t("condition.op.gt", "is greater than"),
22
+ lt: t("condition.op.lt", "is less than")
23
+ };
24
+ const rules = condition?.rules ?? [];
25
+ const logic = condition?.logic ?? "and";
26
+ const commit = (nextRules, nextLogic = logic) => onChange(nextRules.length ? { logic: nextLogic, rules: nextRules } : null);
27
+ const updateRule = (index, patch) => commit(rules.map((rule, i) => i === index ? { ...rule, ...patch } : rule));
28
+ const addRule = () => {
29
+ const first = candidates[0];
30
+ if (!first) return;
31
+ commit([...rules, { field: first.id, operator: "eq", value: "" }]);
32
+ };
33
+ if (!candidates.length) {
34
+ return /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t("condition.no-candidates", "Conditions can only read fields placed earlier in the form.") });
35
+ }
36
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
37
+ rules.map((rule, index) => {
38
+ const target = candidates.find((f) => f.id === rule.field);
39
+ const targetOptions = target?.options?.filter((o) => o.value) ?? [];
40
+ return /* @__PURE__ */ jsx(Box, { background: "neutral100", hasRadius: true, padding: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
41
+ index > 0 && /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: logic === "and" ? t("condition.and", "AND") : t("condition.or", "OR") }),
42
+ /* @__PURE__ */ jsx(Box, { flex: "1", minWidth: "120px", children: /* @__PURE__ */ jsx(
43
+ SingleSelect,
44
+ {
45
+ "aria-label": t("condition.field", "Field"),
46
+ value: rule.field,
47
+ onChange: (v) => updateRule(index, { field: String(v), value: "" }),
48
+ children: candidates.map((f) => /* @__PURE__ */ jsx(SingleSelectOption, { value: f.id, children: f.label || f.name }, f.id))
49
+ }
50
+ ) }),
51
+ /* @__PURE__ */ jsx(Box, { flex: "1", minWidth: "110px", children: /* @__PURE__ */ jsx(
52
+ SingleSelect,
53
+ {
54
+ "aria-label": t("condition.operator", "Operator"),
55
+ value: rule.operator,
56
+ onChange: (v) => updateRule(index, { operator: v }),
57
+ children: OPERATORS.map((op) => /* @__PURE__ */ jsx(SingleSelectOption, { value: op, children: opLabel[op] }, op))
58
+ }
59
+ ) }),
60
+ NEEDS_VALUE.has(rule.operator) && /* @__PURE__ */ jsx(Box, { flex: "1", minWidth: "110px", children: targetOptions.length ? /* @__PURE__ */ jsx(
61
+ SingleSelect,
62
+ {
63
+ "aria-label": t("condition.value", "Value"),
64
+ value: rule.value ?? "",
65
+ onChange: (v) => updateRule(index, { value: String(v) }),
66
+ children: targetOptions.map((o) => /* @__PURE__ */ jsx(SingleSelectOption, { value: o.value, children: o.label || o.value }, o.value))
67
+ }
68
+ ) : /* @__PURE__ */ jsx(
69
+ TextInput,
70
+ {
71
+ "aria-label": t("condition.value", "Value"),
72
+ value: rule.value ?? "",
73
+ onChange: (e) => updateRule(index, { value: e.target.value })
74
+ }
75
+ ) }),
76
+ /* @__PURE__ */ jsx(
77
+ IconButton,
78
+ {
79
+ label: t("condition.remove-rule", "Remove this rule"),
80
+ onClick: () => commit(rules.filter((_, i) => i !== index)),
81
+ children: /* @__PURE__ */ jsx(Trash, {})
82
+ }
83
+ )
84
+ ] }) }, index);
85
+ }),
86
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
87
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: addRule, children: t("condition.add-rule", "Add a rule") }),
88
+ rules.length > 1 && /* @__PURE__ */ jsxs(
89
+ SingleSelect,
90
+ {
91
+ "aria-label": t("condition.logic", "Logic"),
92
+ value: logic,
93
+ onChange: (v) => commit(rules, v),
94
+ children: [
95
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "and", children: t("condition.logic-and", "All rules (AND)") }),
96
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "or", children: t("condition.logic-or", "At least one (OR)") })
97
+ ]
98
+ }
99
+ )
100
+ ] })
101
+ ] });
102
+ };
103
+ const FIELD_TYPES = [
104
+ "text",
105
+ "email",
106
+ "tel",
107
+ "number",
108
+ "website",
109
+ "textarea",
110
+ "select",
111
+ "checkbox",
112
+ "radio"
113
+ ];
114
+ let counter = 0;
115
+ const makeId = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter += 1).toString(36)}${Math.random().toString(36).slice(2, 6)}`;
116
+ function fieldsBefore(definition, beforeId) {
117
+ const out = [];
118
+ for (const step of definition.steps) {
119
+ if (step.id === beforeId) return out;
120
+ for (const field of step.fields) {
121
+ if (field.id === beforeId) return out;
122
+ out.push(field);
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+ const slugifyName = (label) => label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
128
+ const FieldPanel = ({ field, candidates, problem, onChange }) => {
129
+ const intl = useIntl();
130
+ const { formatMessage } = intl;
131
+ const { schema, refresh, refreshing } = useHubspotSchema();
132
+ const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
133
+ const object = field.hubspot?.object || "contact";
134
+ const properties = (schema?.properties ?? []).filter((p) => p.object === object);
135
+ const selected = properties.find((p) => p.name === field.hubspot?.property);
136
+ const isChoice = field.type === "select" || field.type === "radio";
137
+ const setHubspot = (patch) => onChange({ hubspot: { ...field.hubspot, ...patch } });
138
+ const importOptions = () => {
139
+ if (!selected?.options?.length) return;
140
+ onChange({
141
+ options: selected.options.map((o) => ({ value: o.value, label: o.label ?? o.value }))
142
+ });
143
+ };
144
+ const deepLink = schema?.portalId && schema?.uiDomain && selected ? `https://${schema.uiDomain}/property-settings/${schema.portalId}/properties?type=0-${{ contact: 1, company: 2, deal: 3, ticket: 5 }[object] ?? 1}&search=${encodeURIComponent(selected.name)}` : null;
145
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
146
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
147
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.label", "Label") }),
148
+ /* @__PURE__ */ jsx(
149
+ TextInput,
150
+ {
151
+ value: field.label,
152
+ onChange: (e) => {
153
+ const label = e.target.value;
154
+ const auto = !field.name || field.name === slugifyName(field.label);
155
+ onChange({ label, ...auto ? { name: slugifyName(label) } : {} });
156
+ }
157
+ }
158
+ )
159
+ ] }),
160
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
161
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.name", "Payload key") }),
162
+ /* @__PURE__ */ jsx(
163
+ TextInput,
164
+ {
165
+ value: field.name,
166
+ onChange: (e) => onChange({ name: slugifyName(e.target.value) || e.target.value })
167
+ }
168
+ ),
169
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t("panel.name-hint", "Key of the answer in the submission — unique in the form.") })
170
+ ] }),
171
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
172
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.type", "Type") }),
173
+ /* @__PURE__ */ jsx(
174
+ SingleSelect,
175
+ {
176
+ value: field.type,
177
+ onChange: (v) => onChange({ type: v }),
178
+ children: FIELD_TYPES.map((type) => /* @__PURE__ */ jsx(SingleSelectOption, { value: type, children: type }, type))
179
+ }
180
+ )
181
+ ] }),
182
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, children: [
183
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
184
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.placeholder", "Placeholder") }),
185
+ /* @__PURE__ */ jsx(
186
+ TextInput,
187
+ {
188
+ value: field.placeholder ?? "",
189
+ onChange: (e) => onChange({ placeholder: e.target.value })
190
+ }
191
+ )
192
+ ] }) }),
193
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
194
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.width", "Width") }),
195
+ /* @__PURE__ */ jsxs(
196
+ SingleSelect,
197
+ {
198
+ value: field.width ?? "full",
199
+ onChange: (v) => onChange({ width: v }),
200
+ children: [
201
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "full", children: t("panel.width-full", "Full") }),
202
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "half", children: t("panel.width-half", "Half") })
203
+ ]
204
+ }
205
+ )
206
+ ] }) })
207
+ ] }),
208
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
209
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.help", "Help text") }),
210
+ /* @__PURE__ */ jsx(
211
+ TextInput,
212
+ {
213
+ value: field.helpText ?? "",
214
+ onChange: (e) => onChange({ helpText: e.target.value })
215
+ }
216
+ )
217
+ ] }),
218
+ /* @__PURE__ */ jsx(
219
+ Checkbox,
220
+ {
221
+ checked: Boolean(field.required),
222
+ onCheckedChange: (checked) => onChange({ required: checked === true }),
223
+ children: t("panel.required", "Required")
224
+ }
225
+ ),
226
+ isChoice && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
227
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("panel.options", "Choices") }),
228
+ (field.options ?? []).map((option, index) => /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
229
+ /* @__PURE__ */ jsx(
230
+ TextInput,
231
+ {
232
+ "aria-label": t("panel.option-value", "Value"),
233
+ placeholder: t("panel.option-value", "Value"),
234
+ value: option.value,
235
+ onChange: (e) => onChange({
236
+ options: (field.options ?? []).map(
237
+ (o, i) => i === index ? { ...o, value: e.target.value } : o
238
+ )
239
+ })
240
+ }
241
+ ),
242
+ /* @__PURE__ */ jsx(
243
+ TextInput,
244
+ {
245
+ "aria-label": t("panel.option-label", "Label"),
246
+ placeholder: t("panel.option-label", "Label"),
247
+ value: option.label ?? "",
248
+ onChange: (e) => onChange({
249
+ options: (field.options ?? []).map(
250
+ (o, i) => i === index ? { ...o, label: e.target.value } : o
251
+ )
252
+ })
253
+ }
254
+ ),
255
+ /* @__PURE__ */ jsx(
256
+ IconButton,
257
+ {
258
+ label: t("panel.option-remove", "Remove"),
259
+ onClick: () => onChange({ options: (field.options ?? []).filter((_, i) => i !== index) }),
260
+ children: /* @__PURE__ */ jsx(Trash, {})
261
+ }
262
+ )
263
+ ] }, index)),
264
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
265
+ /* @__PURE__ */ jsx(
266
+ Button,
267
+ {
268
+ variant: "tertiary",
269
+ size: "S",
270
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
271
+ onClick: () => onChange({ options: [...field.options ?? [], { value: "", label: "" }] }),
272
+ children: t("panel.option-add", "Add a choice")
273
+ }
274
+ ),
275
+ selected?.type === "enumeration" && Boolean(selected.options?.length) && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "S", onClick: importOptions, children: t("panel.option-import", "Import the {count} options from HubSpot", {
276
+ count: selected.options.length
277
+ }) })
278
+ ] })
279
+ ] }),
280
+ /* @__PURE__ */ jsx(Divider, {}),
281
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
282
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
283
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("panel.hubspot", "HubSpot") }),
284
+ /* @__PURE__ */ jsx(
285
+ IconButton,
286
+ {
287
+ label: t("panel.refresh", "Refresh the portal schema"),
288
+ onClick: refresh,
289
+ disabled: refreshing,
290
+ children: /* @__PURE__ */ jsx(ArrowClockwise, {})
291
+ }
292
+ )
293
+ ] }),
294
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
295
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.object", "Object") }),
296
+ /* @__PURE__ */ jsx(
297
+ SingleSelect,
298
+ {
299
+ value: object,
300
+ onChange: (v) => {
301
+ setHubspot({ object: String(v), property: void 0 });
302
+ },
303
+ children: (schema?.objects ?? ["contact", "company"]).map((name) => /* @__PURE__ */ jsx(SingleSelectOption, { value: name, children: objectLabel(intl, name) }, name))
304
+ }
305
+ )
306
+ ] }),
307
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
308
+ /* @__PURE__ */ jsx(Field.Label, { children: t("panel.property", "Property") }),
309
+ /* @__PURE__ */ jsx(
310
+ Combobox,
311
+ {
312
+ value: field.hubspot?.property ?? "",
313
+ onChange: (v) => setHubspot({ property: v || void 0 }),
314
+ onClear: () => setHubspot({ property: void 0 }),
315
+ placeholder: t("panel.property-placeholder", "Search a property…"),
316
+ children: properties.map((p) => /* @__PURE__ */ jsxs(ComboboxOption, { value: p.name, children: [
317
+ p.group ? `${p.group} · ` : "",
318
+ p.label,
319
+ " (",
320
+ p.name,
321
+ ")"
322
+ ] }, p.name))
323
+ }
324
+ ),
325
+ problem && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: t(`panel.problem-${problem.code}`, "Invalid mapping ({code})", {
326
+ code: problem.code
327
+ }) }),
328
+ !field.hubspot?.property && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t("panel.property-hint", "Without a property, the answer is sent under the payload key.") })
329
+ ] }),
330
+ deepLink && /* @__PURE__ */ jsx("a", { href: deepLink, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "primary600", children: t("panel.view-in-hubspot", "View in HubSpot ↗") }) })
331
+ ] }),
332
+ /* @__PURE__ */ jsx(Divider, {}),
333
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
334
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("panel.condition", "Show this field only when…") }),
335
+ /* @__PURE__ */ jsx(
336
+ ConditionEditor,
337
+ {
338
+ condition: field.visibleIf,
339
+ candidates,
340
+ onChange: (visibleIf) => onChange({ visibleIf })
341
+ }
342
+ )
343
+ ] })
344
+ ] });
345
+ };
346
+ const newField = (label) => ({
347
+ id: makeId("fld"),
348
+ name: "",
349
+ label,
350
+ type: "text",
351
+ width: "full",
352
+ hubspot: { object: "contact" }
353
+ });
354
+ const newStep = () => ({ id: makeId("stp"), title: "", fields: [] });
355
+ const FormEditor = () => {
356
+ const { formatMessage } = useIntl();
357
+ const { get, post, put } = useFetchClient();
358
+ const { documentId } = useParams();
359
+ const [searchParams, setSearchParams] = useSearchParams();
360
+ const locale = searchParams.get("locale") ?? void 0;
361
+ const [entry, setEntry] = React.useState(null);
362
+ const [published, setPublished] = React.useState(false);
363
+ const [locales, setLocales] = React.useState([]);
364
+ const [selection, setSelection] = React.useState({ kind: "form" });
365
+ const [dirty, setDirty] = React.useState(false);
366
+ const [busy, setBusy] = React.useState(false);
367
+ const [errors, setErrors] = React.useState([]);
368
+ const [problems, setProblems] = React.useState([]);
369
+ const [feedback, setFeedback] = React.useState(null);
370
+ const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
371
+ const query = locale ? `?locale=${locale}` : "";
372
+ React.useEffect(() => {
373
+ get("/i18n/locales").then(({ data }) => Array.isArray(data) && setLocales(data)).catch(() => setLocales([]));
374
+ }, [get]);
375
+ React.useEffect(() => {
376
+ let cancelled = false;
377
+ setEntry(null);
378
+ setSelection({ kind: "form" });
379
+ setErrors([]);
380
+ setProblems([]);
381
+ get(
382
+ `/${PLUGIN_ID}/builder/forms/${documentId}${query}`
383
+ ).then(({ data }) => {
384
+ if (cancelled) return;
385
+ setEntry(data.form);
386
+ setPublished(data.published);
387
+ setDirty(false);
388
+ }).catch(async () => {
389
+ if (cancelled || !locale) return;
390
+ try {
391
+ const { data } = await get(
392
+ `/${PLUGIN_ID}/builder/forms/${documentId}`
393
+ );
394
+ if (cancelled) return;
395
+ setEntry({ ...data.form, locale });
396
+ setPublished(false);
397
+ setDirty(true);
398
+ setFeedback({
399
+ tone: "success",
400
+ text: t("editor.locale-copied", "New locale started from the default structure — save to keep it.")
401
+ });
402
+ } catch {
403
+ if (!cancelled) setFeedback({ tone: "danger", text: t("editor.load-error", "Could not load the form.") });
404
+ }
405
+ });
406
+ return () => {
407
+ cancelled = true;
408
+ };
409
+ }, [documentId, locale]);
410
+ if (!entry) {
411
+ return /* @__PURE__ */ jsx(Box, { padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: t("editor.loading", "Loading…") }) });
412
+ }
413
+ const definition = entry.definition ?? { version: 1, steps: [] };
414
+ const patchEntry = (patch) => {
415
+ setEntry((current) => current ? { ...current, ...patch } : current);
416
+ setDirty(true);
417
+ };
418
+ const patchDefinition = (mutate) => patchEntry({ definition: { version: 1, steps: mutate(definition.steps) } });
419
+ const patchField = (fieldId, patch) => patchDefinition(
420
+ (steps) => steps.map((step) => ({
421
+ ...step,
422
+ fields: step.fields.map((f) => f.id === fieldId ? { ...f, ...patch } : f)
423
+ }))
424
+ );
425
+ const patchStep = (stepId, patch) => patchDefinition((steps) => steps.map((s) => s.id === stepId ? { ...s, ...patch } : s));
426
+ const moveField = (stepIndex, fieldIndex, delta) => patchDefinition((steps) => {
427
+ const next = steps.map((s) => ({ ...s, fields: [...s.fields] }));
428
+ const from = next[stepIndex];
429
+ const [field] = from.fields.splice(fieldIndex, 1);
430
+ const target = fieldIndex + delta;
431
+ if (target >= 0 && target <= from.fields.length) {
432
+ from.fields.splice(target, 0, field);
433
+ } else if (delta === -1 && next[stepIndex - 1]) {
434
+ next[stepIndex - 1].fields.push(field);
435
+ } else if (delta === 1 && next[stepIndex + 1]) {
436
+ next[stepIndex + 1].fields.unshift(field);
437
+ } else {
438
+ from.fields.splice(fieldIndex, 0, field);
439
+ }
440
+ return next;
441
+ });
442
+ const moveStep = (stepIndex, delta) => patchDefinition((steps) => {
443
+ const target = stepIndex + delta;
444
+ if (target < 0 || target >= steps.length) return steps;
445
+ const next = [...steps];
446
+ const [step] = next.splice(stepIndex, 1);
447
+ next.splice(target, 0, step);
448
+ return next;
449
+ });
450
+ const save = async () => {
451
+ setBusy(true);
452
+ setFeedback(null);
453
+ setErrors([]);
454
+ try {
455
+ const { data } = await put(
456
+ `/${PLUGIN_ID}/builder/forms/${documentId}${query}`,
457
+ {
458
+ name: entry.name,
459
+ title: entry.title,
460
+ subtitle: entry.subtitle,
461
+ nextLabel: entry.nextLabel,
462
+ submitLabel: entry.submitLabel,
463
+ successMessage: entry.successMessage,
464
+ definition
465
+ }
466
+ );
467
+ setProblems(data.problems ?? []);
468
+ setDirty(false);
469
+ setFeedback({ tone: "success", text: t("editor.saved", "Draft saved.") });
470
+ return true;
471
+ } catch (err) {
472
+ const data = err.response?.data;
473
+ if (data?.errors) {
474
+ setErrors(data.errors);
475
+ setFeedback({ tone: "danger", text: t("editor.invalid", "The structure has problems — see the flagged cards.") });
476
+ } else {
477
+ setFeedback({ tone: "danger", text: t("editor.save-error", "Could not save the form.") });
478
+ }
479
+ return false;
480
+ } finally {
481
+ setBusy(false);
482
+ }
483
+ };
484
+ const publish = async () => {
485
+ if (dirty && !await save()) return;
486
+ setBusy(true);
487
+ setFeedback(null);
488
+ try {
489
+ await post(`/${PLUGIN_ID}/builder/forms/${documentId}/publish${query}`);
490
+ setPublished(true);
491
+ setErrors([]);
492
+ setProblems([]);
493
+ setFeedback({ tone: "success", text: t("editor.published", "Form published.") });
494
+ } catch (err) {
495
+ const data = err.response?.data;
496
+ setErrors(data?.errors ?? []);
497
+ setProblems(data?.problems ?? []);
498
+ setFeedback({
499
+ tone: "danger",
500
+ text: t("editor.publish-blocked", "Publishing is blocked — fix the flagged mappings first.")
501
+ });
502
+ } finally {
503
+ setBusy(false);
504
+ }
505
+ };
506
+ const unpublish = async () => {
507
+ setBusy(true);
508
+ try {
509
+ await post(`/${PLUGIN_ID}/builder/forms/${documentId}/unpublish${query}`);
510
+ setPublished(false);
511
+ setFeedback({ tone: "success", text: t("editor.unpublished", "Form unpublished.") });
512
+ } catch {
513
+ setFeedback({ tone: "danger", text: t("editor.unpublish-error", "Could not unpublish.") });
514
+ } finally {
515
+ setBusy(false);
516
+ }
517
+ };
518
+ const fieldFlags = (fieldId) => ({
519
+ error: errors.find((e) => e.fieldId === fieldId),
520
+ problem: problems.find((p) => p.fieldId === fieldId)
521
+ });
522
+ const selectedField = selection.kind === "field" ? definition.steps.flatMap((s) => s.fields).find((f) => f.id === selection.fieldId) : void 0;
523
+ const selectedStep = selection.kind === "step" ? definition.steps.find((s) => s.id === selection.stepId) : void 0;
524
+ return /* @__PURE__ */ jsxs(Box, { padding: 6, children: [
525
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 4, children: [
526
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", children: [
527
+ /* @__PURE__ */ jsx(Link, { to: `/plugins/${PLUGIN_ID}`, children: /* @__PURE__ */ jsx(IconButton, { label: t("editor.back", "Back to the list"), tag: "span", children: /* @__PURE__ */ jsx(ArrowLeft, {}) }) }),
528
+ /* @__PURE__ */ jsxs(Box, { children: [
529
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
530
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h1", children: entry.name }),
531
+ 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") }),
532
+ dirty && /* @__PURE__ */ jsx(Badge, { backgroundColor: "warning100", textColor: "warning700", children: t("editor.unsaved", "Unsaved changes") })
533
+ ] }),
534
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
535
+ "/",
536
+ entry.slug
537
+ ] })
538
+ ] })
539
+ ] }),
540
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
541
+ locales.length > 1 && /* @__PURE__ */ jsx(
542
+ SingleSelect,
543
+ {
544
+ "aria-label": t("editor.locale", "Locale"),
545
+ value: locale ?? locales.find((l) => l.isDefault)?.code ?? "",
546
+ onChange: (v) => {
547
+ const code = String(v);
548
+ const isDefault = locales.find((l) => l.isDefault)?.code === code;
549
+ setSearchParams(isDefault ? {} : { locale: code });
550
+ },
551
+ children: locales.map((l) => /* @__PURE__ */ jsx(SingleSelectOption, { value: l.code, children: l.name }, l.code))
552
+ }
553
+ ),
554
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: save, disabled: busy || !dirty, children: t("editor.save", "Save") }),
555
+ /* @__PURE__ */ jsx(Button, { onClick: publish, disabled: busy, children: t("editor.publish", "Publish") }),
556
+ published && /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: unpublish, disabled: busy, children: t("editor.unpublish", "Unpublish") })
557
+ ] })
558
+ ] }),
559
+ feedback && /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: feedback.tone === "success" ? "success600" : "danger600", children: feedback.text }) }),
560
+ /* @__PURE__ */ jsxs(Flex, { gap: 6, alignItems: "flex-start", children: [
561
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
562
+ definition.steps.map((step, stepIndex) => /* @__PURE__ */ jsxs(
563
+ Box,
564
+ {
565
+ background: "neutral0",
566
+ hasRadius: true,
567
+ shadow: "tableShadow",
568
+ padding: 4,
569
+ borderColor: selection.kind === "step" && selection.stepId === step.id ? "primary600" : void 0,
570
+ borderStyle: selection.kind === "step" && selection.stepId === step.id ? "solid" : void 0,
571
+ borderWidth: selection.kind === "step" && selection.stepId === step.id ? "1px" : void 0,
572
+ children: [
573
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 2, children: [
574
+ /* @__PURE__ */ jsxs(
575
+ Flex,
576
+ {
577
+ gap: 2,
578
+ alignItems: "center",
579
+ onClick: () => setSelection({ kind: "step", stepId: step.id }),
580
+ style: { cursor: "pointer" },
581
+ children: [
582
+ /* @__PURE__ */ jsxs(Typography, { variant: "delta", children: [
583
+ t("editor.step", "Step {n}", { n: stepIndex + 1 }),
584
+ step.title ? ` — ${step.title}` : ""
585
+ ] }),
586
+ step.visibleIf?.rules?.length ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "alternative100", textColor: "alternative700", children: t("editor.conditional", "Conditional") }) : null,
587
+ errors.some((e) => e.stepId === step.id) && /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger100", textColor: "danger700", children: t("editor.flag-error", "To fix") })
588
+ ]
589
+ }
590
+ ),
591
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
592
+ /* @__PURE__ */ jsx(
593
+ IconButton,
594
+ {
595
+ label: t("editor.step-up", "Move the step up"),
596
+ onClick: () => moveStep(stepIndex, -1),
597
+ disabled: stepIndex === 0,
598
+ children: /* @__PURE__ */ jsx(ArrowUp, {})
599
+ }
600
+ ),
601
+ /* @__PURE__ */ jsx(
602
+ IconButton,
603
+ {
604
+ label: t("editor.step-down", "Move the step down"),
605
+ onClick: () => moveStep(stepIndex, 1),
606
+ disabled: stepIndex === definition.steps.length - 1,
607
+ children: /* @__PURE__ */ jsx(ArrowDown, {})
608
+ }
609
+ ),
610
+ /* @__PURE__ */ jsx(
611
+ IconButton,
612
+ {
613
+ label: t("editor.step-delete", "Delete the step and its fields"),
614
+ onClick: () => {
615
+ setSelection({ kind: "form" });
616
+ patchDefinition((steps) => steps.filter((s) => s.id !== step.id));
617
+ },
618
+ children: /* @__PURE__ */ jsx(Trash, {})
619
+ }
620
+ )
621
+ ] })
622
+ ] }),
623
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
624
+ step.fields.map((field, fieldIndex) => {
625
+ const flags = fieldFlags(field.id);
626
+ const isSelected = selection.kind === "field" && selection.fieldId === field.id;
627
+ return /* @__PURE__ */ jsx(
628
+ Box,
629
+ {
630
+ background: isSelected ? "primary100" : "neutral100",
631
+ hasRadius: true,
632
+ padding: 3,
633
+ onClick: () => setSelection({ kind: "field", fieldId: field.id }),
634
+ style: { cursor: "pointer" },
635
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
636
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
637
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: field.label || field.name || "—" }),
638
+ /* @__PURE__ */ jsx(Badge, { children: field.type }),
639
+ field.required && /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral600", children: t("editor.required", "required") }),
640
+ field.hubspot?.property && /* @__PURE__ */ jsxs(Badge, { backgroundColor: "primary100", textColor: "primary700", children: [
641
+ field.hubspot.object ?? "contact",
642
+ ".",
643
+ field.hubspot.property
644
+ ] }),
645
+ field.visibleIf?.rules?.length ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "alternative100", textColor: "alternative700", children: t("editor.conditional", "Conditional") }) : null,
646
+ (flags.error || flags.problem) && /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger100", textColor: "danger700", children: flags.error?.code ?? flags.problem?.code })
647
+ ] }),
648
+ /* @__PURE__ */ jsxs(Flex, { gap: 1, onClick: (e) => e.stopPropagation(), children: [
649
+ /* @__PURE__ */ jsx(
650
+ IconButton,
651
+ {
652
+ label: t("editor.field-up", "Move up"),
653
+ onClick: () => moveField(stepIndex, fieldIndex, -1),
654
+ disabled: stepIndex === 0 && fieldIndex === 0,
655
+ children: /* @__PURE__ */ jsx(ArrowUp, {})
656
+ }
657
+ ),
658
+ /* @__PURE__ */ jsx(
659
+ IconButton,
660
+ {
661
+ label: t("editor.field-down", "Move down"),
662
+ onClick: () => moveField(stepIndex, fieldIndex, 1),
663
+ disabled: stepIndex === definition.steps.length - 1 && fieldIndex === step.fields.length - 1,
664
+ children: /* @__PURE__ */ jsx(ArrowDown, {})
665
+ }
666
+ ),
667
+ /* @__PURE__ */ jsx(
668
+ IconButton,
669
+ {
670
+ label: t("editor.field-delete", "Delete the field"),
671
+ onClick: () => {
672
+ setSelection({ kind: "form" });
673
+ patchStep(step.id, {
674
+ fields: step.fields.filter((f) => f.id !== field.id)
675
+ });
676
+ },
677
+ children: /* @__PURE__ */ jsx(Trash, {})
678
+ }
679
+ )
680
+ ] })
681
+ ] })
682
+ },
683
+ field.id
684
+ );
685
+ }),
686
+ /* @__PURE__ */ jsx(
687
+ Button,
688
+ {
689
+ variant: "tertiary",
690
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
691
+ onClick: () => {
692
+ const field = newField(t("editor.new-field", "New field"));
693
+ patchStep(step.id, { fields: [...step.fields, field] });
694
+ setSelection({ kind: "field", fieldId: field.id });
695
+ },
696
+ children: t("editor.add-field", "Add a field")
697
+ }
698
+ )
699
+ ] })
700
+ ]
701
+ },
702
+ step.id
703
+ )),
704
+ /* @__PURE__ */ jsx(
705
+ Button,
706
+ {
707
+ variant: "secondary",
708
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
709
+ onClick: () => {
710
+ const step = newStep();
711
+ patchDefinition((steps) => [...steps, step]);
712
+ setSelection({ kind: "step", stepId: step.id });
713
+ },
714
+ children: t("editor.add-step", "Add a step")
715
+ }
716
+ )
717
+ ] }) }),
718
+ /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, width: "380px", style: { flexShrink: 0 }, children: selectedField ? /* @__PURE__ */ jsx(
719
+ FieldPanel,
720
+ {
721
+ field: selectedField,
722
+ candidates: fieldsBefore(definition, selectedField.id),
723
+ problem: problems.find((p) => p.fieldId === selectedField.id),
724
+ onChange: (patch) => patchField(selectedField.id, patch)
725
+ }
726
+ ) : selectedStep ? /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
727
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("editor.step-panel", "Step") }),
728
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
729
+ /* @__PURE__ */ jsx(Field.Label, { children: t("editor.step-title", "Title") }),
730
+ /* @__PURE__ */ jsx(
731
+ TextInput,
732
+ {
733
+ value: selectedStep.title ?? "",
734
+ onChange: (e) => patchStep(selectedStep.id, { title: e.target.value })
735
+ }
736
+ )
737
+ ] }),
738
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
739
+ /* @__PURE__ */ jsx(Field.Label, { children: t("editor.step-description", "Description") }),
740
+ /* @__PURE__ */ jsx(
741
+ Textarea,
742
+ {
743
+ value: selectedStep.description ?? "",
744
+ onChange: (e) => patchStep(selectedStep.id, { description: e.target.value })
745
+ }
746
+ )
747
+ ] }),
748
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("editor.step-condition", "Show this step only when…") }),
749
+ /* @__PURE__ */ jsx(
750
+ ConditionEditor,
751
+ {
752
+ condition: selectedStep.visibleIf,
753
+ candidates: fieldsBefore(definition, selectedStep.id),
754
+ onChange: (visibleIf) => patchStep(selectedStep.id, { visibleIf })
755
+ }
756
+ )
757
+ ] }) : /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
758
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("editor.form-panel", "Form") }),
759
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
760
+ /* @__PURE__ */ jsx(Field.Label, { children: t("editor.form-name", "Internal name") }),
761
+ /* @__PURE__ */ jsx(
762
+ TextInput,
763
+ {
764
+ value: entry.name,
765
+ onChange: (e) => patchEntry({ name: e.target.value })
766
+ }
767
+ )
768
+ ] }),
769
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
770
+ /* @__PURE__ */ jsx(Field.Label, { children: t("editor.form-title", "Title") }),
771
+ /* @__PURE__ */ jsx(
772
+ TextInput,
773
+ {
774
+ value: entry.title ?? "",
775
+ onChange: (e) => patchEntry({ title: e.target.value })
776
+ }
777
+ )
778
+ ] }),
779
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
780
+ /* @__PURE__ */ jsx(Field.Label, { children: t("editor.form-subtitle", "Subtitle") }),
781
+ /* @__PURE__ */ jsx(
782
+ Textarea,
783
+ {
784
+ value: entry.subtitle ?? "",
785
+ onChange: (e) => patchEntry({ subtitle: e.target.value })
786
+ }
787
+ )
788
+ ] }),
789
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
790
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
791
+ /* @__PURE__ */ jsx(Field.Label, { children: t("editor.form-next", "Next label") }),
792
+ /* @__PURE__ */ jsx(
793
+ TextInput,
794
+ {
795
+ value: entry.nextLabel ?? "",
796
+ onChange: (e) => patchEntry({ nextLabel: e.target.value })
797
+ }
798
+ )
799
+ ] }) }),
800
+ /* @__PURE__ */ jsx(Box, { flex: "1", children: /* @__PURE__ */ jsxs(Field.Root, { children: [
801
+ /* @__PURE__ */ jsx(Field.Label, { children: t("editor.form-submit", "Submit label") }),
802
+ /* @__PURE__ */ jsx(
803
+ TextInput,
804
+ {
805
+ value: entry.submitLabel ?? "",
806
+ onChange: (e) => patchEntry({ submitLabel: e.target.value })
807
+ }
808
+ )
809
+ ] }) })
810
+ ] }),
811
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
812
+ /* @__PURE__ */ jsx(Field.Label, { children: t("editor.form-success", "Success message") }),
813
+ /* @__PURE__ */ jsx(
814
+ Textarea,
815
+ {
816
+ value: entry.successMessage ?? "",
817
+ onChange: (e) => patchEntry({ successMessage: e.target.value })
818
+ }
819
+ )
820
+ ] }),
821
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: t(
822
+ "editor.form-hint",
823
+ "Select a step or a field to edit it. The public endpoint serves this form at /api/hubspot/forms/{slug}.",
824
+ { slug: entry.slug }
825
+ ) })
826
+ ] }) })
827
+ ] })
828
+ ] });
829
+ };
830
+ const FormsList = () => {
831
+ const { formatMessage } = useIntl();
832
+ const { get, post, del } = useFetchClient();
833
+ const navigate = useNavigate();
834
+ const [rows, setRows] = React.useState(null);
835
+ const [name, setName] = React.useState("");
836
+ const [busy, setBusy] = React.useState(false);
837
+ const [error, setError] = React.useState(null);
838
+ const [sources, setSources] = React.useState([]);
839
+ const [sourceId, setSourceId] = React.useState("");
840
+ const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
841
+ const load = React.useCallback(async () => {
842
+ const { data } = await get(`/${PLUGIN_ID}/builder/forms`);
843
+ setRows(data.forms);
844
+ }, [get]);
845
+ React.useEffect(() => {
846
+ load().catch(() => setError(t("forms.load-error", "Could not load the forms.")));
847
+ get(
848
+ `/${PLUGIN_ID}/builder/import/sources`
849
+ ).then(({ data }) => setSources(data.sources ?? [])).catch(() => setSources([]));
850
+ }, [load]);
851
+ const runImport = async () => {
852
+ if (!sourceId) return;
853
+ setBusy(true);
854
+ setError(null);
855
+ try {
856
+ const { data } = await post(`/${PLUGIN_ID}/builder/import`, {
857
+ documentId: sourceId
858
+ });
859
+ navigate(data.documentId);
860
+ } catch {
861
+ setError(t("forms.import-error", "Could not import the form."));
862
+ setBusy(false);
863
+ }
864
+ };
865
+ const create = async () => {
866
+ if (!name.trim()) return;
867
+ setBusy(true);
868
+ setError(null);
869
+ try {
870
+ const { data } = await post(
871
+ `/${PLUGIN_ID}/builder/forms`,
872
+ { name: name.trim() }
873
+ );
874
+ navigate(data.form.documentId);
875
+ } catch {
876
+ setError(t("forms.create-error", "Could not create the form."));
877
+ setBusy(false);
878
+ }
879
+ };
880
+ const remove = async (row) => {
881
+ if (!window.confirm(t("forms.delete-confirm", 'Delete "{name}" and all its locales?', { name: row.name }))) {
882
+ return;
883
+ }
884
+ setBusy(true);
885
+ try {
886
+ await del(`/${PLUGIN_ID}/builder/forms/${row.documentId}`);
887
+ await load();
888
+ } catch {
889
+ setError(t("forms.delete-error", "Could not delete the form."));
890
+ } finally {
891
+ setBusy(false);
892
+ }
893
+ };
894
+ return /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
895
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", paddingBottom: 6, children: [
896
+ /* @__PURE__ */ jsxs(Box, { children: [
897
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: t("forms.title", "HubSpot forms") }),
898
+ /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", tag: "p", children: t(
899
+ "forms.subtitle",
900
+ "Multi-step forms with conditional fields, mapped to your portal's real properties."
901
+ ) })
902
+ ] }),
903
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
904
+ /* @__PURE__ */ jsx(
905
+ TextInput,
906
+ {
907
+ "aria-label": t("forms.name-label", "Name of the new form"),
908
+ placeholder: t("forms.name-placeholder", "Name of the new form"),
909
+ value: name,
910
+ onChange: (e) => setName(e.target.value),
911
+ onKeyDown: (e) => e.key === "Enter" && create()
912
+ }
913
+ ),
914
+ /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: create, disabled: busy || !name.trim(), children: t("forms.create", "Create") })
915
+ ] })
916
+ ] }),
917
+ error && /* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: error }) }),
918
+ sources.length > 0 && /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, shadow: "tableShadow", padding: 4, marginBottom: 4, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "flex-end", justifyContent: "space-between", children: [
919
+ /* @__PURE__ */ jsxs(Box, { children: [
920
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: t("forms.import-title", "Import an existing form") }),
921
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", tag: "p", children: t(
922
+ "forms.import-hint",
923
+ "Converts a legacy content-type form (all locales) into a draft here — same slug, source untouched. Re-importing overwrites the draft."
924
+ ) })
925
+ ] }),
926
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
927
+ /* @__PURE__ */ jsx(
928
+ SingleSelect,
929
+ {
930
+ "aria-label": t("forms.import-source", "Form to import"),
931
+ placeholder: t("forms.import-source", "Form to import"),
932
+ value: sourceId,
933
+ onChange: (v) => setSourceId(String(v)),
934
+ children: sources.map((s) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: s.documentId, children: [
935
+ s.name,
936
+ " ",
937
+ s.slug ? `(${s.slug})` : ""
938
+ ] }, s.documentId))
939
+ }
940
+ ),
941
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: runImport, disabled: busy || !sourceId, children: t("forms.import", "Import") })
942
+ ] })
943
+ ] }) }),
944
+ !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: 6, rowCount: rows.length, children: [
945
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
946
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-name", "Name") }) }),
947
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-slug", "Slug") }) }),
948
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-structure", "Structure") }) }),
949
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-status", "Status") }) }),
950
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-updated", "Updated") }) }),
951
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: t("forms.col-actions", "") }) })
952
+ ] }) }),
953
+ /* @__PURE__ */ jsx(Tbody, { children: rows.map((row) => /* @__PURE__ */ jsxs(
954
+ Tr,
955
+ {
956
+ onClick: () => navigate(row.documentId),
957
+ style: { cursor: "pointer" },
958
+ children: [
959
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: row.name }) }),
960
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.slug }) }),
961
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t("forms.structure", "{steps} steps · {fields} fields", {
962
+ steps: row.steps,
963
+ fields: row.fields
964
+ }) }) }),
965
+ /* @__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") }) }),
966
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: row.updatedAt ? new Date(row.updatedAt).toLocaleString() : "—" }) }),
967
+ /* @__PURE__ */ jsx(Td, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
968
+ IconButton,
969
+ {
970
+ label: t("forms.delete", "Delete"),
971
+ onClick: () => remove(row),
972
+ disabled: busy,
973
+ children: /* @__PURE__ */ jsx(Trash, {})
974
+ }
975
+ ) })
976
+ ]
977
+ },
978
+ row.documentId
979
+ )) })
980
+ ] })
981
+ ] });
982
+ };
983
+ const Forms = () => /* @__PURE__ */ jsxs(Routes, { children: [
984
+ /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(FormsList, {}) }),
985
+ /* @__PURE__ */ jsx(Route, { path: ":documentId", element: /* @__PURE__ */ jsx(FormEditor, {}) })
986
+ ] });
987
+ export {
988
+ Forms as default
989
+ };