@dani-builder/strapi-plugin-builder 0.16.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/AGENTS.md +24 -16
  2. package/README.md +24 -4
  3. package/dist/admin/api.d.ts +17 -0
  4. package/dist/admin/api.d.ts.map +1 -0
  5. package/dist/admin/app.d.ts +2 -0
  6. package/dist/admin/app.d.ts.map +1 -0
  7. package/dist/admin/field-editor.d.ts +8 -0
  8. package/dist/admin/field-editor.d.ts.map +1 -0
  9. package/dist/admin/form-editor-page.d.ts +4 -0
  10. package/dist/admin/form-editor-page.d.ts.map +1 -0
  11. package/dist/admin/form-list-page.d.ts +4 -0
  12. package/dist/admin/form-list-page.d.ts.map +1 -0
  13. package/dist/admin/form.d.ts +64 -0
  14. package/dist/admin/form.d.ts.map +1 -0
  15. package/dist/admin/import-form.d.ts +6 -0
  16. package/dist/admin/import-form.d.ts.map +1 -0
  17. package/dist/admin/index.d.ts +6 -0
  18. package/dist/admin/index.d.ts.map +1 -0
  19. package/dist/admin/index.mjs +1570 -0
  20. package/dist/admin/notification-editor.d.ts +16 -0
  21. package/dist/admin/notification-editor.d.ts.map +1 -0
  22. package/dist/admin/notifications.d.ts +8 -0
  23. package/dist/admin/notifications.d.ts.map +1 -0
  24. package/dist/admin/plugin-icon.d.ts +2 -0
  25. package/dist/admin/plugin-icon.d.ts.map +1 -0
  26. package/dist/admin/ui.d.ts +19 -0
  27. package/dist/admin/ui.d.ts.map +1 -0
  28. package/dist/server/src/bootstrap.d.ts.map +1 -1
  29. package/dist/server/src/bootstrap.js +37 -0
  30. package/dist/server/src/bootstrap.js.map +1 -1
  31. package/dist/server/src/components/blocks/inquiry-form.json +5 -1
  32. package/dist/server/src/components/shared/chatbot-setting.json +4 -2
  33. package/dist/server/src/config/builder.d.ts +6 -3
  34. package/dist/server/src/config/builder.d.ts.map +1 -1
  35. package/dist/server/src/config/builder.js +3 -0
  36. package/dist/server/src/config/builder.js.map +1 -1
  37. package/dist/server/src/config/forms.d.ts +58 -0
  38. package/dist/server/src/config/forms.d.ts.map +1 -0
  39. package/dist/server/src/config/forms.js +14 -0
  40. package/dist/server/src/config/forms.js.map +1 -0
  41. package/dist/server/src/config/index.d.ts +2 -22
  42. package/dist/server/src/config/index.d.ts.map +1 -1
  43. package/dist/server/src/config/index.js +17 -82
  44. package/dist/server/src/config/index.js.map +1 -1
  45. package/dist/server/src/content-types/form/index.d.ts +75 -0
  46. package/dist/server/src/content-types/form/index.d.ts.map +1 -0
  47. package/dist/server/src/content-types/form/index.js +8 -0
  48. package/dist/server/src/content-types/form/index.js.map +1 -0
  49. package/dist/server/src/content-types/form/schema.json +75 -0
  50. package/dist/server/src/content-types/index.d.ts +154 -0
  51. package/dist/server/src/content-types/index.d.ts.map +1 -1
  52. package/dist/server/src/content-types/index.js +4 -0
  53. package/dist/server/src/content-types/index.js.map +1 -1
  54. package/dist/server/src/content-types/submission/index.d.ts +83 -0
  55. package/dist/server/src/content-types/submission/index.d.ts.map +1 -0
  56. package/dist/server/src/content-types/submission/index.js +8 -0
  57. package/dist/server/src/content-types/submission/index.js.map +1 -0
  58. package/dist/server/src/content-types/submission/schema.json +88 -0
  59. package/dist/server/src/controllers/form-admin.d.ts +18 -0
  60. package/dist/server/src/controllers/form-admin.d.ts.map +1 -0
  61. package/dist/server/src/controllers/form-admin.js +107 -0
  62. package/dist/server/src/controllers/form-admin.js.map +1 -0
  63. package/dist/server/src/controllers/form.d.ts +8 -0
  64. package/dist/server/src/controllers/form.d.ts.map +1 -0
  65. package/dist/server/src/controllers/form.js +36 -0
  66. package/dist/server/src/controllers/form.js.map +1 -0
  67. package/dist/server/src/controllers/index.d.ts.map +1 -1
  68. package/dist/server/src/controllers/index.js +6 -2
  69. package/dist/server/src/controllers/index.js.map +1 -1
  70. package/dist/server/src/controllers/submission.d.ts +9 -0
  71. package/dist/server/src/controllers/submission.d.ts.map +1 -0
  72. package/dist/server/src/controllers/submission.js +58 -0
  73. package/dist/server/src/controllers/submission.js.map +1 -0
  74. package/dist/server/src/policies/rate-limit.d.ts.map +1 -1
  75. package/dist/server/src/policies/rate-limit.js +4 -4
  76. package/dist/server/src/policies/rate-limit.js.map +1 -1
  77. package/dist/server/src/preview/handler.d.ts +1 -1
  78. package/dist/server/src/preview/handler.d.ts.map +1 -1
  79. package/dist/server/src/preview/handler.js +7 -1
  80. package/dist/server/src/preview/handler.js.map +1 -1
  81. package/dist/server/src/register.d.ts.map +1 -1
  82. package/dist/server/src/register.js +2 -6
  83. package/dist/server/src/register.js.map +1 -1
  84. package/dist/server/src/routes/admin.d.ts +14 -2
  85. package/dist/server/src/routes/admin.d.ts.map +1 -1
  86. package/dist/server/src/routes/admin.js +44 -0
  87. package/dist/server/src/routes/admin.js.map +1 -1
  88. package/dist/server/src/routes/content-api.d.ts +4 -0
  89. package/dist/server/src/routes/content-api.d.ts.map +1 -1
  90. package/dist/server/src/routes/content-api.js +12 -9
  91. package/dist/server/src/routes/content-api.js.map +1 -1
  92. package/dist/server/src/services/form.d.ts +21 -0
  93. package/dist/server/src/services/form.d.ts.map +1 -0
  94. package/dist/server/src/services/form.js +45 -0
  95. package/dist/server/src/services/form.js.map +1 -0
  96. package/dist/server/src/services/idempotency-collision.d.ts +3 -0
  97. package/dist/server/src/services/idempotency-collision.d.ts.map +1 -0
  98. package/dist/server/src/services/idempotency-collision.js +24 -0
  99. package/dist/server/src/services/idempotency-collision.js.map +1 -0
  100. package/dist/server/src/services/index.d.ts.map +1 -1
  101. package/dist/server/src/services/index.js +41 -2
  102. package/dist/server/src/services/index.js.map +1 -1
  103. package/dist/server/src/services/notification.d.ts +26 -0
  104. package/dist/server/src/services/notification.d.ts.map +1 -0
  105. package/dist/server/src/services/notification.js +106 -0
  106. package/dist/server/src/services/notification.js.map +1 -0
  107. package/dist/server/src/services/populate.d.ts +6 -0
  108. package/dist/server/src/services/populate.d.ts.map +1 -1
  109. package/dist/server/src/services/populate.js +2 -0
  110. package/dist/server/src/services/populate.js.map +1 -1
  111. package/dist/server/src/services/submission.d.ts +16 -0
  112. package/dist/server/src/services/submission.d.ts.map +1 -0
  113. package/dist/server/src/services/submission.js +284 -0
  114. package/dist/server/src/services/submission.js.map +1 -0
  115. package/dist/server/src/services/template.d.ts +18 -0
  116. package/dist/server/src/services/template.d.ts.map +1 -0
  117. package/dist/server/src/services/template.js +50 -0
  118. package/dist/server/src/services/template.js.map +1 -0
  119. package/dist/server/src/validation/form.d.ts +48 -0
  120. package/dist/server/src/validation/form.d.ts.map +1 -0
  121. package/dist/server/src/validation/form.js +354 -0
  122. package/dist/server/src/validation/form.js.map +1 -0
  123. package/dist/server/src/validation/notification.d.ts +28 -0
  124. package/dist/server/src/validation/notification.d.ts.map +1 -0
  125. package/dist/server/src/validation/notification.js +142 -0
  126. package/dist/server/src/validation/notification.js.map +1 -0
  127. package/dist/server/src/validation/submission-access.d.ts +3 -0
  128. package/dist/server/src/validation/submission-access.d.ts.map +1 -0
  129. package/dist/server/src/validation/submission-access.js +23 -0
  130. package/dist/server/src/validation/submission-access.js.map +1 -0
  131. package/dist/server/src/validation/submission.d.ts +8 -0
  132. package/dist/server/src/validation/submission.d.ts.map +1 -0
  133. package/dist/server/src/validation/submission.js +113 -0
  134. package/dist/server/src/validation/submission.js.map +1 -0
  135. package/package.json +26 -7
  136. package/strapi-admin.js +1 -0
  137. package/dist/server/src/controllers/inquiry.d.ts +0 -8
  138. package/dist/server/src/controllers/inquiry.d.ts.map +0 -1
  139. package/dist/server/src/controllers/inquiry.js +0 -27
  140. package/dist/server/src/controllers/inquiry.js.map +0 -1
  141. package/dist/server/src/services/inquiry.d.ts +0 -15
  142. package/dist/server/src/services/inquiry.d.ts.map +0 -1
  143. package/dist/server/src/services/inquiry.js +0 -137
  144. package/dist/server/src/services/inquiry.js.map +0 -1
