@10x-media/form-builder 0.1.0-beta.5 → 0.1.0-beta.6

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 (87) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/actions/builtin/confirmation.js +75 -72
  3. package/dist/actions/builtin/confirmation.js.map +1 -1
  4. package/dist/actions/builtin/emailTeam.js +31 -41
  5. package/dist/actions/builtin/emailTeam.js.map +1 -1
  6. package/dist/actions/builtin/index.d.ts +2 -1
  7. package/dist/actions/builtin/index.js +3 -3
  8. package/dist/actions/builtin/index.js.map +1 -1
  9. package/dist/actions/emailRecipients.d.ts +12 -0
  10. package/dist/actions/emailRecipients.js +72 -0
  11. package/dist/actions/emailRecipients.js.map +1 -0
  12. package/dist/client/ConsentBlockLabel.d.ts +15 -0
  13. package/dist/client/ConsentBlockLabel.js +71 -0
  14. package/dist/client/ConsentBlockLabel.js.map +1 -0
  15. package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
  16. package/dist/client/ConsentSourceRowLabel.js +21 -0
  17. package/dist/client/ConsentSourceRowLabel.js.map +1 -0
  18. package/dist/client/EndpointOptionsSelect.d.ts +1 -0
  19. package/dist/client/EndpointOptionsSelect.js +4 -1
  20. package/dist/client/EndpointOptionsSelect.js.map +1 -1
  21. package/dist/client/FieldNameSelect.d.ts +1 -0
  22. package/dist/client/FieldNameSelect.js +4 -1
  23. package/dist/client/FieldNameSelect.js.map +1 -1
  24. package/dist/client/RecipientsSelect.d.ts +31 -0
  25. package/dist/client/RecipientsSelect.js +174 -0
  26. package/dist/client/RecipientsSelect.js.map +1 -0
  27. package/dist/client/admin.css +5 -0
  28. package/dist/collections/formSubmissions.js +2 -1
  29. package/dist/collections/formSubmissions.js.map +1 -1
  30. package/dist/collections/forms.js +18 -2
  31. package/dist/collections/forms.js.map +1 -1
  32. package/dist/consent/captureConsent.d.ts +14 -5
  33. package/dist/consent/captureConsent.js +24 -7
  34. package/dist/consent/captureConsent.js.map +1 -1
  35. package/dist/consent/consentSourcesField.d.ts +4 -0
  36. package/dist/consent/consentSourcesField.js +8 -1
  37. package/dist/consent/consentSourcesField.js.map +1 -1
  38. package/dist/consent/types.d.ts +4 -0
  39. package/dist/email/departments.d.ts +13 -9
  40. package/dist/email/departments.js +7 -50
  41. package/dist/email/departments.js.map +1 -1
  42. package/dist/email/departmentsField.d.ts +4 -0
  43. package/dist/email/departmentsField.js +4 -0
  44. package/dist/email/departmentsField.js.map +1 -1
  45. package/dist/exports/client.d.ts +4 -1
  46. package/dist/exports/client.js +4 -1
  47. package/dist/exports/react.js +1 -1
  48. package/dist/exports/rsc.js +1 -1
  49. package/dist/fields/buildFieldBlocks.js +4 -4
  50. package/dist/fields/buildFieldBlocks.js.map +1 -1
  51. package/dist/fields/builtin/consent.js +6 -1
  52. package/dist/fields/builtin/consent.js.map +1 -1
  53. package/dist/fields/builtin/file.js +9 -3
  54. package/dist/fields/builtin/file.js.map +1 -1
  55. package/dist/fields/builtin/index.d.ts +1 -1
  56. package/dist/fields/builtin/index.js +3 -3
  57. package/dist/fields/builtin/index.js.map +1 -1
  58. package/dist/fields/types.d.ts +9 -2
  59. package/dist/form/toFormDocument.d.ts +20 -3
  60. package/dist/form/toFormDocument.js +16 -4
  61. package/dist/form/toFormDocument.js.map +1 -1
  62. package/dist/form/types.d.ts +5 -5
  63. package/dist/index.d.ts +14 -5
  64. package/dist/index.js +9 -6
  65. package/dist/index.js.map +1 -1
  66. package/dist/plugin/registerCollections.js +2 -1
  67. package/dist/plugin/registerCollections.js.map +1 -1
  68. package/dist/plugin/uploadsCollection.d.ts +3 -0
  69. package/dist/plugin/uploadsCollection.js +10 -1
  70. package/dist/plugin/uploadsCollection.js.map +1 -1
  71. package/dist/submissions/SubmissionAnswers.js.map +1 -1
  72. package/dist/submissions/SubmissionAnswersClient.js +33 -26
  73. package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
  74. package/dist/submissions/runSubmission.js +3 -2
  75. package/dist/submissions/runSubmission.js.map +1 -1
  76. package/dist/submissions/validateSubmission.js +2 -1
  77. package/dist/submissions/validateSubmission.js.map +1 -1
  78. package/dist/translations/de.js +4 -2
  79. package/dist/translations/de.js.map +1 -1
  80. package/dist/translations/en.js +4 -2
  81. package/dist/translations/en.js.map +1 -1
  82. package/dist/translations/keys.d.ts +4 -2
  83. package/dist/translations/keys.js +4 -2
  84. package/dist/translations/keys.js.map +1 -1
  85. package/dist/uploads/resolveFileRef.js +2 -1
  86. package/dist/uploads/resolveFileRef.js.map +1 -1
  87. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @10x-media/form-builder
2
2
 
3
+ ## 0.1.0-beta.6
4
+
5
+ ### Minor Changes
6
+
7
+ - Email actions and consent polish, plus paddle-worldwide handoff asks.
8
+
9
+ - **New creatable multi-recipient email field.** To, Reply-to, CC, and BCC on the Email Team and
10
+ Confirmation actions accept multiple recipients as native badges, mixing picked department
11
+ addresses, free-typed emails, and `{{field}}` recipient tokens (drag-reorderable, `admin.width`
12
+ aware). Configurable via `email.recipients` (`allowCustom`, `fieldTokens`, `tokenFieldTypes`).
13
+ - **Half-width pairing** of the email address fields (To/Reply-to, CC/BCC).
14
+ - **Consent field polish:** no more redundant description (the source statement is the content); the
15
+ field block title and the source array rows now show the consent source's name; the condensed
16
+ department-emails array centers its remove control on the input.
17
+ - **Consent statements on every read.** An `afterRead` hook resolves statements onto the form doc, so
18
+ consent renders wherever the form is fetched (modal/client, not only RSC); `toFormDocument` falls
19
+ back to the doc-carried statements.
20
+ - **`toFormDocument`** accepts a populated redirect `reference` (`value: string | number | object |
21
+ null`) and an optional `resolveRedirect` seam that fills `redirect.url`.
22
+ - **Versioned consent proof.** Submissions snapshot the agreed wording via `consent.snapshot`
23
+ (default `both`: a statement hash, the plain text, and the source name), for an audit trail
24
+ independent of later source edits.
25
+ - **File MIME picker** constrained to the host upload collection's `mimeTypes` (with an optional
26
+ `uploads.mimeTypes` override).
27
+
28
+ **BREAKING:** `emailTeam`/`confirmation` `to`, `cc`, `bcc`, and `replyTo` are now `string[]` (`text`
29
+ `hasMany`). A legacy single-string value reads as one recipient; Postgres consumers regenerate a
30
+ migration.
31
+
3
32
  ## 0.1.0-beta.5
4
33
 
5
34
  ### Minor Changes
@@ -5,6 +5,7 @@ import { keys } from "../../translations/keys.js";
5
5
  import { asTranslate, labelFor } from "../../translations/server.js";
6
6
  import { resolverFor } from "../body/serializeBody.js";
7
7
  import { defineAction } from "../defineAction.js";
8
+ import { buildRecipientField, resolveRecipients } from "../emailRecipients.js";
8
9
  import { buildFromField } from "../fromAddresses.js";
9
10
  //#region src/actions/builtin/confirmation.ts
10
11
  const TO_FIELD_REF = "@10x-media/form-builder/client#FieldNameSelect";
@@ -17,86 +18,88 @@ const validateToField = (value, { data, req }) => {
17
18
  return fieldNamesOfType(data && typeof data === "object" ? data.fields : void 0, ["email"]).includes(value) ? true : asTranslate(req.t)(keys.validationEmailFieldUnknown);
18
19
  };
