@dani-builder/strapi-plugin-builder 1.0.0 → 1.2.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 (35) hide show
  1. package/dist/admin/api.d.ts +14 -2
  2. package/dist/admin/api.d.ts.map +1 -1
  3. package/dist/admin/app.d.ts +1 -0
  4. package/dist/admin/app.d.ts.map +1 -1
  5. package/dist/admin/form-editor-page.d.ts.map +1 -1
  6. package/dist/admin/form.d.ts +24 -2
  7. package/dist/admin/form.d.ts.map +1 -1
  8. package/dist/admin/index.d.ts.map +1 -1
  9. package/dist/admin/index.mjs +1084 -159
  10. package/dist/admin/notification-editor.d.ts +16 -0
  11. package/dist/admin/notification-editor.d.ts.map +1 -0
  12. package/dist/admin/notifications.d.ts +8 -0
  13. package/dist/admin/notifications.d.ts.map +1 -0
  14. package/dist/admin/submission-page.d.ts +4 -0
  15. package/dist/admin/submission-page.d.ts.map +1 -0
  16. package/dist/admin/submission.d.ts +27 -0
  17. package/dist/admin/submission.d.ts.map +1 -0
  18. package/dist/admin/ui.d.ts +3 -1
  19. package/dist/admin/ui.d.ts.map +1 -1
  20. package/dist/server/src/bootstrap.d.ts.map +1 -1
  21. package/dist/server/src/bootstrap.js +6 -0
  22. package/dist/server/src/bootstrap.js.map +1 -1
  23. package/dist/server/src/content-types/submission/schema.json +1 -1
  24. package/dist/server/src/controllers/submission.d.ts +3 -0
  25. package/dist/server/src/controllers/submission.d.ts.map +1 -1
  26. package/dist/server/src/controllers/submission.js +41 -0
  27. package/dist/server/src/controllers/submission.js.map +1 -1
  28. package/dist/server/src/routes/admin.d.ts.map +1 -1
  29. package/dist/server/src/routes/admin.js +15 -6
  30. package/dist/server/src/routes/admin.js.map +1 -1
  31. package/dist/server/src/services/submission.d.ts +64 -0
  32. package/dist/server/src/services/submission.d.ts.map +1 -1
  33. package/dist/server/src/services/submission.js +84 -0
  34. package/dist/server/src/services/submission.js.map +1 -1
  35. package/package.json +2 -2
@@ -14,8 +14,14 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // admin/src/form.ts
17
- function savePayload(form) {
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
+ }
18
23
  return {
24
+ ...includeNotifications ? { notifications } : {},
19
25
  slug: form.slug,
20
26
  name: form.name,
21
27
  targetUid: form.targetUid,
@@ -89,13 +95,19 @@ function formApi(client) {
89
95
  params: { targetUid }
90
96
  })).data.data;
91
97
  },