@@ -0,0 +1,1570 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // admin/src/form.ts
17
+ function savePayload(form, includeNotifications = false) {
18
+ const notifications = structuredClone(form.notifications ?? {});
19
+ if (notifications.email) {
20
+ notifications.email.to = notifications.email.to.map((value) => value.trim()).filter(Boolean);
21
+ notifications.email.cc = notifications.email.cc?.map((value) => value.trim()).filter(Boolean);
22
+ }
23
+ return {
24
+ ...includeNotifications ? { notifications } : {},
25
+ slug: form.slug,
26
+ name: form.name,
27
+ targetUid: form.targetUid,
28
+ fields: form.fields.map((field) => ({
29
+ ...field,
30
+ options: field.options?.map((option) => ({ ...option }))
31
+ })),
32
+ consent: { label: form.consent.label, version: form.consent.version }
33
+ };
34
+ }
35
+ function moveField(fields, index, direction) {
36
+ const next = [...fields];
37
+ const current = next[index];
38
+ const other = next[index + direction];
39
+ if (!current || !other) return fields;
40
+ next[index] = other;
41
+ next[index + direction] = current;
42
+ return next;
43
+ }
44
+ function setRequired(field, required, schema) {
45
+ return { ...field, required: schema.required || required };
46
+ }
47
+ function parseImport(source) {
48
+ let value;
49
+ try {
50
+ value = JSON.parse(source);
51
+ } catch {
52
+ throw new Error("Invalid JSON. Choose a form export JSON file.");
53
+ }
54
+ if (!value || typeof value !== "object" || Array.isArray(value))
55
+ throw new Error("Expected a form definition object.");
56
+ return value;
57
+ }
58
+ function errorMessage(error) {
59
+ if (error && typeof error === "object" && "response" in error) {
60
+ const response = error.response;
61
+ if (response?.data?.error?.message) return response.data.error.message;
62
+ }
63
+ return error instanceof Error ? error.message : "Unable to complete the request.";
64
+ }
65
+ var emptyForm;
66
+ var init_form = __esm({
67
+ "admin/src/form.ts"() {
68
+ "use strict";
69
+ emptyForm = {
70
+ slug: "",
71
+ name: "",
72
+ targetUid: "",
73
+ fields: [],
74
+ consent: { label: "", version: "1" }
75
+ };
76
+ }
77
+ });
78
+
79
+ // admin/src/api.ts
80
+ function formApi(client) {
81
+ return {
82
+ async list() {
83
+ return (await client.get(root)).data.data;
84
+ },
85
+ async get(id) {
86
+ return (await client.get(
87
+ `${root}/${encodeURIComponent(id)}`
88
+ )).data.data;
89
+ },
90
+ async targets() {
91
+ return (await client.get(`${root}/targets`)).data.data;
92
+ },
93
+ async fields(targetUid) {
94
+ return (await client.get(`${root}/fields`, {
95
+ params: { targetUid }
96
+ })).data.data;
97
+ },
98
+ async save(form, id, includeNotifications = false) {
99
+ const body = { data: savePayload(form, includeNotifications) };
100
+ return (id ? await client.put(
101
+ `${root}/${encodeURIComponent(id)}`,
102
+ body
103
+ ) : await client.post(root, body)).data.data;
104
+ },
105
+ async testSend(id, sampleFields) {
106
+ return (await client.post(
107
+ `${root}/${encodeURIComponent(id)}/test-send`,
108
+ { data: sampleFields }
109
+ )).data.data;
110
+ },
111
+ async remove(id) {
112
+ await client.del(`${root}/${encodeURIComponent(id)}`);
113
+ },
114
+ async export(id) {
115
+ return (await client.get(
116
+ `${root}/${encodeURIComponent(id)}/export`
117
+ )).data.data;
118
+ },
119
+ async import(data, id) {
120
+ return (id ? await client.put(
121
+ `${root}/${encodeURIComponent(id)}/import`,
122
+ { data }
123
+ ) : await client.post(`${root}/import`, {
124
+ data
125
+ })).data.data;
126
+ }
127
+ };
128
+ }
129
+ function downloadForm(value, slug) {
130
+ const url = URL.createObjectURL(
131
+ new Blob([JSON.stringify(value, null, 2)], { type: "application/json" })
132
+ );
133
+ const anchor = document.createElement("a");
134
+ anchor.href = url;
135
+ anchor.download = `${slug}.json`;
136
+ anchor.click();
137
+ URL.revokeObjectURL(url);
138
+ }
139
+ var root;
140
+ var init_api = __esm({
141
+ "admin/src/api.ts"() {
142
+ "use strict";
143
+ init_form();
144
+ root = "/plugins/builder/forms";
145
+ }
146
+ });
147
+
148
+ // admin/src/ui.tsx
149
+ import { useId } from "react";
150
+ import { Box, Flex, Typography } from "@strapi/design-system";
151
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
152
+ function Section({
153
+ title,
154
+ children
155
+ }) {
156
+ return /* @__PURE__ */ jsx2(Box, { background: "neutral0", padding: 6, hasRadius: true, shadow: "tableShadow", children: /* @__PURE__ */ jsxs2(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
157
+ /* @__PURE__ */ jsx2(Typography, { tag: "h2", variant: "delta", children: title }),
158
+ children
159
+ ] }) });
160
+ }
161
+ function TextField({
162
+ label,
163
+ value,
164
+ onChange,
165
+ disabled,
166
+ required,
167
+ multiline,
168
+ secret,
169
+ error
170
+ }) {
171
+ const errorId = useId();
172
+ const props = {
173
+ "aria-describedby": error ? errorId : void 0,
174
+ value,
175
+ disabled,
176
+ required,
177
+ onChange: (event) => onChange(event.target.value),
178
+ style: {
179
+ width: "100%",
180
+ padding: "10px",
181
+ border: "1px solid #8e8ea9",
182
+ borderRadius: 4,
183
+ font: "inherit",
184
+ color: "#32324d",
185
+ background: disabled ? "#f6f6f9" : "#fff"
186
+ }
187
+ };
188
+ return /* @__PURE__ */ jsxs2("label", { style: { display: "grid", gap: 6 }, children: [
189
+ /* @__PURE__ */ jsx2(Typography, { children: label }),
190
+ multiline ? /* @__PURE__ */ jsx2("textarea", { ...props, rows: 3, "aria-invalid": Boolean(error) }) : /* @__PURE__ */ jsx2(
191
+ "input",
192
+ {
193
+ ...props,
194
+ type: secret ? "password" : "text",
195
+ autoComplete: secret ? "new-password" : void 0,
196
+ "aria-invalid": Boolean(error)
197
+ }
198
+ ),
199
+ error && /* @__PURE__ */ jsx2(Typography, { id: errorId, textColor: "danger700", role: "alert", children: error })
200
+ ] });
201
+ }
202
+ function ErrorNotice({ message }) {
203
+ return message ? /* @__PURE__ */ jsx2(Box, { role: "alert", padding: 4, background: "danger100", children: /* @__PURE__ */ jsx2(Typography, { textColor: "danger700", children: message }) }) : null;
204
+ }
205
+ var init_ui = __esm({
206
+ "admin/src/ui.tsx"() {
207
+ "use strict";
208
+ }
209
+ });
210
+
211
+ // admin/src/import-form.tsx
212
+ import { useRef, useState } from "react";
213
+ import { Button, Flex as Flex2, Typography as Typography2 } from "@strapi/design-system";
214
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
215
+ function ImportForm({
216
+ replace,
217
+ disabled,
218
+ onImport
219
+ }) {
220
+ const [source, setSource] = useState("");
221
+ const [error, setError] = useState("");
222
+ const [busy, setBusy] = useState(false);
223
+ const lock = useRef(false);
224
+ async function submit() {
225
+ if (lock.current) return;
226
+ lock.current = true;
227
+ setBusy(true);
228
+ setError("");
229
+ try {
230
+ await onImport(parseImport(source));
231
+ } catch (cause) {
232
+ setError(errorMessage(cause));
233
+ } finally {
234
+ lock.current = false;
235
+ setBusy(false);
236
+ }
237
+ }
238
+ return /* @__PURE__ */ jsxs3(Flex2, { direction: "column", alignItems: "stretch", gap: 3, children: [
239
+ /* @__PURE__ */ jsxs3(Typography2, { children: [
240
+ replace ? "Replace this saved form with an exported definition. The slug must match. The server increments the form version." : "Import a form export. A new form starts at version 1.",
241
+ " ",
242
+ "Consent version is preserved. Existing slugs cannot be duplicated."
243
+ ] }),
244
+ /* @__PURE__ */ jsxs3("label", { children: [
245
+ "Choose JSON file",
246
+ " ",
247
+ /* @__PURE__ */ jsx3(
248
+ "input",
249
+ {
250
+ type: "file",
251
+ accept: ".json,application/json",
252
+ disabled: busy || disabled,
253
+ onChange: (event) => {
254
+ const file = event.target.files?.[0];
255
+ if (file)
256
+ void file.text().then(setSource).catch((cause) => setError(errorMessage(cause)));
257
+ event.target.value = "";
258
+ }
259
+ }
260
+ )
261
+ ] }),
262
+ /* @__PURE__ */ jsx3(
263
+ TextField,
264
+ {
265
+ label: "Form JSON",
266
+ value: source,
267
+ onChange: setSource,
268
+ multiline: true,
269
+ disabled: busy || disabled
270
+ }
271
+ ),
272
+ /* @__PURE__ */ jsx3(ErrorNotice, { message: error }),
273
+ /* @__PURE__ */ jsx3(
274
+ Button,
275
+ {
276
+ type: "button",
277
+ disabled: busy || disabled || !source.trim(),
278
+ onClick: () => void submit(),
279
+ children: busy ? "Importing\u2026" : replace ? "Replace saved form from JSON" : "Import new form"
280
+ }
281
+ )
282
+ ] });
283
+ }
284
+ var init_import_form = __esm({
285
+ "admin/src/import-form.tsx"() {
286
+ "use strict";
287
+ init_form();
288
+ init_ui();
289
+ }
290
+ });
291
+
292
+ // admin/src/form-list-page.tsx
293
+ import { useCallback, useEffect, useRef as useRef2, useState as useState2 } from "react";
294
+ import {
295
+ Button as Button2,
296
+ Flex as Flex3,
297
+ Table,
298
+ Tbody,
299
+ Td,
300
+ Th,
301
+ Thead,
302
+ Tr,
303
+ Typography as Typography3
304
+ } from "@strapi/design-system";
305
+ import { Layouts, Page, useFetchClient } from "@strapi/strapi/admin";
306
+ import { useNavigate } from "react-router-dom";
307
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
308
+ function FormListPage({ canUpdate }) {
309
+ const client = useFetchClient();
310
+ const { get } = client;
311
+ const navigate = useNavigate();
312
+ const [rows, setRows] = useState2([]);
313
+ const [loading, setLoading] = useState2(true);
314
+ const [error, setError] = useState2("");
315
+ const [busy, setBusy] = useState2(false);
316
+ const [removing, setRemoving] = useState2(null);
317
+ const lock = useRef2(false);
318
+ const load = useCallback(async () => {
319
+ setLoading(true);
320
+ setError("");
321
+ try {
322
+ setRows(
323
+ (await get("/plugins/builder/forms")).data.data
324
+ );
325
+ } catch (cause) {
326
+ setError(errorMessage(cause));
327
+ } finally {
328
+ setLoading(false);
329
+ }
330
+ }, [get]);
331
+ useEffect(() => {
332
+ void load();
333
+ }, [load]);
334
+ async function mutate(action) {
335
+ if (lock.current) return;
336
+ lock.current = true;
337
+ setBusy(true);
338
+ setError("");
339
+ try {
340
+ await action();
341
+ } catch (cause) {
342
+ setError(errorMessage(cause));
343
+ } finally {
344
+ lock.current = false;
345
+ setBusy(false);
346
+ }
347
+ }
348
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
349
+ /* @__PURE__ */ jsx4(
350
+ Layouts.Header,
351
+ {
352
+ title: "Builder Forms",
353
+ subtitle: "Manage public form definitions and consent.",
354
+ primaryAction: canUpdate ? /* @__PURE__ */ jsx4(
355
+ Button2,
356
+ {
357
+ disabled: busy,
358
+ onClick: () => navigate("/plugins/builder/forms/new"),
359
+ children: "Create form"
360
+ }
361
+ ) : void 0
362
+ }
363
+ ),
364
+ /* @__PURE__ */ jsx4(Layouts.Content, { children: /* @__PURE__ */ jsxs4(Flex3, { direction: "column", alignItems: "stretch", gap: 6, children: [
365
+ /* @__PURE__ */ jsx4(ErrorNotice, { message: error }),
366
+ error && /* @__PURE__ */ jsx4(Button2, { onClick: () => void load(), disabled: busy || loading, children: "Retry list" }),
367
+ /* @__PURE__ */ jsx4(Section, { title: "Forms", children: loading ? /* @__PURE__ */ jsx4(Page.Loading, {}) : rows.length === 0 ? /* @__PURE__ */ jsx4(Typography3, { children: "No forms yet." }) : /* @__PURE__ */ jsxs4(Table, { colCount: 5, rowCount: rows.length, children: [
368
+ /* @__PURE__ */ jsx4(Thead, { children: /* @__PURE__ */ jsx4(Tr, { children: ["Name", "Slug", "Target", "Version", "Actions"].map(
369
+ (name) => /* @__PURE__ */ jsx4(Th, { children: /* @__PURE__ */ jsx4(Typography3, { variant: "sigma", children: name }) }, name)
370
+ ) }) }),
371
+ /* @__PURE__ */ jsx4(Tbody, { children: rows.map((row) => /* @__PURE__ */ jsxs4(Tr, { children: [
372
+ /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsx4(Typography3, { children: row.name }) }),
373
+ /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsx4(Typography3, { children: row.slug }) }),
374
+ /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsx4(Typography3, { children: row.targetUid }) }),
375
+ /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsx4(Typography3, { children: row.version }) }),
376
+ /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsxs4(Flex3, { gap: 2, children: [
377
+ /* @__PURE__ */ jsxs4(
378
+ Button2,
379
+ {
380
+ variant: "secondary",
381
+ disabled: busy,
382
+ onClick: () => navigate(
383
+ `/plugins/builder/forms/${row.documentId}`
384
+ ),
385
+ children: [
386
+ canUpdate ? "Edit" : "View",
387
+ " ",
388
+ row.name
389
+ ]
390
+ }
391
+ ),
392
+ /* @__PURE__ */ jsx4(
393
+ Button2,
394
+ {
395
+ variant: "tertiary",
396
+ disabled: busy,
397
+ onClick: () => void mutate(
398
+ async () => downloadForm(
399
+ await formApi(client).export(row.documentId),
400
+ row.slug
401
+ )
402
+ ),
403
+ children: "Export"
404
+ }
405
+ ),
406
+ canUpdate && /* @__PURE__ */ jsx4(
407
+ Button2,
408
+ {
409
+ variant: "danger-light",
410
+ disabled: busy,
411
+ onClick: () => setRemoving(row),
412
+ children: "Delete"
413
+ }
414
+ )
415
+ ] }) })
416
+ ] }, row.documentId)) })
417
+ ] }) }),
418
+ removing && /* @__PURE__ */ jsxs4(Section, { title: `Delete ${removing.name}?`, children: [
419
+ /* @__PURE__ */ jsx4(Typography3, { children: "This permanently deletes the form definition. Forms with submissions cannot be deleted." }),
420
+ /* @__PURE__ */ jsxs4(Flex3, { gap: 3, children: [
421
+ /* @__PURE__ */ jsx4(
422
+ Button2,
423
+ {
424
+ variant: "danger",
425
+ disabled: busy,
426
+ onClick: () => void mutate(async () => {
427
+ await formApi(client).remove(removing.documentId);
428
+ setRemoving(null);
429
+ await load();
430
+ }),
431
+ children: "Confirm delete"
432
+ }
433
+ ),
434
+ /* @__PURE__ */ jsx4(
435
+ Button2,
436
+ {
437
+ variant: "secondary",
438
+ disabled: busy,
439
+ onClick: () => setRemoving(null),
440
+ children: "Cancel"
441
+ }
442
+ )
443
+ ] })
444
+ ] }),
445
+ canUpdate && /* @__PURE__ */ jsx4(Section, { title: "Import form", children: /* @__PURE__ */ jsx4(
446
+ ImportForm,
447
+ {
448
+ disabled: busy,
449
+ onImport: async (value) => {
450
+ if (lock.current) return;
451
+ lock.current = true;
452
+ setBusy(true);
453
+ try {
454
+ const saved = await formApi(client).import(value);
455
+ navigate(`/plugins/builder/forms/${saved.documentId}`);
456
+ } finally {
457
+ lock.current = false;
458
+ setBusy(false);
459
+ }
460
+ }
461
+ }
462
+ ) })
463
+ ] }) })
464
+ ] });
465
+ }
466
+ var init_form_list_page = __esm({
467
+ "admin/src/form-list-page.tsx"() {
468
+ "use strict";
469
+ init_api();
470
+ init_form();
471
+ init_ui();
472
+ init_import_form();
473
+ }
474
+ });
475
+
476
+ // admin/src/field-editor.tsx
477
+ import { Button as Button3, Flex as Flex4, Typography as Typography4 } from "@strapi/design-system";
478
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
479
+ function FieldEditor({
480
+ fields,
481
+ schema,
482
+ disabled,
483
+ onChange
484
+ }) {
485
+ function update(index, field) {
486
+ onChange(fields.map((item, at) => at === index ? field : item));
487
+ }
488
+ return /* @__PURE__ */ jsxs5(Section, { title: "Fields", children: [
489
+ /* @__PURE__ */ jsx5(Typography4, { children: "Select target fields, then adjust their order and presentation. Required target fields cannot be removed or made optional. Hidden fields remain part of submission validation." }),
490
+ /* @__PURE__ */ jsx5(Flex4, { wrap: "wrap", gap: 4, children: schema.map((item) => /* @__PURE__ */ jsxs5("label", { children: [
491
+ /* @__PURE__ */ jsx5(
492
+ "input",
493
+ {
494
+ type: "checkbox",
495
+ checked: fields.some((field) => field.name === item.name),
496
+ disabled: disabled || item.required,
497
+ onChange: (event) => onChange(
498
+ event.target.checked ? [...fields, { ...item }] : fields.filter((field) => field.name !== item.name)
499
+ )
500
+ }
501
+ ),
502
+ " ",
503
+ item.name,
504
+ item.required ? " (schema required)" : ""
505
+ ] }, item.name)) }),
506
+ fields.map((field, index) => {
507
+ const attribute = schema.find((item) => item.name === field.name);
508
+ return /* @__PURE__ */ jsxs5(
509
+ Section,
510
+ {
511
+ title: `${index + 1}. ${field.name} (${field.type})`,
512
+ children: [
513
+ /* @__PURE__ */ jsxs5(Flex4, { gap: 3, children: [
514
+ /* @__PURE__ */ jsxs5(
515
+ Button3,
516
+ {
517
+ type: "button",
518
+ variant: "secondary",
519
+ disabled: disabled || index === 0,
520
+ onClick: () => onChange(moveField(fields, index, -1)),
521
+ children: [
522
+ "Move ",
523
+ field.name,
524
+ " up"
525
+ ]
526
+ }
527
+ ),
528
+ /* @__PURE__ */ jsxs5(
529
+ Button3,
530
+ {
531
+ type: "button",
532
+ variant: "secondary",
533
+ disabled: disabled || index === fields.length - 1,
534
+ onClick: () => onChange(moveField(fields, index, 1)),
535
+ children: [
536
+ "Move ",
537
+ field.name,
538
+ " down"
539
+ ]
540
+ }
541
+ )
542
+ ] }),
543
+ /* @__PURE__ */ jsx5(
544
+ TextField,
545
+ {
546
+ label: "Label",
547
+ required: true,
548
+ disabled,
549
+ value: field.label,
550
+ onChange: (label) => update(index, { ...field, label })
551
+ }
552
+ ),
553
+ /* @__PURE__ */ jsx5(
554
+ TextField,
555
+ {
556
+ label: "Placeholder",
557
+ disabled,
558
+ value: field.placeholder ?? "",
559
+ onChange: (placeholder) => update(index, { ...field, placeholder })
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsx5(
563
+ TextField,
564
+ {
565
+ label: "Help text",
566
+ disabled,
567
+ value: field.help ?? "",
568
+ onChange: (help) => update(index, { ...field, help })
569
+ }
570
+ ),
571
+ /* @__PURE__ */ jsxs5("label", { children: [
572
+ /* @__PURE__ */ jsx5(
573
+ "input",
574
+ {
575
+ type: "checkbox",
576
+ checked: field.required,
577
+ disabled: disabled || !attribute || attribute.required,
578
+ onChange: (event) => {
579
+ if (attribute)
580
+ update(
581
+ index,
582
+ setRequired(field, event.target.checked, attribute)
583
+ );
584
+ }
585
+ }
586
+ ),
587
+ " ",
588
+ "Required",
589
+ attribute?.required ? " (required by target schema)" : ""
590
+ ] }),
591
+ /* @__PURE__ */ jsxs5("label", { children: [
592
+ /* @__PURE__ */ jsx5(
593
+ "input",
594
+ {
595
+ type: "checkbox",
596
+ checked: field.visibility === "hidden",
597
+ disabled,
598
+ onChange: (event) => update(index, {
599
+ ...field,
600
+ visibility: event.target.checked ? "hidden" : "form"
601
+ })
602
+ }
603
+ ),
604
+ " ",
605
+ "Hidden from form"
606
+ ] }),
607
+ (field.maxLength !== void 0 || field.min !== void 0 || field.max !== void 0) && /* @__PURE__ */ jsxs5(Typography4, { variant: "pi", children: [
608
+ "Constraints:",
609
+ " ",
610
+ JSON.stringify({
611
+ maxLength: field.maxLength,
612
+ min: field.min,
613
+ max: field.max
614
+ })
615
+ ] }),
616
+ field.options?.map((option, optionIndex) => /* @__PURE__ */ jsx5(
617
+ TextField,
618
+ {
619
+ label: `Option label: ${option.value}`,
620
+ required: true,
621
+ disabled,
622
+ value: option.label,
623
+ onChange: (label) => update(index, {
624
+ ...field,
625
+ options: field.options?.map(
626
+ (item, at) => at === optionIndex ? { ...item, label } : item
627
+ )
628
+ })
629
+ },
630
+ option.value
631
+ ))
632
+ ]
633
+ },
634
+ field.name
635
+ );
636
+ })
637
+ ] });
638
+ }
639
+ var init_field_editor = __esm({
640
+ "admin/src/field-editor.tsx"() {
641
+ "use strict";
642
+ init_form();
643
+ init_ui();
644
+ }
645
+ });
646
+
647
+ // admin/src/notifications.ts
648
+ function placeholders(fields) {
649
+ return [
650
+ ...fields.map((field) => `field.${field.name}`),
651
+ "form.name",
652
+ "submission.id",
653
+ "submission.receivedAt"
654
+ ];
655
+ }
656
+ function templateError(value, fields) {
657
+ const allowed = placeholders(fields);
658
+ let invalid = false;
659
+ const rest = value.replace(/{{\s*([^{}]+?)\s*}}/g, (_, key) => {
660
+ if (!allowed.includes(key)) invalid = true;
661
+ return "";
662
+ });
663
+ return invalid || rest.includes("{{") || rest.includes("}}") ? "Use only the available placeholders." : "";
664
+ }
665
+ function notificationErrors(config, fields) {
666
+ const errors = {};
667
+ if (config.email) {
668
+ if (config.email.replyToField && !fields.some(
669
+ (field) => field.type === "email" && field.name === config.email?.replyToField
670
+ ))
671
+ errors.replyToField = "Choose an email field in this form, or None.";
672
+ for (const key of ["subject", "body"]) {
673
+ const error = templateError(config.email[key], fields);
674
+ if (error) errors[key] = error;
675
+ }
676
+ for (const key of ["to", "cc"]) {
677
+ if (config.email[key]?.some((value) => /[{}]/.test(value)))
678
+ errors[key] = "Recipients must be fixed email addresses; placeholders are not allowed.";
679
+ }
680
+ }
681
+ if (config.webhook) {
682
+ try {
683
+ const url = new URL(config.webhook.url);
684
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
685
+ throw Error("url");
686
+ } catch {
687
+ errors.url = "Enter an HTTP or HTTPS URL without credentials.";
688
+ }
689
+ }
690
+ return errors;
691
+ }
692
+ function samplePayload(fields, values) {
693
+ return Object.fromEntries(
694
+ fields.flatMap((field) => {
695
+ const value = values[field.name] ?? "";
696
+ if (!value && !field.required) return [];
697
+ return [
698
+ [
699
+ field.name,
700
+ value && ["integer", "decimal"].includes(field.type) ? Number(value) : value && field.type === "boolean" ? value === "true" : value
701
+ ]
702
+ ];
703
+ })
704
+ );
705
+ }
706
+ function sampleErrors(error) {
707
+ const response = error && typeof error === "object" && "response" in error ? error.response : void 0;
708
+ const details = response?.data?.error?.details?.errors;
709
+ if (!Array.isArray(details)) return {};
710
+ return Object.fromEntries(
711
+ details.flatMap((entry) => {
712
+ if (!entry || typeof entry !== "object" || !("field" in entry) || !("code" in entry) || typeof entry.field !== "string" || typeof entry.code !== "string")
713
+ return [];
714
+ return [[entry.field, entry.code]];
715
+ })
716
+ );
717
+ }
718
+ var init_notifications = __esm({
719
+ "admin/src/notifications.ts"() {
720
+ "use strict";
721
+ }
722
+ });
723
+
724
+ // admin/src/notification-editor.tsx
725
+ import { useState as useState3 } from "react";
726
+ import { Button as Button4, Flex as Flex5, Typography as Typography5 } from "@strapi/design-system";
727
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
728
+ function NotificationEditor({
729
+ value,
730
+ fields,
731
+ opened,
732
+ onOpen,
733
+ onChange,
734
+ disabled,
735
+ saveError
736
+ }) {
737
+ const [target, setTarget] = useState3("body");
738
+ const errors = notificationErrors(value, fields);
739
+ function toggle(channel, enabled) {
740
+ const next = { ...value };
741
+ if (!enabled) delete next[channel];
742
+ else if (channel === "email")
743
+ next.email = {
744
+ to: [],
745
+ cc: [],
746
+ subject: "New submission: {{form.name}}",
747
+ body: "Submission {{submission.id}} received at {{submission.receivedAt}}"
748
+ };
749
+ else next.webhook = { url: "" };
750
+ onChange(next);
751
+ }
752
+ return /* @__PURE__ */ jsxs6(Section, { title: "Notifications", children: [
753
+ /* @__PURE__ */ jsx6(
754
+ Button4,
755
+ {
756
+ type: "button",
757
+ variant: "tertiary",
758
+ onClick: onOpen,
759
+ disabled: opened,
760
+ children: opened ? "Notification settings opened" : "Open notification settings"
761
+ }
762
+ ),
763
+ opened && /* @__PURE__ */ jsxs6(Fragment2, { children: [
764
+ /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Enable a channel to send notifications. Changes take effect after saving the form." }),
765
+ saveError.startsWith("notifications:") && /* @__PURE__ */ jsx6(ErrorNotice, { message: saveError }),
766
+ /* @__PURE__ */ jsxs6("label", { children: [
767
+ /* @__PURE__ */ jsx6(
768
+ "input",
769
+ {
770
+ type: "checkbox",
771
+ disabled,
772
+ checked: Boolean(value.email),
773
+ onChange: (event) => toggle("email", event.target.checked)
774
+ }
775
+ ),
776
+ " ",
777
+ "Email notifications"
778
+ ] }),
779
+ value.email && /* @__PURE__ */ jsxs6(Fragment2, { children: [
780
+ ["to", "cc"].map((key) => /* @__PURE__ */ jsx6(
781
+ TextField,
782
+ {
783
+ label: key === "to" ? "To (one address per line)" : "Cc (one address per line)",
784
+ multiline: true,
785
+ value: (value.email?.[key] ?? []).join("\n"),
786
+ disabled,
787
+ error: errors[key],
788
+ onChange: (text) => onChange({
789
+ ...value,
790
+ email: { ...value.email, [key]: text.split("\n") }
791
+ })
792
+ },
793
+ key
794
+ )),
795
+ /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Use fixed email addresses. Empty lines are ignored when saving." }),
796
+ /* @__PURE__ */ jsxs6("label", { children: [
797
+ "Reply-to field",
798
+ /* @__PURE__ */ jsxs6(
799
+ "select",
800
+ {
801
+ style: { width: "100%", padding: 10 },
802
+ disabled,
803
+ value: value.email.replyToField ?? "",
804
+ "aria-invalid": Boolean(errors.replyToField),
805
+ onChange: (event) => {
806
+ const email = { ...value.email };
807
+ delete email.replyToField;
808
+ if (event.target.value)
809
+ email.replyToField = event.target.value;
810
+ onChange({ ...value, email });
811
+ },
812
+ children: [
813
+ /* @__PURE__ */ jsx6("option", { value: "", children: "None" }),
814
+ fields.filter((field) => field.type === "email").map((field) => /* @__PURE__ */ jsxs6("option", { value: field.name, children: [
815
+ field.label,
816
+ " (",
817
+ field.name,
818
+ ")"
819
+ ] }, field.name))
820
+ ]
821
+ }
822
+ )
823
+ ] }),
824
+ errors.replyToField && /* @__PURE__ */ jsx6(ErrorNotice, { message: errors.replyToField }),
825
+ /* @__PURE__ */ jsx6(
826
+ TextField,
827
+ {
828
+ label: "Email subject",
829
+ value: value.email.subject,
830
+ disabled,
831
+ error: errors.subject,
832
+ onChange: (subject) => onChange({ ...value, email: { ...value.email, subject } })
833
+ }
834
+ ),
835
+ /* @__PURE__ */ jsx6(
836
+ TextField,
837
+ {
838
+ label: "Email body",
839
+ multiline: true,
840
+ value: value.email.body,
841
+ disabled,
842
+ error: errors.body,
843
+ onChange: (body) => onChange({ ...value, email: { ...value.email, body } })
844
+ }
845
+ ),
846
+ /* @__PURE__ */ jsxs6("label", { children: [
847
+ "Insert placeholder into",
848
+ /* @__PURE__ */ jsxs6(
849
+ "select",
850
+ {
851
+ style: { width: "100%", padding: 10 },
852
+ disabled,
853
+ value: target,
854
+ onChange: (event) => setTarget(
855
+ event.target.value === "subject" ? "subject" : "body"
856
+ ),
857
+ children: [
858
+ /* @__PURE__ */ jsx6("option", { value: "subject", children: "Subject" }),
859
+ /* @__PURE__ */ jsx6("option", { value: "body", children: "Body" })
860
+ ]
861
+ }
862
+ )
863
+ ] }),
864
+ /* @__PURE__ */ jsx6(Flex5, { gap: 2, wrap: "wrap", children: placeholders(fields).map((key) => /* @__PURE__ */ jsx6(
865
+ Button4,
866
+ {
867
+ type: "button",
868
+ style: {
869
+ maxWidth: "100%",
870
+ height: "auto",
871
+ whiteSpace: "normal",
872
+ overflowWrap: "anywhere"
873
+ },
874
+ size: "S",
875
+ variant: "tertiary",
876
+ disabled,
877
+ onClick: () => onChange({
878
+ ...value,
879
+ email: {
880
+ ...value.email,
881
+ [target]: `${value.email[target]}{{${key}}}`
882
+ }
883
+ }),
884
+ children: `{{${key}}}`
885
+ },
886
+ key
887
+ )) })
888
+ ] }),
889
+ /* @__PURE__ */ jsxs6("label", { children: [
890
+ /* @__PURE__ */ jsx6(
891
+ "input",
892
+ {
893
+ type: "checkbox",
894
+ disabled,
895
+ checked: Boolean(value.webhook),
896
+ onChange: (event) => toggle("webhook", event.target.checked)
897
+ }
898
+ ),
899
+ " ",
900
+ "Webhook notifications"
901
+ ] }),
902
+ value.webhook && /* @__PURE__ */ jsxs6(Fragment2, { children: [
903
+ /* @__PURE__ */ jsx6(
904
+ TextField,
905
+ {
906
+ label: "Webhook URL",
907
+ value: value.webhook.url,
908
+ disabled,
909
+ error: errors.url,
910
+ onChange: (url) => onChange({ ...value, webhook: { ...value.webhook, url } })
911
+ }
912
+ ),
913
+ /* @__PURE__ */ jsx6(
914
+ TextField,
915
+ {
916
+ label: "Webhook secret",
917
+ secret: true,
918
+ value: value.webhook.secret ?? "",
919
+ disabled,
920
+ onChange: (secret) => {
921
+ const webhook = { ...value.webhook };
922
+ delete webhook.secret;
923
+ if (secret) webhook.secret = secret;
924
+ onChange({ ...value, webhook });
925
+ }
926
+ }
927
+ ),
928
+ /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Stored secrets and header values appear as ********. Leave them untouched to preserve them, or enter a replacement. Clear a secret or remove a header to delete it." }),
929
+ Object.entries(value.webhook.headers ?? {}).map(
930
+ ([name, header]) => /* @__PURE__ */ jsxs6(
931
+ Flex5,
932
+ {
933
+ direction: "column",
934
+ alignItems: "stretch",
935
+ gap: 2,
936
+ children: [
937
+ /* @__PURE__ */ jsx6(
938
+ TextField,
939
+ {
940
+ label: `Header: ${name}`,
941
+ secret: true,
942
+ value: header,
943
+ disabled,
944
+ onChange: (next) => onChange({
945
+ ...value,
946
+ webhook: {
947
+ ...value.webhook,
948
+ headers: {
949
+ ...value.webhook.headers,
950
+ [name]: next
951
+ }
952
+ }
953
+ })
954
+ }
955
+ ),
956
+ /* @__PURE__ */ jsxs6(
957
+ Button4,
958
+ {
959
+ type: "button",
960
+ variant: "tertiary",
961
+ disabled,
962
+ onClick: () => {
963
+ const headers = { ...value.webhook.headers };
964
+ delete headers[name];
965
+ onChange({
966
+ ...value,
967
+ webhook: { ...value.webhook, headers }
968
+ });
969
+ },
970
+ children: [
971
+ "Remove ",
972
+ name
973
+ ]
974
+ }
975
+ )
976
+ ]
977
+ },
978
+ name
979
+ )
980
+ ),
981
+ /* @__PURE__ */ jsx6(
982
+ AddHeader,
983
+ {
984
+ disabled,
985
+ names: Object.keys(value.webhook.headers ?? {}),
986
+ onAdd: (name, header) => onChange({
987
+ ...value,
988
+ webhook: {
989
+ ...value.webhook,
990
+ headers: { ...value.webhook.headers, [name]: header }
991
+ }
992
+ })
993
+ }
994
+ )
995
+ ] })
996
+ ] })
997
+ ] });
998
+ }
999
+ function AddHeader({
1000
+ disabled,
1001
+ names,
1002
+ onAdd
1003
+ }) {
1004
+ const [name, setName] = useState3("");
1005
+ const [value, setValue] = useState3("");
1006
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1007
+ /* @__PURE__ */ jsx6(
1008
+ TextField,
1009
+ {
1010
+ label: "New header name",
1011
+ value: name,
1012
+ disabled,
1013
+ onChange: setName
1014
+ }
1015
+ ),
1016
+ /* @__PURE__ */ jsx6(
1017
+ TextField,
1018
+ {
1019
+ label: "New header value",
1020
+ secret: true,
1021
+ value,
1022
+ disabled,
1023
+ onChange: setValue
1024
+ }
1025
+ ),
1026
+ /* @__PURE__ */ jsx6(
1027
+ Button4,
1028
+ {
1029
+ type: "button",
1030
+ variant: "tertiary",
1031
+ disabled: disabled || !name.trim() || !value || names.some((item) => item.toLowerCase() === name.trim().toLowerCase()),
1032
+ onClick: () => {
1033
+ onAdd(name.trim(), value);
1034
+ setName("");
1035
+ setValue("");
1036
+ },
1037
+ children: "Add header"
1038
+ }
1039
+ )
1040
+ ] });
1041
+ }
1042
+ function NotificationTest({
1043
+ fields,
1044
+ disabled,
1045
+ send
1046
+ }) {
1047
+ const [values, setValues] = useState3({});
1048
+ const [results, setResults] = useState3(null);
1049
+ const [error, setError] = useState3("");
1050
+ const [errors, setErrors] = useState3({});
1051
+ const [busy, setBusy] = useState3(false);
1052
+ return /* @__PURE__ */ jsxs6(Section, { title: "Test notifications", children: [
1053
+ /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Sends real notifications using the saved settings and these sample values, including hidden fields. Save all changes before testing. No submission record is created." }),
1054
+ disabled && /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Testing requires a saved form, no unsaved changes and forms.update permission." }),
1055
+ fields.map((field) => /* @__PURE__ */ jsx6("div", { style: { display: "grid", gap: 6 }, children: field.type === "boolean" ? /* @__PURE__ */ jsxs6("label", { children: [
1056
+ /* @__PURE__ */ jsxs6(Typography5, { children: [
1057
+ field.label,
1058
+ " (",
1059
+ field.name,
1060
+ ")",
1061
+ field.visibility === "hidden" ? " \xB7 hidden" : ""
1062
+ ] }),
1063
+ /* @__PURE__ */ jsxs6(
1064
+ "select",
1065
+ {
1066
+ disabled: disabled || busy,
1067
+ value: values[field.name] ?? "",
1068
+ onChange: (event) => setValues({ ...values, [field.name]: event.target.value }),
1069
+ children: [
1070
+ /* @__PURE__ */ jsx6("option", { value: "", children: "Choose a value" }),
1071
+ /* @__PURE__ */ jsx6("option", { value: "true", children: "True" }),
1072
+ /* @__PURE__ */ jsx6("option", { value: "false", children: "False" })
1073
+ ]
1074
+ }
1075
+ ),
1076
+ errors[field.name] && /* @__PURE__ */ jsx6(ErrorNotice, { message: errors[field.name] ?? "" })
1077
+ ] }) : /* @__PURE__ */ jsx6(
1078
+ TextField,
1079
+ {
1080
+ label: `${field.label} (${field.name})${field.visibility === "hidden" ? " \xB7 hidden" : ""}`,
1081
+ value: values[field.name] ?? "",
1082
+ disabled: disabled || busy,
1083
+ error: errors[field.name],
1084
+ onChange: (next) => setValues({ ...values, [field.name]: next })
1085
+ }
1086
+ ) }, field.name)),
1087
+ /* @__PURE__ */ jsx6(
1088
+ Button4,
1089
+ {
1090
+ type: "button",
1091
+ disabled: disabled || busy,
1092
+ onClick: async () => {
1093
+ setBusy(true);
1094
+ setError("");
1095
+ setErrors({});
1096
+ setResults(null);
1097
+ try {
1098
+ setResults(await send(samplePayload(fields, values)));
1099
+ } catch (cause) {
1100
+ setError(errorMessage(cause));
1101
+ setErrors(sampleErrors(cause));
1102
+ } finally {
1103
+ setBusy(false);
1104
+ }
1105
+ },
1106
+ children: busy ? "Sending test\u2026" : "Send test notifications"
1107
+ }
1108
+ ),
1109
+ /* @__PURE__ */ jsx6(ErrorNotice, { message: error }),
1110
+ /* @__PURE__ */ jsxs6("div", { "aria-live": "polite", children: [
1111
+ results?.length === 0 && /* @__PURE__ */ jsx6(Typography5, { children: "No notification channels are enabled in the saved settings." }),
1112
+ results?.map((delivery) => /* @__PURE__ */ jsx6(
1113
+ "div",
1114
+ {
1115
+ style: { overflowWrap: "anywhere", marginTop: 12 },
1116
+ children: /* @__PURE__ */ jsxs6(
1117
+ Typography5,
1118
+ {
1119
+ textColor: delivery.status === "sent" ? "success700" : "danger700",
1120
+ children: [
1121
+ delivery.channel,
1122
+ ":",
1123
+ " ",
1124
+ delivery.status === "sent" ? "Success" : "Failed",
1125
+ " \xB7",
1126
+ " ",
1127
+ delivery.attempts,
1128
+ " attempt(s)",
1129
+ delivery.lastError ? ` \xB7 ${delivery.lastError}` : "",
1130
+ delivery.sentAt ? ` \xB7 ${delivery.sentAt}` : ""
1131
+ ]
1132
+ }
1133
+ )
1134
+ },
1135
+ delivery.channel
1136
+ ))
1137
+ ] })
1138
+ ] });
1139
+ }
1140
+ var init_notification_editor = __esm({
1141
+ "admin/src/notification-editor.tsx"() {
1142
+ "use strict";
1143
+ init_form();
1144
+ init_notifications();
1145
+ init_ui();
1146
+ }
1147
+ });
1148
+
1149
+ // admin/src/form-editor-page.tsx
1150
+ import { useEffect as useEffect2, useRef as useRef3, useState as useState4 } from "react";
1151
+ import { Button as Button5, Flex as Flex6, Typography as Typography6 } from "@strapi/design-system";
1152
+ import { Layouts as Layouts2, Page as Page2, useFetchClient as useFetchClient2 } from "@strapi/strapi/admin";
1153
+ import { useNavigate as useNavigate2, useParams } from "react-router-dom";
1154
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1155
+ function FormEditorPage({ canUpdate }) {
1156
+ const { documentId } = useParams();
1157
+ const client = useFetchClient2();
1158
+ const { get } = client;
1159
+ const navigate = useNavigate2();
1160
+ const [draft, setDraft] = useState4(emptyForm);
1161
+ const [notificationsOpened, setNotificationsOpened] = useState4(false);
1162
+ const [savedDraft, setSavedDraft] = useState4("");
1163
+ const [version, setVersion] = useState4(null);
1164
+ const [targets, setTargets] = useState4([]);
1165
+ const [schema, setSchema] = useState4([]);
1166
+ const [loading, setLoading] = useState4(true);
1167
+ const [fieldsLoading, setFieldsLoading] = useState4(false);
1168
+ const [error, setError] = useState4("");
1169
+ const [loaded, setLoaded] = useState4(false);
1170
+ const [busy, setBusy] = useState4(false);
1171
+ const [retry, setRetry] = useState4(0);
1172
+ const lock = useRef3(false);
1173
+ const targetRequest = useRef3(0);
1174
+ useEffect2(() => {
1175
+ let active = true;
1176
+ setLoading(true);
1177
+ setNotificationsOpened(false);
1178
+ setLoaded(false);
1179
+ setError("");
1180
+ async function load() {
1181
+ try {
1182
+ const api = formApi(client);
1183
+ const [available, saved] = await Promise.all([
1184
+ api.targets(),
1185
+ documentId ? api.get(documentId) : Promise.resolve(null)
1186
+ ]);
1187
+ const attrs = saved ? await api.fields(saved.targetUid) : [];
1188
+ if (!active) return;
1189
+ setTargets(available);
1190
+ setDraft(
1191
+ saved ?? {
1192
+ ...emptyForm,
1193
+ fields: [],
1194
+ consent: { ...emptyForm.consent }
1195
+ }
1196
+ );
1197
+ setSavedDraft(saved ? JSON.stringify(savePayload(saved, true)) : "");
1198
+ setVersion(saved?.version ?? null);
1199
+ setSchema(attrs);
1200
+ setLoaded(true);
1201
+ } catch (cause) {
1202
+ if (active) setError(errorMessage(cause));
1203
+ } finally {
1204
+ if (active) setLoading(false);
1205
+ }
1206
+ }
1207
+ void load();
1208
+ return () => {
1209
+ active = false;
1210
+ targetRequest.current += 1;
1211
+ };
1212
+ }, [documentId, get, retry]);
1213
+ async function selectTarget(targetUid) {
1214
+ const request = ++targetRequest.current;
1215
+ setDraft((current) => ({ ...current, targetUid, fields: [] }));
1216
+ setSchema([]);
1217
+ setFieldsLoading(true);
1218
+ setError("");
1219
+ try {
1220
+ const attrs = await formApi(client).fields(targetUid);
1221
+ if (request !== targetRequest.current) return;
1222
+ setSchema(attrs);
1223
+ setDraft((current) => ({
1224
+ ...current,
1225
+ fields: attrs.filter((field) => field.required)
1226
+ }));
1227
+ } catch (cause) {
1228
+ if (request === targetRequest.current) setError(errorMessage(cause));
1229
+ } finally {
1230
+ if (request === targetRequest.current) setFieldsLoading(false);
1231
+ }
1232
+ }
1233
+ async function run(action) {
1234
+ if (lock.current) return;
1235
+ lock.current = true;
1236
+ setBusy(true);
1237
+ setError("");
1238
+ try {
1239
+ await action();
1240
+ } catch (cause) {
1241
+ setError(errorMessage(cause));
1242
+ } finally {
1243
+ lock.current = false;
1244
+ setBusy(false);
1245
+ }
1246
+ }
1247
+ if (loading) return /* @__PURE__ */ jsx7(Page2.Loading, {});
1248
+ const disabled = !canUpdate || busy || fieldsLoading;
1249
+ return /* @__PURE__ */ jsxs7(Fragment3, { children: [
1250
+ /* @__PURE__ */ jsx7(
1251
+ Layouts2.Header,
1252
+ {
1253
+ title: documentId ? draft.name || "Form" : "Create form",
1254
+ subtitle: version ? `Form version ${version} \xB7 assigned by server on every save` : "New forms start at version 1.",
1255
+ navigationAction: /* @__PURE__ */ jsx7(
1256
+ Button5,
1257
+ {
1258
+ variant: "tertiary",
1259
+ onClick: () => navigate("/plugins/builder/forms"),
1260
+ children: "Back to forms"
1261
+ }
1262
+ )
1263
+ }
1264
+ ),
1265
+ /* @__PURE__ */ jsx7(Layouts2.Content, { children: /* @__PURE__ */ jsxs7(Flex6, { direction: "column", alignItems: "stretch", gap: 6, children: [
1266
+ /* @__PURE__ */ jsx7(ErrorNotice, { message: error }),
1267
+ !loaded ? /* @__PURE__ */ jsx7(Button5, { onClick: () => setRetry((value) => value + 1), children: "Retry loading form" }) : /* @__PURE__ */ jsxs7(Fragment3, { children: [
1268
+ /* @__PURE__ */ jsx7(
1269
+ "form",
1270
+ {
1271
+ onSubmit: (event) => {
1272
+ event.preventDefault();
1273
+ if (disabled) return;
1274
+ void run(async () => {
1275
+ const saved = await formApi(client).save(
1276
+ draft,
1277
+ documentId,
1278
+ notificationsOpened
1279
+ );
1280
+ setDraft(saved);
1281
+ setSavedDraft(JSON.stringify(savePayload(saved, true)));
1282
+ setVersion(saved.version);
1283
+ if (!documentId)
1284
+ navigate(`/plugins/builder/forms/${saved.documentId}`, {
1285
+ replace: true
1286
+ });
1287
+ });
1288
+ },
1289
+ children: /* @__PURE__ */ jsxs7(Flex6, { direction: "column", alignItems: "stretch", gap: 6, children: [
1290
+ /* @__PURE__ */ jsxs7(Section, { title: "Definition", children: [
1291
+ /* @__PURE__ */ jsx7(
1292
+ TextField,
1293
+ {
1294
+ label: "Name",
1295
+ required: true,
1296
+ disabled,
1297
+ value: draft.name,
1298
+ onChange: (name) => setDraft({ ...draft, name })
1299
+ }
1300
+ ),
1301
+ /* @__PURE__ */ jsx7(
1302
+ TextField,
1303
+ {
1304
+ label: "Slug",
1305
+ required: true,
1306
+ disabled: disabled || Boolean(documentId),
1307
+ value: draft.slug,
1308
+ onChange: (slug) => setDraft({ ...draft, slug })
1309
+ }
1310
+ ),
1311
+ /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Use lowercase letters, numbers and hyphens. The slug cannot change after creation." }),
1312
+ /* @__PURE__ */ jsxs7("label", { children: [
1313
+ "Target content type",
1314
+ " ",
1315
+ /* @__PURE__ */ jsxs7(
1316
+ "select",
1317
+ {
1318
+ required: true,
1319
+ disabled,
1320
+ value: draft.targetUid,
1321
+ onChange: (event) => void selectTarget(event.target.value),
1322
+ style: { padding: 10, width: "100%" },
1323
+ children: [
1324
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Select a target" }),
1325
+ targets.map((target) => /* @__PURE__ */ jsxs7("option", { value: target.uid, children: [
1326
+ target.name,
1327
+ " \u2014 ",
1328
+ target.uid
1329
+ ] }, target.uid))
1330
+ ]
1331
+ }
1332
+ )
1333
+ ] }),
1334
+ fieldsLoading && /* @__PURE__ */ jsx7(Typography6, { children: "Loading target fields\u2026" })
1335
+ ] }),
1336
+ /* @__PURE__ */ jsx7(
1337
+ FieldEditor,
1338
+ {
1339
+ fields: draft.fields,
1340
+ schema,
1341
+ disabled,
1342
+ onChange: (fields) => setDraft({ ...draft, fields })
1343
+ }
1344
+ ),
1345
+ /* @__PURE__ */ jsxs7(Section, { title: "Consent", children: [
1346
+ /* @__PURE__ */ jsx7(
1347
+ TextField,
1348
+ {
1349
+ label: "Consent label",
1350
+ required: true,
1351
+ multiline: true,
1352
+ disabled,
1353
+ value: draft.consent.label,
1354
+ onChange: (label) => setDraft({
1355
+ ...draft,
1356
+ consent: { ...draft.consent, label }
1357
+ })
1358
+ }
1359
+ ),
1360
+ /* @__PURE__ */ jsx7(
1361
+ TextField,
1362
+ {
1363
+ label: "Consent version",
1364
+ required: true,
1365
+ disabled,
1366
+ value: draft.consent.version,
1367
+ onChange: (consentVersion) => setDraft({
1368
+ ...draft,
1369
+ consent: {
1370
+ ...draft.consent,
1371
+ version: consentVersion
1372
+ }
1373
+ })
1374
+ }
1375
+ ),
1376
+ /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Consent version identifies the terms accepted by the visitor and is separate from the server form version." })
1377
+ ] }),
1378
+ /* @__PURE__ */ jsx7(
1379
+ NotificationEditor,
1380
+ {
1381
+ value: draft.notifications ?? {},
1382
+ fields: draft.fields,
1383
+ opened: notificationsOpened,
1384
+ onOpen: () => setNotificationsOpened(true),
1385
+ onChange: (notifications) => setDraft({ ...draft, notifications }),
1386
+ disabled,
1387
+ saveError: error
1388
+ }
1389
+ ),
1390
+ canUpdate && /* @__PURE__ */ jsx7(
1391
+ Button5,
1392
+ {
1393
+ type: "submit",
1394
+ disabled: disabled || !draft.targetUid || !draft.fields.length,
1395
+ children: busy ? "Saving\u2026" : "Save form"
1396
+ }
1397
+ )
1398
+ ] })
1399
+ }
1400
+ ),
1401
+ notificationsOpened && /* @__PURE__ */ jsx7(
1402
+ NotificationTest,
1403
+ {
1404
+ fields: draft.fields,
1405
+ disabled: disabled || !documentId || JSON.stringify(savePayload(draft, true)) !== savedDraft,
1406
+ send: (sample) => formApi(client).testSend(documentId, sample)
1407
+ },
1408
+ savedDraft
1409
+ ),
1410
+ documentId && /* @__PURE__ */ jsxs7(Section, { title: "Portable definition", children: [
1411
+ /* @__PURE__ */ jsx7(
1412
+ Button5,
1413
+ {
1414
+ disabled: busy,
1415
+ onClick: () => void run(
1416
+ async () => downloadForm(
1417
+ await formApi(client).export(documentId),
1418
+ draft.slug
1419
+ )
1420
+ ),
1421
+ children: "Export saved form JSON"
1422
+ }
1423
+ ),
1424
+ /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Export uses the saved definition. Notification credentials are masked by the server." }),
1425
+ canUpdate && /* @__PURE__ */ jsx7(
1426
+ ImportForm,
1427
+ {
1428
+ replace: true,
1429
+ disabled: busy || fieldsLoading,
1430
+ onImport: async (value) => {
1431
+ if (lock.current) return;
1432
+ lock.current = true;
1433
+ setBusy(true);
1434
+ try {
1435
+ const saved = await formApi(client).import(
1436
+ value,
1437
+ documentId
1438
+ );
1439
+ setDraft(saved);
1440
+ setSavedDraft(
1441
+ JSON.stringify(savePayload(saved, true))
1442
+ );
1443
+ setNotificationsOpened(false);
1444
+ setError("");
1445
+ setVersion(saved.version);
1446
+ setSchema(
1447
+ await formApi(client).fields(saved.targetUid)
1448
+ );
1449
+ } finally {
1450
+ lock.current = false;
1451
+ setBusy(false);
1452
+ }
1453
+ }
1454
+ }
1455
+ )
1456
+ ] })
1457
+ ] })
1458
+ ] }) })
1459
+ ] });
1460
+ }
1461
+ var init_form_editor_page = __esm({
1462
+ "admin/src/form-editor-page.tsx"() {
1463
+ "use strict";
1464
+ init_api();
1465
+ init_form();
1466
+ init_ui();
1467
+ init_field_editor();
1468
+ init_notification_editor();
1469
+ init_import_form();
1470
+ }
1471
+ });
1472
+
1473
+ // admin/src/app.tsx
1474
+ var app_exports = {};
1475
+ __export(app_exports, {
1476
+ default: () => App
1477
+ });
1478
+ import { Navigate, Route, Routes } from "react-router-dom";
1479
+ import { Layouts as Layouts3, Page as Page3, useRBAC } from "@strapi/strapi/admin";
1480
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1481
+ function App() {
1482
+ const { allowedActions, isLoading } = useRBAC(
1483
+ ["read", "update"].map((action) => ({
1484
+ action: `plugin::builder.forms.${action}`,
1485
+ subject: null
1486
+ }))
1487
+ );
1488
+ if (isLoading) return /* @__PURE__ */ jsx8(Page3.Loading, {});
1489
+ if (!allowedActions.canRead) return /* @__PURE__ */ jsx8(Page3.NoPermissions, {});
1490
+ const canUpdate = allowedActions.canUpdate === true;
1491
+ return /* @__PURE__ */ jsx8(Layouts3.Root, { children: /* @__PURE__ */ jsxs8(Routes, { children: [
1492
+ /* @__PURE__ */ jsx8(Route, { index: true, element: /* @__PURE__ */ jsx8(Navigate, { to: "forms", replace: true }) }),
1493
+ /* @__PURE__ */ jsx8(Route, { path: "forms", element: /* @__PURE__ */ jsx8(FormListPage, { canUpdate }) }),
1494
+ /* @__PURE__ */ jsx8(
1495
+ Route,
1496
+ {
1497
+ path: "forms/new",
1498
+ element: canUpdate ? /* @__PURE__ */ jsx8(FormEditorPage, { canUpdate: true }) : /* @__PURE__ */ jsx8(Page3.NoPermissions, {})
1499
+ }
1500
+ ),
1501
+ /* @__PURE__ */ jsx8(
1502
+ Route,
1503
+ {
1504
+ path: "forms/:documentId",
1505
+ element: /* @__PURE__ */ jsx8(FormEditorPage, { canUpdate })
1506
+ }
1507
+ ),
1508
+ /* @__PURE__ */ jsx8(
1509
+ Route,
1510
+ {
1511
+ path: "*",
1512
+ element: /* @__PURE__ */ jsx8(Page3.Error, { content: "Form page not found." })
1513
+ }
1514
+ )
1515
+ ] }) });
1516
+ }
1517
+ var init_app = __esm({
1518
+ "admin/src/app.tsx"() {
1519
+ "use strict";
1520
+ init_form_list_page();
1521
+ init_form_editor_page();
1522
+ }
1523
+ });
1524
+
1525
+ // admin/src/plugin-icon.tsx
1526
+ import { jsx, jsxs } from "react/jsx-runtime";
1527
+ function BuilderIcon() {
1528
+ return /* @__PURE__ */ jsxs(
1529
+ "svg",
1530
+ {
1531
+ width: "24",
1532
+ height: "24",
1533
+ viewBox: "0 0 24 24",
1534
+ fill: "none",
1535
+ "aria-hidden": "true",
1536
+ children: [
1537
+ /* @__PURE__ */ jsx(
1538
+ "rect",
1539
+ {
1540
+ x: "5",
1541
+ y: "3",
1542
+ width: "14",
1543
+ height: "18",
1544
+ rx: "2",
1545
+ stroke: "currentColor",
1546
+ strokeWidth: "1.5"
1547
+ }
1548
+ ),
1549
+ /* @__PURE__ */ jsx("path", { d: "M8 8h8M8 12h8M8 16h4", stroke: "currentColor", strokeWidth: "1.5" })
1550
+ ]
1551
+ }
1552
+ );
1553
+ }
1554
+
1555
+ // admin/src/index.ts
1556
+ var index_default = {
1557
+ register(app) {
1558
+ app.addMenuLink({
1559
+ to: "plugins/builder",
1560
+ icon: BuilderIcon,
1561
+ intlLabel: { id: "builder.forms", defaultMessage: "Builder Forms" },
1562
+ Component: () => Promise.resolve().then(() => (init_app(), app_exports)),
1563
+ permissions: [{ action: "plugin::builder.forms.read", subject: null }]
1564
+ });
1565
+ app.registerPlugin({ id: "builder", name: "Builder" });
1566
+ }
1567
+ };
1568
+ export {
1569
+ index_default as default
1570
+ };