19
20
  /**
20
- * `subject` and `body` are email content and follow `localize`; `toField` is an identifier and
21
- * `from` an address, neither of which ever does. `cc`, `bcc`, and `replyTo` are addresses that
22
- * follow `localize` (like `subject`), each interpolated the same way so `{{field}}` merge tags
23
- * work; a comma-separated list is forwarded as-is, since `payload.sendEmail` (nodemailer) accepts
24
- * one directly. `editor` overrides the body field's Lexical/richText editor (from the plugin's
25
- * `richText.editor` option). `fromAddresses`, when given (the plugin's `email.fromAddresses`
26
- * option), adds a `from` select sourced from the host resolver; absent, no `from` field exists and
27
- * every send uses the email adapter's default sender.
21
+ * `toField` names the form's own email field the confirmation is sent to (a `FieldNameSelect`, not a
22
+ * recipient list). `replyTo`, `cc`, and `bcc` are recipient lists (`RecipientsSelect`, `string[]`):
23
+ * literal emails, picked department addresses (when `departments` is set), or `{{field}}` tokens
24
+ * resolved from the submission at send. `from` is a single address, added only when `fromAddresses`
25
+ * is set. `subject`/`body` are content; `editor` overrides the body editor; `recipients` narrows the
26
+ * recipient fields' behavior. Content and recipient fields carry `localized: true` when `localize`.
28
27
  */
