@dani-builder/strapi-plugin-builder 1.0.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.
@@ -1,12 +1,13 @@
1
1
  import type { useFetchClient } from "@strapi/strapi/admin";
2
- import type { FormDocument, FormDraft, FormField, FormTarget } from "./form";
2
+ import type { Delivery, FormDocument, FormDraft, FormField, FormTarget } from "./form";
3
3
  type Client = ReturnType<typeof useFetchClient>;
4
4
  export declare function formApi(client: Client): {
5
5
  list(): Promise<FormDocument[]>;
6
6
  get(id: string): Promise<FormDocument>;
7
7
  targets(): Promise<FormTarget[]>;
8
8
  fields(targetUid: string): Promise<FormField[]>;
9
- save(form: FormDraft, id?: string): Promise<FormDocument>;
9
+ save(form: FormDraft, id?: string, includeNotifications?: boolean): Promise<FormDocument>;
10
+ testSend(id: string, sampleFields: Record<string, unknown>): Promise<Delivery[]>;
10
11
  remove(id: string): Promise<void>;
11
12
  export(id: string): Promise<Record<string, unknown>>;
12
13
  import(data: Record<string, unknown>, id?: string): Promise<FormDocument>;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../admin/src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE7E,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAEhD,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM;;YAKpB,MAAM;;sBAWI,MAAM;eAOb,SAAS,OAAO,MAAM;eAWtB,MAAM;eAGN,MAAM;iBAOJ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,MAAM;EAa1D;AACD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,QASxE"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../admin/src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EACV,QAAQ,EACR,YAAY,EACZ,SAAS,EACT,SAAS,EACT,UAAU,EACX,MAAM,QAAQ,CAAC;AAEhB,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAEhD,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM;;YAKpB,MAAM;;sBAWI,MAAM;eAOb,SAAS,OAAO,MAAM;iBAWpB,MAAM,gBAAgB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;eAQ/C,MAAM;eAGN,MAAM;iBAOJ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,MAAM;EAa1D;AACD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,QASxE"}
@@ -1 +1 @@
1
- {"version":3,"file":"form-editor-page.d.ts","sourceRoot":"","sources":["../../admin/src/form-editor-page.tsx"],"names":[],"mappings":"AAUA,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,+BAwRnE"}
1
+ {"version":3,"file":"form-editor-page.d.ts","sourceRoot":"","sources":["../../admin/src/form-editor-page.tsx"],"names":[],"mappings":"AAWA,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,+BA+TnE"}
@@ -14,7 +14,29 @@ export interface FormField {
14
14
  min?: number;
15
15
  max?: number;
16
16
  }
17
+ export interface NotificationConfig {
18
+ email?: {
19
+ to: string[];
20
+ cc?: string[];
21
+ replyToField?: string;
22
+ subject: string;
23
+ body: string;
24
+ };
25
+ webhook?: {
26
+ url: string;
27
+ secret?: string;
28
+ headers?: Record<string, string>;
29
+ };
30
+ }
31
+ export interface Delivery {
32
+ channel: "email" | "webhook" | "afterSave";
33
+ status: "sent" | "failed";
34
+ attempts: number;
35
+ lastError: string | null;
36
+ sentAt: string | null;
37
+ }
17
38
  export interface FormDraft {
39
+ notifications?: NotificationConfig;
18
40
  slug: string;
19
41
  name: string;
20
42
  targetUid: string;
@@ -33,8 +55,8 @@ export interface FormTarget {
33
55
  name: string;
34
56
  }
35
57
  export declare const emptyForm: FormDraft;
36
- /** Explicitly omit notification configuration and all server-owned metadata. */
37
- export declare function savePayload(form: FormDraft): FormDraft;
58
+ /** Only send notifications after the editor has been opened. */
59
+ export declare function savePayload(form: FormDraft, includeNotifications?: boolean): FormDraft;
38
60
  export declare function moveField(fields: FormField[], index: number, direction: number): FormField[];
39
61
  export declare function setRequired(field: FormField, required: boolean, schema: FormField): FormField;
40
62
  export declare function parseImport(source: string): Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../admin/src/form.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AACD,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AACD,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AACD,eAAO,MAAM,SAAS,EAAE,SAMvB,CAAC;AACF,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAWtD;AACD,wBAAgB,SAAS,CACvB,MAAM,EAAE,SAAS,EAAE,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,SAAS,EAAE,CAQb;AACD,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,SAAS,GAChB,SAAS,CAEX;AACD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUnE;AACD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUnD"}
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../admin/src/form.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE;QACN,EAAE,EAAE,MAAM,EAAE,CAAC;QACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QACd,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CAC9E;AACD,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IAC3C,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AACD,MAAM,WAAW,SAAS;IACxB,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AACD,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AACD,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AACD,eAAO,MAAM,SAAS,EAAE,SAMvB,CAAC;AACF,gEAAgE;AAChE,wBAAgB,WAAW,CACzB,IAAI,EAAE,SAAS,EACf,oBAAoB,UAAQ,GAC3B,SAAS,CAqBX;AACD,wBAAgB,SAAS,CACvB,MAAM,EAAE,SAAS,EAAE,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,SAAS,EAAE,CAQb;AACD,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,SAAS,GAChB,SAAS,CAEX;AACD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUnE;AACD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUnD"}
@@ -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
  },