92
- async save(form, id) {
93
- const body = { data: savePayload(form) };
98
+ async save(form, id, includeNotifications = false) {
99
+ const body = { data: savePayload(form, includeNotifications) };
94
100
  return (id ? await client.put(
95
101
  `${root}/${encodeURIComponent(id)}`,
96
102
  body
97
103
  ) : await client.post(root, body)).data.data;
98
104
  },
105
+ async testSend(id, sampleFields) {
106
+ return (await client.post(
107
+ `${root}/${encodeURIComponent(id)}/test-send`,
108
+ { data: sampleFields }
109
+ )).data.data;
110
+ },
99
111
  async remove(id) {
100
112
  await client.del(`${root}/${encodeURIComponent(id)}`);
101
113
  },
@@ -124,6 +136,30 @@ function downloadForm(value, slug) {
124
136
  anchor.click();
125
137
  URL.revokeObjectURL(url);
126
138
  }
139
+ function submissionApi(client) {
140
+ const root2 = "/plugins/builder/submissions";
141
+ return {
142
+ async list(params) {
143
+ return (await client.get(root2, { params })).data;
144
+ },
145
+ async get(id) {
146
+ return (await client.get(
147
+ `${root2}/${encodeURIComponent(id)}`
148
+ )).data.data;
149
+ },
150
+ async status(id, status) {
151
+ return (await client.put(
152
+ `${root2}/${encodeURIComponent(id)}/status`,
153
+ { data: { status } }
154
+ )).data.data;
155
+ },
156
+ async redeliver(id) {
157
+ return (await client.post(
158
+ `${root2}/${encodeURIComponent(id)}/redeliver`
159
+ )).data.data;
160
+ }
161
+ };
162
+ }
127
163
  var root;
128
164
  var init_api = __esm({
129
165
  "admin/src/api.ts"() {
@@ -133,7 +169,22 @@ var init_api = __esm({
133
169
  }
134
170
  });
135
171
 
172
+ // admin/src/submission.ts
173
+ var submissionStatuses;
174
+ var init_submission = __esm({
175
+ "admin/src/submission.ts"() {
176
+ "use strict";
177
+ submissionStatuses = [
178
+ "received",
179
+ "reviewing",
180
+ "responded",
181
+ "closed"
182
+ ];
183
+ }
184
+ });
185
+
136
186
  // admin/src/ui.tsx
187
+ import { useId } from "react";
137
188
  import { Box, Flex, Typography } from "@strapi/design-system";
138
189
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
139
190
  function Section({
@@ -151,9 +202,13 @@ function TextField({
151
202
  onChange,
152
203
  disabled,
153
204
  required,
154
- multiline
205
+ multiline,
206
+ secret,
207
+ error
155
208
  }) {
209
+ const errorId = useId();
156
210
  const props = {
211
+ "aria-describedby": error ? errorId : void 0,
157
212
  value,
158
213
  disabled,
159
214
  required,
@@ -170,7 +225,16 @@ function TextField({
170
225
  };
171
226
  return /* @__PURE__ */ jsxs2("label", { style: { display: "grid", gap: 6 }, children: [
172
227
  /* @__PURE__ */ jsx2(Typography, { children: label }),
173
- multiline ? /* @__PURE__ */ jsx2("textarea", { ...props, rows: 3 }) : /* @__PURE__ */ jsx2("input", { ...props })
228
+ multiline ? /* @__PURE__ */ jsx2("textarea", { ...props, rows: 3, "aria-invalid": Boolean(error) }) : /* @__PURE__ */ jsx2(
229
+ "input",
230
+ {
231
+ ...props,
232
+ type: secret ? "password" : "text",
233
+ autoComplete: secret ? "new-password" : void 0,
234
+ "aria-invalid": Boolean(error)
235
+ }
236
+ ),
237
+ error && /* @__PURE__ */ jsx2(Typography, { id: errorId, textColor: "danger700", role: "alert", children: error })
174
238
  ] });
175
239
  }
176
240
  function ErrorNotice({ message }) {
@@ -182,19 +246,309 @@ var init_ui = __esm({
182
246
  }
183
247
  });
184
248
 
249
+ // admin/src/submission-page.tsx
250
+ import { useCallback, useEffect, useRef, useState } from "react";
251
+ import {
252
+ Button,
253
+ Flex as Flex2,
254
+ Table,
255
+ Tbody,
256
+ Td,
257
+ Th,
258
+ Thead,
259
+ Tr,
260
+ Typography as Typography2
261
+ } from "@strapi/design-system";
262
+ import { Layouts, Page, useFetchClient, useRBAC } from "@strapi/strapi/admin";
263
+ import { Link } from "react-router-dom";
264
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
265
+ function TargetLink({ row }) {
266
+ const { allowedActions, isLoading } = useRBAC([
267
+ {
268
+ action: "plugin::content-manager.explorer.read",
269
+ subject: row.targetUid
270
+ }
271
+ ]);
272
+ if (isLoading || !allowedActions.canRead || !row.targetDocumentId)
273
+ return null;
274
+ return /* @__PURE__ */ jsx3(
275
+ Link,
276
+ {
277
+ to: `/content-manager/collection-types/${encodeURIComponent(row.targetUid)}/${encodeURIComponent(row.targetDocumentId)}`,
278
+ children: "Open submitted record"
279
+ }
280
+ );
281
+ }
282
+ function SubmissionPage({ canUpdate }) {
283
+ const client = useFetchClient();
284
+ const { get } = client;
285
+ const [slug, setSlug] = useState("");
286
+ const [formFilter, setFormFilter] = useState("");
287
+ const [status, setStatus] = useState("");
288
+ const [page, setPage] = useState(1);
289
+ const [result, setResult] = useState(null);
290
+ const [selected, setSelected] = useState(null);
291
+ const [loading, setLoading] = useState(true);
292
+ const [busy, setBusy] = useState(false);
293
+ const [error, setError] = useState("");
294
+ const lock = useRef(false);
295
+ const generation = useRef(0);
296
+ const load = useCallback(async () => {
297
+ const current = ++generation.current;
298
+ setLoading(true);
299
+ setError("");
300
+ try {
301
+ const response = (await get("/plugins/builder/submissions", {
302
+ params: {
303
+ page,
304
+ ...formFilter ? { slug: formFilter } : {},
305
+ ...status ? { status } : {}
306
+ }
307
+ })).data;
308
+ if (current === generation.current) setResult(response);
309
+ } catch (cause) {
310
+ if (current === generation.current) setError(errorMessage(cause));
311
+ } finally {
312
+ if (current === generation.current) setLoading(false);
313
+ }
314
+ }, [get, page, formFilter, status]);
315
+ useEffect(() => {
316
+ setSelected(null);
317
+ void load();
318
+ return () => {
319
+ generation.current++;
320
+ };
321
+ }, [load]);
322
+ async function mutate(action) {
323
+ if (lock.current) return;
324
+ lock.current = true;
325
+ setBusy(true);
326
+ setError("");
327
+ try {
328
+ await action();
329
+ } catch (cause) {
330
+ setError(errorMessage(cause));
331
+ } finally {
332
+ lock.current = false;
333
+ setBusy(false);
334
+ }
335
+ }
336
+ const api = submissionApi(client);
337
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
338
+ /* @__PURE__ */ jsx3(
339
+ Layouts.Header,
340
+ {
341
+ title: "Builder Submissions",
342
+ subtitle: "Review receipt metadata, delivery history and processing status."
343
+ }
344
+ ),
345
+ /* @__PURE__ */ jsx3(Layouts.Content, { children: /* @__PURE__ */ jsxs3(Flex2, { direction: "column", alignItems: "stretch", gap: 6, children: [
346
+ /* @__PURE__ */ jsx3(ErrorNotice, { message: error }),
347
+ error && /* @__PURE__ */ jsx3(Button, { disabled: loading || busy, onClick: () => void load(), children: "Retry list" }),
348
+ /* @__PURE__ */ jsx3(Section, { title: "Filters", children: /* @__PURE__ */ jsxs3(Flex2, { gap: 4, wrap: "wrap", alignItems: "end", children: [
349
+ /* @__PURE__ */ jsx3(
350
+ TextField,
351
+ {
352
+ label: "Form slug",
353
+ value: slug,
354
+ onChange: setSlug,
355
+ disabled: busy
356
+ }
357
+ ),
358
+ /* @__PURE__ */ jsx3(
359
+ Button,
360
+ {
361
+ disabled: busy,
362
+ onClick: () => {
363
+ setPage(1);
364
+ setFormFilter(slug.trim());
365
+ },
366
+ children: "Apply form filter"
367
+ }
368
+ ),
369
+ /* @__PURE__ */ jsxs3("label", { children: [
370
+ "Status",
371
+ " ",
372
+ /* @__PURE__ */ jsxs3(
373
+ "select",
374
+ {
375
+ "aria-label": "Filter status",
376
+ value: status,
377
+ disabled: busy,
378
+ onChange: (event) => {
379
+ setPage(1);
380
+ setStatus(event.target.value);
381
+ },
382
+ children: [
383
+ /* @__PURE__ */ jsx3("option", { value: "", children: "All statuses" }),
384
+ submissionStatuses.map((value) => /* @__PURE__ */ jsx3("option", { children: value }, value))
385
+ ]
386
+ }
387
+ )
388
+ ] })
389
+ ] }) }),
390
+ /* @__PURE__ */ jsxs3(Section, { title: "Submissions", children: [
391
+ loading ? /* @__PURE__ */ jsx3(Page.Loading, {}) : !result?.data.length ? /* @__PURE__ */ jsx3(Typography2, { children: "No submissions found." }) : /* @__PURE__ */ jsxs3(Table, { colCount: 5, rowCount: result.data.length, children: [
392
+ /* @__PURE__ */ jsx3(Thead, { children: /* @__PURE__ */ jsx3(Tr, { children: [
393
+ "Form",
394
+ "Received",
395
+ "Status",
396
+ "Deliveries",
397
+ "Actions"
398
+ ].map((label) => /* @__PURE__ */ jsx3(Th, { children: /* @__PURE__ */ jsx3(Typography2, { variant: "sigma", children: label }) }, label)) }) }),
399
+ /* @__PURE__ */ jsx3(Tbody, { children: result.data.map((row) => /* @__PURE__ */ jsxs3(Tr, { children: [
400
+ /* @__PURE__ */ jsx3(Td, { children: /* @__PURE__ */ jsx3(Typography2, { children: row.form }) }),
401
+ /* @__PURE__ */ jsx3(Td, { children: /* @__PURE__ */ jsx3(Typography2, { children: row.createdAt }) }),
402
+ /* @__PURE__ */ jsx3(Td, { children: /* @__PURE__ */ jsx3(Typography2, { children: row.status }) }),
403
+ /* @__PURE__ */ jsx3(Td, { children: /* @__PURE__ */ jsx3(Flex2, { direction: "column", alignItems: "start", gap: 2, children: row.deliveries.length ? row.deliveries.map((delivery, i) => /* @__PURE__ */ jsxs3(Typography2, { children: [
404
+ delivery.channel,
405
+ ": ",
406
+ delivery.status,
407
+ " \xB7",
408
+ " ",
409
+ delivery.attempts,
410
+ " attempts \xB7",
411
+ " ",
412
+ delivery.sentAt ?? "Not sent"
413
+ ] }, i)) : /* @__PURE__ */ jsx3(Typography2, { children: "No deliveries recorded" }) }) }),
414
+ /* @__PURE__ */ jsx3(Td, { children: /* @__PURE__ */ jsxs3(
415
+ Button,
416
+ {
417
+ disabled: busy,
418
+ onClick: () => void mutate(async () => {
419
+ setSelected(await api.get(row.documentId));
420
+ }),
421
+ children: [
422
+ "View ",
423
+ row.documentId
424
+ ]
425
+ }
426
+ ) })
427
+ ] }, row.documentId)) })
428
+ ] }),
429
+ /* @__PURE__ */ jsxs3(Flex2, { gap: 3, children: [
430
+ /* @__PURE__ */ jsx3(
431
+ Button,
432
+ {
433
+ disabled: busy || loading || page <= 1,
434
+ onClick: () => setPage(page - 1),
435
+ children: "Previous"
436
+ }
437
+ ),
438
+ /* @__PURE__ */ jsxs3(Typography2, { children: [
439
+ "Page ",
440
+ page,
441
+ " \xB7 ",
442
+ result?.meta.pagination.total ?? 0,
443
+ " submissions"
444
+ ] }),
445
+ /* @__PURE__ */ jsx3(
446
+ Button,
447
+ {
448
+ disabled: busy || loading || page >= (result?.meta.pagination.pageCount ?? 0),
449
+ onClick: () => setPage(page + 1),
450
+ children: "Next"
451
+ }
452
+ )
453
+ ] })
454
+ ] }),
455
+ selected && /* @__PURE__ */ jsxs3(Section, { title: `Submission ${selected.documentId}`, children: [
456
+ /* @__PURE__ */ jsxs3(Typography2, { children: [
457
+ selected.form,
458
+ " \xB7 Received ",
459
+ selected.createdAt,
460
+ " \xB7 Updated",
461
+ " ",
462
+ selected.updatedAt
463
+ ] }),
464
+ /* @__PURE__ */ jsxs3(Typography2, { children: [
465
+ "Consent ",
466
+ selected.consentVersion,
467
+ " \xB7 ",
468
+ selected.consentAt
469
+ ] }),
470
+ /* @__PURE__ */ jsx3(TargetLink, { row: selected }),
471
+ /* @__PURE__ */ jsxs3("label", { children: [
472
+ "Processing status",
473
+ " ",
474
+ /* @__PURE__ */ jsx3(
475
+ "select",
476
+ {
477
+ "aria-label": "Processing status",
478
+ value: selected.status,
479
+ disabled: !canUpdate || busy,
480
+ onChange: (event) => {
481
+ const value = submissionStatuses.find(
482
+ (value2) => value2 === event.target.value
483
+ );
484
+ if (value)
485
+ void mutate(async () => {
486
+ const updated = await api.status(
487
+ selected.documentId,
488
+ value
489
+ );
490
+ await load();
491
+ setSelected(updated);
492
+ });
493
+ },
494
+ children: submissionStatuses.map((value) => /* @__PURE__ */ jsx3("option", { children: value }, value))
495
+ }
496
+ )
497
+ ] }),
498
+ selected.deliveries.map((delivery, i) => /* @__PURE__ */ jsxs3(Typography2, { children: [
499
+ delivery.channel,
500
+ ": ",
501
+ delivery.status,
502
+ " \xB7 ",
503
+ delivery.attempts,
504
+ " ",
505
+ "attempts \xB7 Sent: ",
506
+ delivery.sentAt ?? "Not sent",
507
+ delivery.lastError ? ` \xB7 ${delivery.lastError}` : ""
508
+ ] }, i)),
509
+ /* @__PURE__ */ jsx3(
510
+ Button,
511
+ {
512
+ disabled: !canUpdate || busy || !selected.deliveries.some(
513
+ (delivery) => delivery.channel !== "afterSave" && delivery.status === "failed"
514
+ ),
515
+ onClick: () => void mutate(async () => {
516
+ await api.redeliver(selected.documentId);
517
+ const updated = await api.get(selected.documentId);
518
+ await load();
519
+ setSelected(updated);
520
+ }),
521
+ children: "Retry notifications"
522
+ }
523
+ ),
524
+ !canUpdate && /* @__PURE__ */ jsx3(Typography2, { children: "Read only. Submission update permission is required to change status or retry notifications." })
525
+ ] })
526
+ ] }) })
527
+ ] });
528
+ }
529
+ var init_submission_page = __esm({
530
+ "admin/src/submission-page.tsx"() {
531
+ "use strict";
532
+ init_api();
533
+ init_form();
534
+ init_submission();
535
+ init_ui();
536
+ }
537
+ });
538
+
185
539
  // admin/src/import-form.tsx
186
- import { useRef, useState } from "react";
187
- import { Button, Flex as Flex2, Typography as Typography2 } from "@strapi/design-system";
188
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
540
+ import { useRef as useRef2, useState as useState2 } from "react";
541
+ import { Button as Button2, Flex as Flex3, Typography as Typography3 } from "@strapi/design-system";
542
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
189
543
  function ImportForm({
190
544
  replace,
191
545
  disabled,
192
546
  onImport
193
547
  }) {
194
- const [source, setSource] = useState("");
195
- const [error, setError] = useState("");
196
- const [busy, setBusy] = useState(false);
197
- const lock = useRef(false);
548
+ const [source, setSource] = useState2("");
549
+ const [error, setError] = useState2("");
550
+ const [busy, setBusy] = useState2(false);
551
+ const lock = useRef2(false);
198
552
  async function submit() {
199
553
  if (lock.current) return;
200
554
  lock.current = true;
@@ -209,16 +563,16 @@ function ImportForm({
209
563
  setBusy(false);
210
564
  }
211
565
  }
212
- return /* @__PURE__ */ jsxs3(Flex2, { direction: "column", alignItems: "stretch", gap: 3, children: [
213
- /* @__PURE__ */ jsxs3(Typography2, { children: [
566
+ return /* @__PURE__ */ jsxs4(Flex3, { direction: "column", alignItems: "stretch", gap: 3, children: [
567
+ /* @__PURE__ */ jsxs4(Typography3, { children: [
214
568
  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.",
215
569
  " ",
216
570
  "Consent version is preserved. Existing slugs cannot be duplicated."
217
571
  ] }),
218
- /* @__PURE__ */ jsxs3("label", { children: [
572
+ /* @__PURE__ */ jsxs4("label", { children: [
219
573
  "Choose JSON file",
220
574
  " ",
221
- /* @__PURE__ */ jsx3(
575
+ /* @__PURE__ */ jsx4(
222
576
  "input",
223
577
  {
224
578
  type: "file",
@@ -233,7 +587,7 @@ function ImportForm({
233
587
  }
234
588
  )
235
589
  ] }),
236
- /* @__PURE__ */ jsx3(
590
+ /* @__PURE__ */ jsx4(
237
591
  TextField,
238
592
  {
239
593
  label: "Form JSON",
@@ -243,9 +597,9 @@ function ImportForm({
243
597
  disabled: busy || disabled
244
598
  }
245
599
  ),
246
- /* @__PURE__ */ jsx3(ErrorNotice, { message: error }),
247
- /* @__PURE__ */ jsx3(
248
- Button,
600
+ /* @__PURE__ */ jsx4(ErrorNotice, { message: error }),
601
+ /* @__PURE__ */ jsx4(
602
+ Button2,
249
603
  {
250
604
  type: "button",
251
605
  disabled: busy || disabled || !source.trim(),
@@ -264,32 +618,32 @@ var init_import_form = __esm({
264
618
  });
265
619
 
266
620
  // admin/src/form-list-page.tsx
267
- import { useCallback, useEffect, useRef as useRef2, useState as useState2 } from "react";
621
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
268
622
  import {
269
- Button as Button2,
270
- Flex as Flex3,
271
- Table,
272
- Tbody,
273
- Td,
274
- Th,
275
- Thead,
276
- Tr,
277
- Typography as Typography3
623
+ Button as Button3,
624
+ Flex as Flex4,
625
+ Table as Table2,
626
+ Tbody as Tbody2,
627
+ Td as Td2,
628
+ Th as Th2,
629
+ Thead as Thead2,
630
+ Tr as Tr2,
631
+ Typography as Typography4
278
632
  } from "@strapi/design-system";
279
- import { Layouts, Page, useFetchClient } from "@strapi/strapi/admin";
633
+ import { Layouts as Layouts2, Page as Page2, useFetchClient as useFetchClient2 } from "@strapi/strapi/admin";
280
634
  import { useNavigate } from "react-router-dom";
281
- import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
635
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
282
636
  function FormListPage({ canUpdate }) {
283
- const client = useFetchClient();
637
+ const client = useFetchClient2();
284
638
  const { get } = client;
285
639
  const navigate = useNavigate();
286
- const [rows, setRows] = useState2([]);
287
- const [loading, setLoading] = useState2(true);
288
- const [error, setError] = useState2("");
289
- const [busy, setBusy] = useState2(false);
290
- const [removing, setRemoving] = useState2(null);
291
- const lock = useRef2(false);
292
- const load = useCallback(async () => {
640
+ const [rows, setRows] = useState3([]);
641
+ const [loading, setLoading] = useState3(true);
642
+ const [error, setError] = useState3("");
643
+ const [busy, setBusy] = useState3(false);
644
+ const [removing, setRemoving] = useState3(null);
645
+ const lock = useRef3(false);
646
+ const load = useCallback2(async () => {
293
647
  setLoading(true);
294
648
  setError("");
295
649
  try {
@@ -302,7 +656,7 @@ function FormListPage({ canUpdate }) {
302
656
  setLoading(false);
303
657
  }
304
658
  }, [get]);
305
- useEffect(() => {
659
+ useEffect2(() => {
306
660
  void load();
307
661
  }, [load]);
308
662
  async function mutate(action) {
@@ -319,14 +673,14 @@ function FormListPage({ canUpdate }) {
319
673
  setBusy(false);
320
674
  }
321
675
  }
322
- return /* @__PURE__ */ jsxs4(Fragment, { children: [
323
- /* @__PURE__ */ jsx4(
324
- Layouts.Header,
676
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
677
+ /* @__PURE__ */ jsx5(
678
+ Layouts2.Header,
325
679
  {
326
680
  title: "Builder Forms",
327
681
  subtitle: "Manage public form definitions and consent.",
328
- primaryAction: canUpdate ? /* @__PURE__ */ jsx4(
329
- Button2,
682
+ primaryAction: canUpdate ? /* @__PURE__ */ jsx5(
683
+ Button3,
330
684
  {
331
685
  disabled: busy,
332
686
  onClick: () => navigate("/plugins/builder/forms/new"),
@@ -335,21 +689,21 @@ function FormListPage({ canUpdate }) {
335
689
  ) : void 0
336
690
  }
337
691
  ),
338
- /* @__PURE__ */ jsx4(Layouts.Content, { children: /* @__PURE__ */ jsxs4(Flex3, { direction: "column", alignItems: "stretch", gap: 6, children: [
339
- /* @__PURE__ */ jsx4(ErrorNotice, { message: error }),
340
- error && /* @__PURE__ */ jsx4(Button2, { onClick: () => void load(), disabled: busy || loading, children: "Retry list" }),
341
- /* @__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: [
342
- /* @__PURE__ */ jsx4(Thead, { children: /* @__PURE__ */ jsx4(Tr, { children: ["Name", "Slug", "Target", "Version", "Actions"].map(
343
- (name) => /* @__PURE__ */ jsx4(Th, { children: /* @__PURE__ */ jsx4(Typography3, { variant: "sigma", children: name }) }, name)
692
+ /* @__PURE__ */ jsx5(Layouts2.Content, { children: /* @__PURE__ */ jsxs5(Flex4, { direction: "column", alignItems: "stretch", gap: 6, children: [
693
+ /* @__PURE__ */ jsx5(ErrorNotice, { message: error }),
694
+ error && /* @__PURE__ */ jsx5(Button3, { onClick: () => void load(), disabled: busy || loading, children: "Retry list" }),
695
+ /* @__PURE__ */ jsx5(Section, { title: "Forms", children: loading ? /* @__PURE__ */ jsx5(Page2.Loading, {}) : rows.length === 0 ? /* @__PURE__ */ jsx5(Typography4, { children: "No forms yet." }) : /* @__PURE__ */ jsxs5(Table2, { colCount: 5, rowCount: rows.length, children: [
696
+ /* @__PURE__ */ jsx5(Thead2, { children: /* @__PURE__ */ jsx5(Tr2, { children: ["Name", "Slug", "Target", "Version", "Actions"].map(
697
+ (name) => /* @__PURE__ */ jsx5(Th2, { children: /* @__PURE__ */ jsx5(Typography4, { variant: "sigma", children: name }) }, name)
344
698
  ) }) }),
345
- /* @__PURE__ */ jsx4(Tbody, { children: rows.map((row) => /* @__PURE__ */ jsxs4(Tr, { children: [
346
- /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsx4(Typography3, { children: row.name }) }),
347
- /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsx4(Typography3, { children: row.slug }) }),
348
- /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsx4(Typography3, { children: row.targetUid }) }),
349
- /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsx4(Typography3, { children: row.version }) }),
350
- /* @__PURE__ */ jsx4(Td, { children: /* @__PURE__ */ jsxs4(Flex3, { gap: 2, children: [
351
- /* @__PURE__ */ jsxs4(
352
- Button2,
699
+ /* @__PURE__ */ jsx5(Tbody2, { children: rows.map((row) => /* @__PURE__ */ jsxs5(Tr2, { children: [
700
+ /* @__PURE__ */ jsx5(Td2, { children: /* @__PURE__ */ jsx5(Typography4, { children: row.name }) }),
701
+ /* @__PURE__ */ jsx5(Td2, { children: /* @__PURE__ */ jsx5(Typography4, { children: row.slug }) }),
702
+ /* @__PURE__ */ jsx5(Td2, { children: /* @__PURE__ */ jsx5(Typography4, { children: row.targetUid }) }),
703
+ /* @__PURE__ */ jsx5(Td2, { children: /* @__PURE__ */ jsx5(Typography4, { children: row.version }) }),
704
+ /* @__PURE__ */ jsx5(Td2, { children: /* @__PURE__ */ jsxs5(Flex4, { gap: 2, children: [
705
+ /* @__PURE__ */ jsxs5(
706
+ Button3,
353
707
  {
354
708
  variant: "secondary",
355
709
  disabled: busy,
@@ -363,8 +717,8 @@ function FormListPage({ canUpdate }) {
363
717
  ]
364
718
  }
365
719
  ),
366
- /* @__PURE__ */ jsx4(
367
- Button2,
720
+ /* @__PURE__ */ jsx5(
721
+ Button3,
368
722
  {
369
723
  variant: "tertiary",
370
724
  disabled: busy,
@@ -377,8 +731,8 @@ function FormListPage({ canUpdate }) {
377
731
  children: "Export"
378
732
  }
379
733
  ),
380
- canUpdate && /* @__PURE__ */ jsx4(
381
- Button2,
734
+ canUpdate && /* @__PURE__ */ jsx5(
735
+ Button3,
382
736
  {
383
737
  variant: "danger-light",
384
738
  disabled: busy,
@@ -389,11 +743,11 @@ function FormListPage({ canUpdate }) {
389
743
  ] }) })
390
744
  ] }, row.documentId)) })
391
745
  ] }) }),
392
- removing && /* @__PURE__ */ jsxs4(Section, { title: `Delete ${removing.name}?`, children: [
393
- /* @__PURE__ */ jsx4(Typography3, { children: "This permanently deletes the form definition. Forms with submissions cannot be deleted." }),
394
- /* @__PURE__ */ jsxs4(Flex3, { gap: 3, children: [
395
- /* @__PURE__ */ jsx4(
396
- Button2,
746
+ removing && /* @__PURE__ */ jsxs5(Section, { title: `Delete ${removing.name}?`, children: [
747
+ /* @__PURE__ */ jsx5(Typography4, { children: "This permanently deletes the form definition. Forms with submissions cannot be deleted." }),
748
+ /* @__PURE__ */ jsxs5(Flex4, { gap: 3, children: [
749
+ /* @__PURE__ */ jsx5(
750
+ Button3,
397
751
  {
398
752
  variant: "danger",
399
753
  disabled: busy,
@@ -405,8 +759,8 @@ function FormListPage({ canUpdate }) {
405
759
  children: "Confirm delete"
406
760
  }
407
761
  ),
408
- /* @__PURE__ */ jsx4(
409
- Button2,
762
+ /* @__PURE__ */ jsx5(
763
+ Button3,
410
764
  {
411
765
  variant: "secondary",
412
766
  disabled: busy,
@@ -416,7 +770,7 @@ function FormListPage({ canUpdate }) {
416
770
  )
417
771
  ] })
418
772
  ] }),
419
- canUpdate && /* @__PURE__ */ jsx4(Section, { title: "Import form", children: /* @__PURE__ */ jsx4(
773
+ canUpdate && /* @__PURE__ */ jsx5(Section, { title: "Import form", children: /* @__PURE__ */ jsx5(
420
774
  ImportForm,
421
775
  {
422
776
  disabled: busy,
@@ -448,8 +802,8 @@ var init_form_list_page = __esm({
448
802
  });
449
803
 
450
804
  // admin/src/field-editor.tsx
451
- import { Button as Button3, Flex as Flex4, Typography as Typography4 } from "@strapi/design-system";
452
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
805
+ import { Button as Button4, Flex as Flex5, Typography as Typography5 } from "@strapi/design-system";
806
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
453
807
  function FieldEditor({
454
808
  fields,
455
809
  schema,
@@ -459,10 +813,10 @@ function FieldEditor({
459
813
  function update(index, field) {
460
814
  onChange(fields.map((item, at) => at === index ? field : item));
461
815
  }
462
- return /* @__PURE__ */ jsxs5(Section, { title: "Fields", children: [
463
- /* @__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." }),
464
- /* @__PURE__ */ jsx5(Flex4, { wrap: "wrap", gap: 4, children: schema.map((item) => /* @__PURE__ */ jsxs5("label", { children: [
465
- /* @__PURE__ */ jsx5(
816
+ return /* @__PURE__ */ jsxs6(Section, { title: "Fields", children: [
817
+ /* @__PURE__ */ jsx6(Typography5, { 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." }),
818
+ /* @__PURE__ */ jsx6(Flex5, { wrap: "wrap", gap: 4, children: schema.map((item) => /* @__PURE__ */ jsxs6("label", { children: [
819
+ /* @__PURE__ */ jsx6(
466
820
  "input",
467
821
  {
468
822
  type: "checkbox",
@@ -479,14 +833,14 @@ function FieldEditor({
479
833
  ] }, item.name)) }),
480
834
  fields.map((field, index) => {
481
835
  const attribute = schema.find((item) => item.name === field.name);
482
- return /* @__PURE__ */ jsxs5(
836
+ return /* @__PURE__ */ jsxs6(
483
837
  Section,
484
838
  {
485
839
  title: `${index + 1}. ${field.name} (${field.type})`,
486
840
  children: [
487
- /* @__PURE__ */ jsxs5(Flex4, { gap: 3, children: [
488
- /* @__PURE__ */ jsxs5(
489
- Button3,
841
+ /* @__PURE__ */ jsxs6(Flex5, { gap: 3, children: [
842
+ /* @__PURE__ */ jsxs6(
843
+ Button4,
490
844
  {
491
845
  type: "button",
492
846
  variant: "secondary",
@@ -499,8 +853,8 @@ function FieldEditor({
499
853
  ]
500
854
  }
501
855
  ),
502
- /* @__PURE__ */ jsxs5(
503
- Button3,
856
+ /* @__PURE__ */ jsxs6(
857
+ Button4,
504
858
  {
505
859
  type: "button",
506
860
  variant: "secondary",
@@ -514,7 +868,7 @@ function FieldEditor({
514
868
  }
515
869
  )
516
870
  ] }),
517
- /* @__PURE__ */ jsx5(
871
+ /* @__PURE__ */ jsx6(
518
872
  TextField,
519
873
  {
520
874
  label: "Label",
@@ -524,7 +878,7 @@ function FieldEditor({
524
878
  onChange: (label) => update(index, { ...field, label })
525
879
  }
526
880
  ),
527
- /* @__PURE__ */ jsx5(
881
+ /* @__PURE__ */ jsx6(
528
882
  TextField,
529
883
  {
530
884
  label: "Placeholder",
@@ -533,7 +887,7 @@ function FieldEditor({
533
887
  onChange: (placeholder) => update(index, { ...field, placeholder })
534
888
  }
535
889
  ),
536
- /* @__PURE__ */ jsx5(
890
+ /* @__PURE__ */ jsx6(
537
891
  TextField,
538
892
  {
539
893
  label: "Help text",
@@ -542,8 +896,8 @@ function FieldEditor({
542
896
  onChange: (help) => update(index, { ...field, help })
543
897
  }
544
898
  ),
545
- /* @__PURE__ */ jsxs5("label", { children: [
546
- /* @__PURE__ */ jsx5(
899
+ /* @__PURE__ */ jsxs6("label", { children: [
900
+ /* @__PURE__ */ jsx6(
547
901
  "input",
548
902
  {
549
903
  type: "checkbox",
@@ -562,8 +916,8 @@ function FieldEditor({
562
916
  "Required",
563
917
  attribute?.required ? " (required by target schema)" : ""
564
918
  ] }),
565
- /* @__PURE__ */ jsxs5("label", { children: [
566
- /* @__PURE__ */ jsx5(
919
+ /* @__PURE__ */ jsxs6("label", { children: [
920
+ /* @__PURE__ */ jsx6(
567
921
  "input",
568
922
  {
569
923
  type: "checkbox",
@@ -578,7 +932,7 @@ function FieldEditor({
578
932
  " ",
579
933
  "Hidden from form"
580
934
  ] }),
581
- (field.maxLength !== void 0 || field.min !== void 0 || field.max !== void 0) && /* @__PURE__ */ jsxs5(Typography4, { variant: "pi", children: [
935
+ (field.maxLength !== void 0 || field.min !== void 0 || field.max !== void 0) && /* @__PURE__ */ jsxs6(Typography5, { variant: "pi", children: [
582
936
  "Constraints:",
583
937
  " ",
584
938
  JSON.stringify({
@@ -587,7 +941,7 @@ function FieldEditor({
587
941
  max: field.max
588
942
  })
589
943
  ] }),
590
- field.options?.map((option, optionIndex) => /* @__PURE__ */ jsx5(
944
+ field.options?.map((option, optionIndex) => /* @__PURE__ */ jsx6(
591
945
  TextField,
592
946
  {
593
947
  label: `Option label: ${option.value}`,
@@ -618,32 +972,537 @@ var init_field_editor = __esm({
618
972
  }
619
973
  });
620
974
 
975
+ // admin/src/notifications.ts
976
+ function placeholders(fields) {
977
+ return [
978
+ ...fields.map((field) => `field.${field.name}`),
979
+ "form.name",
980
+ "submission.id",
981
+ "submission.receivedAt"
982
+ ];
983
+ }
984
+ function templateError(value, fields) {
985
+ const allowed = placeholders(fields);
986
+ let invalid = false;
987
+ const rest = value.replace(/{{\s*([^{}]+?)\s*}}/g, (_, key) => {
988
+ if (!allowed.includes(key)) invalid = true;
989
+ return "";
990
+ });
991
+ return invalid || rest.includes("{{") || rest.includes("}}") ? "Use only the available placeholders." : "";
992
+ }
993
+ function notificationErrors(config, fields) {
994
+ const errors = {};
995
+ if (config.email) {
996
+ if (config.email.replyToField && !fields.some(
997
+ (field) => field.type === "email" && field.name === config.email?.replyToField
998
+ ))
999
+ errors.replyToField = "Choose an email field in this form, or None.";
1000
+ for (const key of ["subject", "body"]) {
1001
+ const error = templateError(config.email[key], fields);
1002
+ if (error) errors[key] = error;
1003
+ }
1004
+ for (const key of ["to", "cc"]) {
1005
+ if (config.email[key]?.some((value) => /[{}]/.test(value)))
1006
+ errors[key] = "Recipients must be fixed email addresses; placeholders are not allowed.";
1007
+ }
1008
+ }
1009
+ if (config.webhook) {
1010
+ try {
1011
+ const url = new URL(config.webhook.url);
1012
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
1013
+ throw Error("url");
1014
+ } catch {
1015
+ errors.url = "Enter an HTTP or HTTPS URL without credentials.";
1016
+ }
1017
+ }
1018
+ return errors;
1019
+ }
1020
+ function samplePayload(fields, values) {
1021
+ return Object.fromEntries(
1022
+ fields.flatMap((field) => {
1023
+ const value = values[field.name] ?? "";
1024
+ if (!value && !field.required) return [];
1025
+ return [
1026
+ [
1027
+ field.name,
1028
+ value && ["integer", "decimal"].includes(field.type) ? Number(value) : value && field.type === "boolean" ? value === "true" : value
1029
+ ]
1030
+ ];
1031
+ })
1032
+ );
1033
+ }
1034
+ function sampleErrors(error) {
1035
+ const response = error && typeof error === "object" && "response" in error ? error.response : void 0;
1036
+ const details = response?.data?.error?.details?.errors;
1037
+ if (!Array.isArray(details)) return {};
1038
+ return Object.fromEntries(
1039
+ details.flatMap((entry) => {
1040
+ if (!entry || typeof entry !== "object" || !("field" in entry) || !("code" in entry) || typeof entry.field !== "string" || typeof entry.code !== "string")
1041
+ return [];
1042
+ return [[entry.field, entry.code]];
1043
+ })
1044
+ );
1045
+ }
1046
+ var init_notifications = __esm({
1047
+ "admin/src/notifications.ts"() {
1048
+ "use strict";
1049
+ }
1050
+ });
1051
+
1052
+ // admin/src/notification-editor.tsx
1053
+ import { useState as useState4 } from "react";
1054
+ import { Button as Button5, Flex as Flex6, Typography as Typography6 } from "@strapi/design-system";
1055
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1056
+ function NotificationEditor({
1057
+ value,
1058
+ fields,
1059
+ opened,
1060
+ onOpen,
1061
+ onChange,
1062
+ disabled,
1063
+ saveError
1064
+ }) {
1065
+ const [target, setTarget] = useState4("body");
1066
+ const errors = notificationErrors(value, fields);
1067
+ function toggle(channel, enabled) {
1068
+ const next = { ...value };
1069
+ if (!enabled) delete next[channel];
1070
+ else if (channel === "email")
1071
+ next.email = {
1072
+ to: [],
1073
+ cc: [],
1074
+ subject: "New submission: {{form.name}}",
1075
+ body: "Submission {{submission.id}} received at {{submission.receivedAt}}"
1076
+ };
1077
+ else next.webhook = { url: "" };
1078
+ onChange(next);
1079
+ }
1080
+ return /* @__PURE__ */ jsxs7(Section, { title: "Notifications", children: [
1081
+ /* @__PURE__ */ jsx7(
1082
+ Button5,
1083
+ {
1084
+ type: "button",
1085
+ variant: "tertiary",
1086
+ onClick: onOpen,
1087
+ disabled: opened,
1088
+ children: opened ? "Notification settings opened" : "Open notification settings"
1089
+ }
1090
+ ),
1091
+ opened && /* @__PURE__ */ jsxs7(Fragment3, { children: [
1092
+ /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Enable a channel to send notifications. Changes take effect after saving the form." }),
1093
+ saveError.startsWith("notifications:") && /* @__PURE__ */ jsx7(ErrorNotice, { message: saveError }),
1094
+ /* @__PURE__ */ jsxs7("label", { children: [
1095
+ /* @__PURE__ */ jsx7(
1096
+ "input",
1097
+ {
1098
+ type: "checkbox",
1099
+ disabled,
1100
+ checked: Boolean(value.email),
1101
+ onChange: (event) => toggle("email", event.target.checked)
1102
+ }
1103
+ ),
1104
+ " ",
1105
+ "Email notifications"
1106
+ ] }),
1107
+ value.email && /* @__PURE__ */ jsxs7(Fragment3, { children: [
1108
+ ["to", "cc"].map((key) => /* @__PURE__ */ jsx7(
1109
+ TextField,
1110
+ {
1111
+ label: key === "to" ? "To (one address per line)" : "Cc (one address per line)",
1112
+ multiline: true,
1113
+ value: (value.email?.[key] ?? []).join("\n"),
1114
+ disabled,
1115
+ error: errors[key],
1116
+ onChange: (text) => onChange({
1117
+ ...value,
1118
+ email: { ...value.email, [key]: text.split("\n") }
1119
+ })
1120
+ },
1121
+ key
1122
+ )),
1123
+ /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Use fixed email addresses. Empty lines are ignored when saving." }),
1124
+ /* @__PURE__ */ jsxs7("label", { children: [
1125
+ "Reply-to field",
1126
+ /* @__PURE__ */ jsxs7(
1127
+ "select",
1128
+ {
1129
+ style: { width: "100%", padding: 10 },
1130
+ disabled,
1131
+ value: value.email.replyToField ?? "",
1132
+ "aria-invalid": Boolean(errors.replyToField),
1133
+ onChange: (event) => {
1134
+ const email = { ...value.email };
1135
+ delete email.replyToField;
1136
+ if (event.target.value)
1137
+ email.replyToField = event.target.value;
1138
+ onChange({ ...value, email });
1139
+ },
1140
+ children: [
1141
+ /* @__PURE__ */ jsx7("option", { value: "", children: "None" }),
1142
+ fields.filter((field) => field.type === "email").map((field) => /* @__PURE__ */ jsxs7("option", { value: field.name, children: [
1143
+ field.label,
1144
+ " (",
1145
+ field.name,
1146
+ ")"
1147
+ ] }, field.name))
1148
+ ]
1149
+ }
1150
+ )
1151
+ ] }),
1152
+ errors.replyToField && /* @__PURE__ */ jsx7(ErrorNotice, { message: errors.replyToField }),
1153
+ /* @__PURE__ */ jsx7(
1154
+ TextField,
1155
+ {
1156
+ label: "Email subject",
1157
+ value: value.email.subject,
1158
+ disabled,
1159
+ error: errors.subject,
1160
+ onChange: (subject) => onChange({ ...value, email: { ...value.email, subject } })
1161
+ }
1162
+ ),
1163
+ /* @__PURE__ */ jsx7(
1164
+ TextField,
1165
+ {
1166
+ label: "Email body",
1167
+ multiline: true,
1168
+ value: value.email.body,
1169
+ disabled,
1170
+ error: errors.body,
1171
+ onChange: (body) => onChange({ ...value, email: { ...value.email, body } })
1172
+ }
1173
+ ),
1174
+ /* @__PURE__ */ jsxs7("label", { children: [
1175
+ "Insert placeholder into",
1176
+ /* @__PURE__ */ jsxs7(
1177
+ "select",
1178
+ {
1179
+ style: { width: "100%", padding: 10 },
1180
+ disabled,
1181
+ value: target,
1182
+ onChange: (event) => setTarget(
1183
+ event.target.value === "subject" ? "subject" : "body"
1184
+ ),
1185
+ children: [
1186
+ /* @__PURE__ */ jsx7("option", { value: "subject", children: "Subject" }),
1187
+ /* @__PURE__ */ jsx7("option", { value: "body", children: "Body" })
1188
+ ]
1189
+ }
1190
+ )
1191
+ ] }),
1192
+ /* @__PURE__ */ jsx7(Flex6, { gap: 2, wrap: "wrap", children: placeholders(fields).map((key) => /* @__PURE__ */ jsx7(
1193
+ Button5,
1194
+ {
1195
+ type: "button",
1196
+ style: {
1197
+ maxWidth: "100%",
1198
+ height: "auto",
1199
+ whiteSpace: "normal",
1200
+ overflowWrap: "anywhere"
1201
+ },
1202
+ size: "S",
1203
+ variant: "tertiary",
1204
+ disabled,
1205
+ onClick: () => onChange({
1206
+ ...value,
1207
+ email: {
1208
+ ...value.email,
1209
+ [target]: `${value.email[target]}{{${key}}}`
1210
+ }
1211
+ }),
1212
+ children: `{{${key}}}`
1213
+ },
1214
+ key
1215
+ )) })
1216
+ ] }),
1217
+ /* @__PURE__ */ jsxs7("label", { children: [
1218
+ /* @__PURE__ */ jsx7(
1219
+ "input",
1220
+ {
1221
+ type: "checkbox",
1222
+ disabled,
1223
+ checked: Boolean(value.webhook),
1224
+ onChange: (event) => toggle("webhook", event.target.checked)
1225
+ }
1226
+ ),
1227
+ " ",
1228
+ "Webhook notifications"
1229
+ ] }),
1230
+ value.webhook && /* @__PURE__ */ jsxs7(Fragment3, { children: [
1231
+ /* @__PURE__ */ jsx7(
1232
+ TextField,
1233
+ {
1234
+ label: "Webhook URL",
1235
+ value: value.webhook.url,
1236
+ disabled,
1237
+ error: errors.url,
1238
+ onChange: (url) => onChange({ ...value, webhook: { ...value.webhook, url } })
1239
+ }
1240
+ ),
1241
+ /* @__PURE__ */ jsx7(
1242
+ TextField,
1243
+ {
1244
+ label: "Webhook secret",
1245
+ secret: true,
1246
+ value: value.webhook.secret ?? "",
1247
+ disabled,
1248
+ onChange: (secret) => {
1249
+ const webhook = { ...value.webhook };
1250
+ delete webhook.secret;
1251
+ if (secret) webhook.secret = secret;
1252
+ onChange({ ...value, webhook });
1253
+ }
1254
+ }
1255
+ ),
1256
+ /* @__PURE__ */ jsx7(Typography6, { 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." }),
1257
+ Object.entries(value.webhook.headers ?? {}).map(
1258
+ ([name, header]) => /* @__PURE__ */ jsxs7(
1259
+ Flex6,
1260
+ {
1261
+ direction: "column",
1262
+ alignItems: "stretch",
1263
+ gap: 2,
1264
+ children: [
1265
+ /* @__PURE__ */ jsx7(
1266
+ TextField,
1267
+ {
1268
+ label: `Header: ${name}`,
1269
+ secret: true,
1270
+ value: header,
1271
+ disabled,
1272
+ onChange: (next) => onChange({
1273
+ ...value,
1274
+ webhook: {
1275
+ ...value.webhook,
1276
+ headers: {
1277
+ ...value.webhook.headers,
1278
+ [name]: next
1279
+ }
1280
+ }
1281
+ })
1282
+ }
1283
+ ),
1284
+ /* @__PURE__ */ jsxs7(
1285
+ Button5,
1286
+ {
1287
+ type: "button",
1288
+ variant: "tertiary",
1289
+ disabled,
1290
+ onClick: () => {
1291
+ const headers = { ...value.webhook.headers };
1292
+ delete headers[name];
1293
+ onChange({
1294
+ ...value,
1295
+ webhook: { ...value.webhook, headers }
1296
+ });
1297
+ },
1298
+ children: [
1299
+ "Remove ",
1300
+ name
1301
+ ]
1302
+ }
1303
+ )
1304
+ ]
1305
+ },
1306
+ name
1307
+ )
1308
+ ),
1309
+ /* @__PURE__ */ jsx7(
1310
+ AddHeader,
1311
+ {
1312
+ disabled,
1313
+ names: Object.keys(value.webhook.headers ?? {}),
1314
+ onAdd: (name, header) => onChange({
1315
+ ...value,
1316
+ webhook: {
1317
+ ...value.webhook,
1318
+ headers: { ...value.webhook.headers, [name]: header }
1319
+ }
1320
+ })
1321
+ }
1322
+ )
1323
+ ] })
1324
+ ] })
1325
+ ] });
1326
+ }
1327
+ function AddHeader({
1328
+ disabled,
1329
+ names,
1330
+ onAdd
1331
+ }) {
1332
+ const [name, setName] = useState4("");
1333
+ const [value, setValue] = useState4("");
1334
+ return /* @__PURE__ */ jsxs7(Fragment3, { children: [
1335
+ /* @__PURE__ */ jsx7(
1336
+ TextField,
1337
+ {
1338
+ label: "New header name",
1339
+ value: name,
1340
+ disabled,
1341
+ onChange: setName
1342
+ }
1343
+ ),
1344
+ /* @__PURE__ */ jsx7(
1345
+ TextField,
1346
+ {
1347
+ label: "New header value",
1348
+ secret: true,
1349
+ value,
1350
+ disabled,
1351
+ onChange: setValue
1352
+ }
1353
+ ),
1354
+ /* @__PURE__ */ jsx7(
1355
+ Button5,
1356
+ {
1357
+ type: "button",
1358
+ variant: "tertiary",
1359
+ disabled: disabled || !name.trim() || !value || names.some((item) => item.toLowerCase() === name.trim().toLowerCase()),
1360
+ onClick: () => {
1361
+ onAdd(name.trim(), value);
1362
+ setName("");
1363
+ setValue("");
1364
+ },
1365
+ children: "Add header"
1366
+ }
1367
+ )
1368
+ ] });
1369
+ }
1370
+ function NotificationTest({
1371
+ fields,
1372
+ disabled,
1373
+ send
1374
+ }) {
1375
+ const [values, setValues] = useState4({});
1376
+ const [results, setResults] = useState4(null);
1377
+ const [error, setError] = useState4("");
1378
+ const [errors, setErrors] = useState4({});
1379
+ const [busy, setBusy] = useState4(false);
1380
+ return /* @__PURE__ */ jsxs7(Section, { title: "Test notifications", children: [
1381
+ /* @__PURE__ */ jsx7(Typography6, { 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." }),
1382
+ disabled && /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Testing requires a saved form, no unsaved changes and forms.update permission." }),
1383
+ fields.map((field) => /* @__PURE__ */ jsx7("div", { style: { display: "grid", gap: 6 }, children: field.type === "boolean" ? /* @__PURE__ */ jsxs7("label", { children: [
1384
+ /* @__PURE__ */ jsxs7(Typography6, { children: [
1385
+ field.label,
1386
+ " (",
1387
+ field.name,
1388
+ ")",
1389
+ field.visibility === "hidden" ? " \xB7 hidden" : ""
1390
+ ] }),
1391
+ /* @__PURE__ */ jsxs7(
1392
+ "select",
1393
+ {
1394
+ disabled: disabled || busy,
1395
+ value: values[field.name] ?? "",
1396
+ onChange: (event) => setValues({ ...values, [field.name]: event.target.value }),
1397
+ children: [
1398
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Choose a value" }),
1399
+ /* @__PURE__ */ jsx7("option", { value: "true", children: "True" }),
1400
+ /* @__PURE__ */ jsx7("option", { value: "false", children: "False" })
1401
+ ]
1402
+ }
1403
+ ),
1404
+ errors[field.name] && /* @__PURE__ */ jsx7(ErrorNotice, { message: errors[field.name] ?? "" })
1405
+ ] }) : /* @__PURE__ */ jsx7(
1406
+ TextField,
1407
+ {
1408
+ label: `${field.label} (${field.name})${field.visibility === "hidden" ? " \xB7 hidden" : ""}`,
1409
+ value: values[field.name] ?? "",
1410
+ disabled: disabled || busy,
1411
+ error: errors[field.name],
1412
+ onChange: (next) => setValues({ ...values, [field.name]: next })
1413
+ }
1414
+ ) }, field.name)),
1415
+ /* @__PURE__ */ jsx7(
1416
+ Button5,
1417
+ {
1418
+ type: "button",
1419
+ disabled: disabled || busy,
1420
+ onClick: async () => {
1421
+ setBusy(true);
1422
+ setError("");
1423
+ setErrors({});
1424
+ setResults(null);
1425
+ try {
1426
+ setResults(await send(samplePayload(fields, values)));
1427
+ } catch (cause) {
1428
+ setError(errorMessage(cause));
1429
+ setErrors(sampleErrors(cause));
1430
+ } finally {
1431
+ setBusy(false);
1432
+ }
1433
+ },
1434
+ children: busy ? "Sending test\u2026" : "Send test notifications"
1435
+ }
1436
+ ),
1437
+ /* @__PURE__ */ jsx7(ErrorNotice, { message: error }),
1438
+ /* @__PURE__ */ jsxs7("div", { "aria-live": "polite", children: [
1439
+ results?.length === 0 && /* @__PURE__ */ jsx7(Typography6, { children: "No notification channels are enabled in the saved settings." }),
1440
+ results?.map((delivery) => /* @__PURE__ */ jsx7(
1441
+ "div",
1442
+ {
1443
+ style: { overflowWrap: "anywhere", marginTop: 12 },
1444
+ children: /* @__PURE__ */ jsxs7(
1445
+ Typography6,
1446
+ {
1447
+ textColor: delivery.status === "sent" ? "success700" : "danger700",
1448
+ children: [
1449
+ delivery.channel,
1450
+ ":",
1451
+ " ",
1452
+ delivery.status === "sent" ? "Success" : "Failed",
1453
+ " \xB7",
1454
+ " ",
1455
+ delivery.attempts,
1456
+ " attempt(s)",
1457
+ delivery.lastError ? ` \xB7 ${delivery.lastError}` : "",
1458
+ delivery.sentAt ? ` \xB7 ${delivery.sentAt}` : ""
1459
+ ]
1460
+ }
1461
+ )
1462
+ },
1463
+ delivery.channel
1464
+ ))
1465
+ ] })
1466
+ ] });
1467
+ }
1468
+ var init_notification_editor = __esm({
1469
+ "admin/src/notification-editor.tsx"() {
1470
+ "use strict";
1471
+ init_form();
1472
+ init_notifications();
1473
+ init_ui();
1474
+ }
1475
+ });
1476
+
621
1477
  // admin/src/form-editor-page.tsx
622
- import { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
623
- import { Button as Button4, Flex as Flex5, Typography as Typography5 } from "@strapi/design-system";
624
- import { Layouts as Layouts2, Page as Page2, useFetchClient as useFetchClient2 } from "@strapi/strapi/admin";
1478
+ import { useEffect as useEffect3, useRef as useRef4, useState as useState5 } from "react";
1479
+ import { Button as Button6, Flex as Flex7, Typography as Typography7 } from "@strapi/design-system";
1480
+ import { Layouts as Layouts3, Page as Page3, useFetchClient as useFetchClient3 } from "@strapi/strapi/admin";
625
1481
  import { useNavigate as useNavigate2, useParams } from "react-router-dom";
626
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1482
+ import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
627
1483
  function FormEditorPage({ canUpdate }) {
628
1484
  const { documentId } = useParams();
629
- const client = useFetchClient2();
1485
+ const client = useFetchClient3();
630
1486
  const { get } = client;
631
1487
  const navigate = useNavigate2();
632
- const [draft, setDraft] = useState3(emptyForm);
633
- const [version, setVersion] = useState3(null);
634
- const [targets, setTargets] = useState3([]);
635
- const [schema, setSchema] = useState3([]);
636
- const [loading, setLoading] = useState3(true);
637
- const [fieldsLoading, setFieldsLoading] = useState3(false);
638
- const [error, setError] = useState3("");
639
- const [loaded, setLoaded] = useState3(false);
640
- const [busy, setBusy] = useState3(false);
641
- const [retry, setRetry] = useState3(0);
642
- const lock = useRef3(false);
643
- const targetRequest = useRef3(0);
644
- useEffect2(() => {
1488
+ const [draft, setDraft] = useState5(emptyForm);
1489
+ const [notificationsOpened, setNotificationsOpened] = useState5(false);
1490
+ const [savedDraft, setSavedDraft] = useState5("");
1491
+ const [version, setVersion] = useState5(null);
1492
+ const [targets, setTargets] = useState5([]);
1493
+ const [schema, setSchema] = useState5([]);
1494
+ const [loading, setLoading] = useState5(true);
1495
+ const [fieldsLoading, setFieldsLoading] = useState5(false);
1496
+ const [error, setError] = useState5("");
1497
+ const [loaded, setLoaded] = useState5(false);
1498
+ const [busy, setBusy] = useState5(false);
1499
+ const [retry, setRetry] = useState5(0);
1500
+ const lock = useRef4(false);
1501
+ const targetRequest = useRef4(0);
1502
+ useEffect3(() => {
645
1503
  let active = true;
646
1504
  setLoading(true);
1505
+ setNotificationsOpened(false);
647
1506
  setLoaded(false);
648
1507
  setError("");
649
1508
  async function load() {
@@ -663,6 +1522,7 @@ function FormEditorPage({ canUpdate }) {
663
1522
  consent: { ...emptyForm.consent }
664
1523
  }
665
1524
  );
1525
+ setSavedDraft(saved ? JSON.stringify(savePayload(saved, true)) : "");
666
1526
  setVersion(saved?.version ?? null);
667
1527
  setSchema(attrs);
668
1528
  setLoaded(true);
@@ -712,16 +1572,16 @@ function FormEditorPage({ canUpdate }) {
712
1572
  setBusy(false);
713
1573
  }
714
1574
  }
715
- if (loading) return /* @__PURE__ */ jsx6(Page2.Loading, {});
1575
+ if (loading) return /* @__PURE__ */ jsx8(Page3.Loading, {});
716
1576
  const disabled = !canUpdate || busy || fieldsLoading;
717
- return /* @__PURE__ */ jsxs6(Fragment2, { children: [
718
- /* @__PURE__ */ jsx6(
719
- Layouts2.Header,
1577
+ return /* @__PURE__ */ jsxs8(Fragment4, { children: [
1578
+ /* @__PURE__ */ jsx8(
1579
+ Layouts3.Header,
720
1580
  {
721
1581
  title: documentId ? draft.name || "Form" : "Create form",
722
1582
  subtitle: version ? `Form version ${version} \xB7 assigned by server on every save` : "New forms start at version 1.",
723
- navigationAction: /* @__PURE__ */ jsx6(
724
- Button4,
1583
+ navigationAction: /* @__PURE__ */ jsx8(
1584
+ Button6,
725
1585
  {
726
1586
  variant: "tertiary",
727
1587
  onClick: () => navigate("/plugins/builder/forms"),
@@ -730,18 +1590,23 @@ function FormEditorPage({ canUpdate }) {
730
1590
  )
731
1591
  }
732
1592
  ),
733
- /* @__PURE__ */ jsx6(Layouts2.Content, { children: /* @__PURE__ */ jsxs6(Flex5, { direction: "column", alignItems: "stretch", gap: 6, children: [
734
- /* @__PURE__ */ jsx6(ErrorNotice, { message: error }),
735
- !loaded ? /* @__PURE__ */ jsx6(Button4, { onClick: () => setRetry((value) => value + 1), children: "Retry loading form" }) : /* @__PURE__ */ jsxs6(Fragment2, { children: [
736
- /* @__PURE__ */ jsx6(
1593
+ /* @__PURE__ */ jsx8(Layouts3.Content, { children: /* @__PURE__ */ jsxs8(Flex7, { direction: "column", alignItems: "stretch", gap: 6, children: [
1594
+ /* @__PURE__ */ jsx8(ErrorNotice, { message: error }),
1595
+ !loaded ? /* @__PURE__ */ jsx8(Button6, { onClick: () => setRetry((value) => value + 1), children: "Retry loading form" }) : /* @__PURE__ */ jsxs8(Fragment4, { children: [
1596
+ /* @__PURE__ */ jsx8(
737
1597
  "form",
738
1598
  {
739
1599
  onSubmit: (event) => {
740
1600
  event.preventDefault();
741
1601
  if (disabled) return;
742
1602
  void run(async () => {
743
- const saved = await formApi(client).save(draft, documentId);
1603
+ const saved = await formApi(client).save(
1604
+ draft,
1605
+ documentId,
1606
+ notificationsOpened
1607
+ );
744
1608
  setDraft(saved);
1609
+ setSavedDraft(JSON.stringify(savePayload(saved, true)));
745
1610
  setVersion(saved.version);
746
1611
  if (!documentId)
747
1612
  navigate(`/plugins/builder/forms/${saved.documentId}`, {
@@ -749,9 +1614,9 @@ function FormEditorPage({ canUpdate }) {
749
1614
  });
750
1615
  });
751
1616
  },
752
- children: /* @__PURE__ */ jsxs6(Flex5, { direction: "column", alignItems: "stretch", gap: 6, children: [
753
- /* @__PURE__ */ jsxs6(Section, { title: "Definition", children: [
754
- /* @__PURE__ */ jsx6(
1617
+ children: /* @__PURE__ */ jsxs8(Flex7, { direction: "column", alignItems: "stretch", gap: 6, children: [
1618
+ /* @__PURE__ */ jsxs8(Section, { title: "Definition", children: [
1619
+ /* @__PURE__ */ jsx8(
755
1620
  TextField,
756
1621
  {
757
1622
  label: "Name",
@@ -761,7 +1626,7 @@ function FormEditorPage({ canUpdate }) {
761
1626
  onChange: (name) => setDraft({ ...draft, name })
762
1627
  }
763
1628
  ),
764
- /* @__PURE__ */ jsx6(
1629
+ /* @__PURE__ */ jsx8(
765
1630
  TextField,
766
1631
  {
767
1632
  label: "Slug",
@@ -771,11 +1636,11 @@ function FormEditorPage({ canUpdate }) {
771
1636
  onChange: (slug) => setDraft({ ...draft, slug })
772
1637
  }
773
1638
  ),
774
- /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Use lowercase letters, numbers and hyphens. The slug cannot change after creation." }),
775
- /* @__PURE__ */ jsxs6("label", { children: [
1639
+ /* @__PURE__ */ jsx8(Typography7, { variant: "pi", children: "Use lowercase letters, numbers and hyphens. The slug cannot change after creation." }),
1640
+ /* @__PURE__ */ jsxs8("label", { children: [
776
1641
  "Target content type",
777
1642
  " ",
778
- /* @__PURE__ */ jsxs6(
1643
+ /* @__PURE__ */ jsxs8(
779
1644
  "select",
780
1645
  {
781
1646
  required: true,
@@ -784,8 +1649,8 @@ function FormEditorPage({ canUpdate }) {
784
1649
  onChange: (event) => void selectTarget(event.target.value),
785
1650
  style: { padding: 10, width: "100%" },
786
1651
  children: [
787
- /* @__PURE__ */ jsx6("option", { value: "", children: "Select a target" }),
788
- targets.map((target) => /* @__PURE__ */ jsxs6("option", { value: target.uid, children: [
1652
+ /* @__PURE__ */ jsx8("option", { value: "", children: "Select a target" }),
1653
+ targets.map((target) => /* @__PURE__ */ jsxs8("option", { value: target.uid, children: [
789
1654
  target.name,
790
1655
  " \u2014 ",
791
1656
  target.uid
@@ -794,9 +1659,9 @@ function FormEditorPage({ canUpdate }) {
794
1659
  }
795
1660
  )
796
1661
  ] }),
797
- fieldsLoading && /* @__PURE__ */ jsx6(Typography5, { children: "Loading target fields\u2026" })
1662
+ fieldsLoading && /* @__PURE__ */ jsx8(Typography7, { children: "Loading target fields\u2026" })
798
1663
  ] }),
799
- /* @__PURE__ */ jsx6(
1664
+ /* @__PURE__ */ jsx8(
800
1665
  FieldEditor,
801
1666
  {
802
1667
  fields: draft.fields,
@@ -805,8 +1670,8 @@ function FormEditorPage({ canUpdate }) {
805
1670
  onChange: (fields) => setDraft({ ...draft, fields })
806
1671
  }
807
1672
  ),
808
- /* @__PURE__ */ jsxs6(Section, { title: "Consent", children: [
809
- /* @__PURE__ */ jsx6(
1673
+ /* @__PURE__ */ jsxs8(Section, { title: "Consent", children: [
1674
+ /* @__PURE__ */ jsx8(
810
1675
  TextField,
811
1676
  {
812
1677
  label: "Consent label",
@@ -820,7 +1685,7 @@ function FormEditorPage({ canUpdate }) {
820
1685
  })
821
1686
  }
822
1687
  ),
823
- /* @__PURE__ */ jsx6(
1688
+ /* @__PURE__ */ jsx8(
824
1689
  TextField,
825
1690
  {
826
1691
  label: "Consent version",
@@ -836,10 +1701,22 @@ function FormEditorPage({ canUpdate }) {
836
1701
  })
837
1702
  }
838
1703
  ),
839
- /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Consent version identifies the terms accepted by the visitor and is separate from the server form version." })
1704
+ /* @__PURE__ */ jsx8(Typography7, { variant: "pi", children: "Consent version identifies the terms accepted by the visitor and is separate from the server form version." })
840
1705
  ] }),
841
- canUpdate && /* @__PURE__ */ jsx6(
842
- Button4,
1706
+ /* @__PURE__ */ jsx8(
1707
+ NotificationEditor,
1708
+ {
1709
+ value: draft.notifications ?? {},
1710
+ fields: draft.fields,
1711
+ opened: notificationsOpened,
1712
+ onOpen: () => setNotificationsOpened(true),
1713
+ onChange: (notifications) => setDraft({ ...draft, notifications }),
1714
+ disabled,
1715
+ saveError: error
1716
+ }
1717
+ ),
1718
+ canUpdate && /* @__PURE__ */ jsx8(
1719
+ Button6,
843
1720
  {
844
1721
  type: "submit",
845
1722
  disabled: disabled || !draft.targetUid || !draft.fields.length,
@@ -849,9 +1726,18 @@ function FormEditorPage({ canUpdate }) {
849
1726
  ] })
850
1727
  }
851
1728
  ),
852
- documentId && /* @__PURE__ */ jsxs6(Section, { title: "Portable definition", children: [
853
- /* @__PURE__ */ jsx6(
854
- Button4,
1729
+ notificationsOpened && /* @__PURE__ */ jsx8(
1730
+ NotificationTest,
1731
+ {
1732
+ fields: draft.fields,
1733
+ disabled: disabled || !documentId || JSON.stringify(savePayload(draft, true)) !== savedDraft,
1734
+ send: (sample) => formApi(client).testSend(documentId, sample)
1735
+ },
1736
+ savedDraft
1737
+ ),
1738
+ documentId && /* @__PURE__ */ jsxs8(Section, { title: "Portable definition", children: [
1739
+ /* @__PURE__ */ jsx8(
1740
+ Button6,
855
1741
  {
856
1742
  disabled: busy,
857
1743
  onClick: () => void run(
@@ -863,8 +1749,8 @@ function FormEditorPage({ canUpdate }) {
863
1749
  children: "Export saved form JSON"
864
1750
  }
865
1751
  ),
866
- /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Export uses the saved definition. Notification credentials are masked by the server." }),
867
- canUpdate && /* @__PURE__ */ jsx6(
1752
+ /* @__PURE__ */ jsx8(Typography7, { variant: "pi", children: "Export uses the saved definition. Notification credentials are masked by the server." }),
1753
+ canUpdate && /* @__PURE__ */ jsx8(
868
1754
  ImportForm,
869
1755
  {
870
1756
  replace: true,
@@ -879,6 +1765,11 @@ function FormEditorPage({ canUpdate }) {
879
1765
  documentId
880
1766
  );
881
1767
  setDraft(saved);
1768
+ setSavedDraft(
1769
+ JSON.stringify(savePayload(saved, true))
1770
+ );
1771
+ setNotificationsOpened(false);
1772
+ setError("");
882
1773
  setVersion(saved.version);
883
1774
  setSchema(
884
1775
  await formApi(client).fields(saved.targetUid)
@@ -902,6 +1793,7 @@ var init_form_editor_page = __esm({
902
1793
  init_form();
903
1794
  init_ui();
904
1795
  init_field_editor();
1796
+ init_notification_editor();
905
1797
  init_import_form();
906
1798
  }
907
1799
  });
@@ -909,50 +1801,69 @@ var init_form_editor_page = __esm({
909
1801
  // admin/src/app.tsx
910
1802
  var app_exports = {};
911
1803
  __export(app_exports, {
1804
+ SubmissionsApp: () => SubmissionsApp,
912
1805
  default: () => App
913
1806
  });
914
1807
  import { Navigate, Route, Routes } from "react-router-dom";
915
- import { Layouts as Layouts3, Page as Page3, useRBAC } from "@strapi/strapi/admin";
916
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
917
- function App() {
918
- const { allowedActions, isLoading } = useRBAC(
1808
+ import { Layouts as Layouts4, Page as Page4, useRBAC as useRBAC2 } from "@strapi/strapi/admin";
1809
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1810
+ function FormsApp() {
1811
+ const { allowedActions, isLoading } = useRBAC2(
919
1812
  ["read", "update"].map((action) => ({
920
1813
  action: `plugin::builder.forms.${action}`,
921
1814
  subject: null
922
1815
  }))
923
1816
  );
924
- if (isLoading) return /* @__PURE__ */ jsx7(Page3.Loading, {});
925
- if (!allowedActions.canRead) return /* @__PURE__ */ jsx7(Page3.NoPermissions, {});
1817
+ if (isLoading) return /* @__PURE__ */ jsx9(Page4.Loading, {});
1818
+ if (!allowedActions.canRead) return /* @__PURE__ */ jsx9(Page4.NoPermissions, {});
926
1819
  const canUpdate = allowedActions.canUpdate === true;
927
- return /* @__PURE__ */ jsx7(Layouts3.Root, { children: /* @__PURE__ */ jsxs7(Routes, { children: [
928
- /* @__PURE__ */ jsx7(Route, { index: true, element: /* @__PURE__ */ jsx7(Navigate, { to: "forms", replace: true }) }),
929
- /* @__PURE__ */ jsx7(Route, { path: "forms", element: /* @__PURE__ */ jsx7(FormListPage, { canUpdate }) }),
930
- /* @__PURE__ */ jsx7(
1820
+ return /* @__PURE__ */ jsx9(Layouts4.Root, { children: /* @__PURE__ */ jsxs9(Routes, { children: [
1821
+ /* @__PURE__ */ jsx9(Route, { index: true, element: /* @__PURE__ */ jsx9(Navigate, { to: "forms", replace: true }) }),
1822
+ /* @__PURE__ */ jsx9(Route, { path: "forms", element: /* @__PURE__ */ jsx9(FormListPage, { canUpdate }) }),
1823
+ /* @__PURE__ */ jsx9(
931
1824
  Route,
932
1825
  {
933
1826
  path: "forms/new",
934
- element: canUpdate ? /* @__PURE__ */ jsx7(FormEditorPage, { canUpdate: true }) : /* @__PURE__ */ jsx7(Page3.NoPermissions, {})
1827
+ element: canUpdate ? /* @__PURE__ */ jsx9(FormEditorPage, { canUpdate: true }) : /* @__PURE__ */ jsx9(Page4.NoPermissions, {})
935
1828
  }
936
1829
  ),
937
- /* @__PURE__ */ jsx7(
1830
+ /* @__PURE__ */ jsx9(
938
1831
  Route,
939
1832
  {
940
1833
  path: "forms/:documentId",
941
- element: /* @__PURE__ */ jsx7(FormEditorPage, { canUpdate })
1834
+ element: /* @__PURE__ */ jsx9(FormEditorPage, { canUpdate })
942
1835
  }
943
1836
  ),
944
- /* @__PURE__ */ jsx7(
1837
+ /* @__PURE__ */ jsx9(
945
1838
  Route,
946
1839
  {
947
1840
  path: "*",
948
- element: /* @__PURE__ */ jsx7(Page3.Error, { content: "Form page not found." })
1841
+ element: /* @__PURE__ */ jsx9(Page4.Error, { content: "Form page not found." })
949
1842
  }
950
1843
  )
951
1844
  ] }) });
952
1845
  }
1846
+ function SubmissionsApp() {
1847
+ const { allowedActions, isLoading } = useRBAC2(
1848
+ ["read", "update"].map((action) => ({
1849
+ action: `plugin::builder.submissions.${action}`,
1850
+ subject: null
1851
+ }))
1852
+ );
1853
+ if (isLoading) return /* @__PURE__ */ jsx9(Page4.Loading, {});
1854
+ if (!allowedActions.canRead) return /* @__PURE__ */ jsx9(Page4.NoPermissions, {});
1855
+ return /* @__PURE__ */ jsx9(Layouts4.Root, { children: /* @__PURE__ */ jsx9(SubmissionPage, { canUpdate: allowedActions.canUpdate === true }) });
1856
+ }
1857
+ function App() {
1858
+ return /* @__PURE__ */ jsxs9(Routes, { children: [
1859
+ /* @__PURE__ */ jsx9(Route, { path: "submissions/*", element: /* @__PURE__ */ jsx9(SubmissionsApp, {}) }),
1860
+ /* @__PURE__ */ jsx9(Route, { path: "*", element: /* @__PURE__ */ jsx9(FormsApp, {}) })
1861
+ ] });
1862
+ }
953
1863
  var init_app = __esm({
954
1864
  "admin/src/app.tsx"() {
955
1865
  "use strict";
1866
+ init_submission_page();
956
1867
  init_form_list_page();
957
1868
  init_form_editor_page();
958
1869
  }
@@ -998,6 +1909,20 @@ var index_default = {
998
1909
  Component: () => Promise.resolve().then(() => (init_app(), app_exports)),
999
1910
  permissions: [{ action: "plugin::builder.forms.read", subject: null }]
1000
1911
  });
1912
+ app.addMenuLink({
1913
+ to: "plugins/builder/submissions",
1914
+ icon: BuilderIcon,
1915
+ intlLabel: {
1916
+ id: "builder.submissions",
1917
+ defaultMessage: "Builder Submissions"
1918
+ },
1919
+ Component: () => Promise.resolve().then(() => (init_app(), app_exports)).then(({ SubmissionsApp: SubmissionsApp2 }) => ({
1920
+ default: SubmissionsApp2
1921
+ })),
1922
+ permissions: [
1923
+ { action: "plugin::builder.submissions.read", subject: null }
1924
+ ]
1925
+ });
1001
1926
  app.registerPlugin({ id: "builder", name: "Builder" });
1002
1927
  }
1003
1928
  };