29
- const buildConfirmation = (localize, editor, fromAddresses) => defineAction({
30
- type: "confirmation",
31
- label: keys.actionConfirmation,
32
- config: [
33
- {
34
- name: "toField",
35
- type: "text",
36
- label: labelFor(keys.actionConfigToField),
37
- admin: { components: { Field: {
28
+ const buildConfirmation = (localize, editor, fromAddresses, departments, recipients) => {
29
+ const endpoint = departments ? "departments" : void 0;
30
+ const recip = (name, labelKey) => buildRecipientField(name, labelKey, localize, {
31
+ endpoint,
32
+ recipients,
33
+ width: "50%"
34
+ });
35
+ const toField = {
36
+ name: "toField",
37
+ type: "text",
38
+ label: labelFor(keys.actionConfigToField),
39
+ admin: {
40
+ width: "50%",
41
+ components: { Field: {
38
42
  path: TO_FIELD_REF,
39
43
  clientProps: {
40
44
  types: ["email"],
41
45
  descriptionKey: keys.actionConfigToFieldDescription
42
46
  }
43
- } } },
44
- validate: validateToField
47
+ } }
45
48
  },
46
- ...fromAddresses ? [buildFromField(fromAddresses)] : [],
47
- {
48
- name: "cc",
49
- type: "text",
50
- label: labelFor(keys.actionConfigCc),
51
- ...localizedIf(localize)
52
- },
53
- {
54
- name: "bcc",
55
- type: "text",
56
- label: labelFor(keys.actionConfigBcc),
57
- ...localizedIf(localize)
58
- },
59
- {
60
- name: "replyTo",
61
- type: "text",
62
- label: labelFor(keys.actionConfigReplyTo),
63
- ...localizedIf(localize)
64
- },
65
- {
66
- name: "subject",
67
- type: "text",
68
- label: labelFor(keys.actionConfigSubject),
69
- ...localizedIf(localize)
70
- },
71
- {
72
- name: "body",
73
- type: "richText",
74
- label: labelFor(keys.actionConfigBody),
75
- admin: { description: labelFor(keys.actionConfigBodyDescription) },
76
- ...localizedIf(localize),
77
- ...editor ? { editor } : {}
49
+ validate: validateToField
50
+ };
51
+ return defineAction({
52
+ type: "confirmation",
53
+ label: keys.actionConfirmation,
54
+ config: [
55
+ {
56
+ type: "row",
57
+ fields: [toField, recip("replyTo", keys.actionConfigReplyTo)]
58
+ },
59
+ ...fromAddresses ? [buildFromField(fromAddresses)] : [],
60
+ {
61
+ type: "row",
62
+ fields: [recip("cc", keys.actionConfigCc), recip("bcc", keys.actionConfigBcc)]
63
+ },
64
+ {
65
+ name: "subject",
66
+ type: "text",
67
+ label: labelFor(keys.actionConfigSubject),
68
+ ...localizedIf(localize)
69
+ },
70
+ {
71
+ name: "body",
72
+ type: "richText",
73
+ label: labelFor(keys.actionConfigBody),
74
+ admin: { description: labelFor(keys.actionConfigBodyDescription) },
75
+ ...localizedIf(localize),
76
+ ...editor ? { editor } : {}
77
+ }
78
+ ],
79
+ run: async (args) => {
80
+ const { config, values, payload, renderBody } = args;
81
+ if (!config.toField) return;
82
+ const resolve = resolverFor(values);
83
+ const to = resolve(config.toField);
84
+ if (!to) return;
85
+ if (typeof payload.sendEmail !== "function") throw new Error("confirmation: no email adapter configured");
86
+ const subject = interpolate(config.subject ?? "", resolve);
87
+ const html = await renderBody(config.body);
88
+ const cc = resolveRecipients(config.cc, resolve);
89
+ const bcc = resolveRecipients(config.bcc, resolve);
90
+ const replyTo = resolveRecipients(config.replyTo, resolve);
91
+ await payload.sendEmail({
92
+ to,
93
+ subject,
94
+ html,
95
+ ...config.from ? { from: config.from } : {},
96
+ ...cc ? { cc } : {},
97
+ ...bcc ? { bcc } : {},
98
+ ...replyTo ? { replyTo } : {}
99
+ });
78
100
  }
79
- ],
80
- run: async (args) => {
81
- const { config, values, payload, renderBody } = args;
82
- if (!config.toField) return;
83
- const resolve = resolverFor(values);
84
- const to = resolve(config.toField);
85
- if (!to) return;
86
- if (typeof payload.sendEmail !== "function") throw new Error("confirmation: no email adapter configured");
87
- const subject = interpolate(config.subject ?? "", resolve);
88
- const html = await renderBody(config.body);
89
- await payload.sendEmail({
90
- to,
91
- subject,
92
- html,
93
- ...config.from ? { from: config.from } : {},
94
- ...config.cc ? { cc: interpolate(config.cc, resolve) } : {},
95
- ...config.bcc ? { bcc: interpolate(config.bcc, resolve) } : {},
96
- ...config.replyTo ? { replyTo: interpolate(config.replyTo, resolve) } : {}
97
- });
98
- }
99
- });
101
+ });
102
+ };
100
103
  buildConfirmation(true);
101
104
  //#endregion
102
105
  export { buildConfirmation };
@@ -1 +1 @@
1
- {"version":3,"file":"confirmation.js","names":[],"sources":["../../../src/actions/builtin/confirmation.ts"],"sourcesContent":["import type { PayloadRequest, RichTextField } from 'payload'\nimport { fieldNamesOfType } from '../../fields/fieldNamesOfType'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { asTranslate, labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\nimport { buildFromField, type FromAddressesResolver } from '../fromAddresses'\n\ntype ConfirmationConfig = {\n\ttoField?: string\n\tfrom?: string\n\tcc?: string\n\tbcc?: string\n\treplyTo?: string\n\tsubject?: string\n\tbody?: unknown\n}\n\nconst TO_FIELD_REF = '@10x-media/form-builder/client#FieldNameSelect'\n\n/**\n * Validates the confirmation action's `toField`: unset is fine, otherwise it must name an\n * existing `email`-type field on the form. Exported for unit testing.\n */\nexport const validateToField = (\n\tvalue: unknown,\n\t{ data, req }: { data?: unknown; req: PayloadRequest }\n): string | true => {\n\tif (typeof value !== 'string' || value.length === 0) {\n\t\treturn true\n\t}\n\tconst fields =\n\t\tdata && typeof data === 'object' ? (data as Record<string, unknown>).fields : undefined\n\treturn fieldNamesOfType(fields, ['email']).includes(value)\n\t\t? true\n\t\t: asTranslate(req.t)(keys.validationEmailFieldUnknown)\n}\n\n/**\n * `subject` and `body` are email content and follow `localize`; `toField` is an identifier and\n * `from` an address, neither of which ever does. `cc`, `bcc`, and `replyTo` are addresses that\n * follow `localize` (like `subject`), each interpolated the same way so `{{field}}` merge tags\n * work; a comma-separated list is forwarded as-is, since `payload.sendEmail` (nodemailer) accepts\n * one directly. `editor` overrides the body field's Lexical/richText editor (from the plugin's\n * `richText.editor` option). `fromAddresses`, when given (the plugin's `email.fromAddresses`\n * option), adds a `from` select sourced from the host resolver; absent, no `from` field exists and\n * every send uses the email adapter's default sender.\n */\nexport const buildConfirmation = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver\n) =>\n\tdefineAction<ConfirmationConfig>({\n\t\ttype: 'confirmation',\n\t\tlabel: keys.actionConfirmation,\n\t\tconfig: [\n\t\t\t{\n\t\t\t\tname: 'toField',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigToField),\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: {\n\t\t\t\t\t\tField: {\n\t\t\t\t\t\t\tpath: TO_FIELD_REF,\n\t\t\t\t\t\t\tclientProps: {\n\t\t\t\t\t\t\t\ttypes: ['email'],\n\t\t\t\t\t\t\t\tdescriptionKey: keys.actionConfigToFieldDescription,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvalidate: validateToField,\n\t\t\t},\n\t\t\t...(fromAddresses ? [buildFromField(fromAddresses)] : []),\n\t\t\t{\n\t\t\t\tname: 'cc',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigCc),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'bcc',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigBcc),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'replyTo',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigReplyTo),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tif (!config.toField) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst to = resolve(config.toField)\n\n\t\t\tif (!to) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('confirmation: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\n\t\t\t// `from` was validated at save time against `fromAddresses(req)`; not re-checked here\n\t\t\t// (the job's `req` may differ from the authoring admin's, and the config is\n\t\t\t// admin-authored, not visitor-controlled), so the stored value is forwarded verbatim.\n\t\t\tawait payload.sendEmail({\n\t\t\t\tto,\n\t\t\t\tsubject,\n\t\t\t\thtml,\n\t\t\t\t...(config.from ? { from: config.from } : {}),\n\t\t\t\t...(config.cc ? { cc: interpolate(config.cc, resolve) } : {}),\n\t\t\t\t...(config.bcc ? { bcc: interpolate(config.bcc, resolve) } : {}),\n\t\t\t\t...(config.replyTo ? { replyTo: interpolate(config.replyTo, resolve) } : {}),\n\t\t\t})\n\t\t},\n\t})\n\nexport const confirmation = buildConfirmation(true)\n"],"mappings":";;;;;;;;;AAoBA,MAAM,eAAe;;;;;AAMrB,MAAa,mBACZ,OACA,EAAE,MAAM,UACW;CACnB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAIR,OAAO,iBADN,QAAQ,OAAO,SAAS,WAAY,KAAiC,SAAS,KAAA,GAC/C,CAAC,OAAO,CAAC,EAAE,SAAS,KAAK,IACtD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,2BAA2B;AACvD;;;;;;;;;;;AAYA,MAAa,qBACZ,UACA,QACA,kBAEA,aAAiC;CAChC,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ;EACP;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,mBAAmB;GACxC,OAAO,EACN,YAAY,EACX,OAAO;IACN,MAAM;IACN,aAAa;KACZ,OAAO,CAAC,OAAO;KACf,gBAAgB,KAAK;IACtB;GACD,EACD,EACD;GACA,UAAU;EACX;EACA,GAAI,gBAAgB,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC;EACvD;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,cAAc;GACnC,GAAG,YAAY,QAAQ;EACxB;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,eAAe;GACpC,GAAG,YAAY,QAAQ;EACxB;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,mBAAmB;GACxC,GAAG,YAAY,QAAQ;EACxB;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,mBAAmB;GACxC,GAAG,YAAY,QAAQ;EACxB;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,gBAAgB;GACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;GACjE,GAAG,YAAY,QAAQ;GACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC5B;CACD;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;EAEhD,IAAI,CAAC,OAAO,SACX;EAGD,MAAM,UAAU,YAAY,MAAM;EAClC,MAAM,KAAK,QAAQ,OAAO,OAAO;EAEjC,IAAI,CAAC,IACJ;EAGD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,2CAA2C;EAG5D,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;EACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;EAKzC,MAAM,QAAQ,UAAU;GACvB;GACA;GACA;GACA,GAAI,OAAO,OAAO,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;GAC3C,GAAI,OAAO,KAAK,EAAE,IAAI,YAAY,OAAO,IAAI,OAAO,EAAE,IAAI,CAAC;GAC3D,GAAI,OAAO,MAAM,EAAE,KAAK,YAAY,OAAO,KAAK,OAAO,EAAE,IAAI,CAAC;GAC9D,GAAI,OAAO,UAAU,EAAE,SAAS,YAAY,OAAO,SAAS,OAAO,EAAE,IAAI,CAAC;EAC3E,CAAC;CACF;AACD,CAAC;AAE0B,kBAAkB,IAAI"}
1
+ {"version":3,"file":"confirmation.js","names":[],"sources":["../../../src/actions/builtin/confirmation.ts"],"sourcesContent":["import type { Field, PayloadRequest, RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport { fieldNamesOfType } from '../../fields/fieldNamesOfType'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { asTranslate, labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\nimport { buildRecipientField, type RecipientsConfig, resolveRecipients } from '../emailRecipients'\nimport { buildFromField, type FromAddressesResolver } from '../fromAddresses'\n\ntype ConfirmationConfig = {\n\ttoField?: string\n\tfrom?: string\n\tcc?: string[]\n\tbcc?: string[]\n\treplyTo?: string[]\n\tsubject?: string\n\tbody?: unknown\n}\n\nconst TO_FIELD_REF = '@10x-media/form-builder/client#FieldNameSelect'\n\n/**\n * Validates the confirmation action's `toField`: unset is fine, otherwise it must name an\n * existing `email`-type field on the form. Exported for unit testing.\n */\nexport const validateToField = (\n\tvalue: unknown,\n\t{ data, req }: { data?: unknown; req: PayloadRequest }\n): string | true => {\n\tif (typeof value !== 'string' || value.length === 0) {\n\t\treturn true\n\t}\n\tconst fields =\n\t\tdata && typeof data === 'object' ? (data as Record<string, unknown>).fields : undefined\n\treturn fieldNamesOfType(fields, ['email']).includes(value)\n\t\t? true\n\t\t: asTranslate(req.t)(keys.validationEmailFieldUnknown)\n}\n\n/**\n * `toField` names the form's own email field the confirmation is sent to (a `FieldNameSelect`, not a\n * recipient list). `replyTo`, `cc`, and `bcc` are recipient lists (`RecipientsSelect`, `string[]`):\n * literal emails, picked department addresses (when `departments` is set), or `{{field}}` tokens\n * resolved from the submission at send. `from` is a single address, added only when `fromAddresses`\n * is set. `subject`/`body` are content; `editor` overrides the body editor; `recipients` narrows the\n * recipient fields' behavior. Content and recipient fields carry `localized: true` when `localize`.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args thread plugin options (localize, editor, fromAddresses, departments, recipients)\nexport const buildConfirmation = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver,\n\trecipients?: RecipientsConfig\n) => {\n\tconst endpoint = departments ? 'departments' : undefined\n\tconst recip = (name: string, labelKey: string) =>\n\t\tbuildRecipientField(name, labelKey, localize, { endpoint, recipients, width: '50%' })\n\tconst toField: Field = {\n\t\tname: 'toField',\n\t\ttype: 'text',\n\t\tlabel: labelFor(keys.actionConfigToField),\n\t\tadmin: {\n\t\t\twidth: '50%',\n\t\t\tcomponents: {\n\t\t\t\tField: {\n\t\t\t\t\tpath: TO_FIELD_REF,\n\t\t\t\t\tclientProps: { types: ['email'], descriptionKey: keys.actionConfigToFieldDescription },\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tvalidate: validateToField,\n\t}\n\treturn defineAction<ConfirmationConfig>({\n\t\ttype: 'confirmation',\n\t\tlabel: keys.actionConfirmation,\n\t\tconfig: [\n\t\t\t{ type: 'row', fields: [toField, recip('replyTo', keys.actionConfigReplyTo)] },\n\t\t\t...(fromAddresses ? [buildFromField(fromAddresses)] : []),\n\t\t\t{\n\t\t\t\ttype: 'row',\n\t\t\t\tfields: [recip('cc', keys.actionConfigCc), recip('bcc', keys.actionConfigBcc)],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tif (!config.toField) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst to = resolve(config.toField)\n\n\t\t\tif (!to) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('confirmation: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\t\t\tconst cc = resolveRecipients(config.cc, resolve)\n\t\t\tconst bcc = resolveRecipients(config.bcc, resolve)\n\t\t\tconst replyTo = resolveRecipients(config.replyTo, resolve)\n\n\t\t\t// `from` was validated at save time against `fromAddresses(req)`; not re-checked here\n\t\t\t// (the job's `req` may differ from the authoring admin's, and the config is\n\t\t\t// admin-authored, not visitor-controlled), so the stored value is forwarded verbatim.\n\t\t\tawait payload.sendEmail({\n\t\t\t\tto,\n\t\t\t\tsubject,\n\t\t\t\thtml,\n\t\t\t\t...(config.from ? { from: config.from } : {}),\n\t\t\t\t...(cc ? { cc } : {}),\n\t\t\t\t...(bcc ? { bcc } : {}),\n\t\t\t\t...(replyTo ? { replyTo } : {}),\n\t\t\t})\n\t\t},\n\t})\n}\n\nexport const confirmation = buildConfirmation(true)\n"],"mappings":";;;;;;;;;;AAsBA,MAAM,eAAe;;;;;AAMrB,MAAa,mBACZ,OACA,EAAE,MAAM,UACW;CACnB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAIR,OAAO,iBADN,QAAQ,OAAO,SAAS,WAAY,KAAiC,SAAS,KAAA,GAC/C,CAAC,OAAO,CAAC,EAAE,SAAS,KAAK,IACtD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,2BAA2B;AACvD;;;;;;;;;AAWA,MAAa,qBACZ,UACA,QACA,eACA,aACA,eACI;CACJ,MAAM,WAAW,cAAc,gBAAgB,KAAA;CAC/C,MAAM,SAAS,MAAc,aAC5B,oBAAoB,MAAM,UAAU,UAAU;EAAE;EAAU;EAAY,OAAO;CAAM,CAAC;CACrF,MAAM,UAAiB;EACtB,MAAM;EACN,MAAM;EACN,OAAO,SAAS,KAAK,mBAAmB;EACxC,OAAO;GACN,OAAO;GACP,YAAY,EACX,OAAO;IACN,MAAM;IACN,aAAa;KAAE,OAAO,CAAC,OAAO;KAAG,gBAAgB,KAAK;IAA+B;GACtF,EACD;EACD;EACA,UAAU;CACX;CACA,OAAO,aAAiC;EACvC,MAAM;EACN,OAAO,KAAK;EACZ,QAAQ;GACP;IAAE,MAAM;IAAO,QAAQ,CAAC,SAAS,MAAM,WAAW,KAAK,mBAAmB,CAAC;GAAE;GAC7E,GAAI,gBAAgB,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC;GACvD;IACC,MAAM;IACN,QAAQ,CAAC,MAAM,MAAM,KAAK,cAAc,GAAG,MAAM,OAAO,KAAK,eAAe,CAAC;GAC9E;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,gBAAgB;IACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;IACjE,GAAG,YAAY,QAAQ;IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAC5B;EACD;EACA,KAAK,OAAO,SAAS;GACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;GAEhD,IAAI,CAAC,OAAO,SACX;GAGD,MAAM,UAAU,YAAY,MAAM;GAClC,MAAM,KAAK,QAAQ,OAAO,OAAO;GAEjC,IAAI,CAAC,IACJ;GAGD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,2CAA2C;GAG5D,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;GACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;GACzC,MAAM,KAAK,kBAAkB,OAAO,IAAI,OAAO;GAC/C,MAAM,MAAM,kBAAkB,OAAO,KAAK,OAAO;GACjD,MAAM,UAAU,kBAAkB,OAAO,SAAS,OAAO;GAKzD,MAAM,QAAQ,UAAU;IACvB;IACA;IACA;IACA,GAAI,OAAO,OAAO,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;IAC3C,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC;IACnB,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;IACrB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;GAC9B,CAAC;EACF;CACD,CAAC;AACF;AAE4B,kBAAkB,IAAI"}
@@ -4,52 +4,38 @@ import { keys } from "../../translations/keys.js";
4
4
  import { labelFor } from "../../translations/server.js";
5
5
  import { resolverFor } from "../body/serializeBody.js";
6
6
  import { defineAction } from "../defineAction.js";
7
+ import { buildRecipientField, resolveRecipients } from "../emailRecipients.js";
7
8
  import { buildFromField } from "../fromAddresses.js";
8
- import { buildToField } from "../../email/departments.js";
9
9
  //#region src/actions/builtin/emailTeam.ts
10
10
  /**
11
- * `subject` and `body` are email content and follow `localize`; `from` is an address that never
12
- * does. `to` is localized (following `localize`): a submission's locale selects the stored `to` at
13
- * send, so a German submission routes to the German address. `cc`, `bcc`, and `replyTo` follow the
14
- * same localized-address posture as `to`, each interpolated the same as `subject` so `{{field}}`
15
- * merge tags work; a comma-separated list is forwarded as-is, since `payload.sendEmail`
16
- * (nodemailer) accepts one directly. `editor` overrides the body field's Lexical/richText editor
17
- * (from the plugin's `richText.editor` option). `fromAddresses`, when given (the plugin's
18
- * `email.fromAddresses` option), adds a `from` select sourced from the host resolver; absent, no
19
- * `from` field exists and every send uses the email adapter's default sender. `departments`, when
20
- * given (the plugin's `email.departments` option), turns `to` into a select whose options are the
21
- * host's resolved departments; absent, `to` stays a plain (localized) text field.
11
+ * `to`, `replyTo`, `cc`, and `bcc` are recipient lists (`RecipientsSelect`, stored as `string[]`):
12
+ * each entry is a literal email, a picked department address (when `departments` is set its options
13
+ * back the select), or a `{{field}}` token resolved from the submission at send. `from` is a single
14
+ * address that never localizes, added only when `fromAddresses` is set. `subject` and `body` are email
15
+ * content; `editor` overrides the body's richText editor. `recipients` narrows the recipient fields'
16
+ * behavior (free-typed emails, field tokens). Content and recipient fields carry `localized: true`
17
+ * when `localize` is true. At send, every recipient list is interpolated, empties dropped, and joined
18
+ * into the comma-separated string `payload.sendEmail` (nodemailer) accepts.
22
19
  */
23
- const buildEmailTeam = (localize, editor, fromAddresses, departments) => {
24
- const toField = departments ? buildToField(departments, localize) : {
25
- name: "to",
26
- type: "text",
27
- label: labelFor(keys.actionConfigTo),
28
- ...localizedIf(localize)
29
- };
20
+ const buildEmailTeam = (localize, editor, fromAddresses, departments, recipients) => {
21
+ const endpoint = departments ? "departments" : void 0;
22
+ const recip = (name, labelKey) => buildRecipientField(name, labelKey, localize, {
23
+ endpoint,
24
+ recipients,
25
+ width: "50%"
26
+ });
30
27
  return defineAction({
31
28
  type: "emailTeam",
32
29
  label: keys.actionEmailTeam,
33
30
  config: [
34
- toField,
35
- ...fromAddresses ? [buildFromField(fromAddresses)] : [],
36
31
  {
37
- name: "cc",
38
- type: "text",
39
- label: labelFor(keys.actionConfigCc),
40
- ...localizedIf(localize)
41
- },
42
- {
43
- name: "bcc",
44
- type: "text",
45
- label: labelFor(keys.actionConfigBcc),
46
- ...localizedIf(localize)
32
+ type: "row",
33
+ fields: [recip("to", keys.actionConfigTo), recip("replyTo", keys.actionConfigReplyTo)]
47
34
  },
35
+ ...fromAddresses ? [buildFromField(fromAddresses)] : [],
48
36
  {
49
- name: "replyTo",
50
- type: "text",
51
- label: labelFor(keys.actionConfigReplyTo),
52
- ...localizedIf(localize)
37
+ type: "row",
38
+ fields: [recip("cc", keys.actionConfigCc), recip("bcc", keys.actionConfigBcc)]
53
39
  },
54
40
  {
55
41
  name: "subject",
@@ -68,19 +54,23 @@ const buildEmailTeam = (localize, editor, fromAddresses, departments) => {
68
54
  ],
69
55
  run: async (args) => {
70
56
  const { config, values, payload, renderBody } = args;
71
- if (!config.to) throw new Error("emailTeam: missing \"to\" address");
72
- if (typeof payload.sendEmail !== "function") throw new Error("emailTeam: no email adapter configured");
73
57
  const resolve = resolverFor(values);
58
+ const to = resolveRecipients(config.to, resolve);
59
+ if (!to) throw new Error("emailTeam: missing \"to\" address");
60
+ if (typeof payload.sendEmail !== "function") throw new Error("emailTeam: no email adapter configured");
74
61
  const subject = interpolate(config.subject ?? "", resolve);
75
62
  const html = await renderBody(config.body);
63
+ const cc = resolveRecipients(config.cc, resolve);
64
+ const bcc = resolveRecipients(config.bcc, resolve);
65
+ const replyTo = resolveRecipients(config.replyTo, resolve);
76
66
  await payload.sendEmail({
77
- to: config.to,
67
+ to,
78
68
  subject,
79
69
  html,
80
70
  ...config.from ? { from: config.from } : {},
81
- ...config.cc ? { cc: interpolate(config.cc, resolve) } : {},
82
- ...config.bcc ? { bcc: interpolate(config.bcc, resolve) } : {},
83
- ...config.replyTo ? { replyTo: interpolate(config.replyTo, resolve) } : {}
71
+ ...cc ? { cc } : {},
72
+ ...bcc ? { bcc } : {},
73
+ ...replyTo ? { replyTo } : {}
84
74
  });
85
75
  }
86
76
  });
@@ -1 +1 @@
1
- {"version":3,"file":"emailTeam.js","names":[],"sources":["../../../src/actions/builtin/emailTeam.ts"],"sourcesContent":["import type { RichTextField, TextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport { buildToField } from '../../email/departments'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\nimport { buildFromField, type FromAddressesResolver } from '../fromAddresses'\n\ntype EmailTeamConfig = {\n\tto?: string\n\tfrom?: string\n\tcc?: string\n\tbcc?: string\n\treplyTo?: string\n\tsubject?: string\n\tbody?: unknown\n}\n\n/**\n * `subject` and `body` are email content and follow `localize`; `from` is an address that never\n * does. `to` is localized (following `localize`): a submission's locale selects the stored `to` at\n * send, so a German submission routes to the German address. `cc`, `bcc`, and `replyTo` follow the\n * same localized-address posture as `to`, each interpolated the same as `subject` so `{{field}}`\n * merge tags work; a comma-separated list is forwarded as-is, since `payload.sendEmail`\n * (nodemailer) accepts one directly. `editor` overrides the body field's Lexical/richText editor\n * (from the plugin's `richText.editor` option). `fromAddresses`, when given (the plugin's\n * `email.fromAddresses` option), adds a `from` select sourced from the host resolver; absent, no\n * `from` field exists and every send uses the email adapter's default sender. `departments`, when\n * given (the plugin's `email.departments` option), turns `to` into a select whose options are the\n * host's resolved departments; absent, `to` stays a plain (localized) text field.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args mirror the fromAddresses threading (localize, editor, fromAddresses, departments)\nexport const buildEmailTeam = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver\n) => {\n\tconst toField: TextField = departments\n\t\t? buildToField(departments, localize)\n\t\t: { name: 'to', type: 'text', label: labelFor(keys.actionConfigTo), ...localizedIf(localize) }\n\treturn defineAction<EmailTeamConfig>({\n\t\ttype: 'emailTeam',\n\t\tlabel: keys.actionEmailTeam,\n\t\tconfig: [\n\t\t\ttoField,\n\t\t\t...(fromAddresses ? [buildFromField(fromAddresses)] : []),\n\t\t\t{\n\t\t\t\tname: 'cc',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigCc),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'bcc',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigBcc),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'replyTo',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigReplyTo),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tif (!config.to) {\n\t\t\t\tthrow new Error('emailTeam: missing \"to\" address')\n\t\t\t}\n\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('emailTeam: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\n\t\t\t// `from` was validated at save time against `fromAddresses(req)`; not re-checked here\n\t\t\t// (the job's `req` may differ from the authoring admin's, and the config is\n\t\t\t// admin-authored, not visitor-controlled), so the stored value is forwarded verbatim.\n\t\t\tawait payload.sendEmail({\n\t\t\t\tto: config.to,\n\t\t\t\tsubject,\n\t\t\t\thtml,\n\t\t\t\t...(config.from ? { from: config.from } : {}),\n\t\t\t\t...(config.cc ? { cc: interpolate(config.cc, resolve) } : {}),\n\t\t\t\t...(config.bcc ? { bcc: interpolate(config.bcc, resolve) } : {}),\n\t\t\t\t...(config.replyTo ? { replyTo: interpolate(config.replyTo, resolve) } : {}),\n\t\t\t})\n\t\t},\n\t})\n}\n\nexport const emailTeam = buildEmailTeam(true)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,kBACZ,UACA,QACA,eACA,gBACI;CACJ,MAAM,UAAqB,cACxB,aAAa,aAAa,QAAQ,IAClC;EAAE,MAAM;EAAM,MAAM;EAAQ,OAAO,SAAS,KAAK,cAAc;EAAG,GAAG,YAAY,QAAQ;CAAE;CAC9F,OAAO,aAA8B;EACpC,MAAM;EACN,OAAO,KAAK;EACZ,QAAQ;GACP;GACA,GAAI,gBAAgB,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC;GACvD;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,cAAc;IACnC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,eAAe;IACpC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,gBAAgB;IACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;IACjE,GAAG,YAAY,QAAQ;IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAC5B;EACD;EACA,KAAK,OAAO,SAAS;GACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;GAEhD,IAAI,CAAC,OAAO,IACX,MAAM,IAAI,MAAM,mCAAiC;GAGlD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,wCAAwC;GAGzD,MAAM,UAAU,YAAY,MAAM;GAClC,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;GACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;GAKzC,MAAM,QAAQ,UAAU;IACvB,IAAI,OAAO;IACX;IACA;IACA,GAAI,OAAO,OAAO,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;IAC3C,GAAI,OAAO,KAAK,EAAE,IAAI,YAAY,OAAO,IAAI,OAAO,EAAE,IAAI,CAAC;IAC3D,GAAI,OAAO,MAAM,EAAE,KAAK,YAAY,OAAO,KAAK,OAAO,EAAE,IAAI,CAAC;IAC9D,GAAI,OAAO,UAAU,EAAE,SAAS,YAAY,OAAO,SAAS,OAAO,EAAE,IAAI,CAAC;GAC3E,CAAC;EACF;CACD,CAAC;AACF;AAEyB,eAAe,IAAI"}
1
+ {"version":3,"file":"emailTeam.js","names":[],"sources":["../../../src/actions/builtin/emailTeam.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\nimport { buildRecipientField, type RecipientsConfig, resolveRecipients } from '../emailRecipients'\nimport { buildFromField, type FromAddressesResolver } from '../fromAddresses'\n\ntype EmailTeamConfig = {\n\tto?: string[]\n\tfrom?: string\n\tcc?: string[]\n\tbcc?: string[]\n\treplyTo?: string[]\n\tsubject?: string\n\tbody?: unknown\n}\n\n/**\n * `to`, `replyTo`, `cc`, and `bcc` are recipient lists (`RecipientsSelect`, stored as `string[]`):\n * each entry is a literal email, a picked department address (when `departments` is set its options\n * back the select), or a `{{field}}` token resolved from the submission at send. `from` is a single\n * address that never localizes, added only when `fromAddresses` is set. `subject` and `body` are email\n * content; `editor` overrides the body's richText editor. `recipients` narrows the recipient fields'\n * behavior (free-typed emails, field tokens). Content and recipient fields carry `localized: true`\n * when `localize` is true. At send, every recipient list is interpolated, empties dropped, and joined\n * into the comma-separated string `payload.sendEmail` (nodemailer) accepts.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args thread plugin options (localize, editor, fromAddresses, departments, recipients)\nexport const buildEmailTeam = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver,\n\trecipients?: RecipientsConfig\n) => {\n\tconst endpoint = departments ? 'departments' : undefined\n\tconst recip = (name: string, labelKey: string) =>\n\t\tbuildRecipientField(name, labelKey, localize, { endpoint, recipients, width: '50%' })\n\treturn defineAction<EmailTeamConfig>({\n\t\ttype: 'emailTeam',\n\t\tlabel: keys.actionEmailTeam,\n\t\tconfig: [\n\t\t\t{\n\t\t\t\ttype: 'row',\n\t\t\t\tfields: [recip('to', keys.actionConfigTo), recip('replyTo', keys.actionConfigReplyTo)],\n\t\t\t},\n\t\t\t...(fromAddresses ? [buildFromField(fromAddresses)] : []),\n\t\t\t{\n\t\t\t\ttype: 'row',\n\t\t\t\tfields: [recip('cc', keys.actionConfigCc), recip('bcc', keys.actionConfigBcc)],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst to = resolveRecipients(config.to, resolve)\n\t\t\tif (!to) {\n\t\t\t\tthrow new Error('emailTeam: missing \"to\" address')\n\t\t\t}\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('emailTeam: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\t\t\tconst cc = resolveRecipients(config.cc, resolve)\n\t\t\tconst bcc = resolveRecipients(config.bcc, resolve)\n\t\t\tconst replyTo = resolveRecipients(config.replyTo, resolve)\n\n\t\t\t// `from` was validated at save time against `fromAddresses(req)`; not re-checked here\n\t\t\t// (the job's `req` may differ from the authoring admin's, and the config is\n\t\t\t// admin-authored, not visitor-controlled), so the stored value is forwarded verbatim.\n\t\t\tawait payload.sendEmail({\n\t\t\t\tto,\n\t\t\t\tsubject,\n\t\t\t\thtml,\n\t\t\t\t...(config.from ? { from: config.from } : {}),\n\t\t\t\t...(cc ? { cc } : {}),\n\t\t\t\t...(bcc ? { bcc } : {}),\n\t\t\t\t...(replyTo ? { replyTo } : {}),\n\t\t\t})\n\t\t},\n\t})\n}\n\nexport const emailTeam = buildEmailTeam(true)\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgCA,MAAa,kBACZ,UACA,QACA,eACA,aACA,eACI;CACJ,MAAM,WAAW,cAAc,gBAAgB,KAAA;CAC/C,MAAM,SAAS,MAAc,aAC5B,oBAAoB,MAAM,UAAU,UAAU;EAAE;EAAU;EAAY,OAAO;CAAM,CAAC;CACrF,OAAO,aAA8B;EACpC,MAAM;EACN,OAAO,KAAK;EACZ,QAAQ;GACP;IACC,MAAM;IACN,QAAQ,CAAC,MAAM,MAAM,KAAK,cAAc,GAAG,MAAM,WAAW,KAAK,mBAAmB,CAAC;GACtF;GACA,GAAI,gBAAgB,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC;GACvD;IACC,MAAM;IACN,QAAQ,CAAC,MAAM,MAAM,KAAK,cAAc,GAAG,MAAM,OAAO,KAAK,eAAe,CAAC;GAC9E;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,gBAAgB;IACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;IACjE,GAAG,YAAY,QAAQ;IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAC5B;EACD;EACA,KAAK,OAAO,SAAS;GACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;GAEhD,MAAM,UAAU,YAAY,MAAM;GAClC,MAAM,KAAK,kBAAkB,OAAO,IAAI,OAAO;GAC/C,IAAI,CAAC,IACJ,MAAM,IAAI,MAAM,mCAAiC;GAElD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,wCAAwC;GAGzD,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;GACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;GACzC,MAAM,KAAK,kBAAkB,OAAO,IAAI,OAAO;GAC/C,MAAM,MAAM,kBAAkB,OAAO,KAAK,OAAO;GACjD,MAAM,UAAU,kBAAkB,OAAO,SAAS,OAAO;GAKzD,MAAM,QAAQ,UAAU;IACvB;IACA;IACA;IACA,GAAI,OAAO,OAAO,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;IAC3C,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC;IACnB,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;IACrB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;GAC9B,CAAC;EACF;CACD,CAAC;AACF;AAEyB,eAAe,IAAI"}
@@ -1,5 +1,6 @@
1
1
  import { AnyActionDefinition } from "../defineAction.js";
2
2
  import { FromAddressesResolver } from "../fromAddresses.js";
3
+ import { RecipientsConfig } from "../emailRecipients.js";
3
4
  import { DepartmentEmailsResolver } from "../../email/departments.js";
4
5
  import { RichTextField } from "payload";
5
6
 
@@ -13,7 +14,7 @@ import { RichTextField } from "payload";
13
14
  * given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the
14
15
  * host's resolved departments. `signedWebhook` has no content fields and is shared static.
15
16
  */
16
- declare const buildDefaultActionDefinitions: (localize: boolean, editor?: RichTextField["editor"], fromAddresses?: FromAddressesResolver, departments?: DepartmentEmailsResolver) => Record<string, AnyActionDefinition>;
17
+ declare const buildDefaultActionDefinitions: (localize: boolean, editor?: RichTextField["editor"], fromAddresses?: FromAddressesResolver, departments?: DepartmentEmailsResolver, recipients?: RecipientsConfig) => Record<string, AnyActionDefinition>;
17
18
  /**
18
19
  * Prebuilt with content localization on; with `localizeContent: false`, derive overrides from
19
20
  * `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.
@@ -11,9 +11,9 @@ import { signedWebhook } from "./signedWebhook.js";
11
11
  * given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the
12
12
  * host's resolved departments. `signedWebhook` has no content fields and is shared static.
13
13
  */
14
- const buildDefaultActionDefinitions = (localize, editor, fromAddresses, departments) => ({
15
- emailTeam: buildEmailTeam(localize, editor, fromAddresses, departments),
16
- confirmation: buildConfirmation(localize, editor, fromAddresses),
14
+ const buildDefaultActionDefinitions = (localize, editor, fromAddresses, departments, recipients) => ({
15
+ emailTeam: buildEmailTeam(localize, editor, fromAddresses, departments, recipients),
16
+ confirmation: buildConfirmation(localize, editor, fromAddresses, departments, recipients),
17
17
  signedWebhook
18
18
  });
19
19
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/actions/builtin/index.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport type { AnyActionDefinition } from '../defineAction'\nimport type { FromAddressesResolver } from '../fromAddresses'\nimport { buildConfirmation } from './confirmation'\nimport { buildEmailTeam } from './emailTeam'\nimport { signedWebhook } from './signedWebhook'\n\n/**\n * Built-in action definitions with content-bearing config fields (email subjects and rich text\n * bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides\n * the Lexical/richText editor on both action bodies (the plugin passes\n * `richText.bodyEditor ?? richText.editor`). `fromAddresses`, when given (the plugin's\n * `email.fromAddresses` option), adds a `from` select to both email actions. `departments`, when\n * given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the\n * host's resolved departments. `signedWebhook` has no content fields and is shared static.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args thread the plugin editor and email resolvers (localize, editor, fromAddresses, departments)\nexport const buildDefaultActionDefinitions = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver\n): Record<string, AnyActionDefinition> => ({\n\temailTeam: buildEmailTeam(localize, editor, fromAddresses, departments) as AnyActionDefinition,\n\tconfirmation: buildConfirmation(localize, editor, fromAddresses) as AnyActionDefinition,\n\tsignedWebhook: signedWebhook as AnyActionDefinition,\n})\n\n/**\n * Prebuilt with content localization on; with `localizeContent: false`, derive overrides from\n * `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.\n */\nexport const defaultActionDefinitions: Record<string, AnyActionDefinition> =\n\tbuildDefaultActionDefinitions(true)\n"],"mappings":";;;;;;;;;;;;;AAkBA,MAAa,iCACZ,UACA,QACA,eACA,iBAC0C;CAC1C,WAAW,eAAe,UAAU,QAAQ,eAAe,WAAW;CACtE,cAAc,kBAAkB,UAAU,QAAQ,aAAa;CAChD;AAChB;;;;;AAMA,MAAa,2BACZ,8BAA8B,IAAI"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/actions/builtin/index.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport type { AnyActionDefinition } from '../defineAction'\nimport type { RecipientsConfig } from '../emailRecipients'\nimport type { FromAddressesResolver } from '../fromAddresses'\nimport { buildConfirmation } from './confirmation'\nimport { buildEmailTeam } from './emailTeam'\nimport { signedWebhook } from './signedWebhook'\n\n/**\n * Built-in action definitions with content-bearing config fields (email subjects and rich text\n * bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides\n * the Lexical/richText editor on both action bodies (the plugin passes\n * `richText.bodyEditor ?? richText.editor`). `fromAddresses`, when given (the plugin's\n * `email.fromAddresses` option), adds a `from` select to both email actions. `departments`, when\n * given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the\n * host's resolved departments. `signedWebhook` has no content fields and is shared static.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args thread the plugin editor and email options (localize, editor, fromAddresses, departments, recipients)\nexport const buildDefaultActionDefinitions = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver,\n\trecipients?: RecipientsConfig\n): Record<string, AnyActionDefinition> => ({\n\temailTeam: buildEmailTeam(\n\t\tlocalize,\n\t\teditor,\n\t\tfromAddresses,\n\t\tdepartments,\n\t\trecipients\n\t) as AnyActionDefinition,\n\tconfirmation: buildConfirmation(\n\t\tlocalize,\n\t\teditor,\n\t\tfromAddresses,\n\t\tdepartments,\n\t\trecipients\n\t) as AnyActionDefinition,\n\tsignedWebhook: signedWebhook as AnyActionDefinition,\n})\n\n/**\n * Prebuilt with content localization on; with `localizeContent: false`, derive overrides from\n * `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.\n */\nexport const defaultActionDefinitions: Record<string, AnyActionDefinition> =\n\tbuildDefaultActionDefinitions(true)\n"],"mappings":";;;;;;;;;;;;;AAmBA,MAAa,iCACZ,UACA,QACA,eACA,aACA,gBAC0C;CAC1C,WAAW,eACV,UACA,QACA,eACA,aACA,UACD;CACA,cAAc,kBACb,UACA,QACA,eACA,aACA,UACD;CACe;AAChB;;;;;AAMA,MAAa,2BACZ,8BAA8B,IAAI"}
@@ -0,0 +1,12 @@
1
+ import { PayloadRequest } from "payload";
2
+
3
+ //#region src/actions/emailRecipients.d.ts
4
+ /** Host-configurable behavior for the recipient fields (plugin option `email.recipients`). */
5
+ type RecipientsConfig = {
6
+ /** Allow free-typed emails (default true). */allowCustom?: boolean; /** Offer the form's own fields as recipient tokens (default true). */
7
+ fieldTokens?: boolean; /** Field types eligible as tokens (default `['email']`). */
8
+ tokenFieldTypes?: string[];
9
+ };
10
+ //#endregion
11
+ export { RecipientsConfig };
12
+ //# sourceMappingURL=emailRecipients.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { fieldNames, fieldNamesOfType } from "../fields/fieldNamesOfType.js";
2
+ import { localizedIf } from "../fields/localizedIf.js";
3
+ import { interpolate } from "../recall/interpolate.js";
4
+ import { keys } from "../translations/keys.js";
5
+ import { asTranslate, labelFor } from "../translations/server.js";
6
+ //#region src/actions/emailRecipients.ts
7
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8
+ const TOKEN_RE = /^\{\{\s*([\w.-]+)\s*\}\}$/;
9
+ /** A plausible email address (a permissive shape check, not full RFC validation). */
10
+ const isPlausibleEmail = (value) => EMAIL_RE.test(value.trim());
11
+ /** The field name inside a `{{name}}` recipient token, or undefined when the string is not a token. */
12
+ const parseFieldToken = (value) => {
13
+ const match = TOKEN_RE.exec(value.trim());
14
+ return match ? match[1] : void 0;
15
+ };
16
+ const toList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : typeof value === "string" && value.length > 0 ? [value] : [];
17
+ /**
18
+ * Resolve a stored recipient value (a `string[]`, or a legacy single string) to the comma-separated
19
+ * list `payload.sendEmail` accepts: each entry is interpolated (a `{{field}}` token resolves from the
20
+ * submission, a plain email passes through), trimmed, empties dropped, joined with `, `.
21
+ */
22
+ const resolveRecipients = (value, resolve) => toList(value).map((entry) => interpolate(entry, resolve).trim()).filter((entry) => entry.length > 0).join(", ");
23
+ /**
24
+ * Field `validate` for a recipient list: unset is fine; otherwise every entry must be a valid email
25
+ * or a `{{field}}` token naming an existing field of an allowed token type (`tokenFieldTypes`, or any
26
+ * named field when empty). Reads the form's `fields` off `data`, like the confirmation `toField`.
27
+ */
28
+ const validateRecipients = (tokenFieldTypes) => (value, { data, req }) => {
29
+ const list = toList(value);
30
+ if (list.length === 0) return true;
31
+ const fields = data && typeof data === "object" ? data.fields : void 0;
32
+ const allowed = new Set(tokenFieldTypes && tokenFieldTypes.length > 0 ? fieldNamesOfType(fields, tokenFieldTypes) : fieldNames(fields));
33
+ for (const entry of list) {
34
+ const token = parseFieldToken(entry);
35
+ if (token) {
36
+ if (!allowed.has(token)) return asTranslate(req.t)(keys.validationRecipientUnknownField);
37
+ continue;
38
+ }
39
+ if (!isPlausibleEmail(entry)) return asTranslate(req.t)(keys.validationRecipientInvalid);
40
+ }
41
+ return true;
42
+ };
43
+ const RECIPIENTS_FIELD_REF = "@10x-media/form-builder/client#RecipientsSelect";
44
+ /**
45
+ * A `text hasMany` field rendered by `RecipientsSelect`, used for every email address list. `endpoint`
46
+ * (when set) supplies preset options (e.g. the host's departments); `recipients` narrows the field's
47
+ * behavior; `width` sets `admin.width` so a pair can share a row.
48
+ */
49
+ const buildRecipientField = (name, labelKey, localize, opts = {}) => ({
50
+ name,
51
+ type: "text",
52
+ hasMany: true,
53
+ label: labelFor(labelKey),
54
+ validate: validateRecipients(opts.recipients?.tokenFieldTypes ?? ["email"]),
55
+ ...localizedIf(localize),
56
+ admin: {
57
+ ...opts.width ? { width: opts.width } : {},
58
+ components: { Field: {
59
+ path: RECIPIENTS_FIELD_REF,
60
+ clientProps: {
61
+ ...opts.endpoint ? { endpoint: opts.endpoint } : {},
62
+ ...opts.recipients?.allowCustom === false ? { allowCustom: false } : {},
63
+ ...opts.recipients?.fieldTokens === false ? { fieldTokens: false } : {},
64
+ ...opts.recipients?.tokenFieldTypes ? { tokenFieldTypes: opts.recipients.tokenFieldTypes } : {}
65
+ }
66
+ } }
67
+ }
68
+ });
69
+ //#endregion
70
+ export { buildRecipientField, isPlausibleEmail, resolveRecipients };
71
+
72
+ //# sourceMappingURL=emailRecipients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emailRecipients.js","names":[],"sources":["../../src/actions/emailRecipients.ts"],"sourcesContent":["import type { PayloadRequest, TextField } from 'payload'\nimport { fieldNames, fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport { localizedIf } from '../fields/localizedIf'\nimport { interpolate } from '../recall/interpolate'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelFor } from '../translations/server'\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst TOKEN_RE = /^\\{\\{\\s*([\\w.-]+)\\s*\\}\\}$/\n\n/** A plausible email address (a permissive shape check, not full RFC validation). */\nexport const isPlausibleEmail = (value: string): boolean => EMAIL_RE.test(value.trim())\n\n/** The field name inside a `{{name}}` recipient token, or undefined when the string is not a token. */\nexport const parseFieldToken = (value: string): string | undefined => {\n\tconst match = TOKEN_RE.exec(value.trim())\n\treturn match ? match[1] : undefined\n}\n\nexport const isFieldToken = (value: string): boolean => parseFieldToken(value) !== undefined\n\nconst toList = (value: unknown): string[] =>\n\tArray.isArray(value)\n\t\t? value.filter((entry): entry is string => typeof entry === 'string')\n\t\t: typeof value === 'string' && value.length > 0\n\t\t\t? [value]\n\t\t\t: []\n\n/**\n * Resolve a stored recipient value (a `string[]`, or a legacy single string) to the comma-separated\n * list `payload.sendEmail` accepts: each entry is interpolated (a `{{field}}` token resolves from the\n * submission, a plain email passes through), trimmed, empties dropped, joined with `, `.\n */\nexport const resolveRecipients = (value: unknown, resolve: (name: string) => string): string =>\n\ttoList(value)\n\t\t.map((entry) => interpolate(entry, resolve).trim())\n\t\t.filter((entry) => entry.length > 0)\n\t\t.join(', ')\n\n/**\n * Field `validate` for a recipient list: unset is fine; otherwise every entry must be a valid email\n * or a `{{field}}` token naming an existing field of an allowed token type (`tokenFieldTypes`, or any\n * named field when empty). Reads the form's `fields` off `data`, like the confirmation `toField`.\n */\nexport const validateRecipients =\n\t(tokenFieldTypes?: string[]) =>\n\t(value: unknown, { data, req }: { data?: unknown; req: PayloadRequest }): string | true => {\n\t\tconst list = toList(value)\n\t\tif (list.length === 0) {\n\t\t\treturn true\n\t\t}\n\t\tconst fields =\n\t\t\tdata && typeof data === 'object' ? (data as Record<string, unknown>).fields : undefined\n\t\tconst allowed = new Set(\n\t\t\ttokenFieldTypes && tokenFieldTypes.length > 0\n\t\t\t\t? fieldNamesOfType(fields, tokenFieldTypes)\n\t\t\t\t: fieldNames(fields)\n\t\t)\n\t\tfor (const entry of list) {\n\t\t\tconst token = parseFieldToken(entry)\n\t\t\tif (token) {\n\t\t\t\tif (!allowed.has(token)) {\n\t\t\t\t\treturn asTranslate(req.t)(keys.validationRecipientUnknownField)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (!isPlausibleEmail(entry)) {\n\t\t\t\treturn asTranslate(req.t)(keys.validationRecipientInvalid)\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n/** Host-configurable behavior for the recipient fields (plugin option `email.recipients`). */\nexport type RecipientsConfig = {\n\t/** Allow free-typed emails (default true). */\n\tallowCustom?: boolean\n\t/** Offer the form's own fields as recipient tokens (default true). */\n\tfieldTokens?: boolean\n\t/** Field types eligible as tokens (default `['email']`). */\n\ttokenFieldTypes?: string[]\n}\n\nconst RECIPIENTS_FIELD_REF = '@10x-media/form-builder/client#RecipientsSelect'\n\n/**\n * A `text hasMany` field rendered by `RecipientsSelect`, used for every email address list. `endpoint`\n * (when set) supplies preset options (e.g. the host's departments); `recipients` narrows the field's\n * behavior; `width` sets `admin.width` so a pair can share a row.\n */\n// biome-ignore lint/complexity/useMaxParams: the field identity (name, label, localize) plus its grouped options is the minimal surface\nexport const buildRecipientField = (\n\tname: string,\n\tlabelKey: string,\n\tlocalize: boolean,\n\topts: { endpoint?: string; recipients?: RecipientsConfig; width?: string } = {}\n): TextField => ({\n\tname,\n\ttype: 'text',\n\thasMany: true,\n\tlabel: labelFor(labelKey),\n\tvalidate: validateRecipients(opts.recipients?.tokenFieldTypes ?? ['email']),\n\t...localizedIf(localize),\n\tadmin: {\n\t\t...(opts.width ? { width: opts.width } : {}),\n\t\tcomponents: {\n\t\t\tField: {\n\t\t\t\tpath: RECIPIENTS_FIELD_REF,\n\t\t\t\tclientProps: {\n\t\t\t\t\t...(opts.endpoint ? { endpoint: opts.endpoint } : {}),\n\t\t\t\t\t...(opts.recipients?.allowCustom === false ? { allowCustom: false } : {}),\n\t\t\t\t\t...(opts.recipients?.fieldTokens === false ? { fieldTokens: false } : {}),\n\t\t\t\t\t...(opts.recipients?.tokenFieldTypes\n\t\t\t\t\t\t? { tokenFieldTypes: opts.recipients.tokenFieldTypes }\n\t\t\t\t\t\t: {}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n})\n"],"mappings":";;;;;;AAOA,MAAM,WAAW;AACjB,MAAM,WAAW;;AAGjB,MAAa,oBAAoB,UAA2B,SAAS,KAAK,MAAM,KAAK,CAAC;;AAGtF,MAAa,mBAAmB,UAAsC;CACrE,MAAM,QAAQ,SAAS,KAAK,MAAM,KAAK,CAAC;CACxC,OAAO,QAAQ,MAAM,KAAK,KAAA;AAC3B;AAIA,MAAM,UAAU,UACf,MAAM,QAAQ,KAAK,IAChB,MAAM,QAAQ,UAA2B,OAAO,UAAU,QAAQ,IAClE,OAAO,UAAU,YAAY,MAAM,SAAS,IAC3C,CAAC,KAAK,IACN,CAAC;;;;;;AAON,MAAa,qBAAqB,OAAgB,YACjD,OAAO,KAAK,EACV,KAAK,UAAU,YAAY,OAAO,OAAO,EAAE,KAAK,CAAC,EACjD,QAAQ,UAAU,MAAM,SAAS,CAAC,EAClC,KAAK,IAAI;;;;;;AAOZ,MAAa,sBACX,qBACA,OAAgB,EAAE,MAAM,UAAkE;CAC1F,MAAM,OAAO,OAAO,KAAK;CACzB,IAAI,KAAK,WAAW,GACnB,OAAO;CAER,MAAM,SACL,QAAQ,OAAO,SAAS,WAAY,KAAiC,SAAS,KAAA;CAC/E,MAAM,UAAU,IAAI,IACnB,mBAAmB,gBAAgB,SAAS,IACzC,iBAAiB,QAAQ,eAAe,IACxC,WAAW,MAAM,CACrB;CACA,KAAK,MAAM,SAAS,MAAM;EACzB,MAAM,QAAQ,gBAAgB,KAAK;EACnC,IAAI,OAAO;GACV,IAAI,CAAC,QAAQ,IAAI,KAAK,GACrB,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,+BAA+B;GAE/D;EACD;EACA,IAAI,CAAC,iBAAiB,KAAK,GAC1B,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,0BAA0B;CAE3D;CACA,OAAO;AACR;AAYD,MAAM,uBAAuB;;;;;;AAQ7B,MAAa,uBACZ,MACA,UACA,UACA,OAA6E,CAAC,OAC9D;CAChB;CACA,MAAM;CACN,SAAS;CACT,OAAO,SAAS,QAAQ;CACxB,UAAU,mBAAmB,KAAK,YAAY,mBAAmB,CAAC,OAAO,CAAC;CAC1E,GAAG,YAAY,QAAQ;CACvB,OAAO;EACN,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;EAC1C,YAAY,EACX,OAAO;GACN,MAAM;GACN,aAAa;IACZ,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;IACnD,GAAI,KAAK,YAAY,gBAAgB,QAAQ,EAAE,aAAa,MAAM,IAAI,CAAC;IACvE,GAAI,KAAK,YAAY,gBAAgB,QAAQ,EAAE,aAAa,MAAM,IAAI,CAAC;IACvE,GAAI,KAAK,YAAY,kBAClB,EAAE,iBAAiB,KAAK,WAAW,gBAAgB,IACnD,CAAC;GACL;EACD,EACD;CACD;AACD"}
@@ -0,0 +1,15 @@
1
+ //#region src/client/ConsentBlockLabel.d.ts
2
+ /**
3
+ * Block row header for a consent field. The consent field stores only the source id, so this resolves
4
+ * it to the source's name via the same `consent-sources` endpoint the Source select uses, rendering
5
+ * "NN Consent {name}". Falls back to "NN Consent" while loading, when unset, or on an unsaved form
6
+ * (no document id yet, so the endpoint cannot resolve options).
7
+ */
8
+ declare const ConsentBlockLabel: ({
9
+ typeLabelKey
10
+ }: {
11
+ typeLabelKey?: string;
12
+ }) => import("react/jsx-runtime").JSX.Element;
13
+ //#endregion
14
+ export { ConsentBlockLabel };
15
+ //# sourceMappingURL=ConsentBlockLabel.d.ts.map