@@ -134,6 +146,7 @@ var init_api = __esm({
134
146
  });
135
147
 
136
148
  // admin/src/ui.tsx
149
+ import { useId } from "react";
137
150
  import { Box, Flex, Typography } from "@strapi/design-system";
138
151
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
139
152
  function Section({
@@ -151,9 +164,13 @@ function TextField({
151
164
  onChange,
152
165
  disabled,
153
166
  required,
154
- multiline
167
+ multiline,
168
+ secret,
169
+ error
155
170
  }) {
171
+ const errorId = useId();
156
172
  const props = {
173
+ "aria-describedby": error ? errorId : void 0,
157
174
  value,
158
175
  disabled,
159
176
  required,
@@ -170,7 +187,16 @@ function TextField({
170
187
  };
171
188
  return /* @__PURE__ */ jsxs2("label", { style: { display: "grid", gap: 6 }, children: [
172
189
  /* @__PURE__ */ jsx2(Typography, { children: label }),
173
- multiline ? /* @__PURE__ */ jsx2("textarea", { ...props, rows: 3 }) : /* @__PURE__ */ jsx2("input", { ...props })
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 })
174
200
  ] });
175
201
  }
176
202
  function ErrorNotice({ message }) {
@@ -618,32 +644,537 @@ var init_field_editor = __esm({
618
644
  }
619
645
  });
620
646
 
621
- // admin/src/form-editor-page.tsx
622
- import { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
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";
623
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";
624
1152
  import { Layouts as Layouts2, Page as Page2, useFetchClient as useFetchClient2 } from "@strapi/strapi/admin";
625
1153
  import { useNavigate as useNavigate2, useParams } from "react-router-dom";
626
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1154
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
627
1155
  function FormEditorPage({ canUpdate }) {
628
1156
  const { documentId } = useParams();
629
1157
  const client = useFetchClient2();
630
1158
  const { get } = client;
631
1159
  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);
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);
642
1172
  const lock = useRef3(false);
643
1173
  const targetRequest = useRef3(0);
644
1174
  useEffect2(() => {
645
1175
  let active = true;
646
1176
  setLoading(true);
1177
+ setNotificationsOpened(false);
647
1178
  setLoaded(false);
648
1179
  setError("");
649
1180
  async function load() {
@@ -663,6 +1194,7 @@ function FormEditorPage({ canUpdate }) {
663
1194
  consent: { ...emptyForm.consent }
664
1195
  }
665
1196
  );
1197
+ setSavedDraft(saved ? JSON.stringify(savePayload(saved, true)) : "");
666
1198
  setVersion(saved?.version ?? null);
667
1199
  setSchema(attrs);
668
1200
  setLoaded(true);
@@ -712,16 +1244,16 @@ function FormEditorPage({ canUpdate }) {
712
1244
  setBusy(false);
713
1245
  }
714
1246
  }
715
- if (loading) return /* @__PURE__ */ jsx6(Page2.Loading, {});
1247
+ if (loading) return /* @__PURE__ */ jsx7(Page2.Loading, {});
716
1248
  const disabled = !canUpdate || busy || fieldsLoading;
717
- return /* @__PURE__ */ jsxs6(Fragment2, { children: [
718
- /* @__PURE__ */ jsx6(
1249
+ return /* @__PURE__ */ jsxs7(Fragment3, { children: [
1250
+ /* @__PURE__ */ jsx7(
719
1251
  Layouts2.Header,
720
1252
  {
721
1253
  title: documentId ? draft.name || "Form" : "Create form",
722
1254
  subtitle: version ? `Form version ${version} \xB7 assigned by server on every save` : "New forms start at version 1.",
723
- navigationAction: /* @__PURE__ */ jsx6(
724
- Button4,
1255
+ navigationAction: /* @__PURE__ */ jsx7(
1256
+ Button5,
725
1257
  {
726
1258
  variant: "tertiary",
727
1259
  onClick: () => navigate("/plugins/builder/forms"),
@@ -730,18 +1262,23 @@ function FormEditorPage({ canUpdate }) {
730
1262
  )
731
1263
  }
732
1264
  ),
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(
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(
737
1269
  "form",
738
1270
  {
739
1271
  onSubmit: (event) => {
740
1272
  event.preventDefault();
741
1273
  if (disabled) return;
742
1274
  void run(async () => {
743
- const saved = await formApi(client).save(draft, documentId);
1275
+ const saved = await formApi(client).save(
1276
+ draft,
1277
+ documentId,
1278
+ notificationsOpened
1279
+ );
744
1280
  setDraft(saved);
1281
+ setSavedDraft(JSON.stringify(savePayload(saved, true)));
745
1282
  setVersion(saved.version);
746
1283
  if (!documentId)
747
1284
  navigate(`/plugins/builder/forms/${saved.documentId}`, {
@@ -749,9 +1286,9 @@ function FormEditorPage({ canUpdate }) {
749
1286
  });
750
1287
  });
751
1288
  },
752
- children: /* @__PURE__ */ jsxs6(Flex5, { direction: "column", alignItems: "stretch", gap: 6, children: [
753
- /* @__PURE__ */ jsxs6(Section, { title: "Definition", children: [
754
- /* @__PURE__ */ jsx6(
1289
+ children: /* @__PURE__ */ jsxs7(Flex6, { direction: "column", alignItems: "stretch", gap: 6, children: [
1290
+ /* @__PURE__ */ jsxs7(Section, { title: "Definition", children: [
1291
+ /* @__PURE__ */ jsx7(
755
1292
  TextField,
756
1293
  {
757
1294
  label: "Name",
@@ -761,7 +1298,7 @@ function FormEditorPage({ canUpdate }) {
761
1298
  onChange: (name) => setDraft({ ...draft, name })
762
1299
  }
763
1300
  ),
764
- /* @__PURE__ */ jsx6(
1301
+ /* @__PURE__ */ jsx7(
765
1302
  TextField,
766
1303
  {
767
1304
  label: "Slug",
@@ -771,11 +1308,11 @@ function FormEditorPage({ canUpdate }) {
771
1308
  onChange: (slug) => setDraft({ ...draft, slug })
772
1309
  }
773
1310
  ),
774
- /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Use lowercase letters, numbers and hyphens. The slug cannot change after creation." }),
775
- /* @__PURE__ */ jsxs6("label", { children: [
1311
+ /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Use lowercase letters, numbers and hyphens. The slug cannot change after creation." }),
1312
+ /* @__PURE__ */ jsxs7("label", { children: [
776
1313
  "Target content type",
777
1314
  " ",
778
- /* @__PURE__ */ jsxs6(
1315
+ /* @__PURE__ */ jsxs7(
779
1316
  "select",
780
1317
  {
781
1318
  required: true,
@@ -784,8 +1321,8 @@ function FormEditorPage({ canUpdate }) {
784
1321
  onChange: (event) => void selectTarget(event.target.value),
785
1322
  style: { padding: 10, width: "100%" },
786
1323
  children: [
787
- /* @__PURE__ */ jsx6("option", { value: "", children: "Select a target" }),
788
- targets.map((target) => /* @__PURE__ */ jsxs6("option", { value: target.uid, children: [
1324
+ /* @__PURE__ */ jsx7("option", { value: "", children: "Select a target" }),
1325
+ targets.map((target) => /* @__PURE__ */ jsxs7("option", { value: target.uid, children: [
789
1326
  target.name,
790
1327
  " \u2014 ",
791
1328
  target.uid
@@ -794,9 +1331,9 @@ function FormEditorPage({ canUpdate }) {
794
1331
  }
795
1332
  )
796
1333
  ] }),
797
- fieldsLoading && /* @__PURE__ */ jsx6(Typography5, { children: "Loading target fields\u2026" })
1334
+ fieldsLoading && /* @__PURE__ */ jsx7(Typography6, { children: "Loading target fields\u2026" })
798
1335
  ] }),
799
- /* @__PURE__ */ jsx6(
1336
+ /* @__PURE__ */ jsx7(
800
1337
  FieldEditor,
801
1338
  {
802
1339
  fields: draft.fields,
@@ -805,8 +1342,8 @@ function FormEditorPage({ canUpdate }) {
805
1342
  onChange: (fields) => setDraft({ ...draft, fields })
806
1343
  }
807
1344
  ),
808
- /* @__PURE__ */ jsxs6(Section, { title: "Consent", children: [
809
- /* @__PURE__ */ jsx6(
1345
+ /* @__PURE__ */ jsxs7(Section, { title: "Consent", children: [
1346
+ /* @__PURE__ */ jsx7(
810
1347
  TextField,
811
1348
  {
812
1349
  label: "Consent label",
@@ -820,7 +1357,7 @@ function FormEditorPage({ canUpdate }) {
820
1357
  })
821
1358
  }
822
1359
  ),
823
- /* @__PURE__ */ jsx6(
1360
+ /* @__PURE__ */ jsx7(
824
1361
  TextField,
825
1362
  {
826
1363
  label: "Consent version",
@@ -836,10 +1373,22 @@ function FormEditorPage({ canUpdate }) {
836
1373
  })
837
1374
  }
838
1375
  ),
839
- /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Consent version identifies the terms accepted by the visitor and is separate from the server form version." })
1376
+ /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Consent version identifies the terms accepted by the visitor and is separate from the server form version." })
840
1377
  ] }),
841
- canUpdate && /* @__PURE__ */ jsx6(
842
- Button4,
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,
843
1392
  {
844
1393
  type: "submit",
845
1394
  disabled: disabled || !draft.targetUid || !draft.fields.length,
@@ -849,9 +1398,18 @@ function FormEditorPage({ canUpdate }) {
849
1398
  ] })
850
1399
  }
851
1400
  ),
852
- documentId && /* @__PURE__ */ jsxs6(Section, { title: "Portable definition", children: [
853
- /* @__PURE__ */ jsx6(
854
- Button4,
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,
855
1413
  {
856
1414
  disabled: busy,
857
1415
  onClick: () => void run(
@@ -863,8 +1421,8 @@ function FormEditorPage({ canUpdate }) {
863
1421
  children: "Export saved form JSON"
864
1422
  }
865
1423
  ),
866
- /* @__PURE__ */ jsx6(Typography5, { variant: "pi", children: "Export uses the saved definition. Notification credentials are masked by the server." }),
867
- canUpdate && /* @__PURE__ */ jsx6(
1424
+ /* @__PURE__ */ jsx7(Typography6, { variant: "pi", children: "Export uses the saved definition. Notification credentials are masked by the server." }),
1425
+ canUpdate && /* @__PURE__ */ jsx7(
868
1426
  ImportForm,
869
1427
  {
870
1428
  replace: true,
@@ -879,6 +1437,11 @@ function FormEditorPage({ canUpdate }) {
879
1437
  documentId
880
1438
  );
881
1439
  setDraft(saved);
1440
+ setSavedDraft(
1441
+ JSON.stringify(savePayload(saved, true))
1442
+ );
1443
+ setNotificationsOpened(false);
1444
+ setError("");
882
1445
  setVersion(saved.version);
883
1446
  setSchema(
884
1447
  await formApi(client).fields(saved.targetUid)
@@ -902,6 +1465,7 @@ var init_form_editor_page = __esm({
902
1465
  init_form();
903
1466
  init_ui();
904
1467
  init_field_editor();
1468
+ init_notification_editor();
905
1469
  init_import_form();
906
1470
  }
907
1471
  });
@@ -913,7 +1477,7 @@ __export(app_exports, {
913
1477
  });
914
1478
  import { Navigate, Route, Routes } from "react-router-dom";
915
1479
  import { Layouts as Layouts3, Page as Page3, useRBAC } from "@strapi/strapi/admin";
916
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1480
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
917
1481
  function App() {
918
1482
  const { allowedActions, isLoading } = useRBAC(
919
1483
  ["read", "update"].map((action) => ({
@@ -921,31 +1485,31 @@ function App() {
921
1485
  subject: null
922
1486
  }))
923
1487
  );
924
- if (isLoading) return /* @__PURE__ */ jsx7(Page3.Loading, {});
925
- if (!allowedActions.canRead) return /* @__PURE__ */ jsx7(Page3.NoPermissions, {});
1488
+ if (isLoading) return /* @__PURE__ */ jsx8(Page3.Loading, {});
1489
+ if (!allowedActions.canRead) return /* @__PURE__ */ jsx8(Page3.NoPermissions, {});
926
1490
  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(
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(
931
1495
  Route,
932
1496
  {
933
1497
  path: "forms/new",
934
- element: canUpdate ? /* @__PURE__ */ jsx7(FormEditorPage, { canUpdate: true }) : /* @__PURE__ */ jsx7(Page3.NoPermissions, {})
1498
+ element: canUpdate ? /* @__PURE__ */ jsx8(FormEditorPage, { canUpdate: true }) : /* @__PURE__ */ jsx8(Page3.NoPermissions, {})
935
1499
  }
936
1500
  ),
937
- /* @__PURE__ */ jsx7(
1501
+ /* @__PURE__ */ jsx8(
938
1502
  Route,
939
1503
  {
940
1504
  path: "forms/:documentId",
941
- element: /* @__PURE__ */ jsx7(FormEditorPage, { canUpdate })
1505
+ element: /* @__PURE__ */ jsx8(FormEditorPage, { canUpdate })
942
1506
  }
943
1507
  ),
944
- /* @__PURE__ */ jsx7(
1508
+ /* @__PURE__ */ jsx8(
945
1509
  Route,
946
1510
  {
947
1511
  path: "*",
948
- element: /* @__PURE__ */ jsx7(Page3.Error, { content: "Form page not found." })
1512
+ element: /* @__PURE__ */ jsx8(Page3.Error, { content: "Form page not found." })
949
1513
  }
950
1514
  )
951
1515
  ] }) });
@@ -0,0 +1,16 @@
1
+ import type { Delivery, FormField, NotificationConfig } from "./form";
2
+ export declare function NotificationEditor({ value, fields, opened, onOpen, onChange, disabled, saveError, }: {
3
+ value: NotificationConfig;
4
+ fields: FormField[];
5
+ opened: boolean;
6
+ onOpen: () => void;
7
+ onChange: (value: NotificationConfig) => void;
8
+ disabled: boolean;
9
+ saveError: string;
10
+ }): import("react").JSX.Element;
11
+ export declare function NotificationTest({ fields, disabled, send, }: {
12
+ fields: FormField[];
13
+ disabled: boolean;
14
+ send: (sample: Record<string, unknown>) => Promise<Delivery[]>;
15
+ }): import("react").JSX.Element;
16
+ //# sourceMappingURL=notification-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-editor.d.ts","sourceRoot":"","sources":["../../admin/src/notification-editor.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAUtE,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,MAAM,EACN,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,GACV,EAAE;IACD,KAAK,EAAE,kBAAkB,CAAC;IAC1B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC9C,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,+BAoQA;AA+CD,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,QAAQ,EACR,IAAI,GACL,EAAE;IACD,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CAChE,+BAqGA"}
@@ -0,0 +1,8 @@
1
+ import type { FormField, NotificationConfig } from "./form";
2
+ export declare function placeholders(fields: FormField[]): string[];
3
+ export declare function templateError(value: string, fields: FormField[]): string;
4
+ export declare function notificationErrors(config: NotificationConfig, fields: FormField[]): Record<string, string>;
5
+ /** Empty optional values are omitted; hidden fields use the same types as visible fields. */
6
+ export declare function samplePayload(fields: FormField[], values: Record<string, string>): Record<string, unknown>;
7
+ export declare function sampleErrors(error: unknown): Record<string, string>;
8
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../admin/src/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAE5D,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAO1D;AACD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAUxE;AACD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,SAAS,EAAE,GAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmCxB;AACD,6FAA6F;AAC7F,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,EAAE,EACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiBzB;AACD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuBnE"}
@@ -3,13 +3,15 @@ export declare function Section({ title, children, }: {
3
3
  title: string;
4
4
  children: ReactNode;
5
5
  }): import("react").JSX.Element;
6
- export declare function TextField({ label, value, onChange, disabled, required, multiline, }: {
6
+ export declare function TextField({ label, value, onChange, disabled, required, multiline, secret, error, }: {
7
7
  label: string;
8
8
  value: string;
9
9
  onChange: (value: string) => void;
10
10
  disabled?: boolean;
11
11
  required?: boolean;
12
12
  multiline?: boolean;
13
+ secret?: boolean;
14
+ error?: string;
13
15
  }): import("react").JSX.Element;
14
16
  export declare function ErrorNotice({ message }: {
15
17
  message: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../admin/src/ui.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAWA;AACD,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,GACV,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,+BAwBA;AACD,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,sCAM3D"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../admin/src/ui.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAWA;AACD,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,MAAM,EACN,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,+BAwCA;AACD,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,sCAM3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dani-builder/strapi-plugin-builder",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Daniworks Builder — reusable CMS components, content types, and smart API for Strapi v5",
5
5
  "license": "MIT",
6
6
  "strapi": {
@@ -72,7 +72,7 @@
72
72
  "directory": "packages/strapi-plugin-builder"
73
73
  },
74
74
  "dependencies": {
75
- "@dani-builder/strapi-client": "^1.0.0"
75
+ "@dani-builder/strapi-client": "^1.1.0"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@strapi/strapi": "5.54.0",