@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.
- package/CHANGELOG.md +29 -0
- package/dist/actions/builtin/confirmation.js +75 -72
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailTeam.js +31 -41
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/index.d.ts +2 -1
- package/dist/actions/builtin/index.js +3 -3
- package/dist/actions/builtin/index.js.map +1 -1
- package/dist/actions/emailRecipients.d.ts +12 -0
- package/dist/actions/emailRecipients.js +72 -0
- package/dist/actions/emailRecipients.js.map +1 -0
- package/dist/client/ConsentBlockLabel.d.ts +15 -0
- package/dist/client/ConsentBlockLabel.js +71 -0
- package/dist/client/ConsentBlockLabel.js.map +1 -0
- package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
- package/dist/client/ConsentSourceRowLabel.js +21 -0
- package/dist/client/ConsentSourceRowLabel.js.map +1 -0
- package/dist/client/EndpointOptionsSelect.d.ts +1 -0
- package/dist/client/EndpointOptionsSelect.js +4 -1
- package/dist/client/EndpointOptionsSelect.js.map +1 -1
- package/dist/client/FieldNameSelect.d.ts +1 -0
- package/dist/client/FieldNameSelect.js +4 -1
- package/dist/client/FieldNameSelect.js.map +1 -1
- package/dist/client/RecipientsSelect.d.ts +31 -0
- package/dist/client/RecipientsSelect.js +174 -0
- package/dist/client/RecipientsSelect.js.map +1 -0
- package/dist/client/admin.css +5 -0
- package/dist/collections/formSubmissions.js +2 -1
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +18 -2
- package/dist/collections/forms.js.map +1 -1
- package/dist/consent/captureConsent.d.ts +14 -5
- package/dist/consent/captureConsent.js +24 -7
- package/dist/consent/captureConsent.js.map +1 -1
- package/dist/consent/consentSourcesField.d.ts +4 -0
- package/dist/consent/consentSourcesField.js +8 -1
- package/dist/consent/consentSourcesField.js.map +1 -1
- package/dist/consent/types.d.ts +4 -0
- package/dist/email/departments.d.ts +13 -9
- package/dist/email/departments.js +7 -50
- package/dist/email/departments.js.map +1 -1
- package/dist/email/departmentsField.d.ts +4 -0
- package/dist/email/departmentsField.js +4 -0
- package/dist/email/departmentsField.js.map +1 -1
- package/dist/exports/client.d.ts +4 -1
- package/dist/exports/client.js +4 -1
- package/dist/exports/react.js +1 -1
- package/dist/exports/rsc.js +1 -1
- package/dist/fields/buildFieldBlocks.js +4 -4
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/consent.js +6 -1
- package/dist/fields/builtin/consent.js.map +1 -1
- package/dist/fields/builtin/file.js +9 -3
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +1 -1
- package/dist/fields/builtin/index.js +3 -3
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/types.d.ts +9 -2
- package/dist/form/toFormDocument.d.ts +20 -3
- package/dist/form/toFormDocument.js +16 -4
- package/dist/form/toFormDocument.js.map +1 -1
- package/dist/form/types.d.ts +5 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/plugin/registerCollections.js +2 -1
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/plugin/uploadsCollection.d.ts +3 -0
- package/dist/plugin/uploadsCollection.js +10 -1
- package/dist/plugin/uploadsCollection.js.map +1 -1
- package/dist/submissions/SubmissionAnswers.js.map +1 -1
- package/dist/submissions/SubmissionAnswersClient.js +33 -26
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
- package/dist/submissions/runSubmission.js +3 -2
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/validateSubmission.js +2 -1
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/translations/de.js +4 -2
- package/dist/translations/de.js.map +1 -1
- package/dist/translations/en.js +4 -2
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +4 -2
- package/dist/translations/keys.js +4 -2
- package/dist/translations/keys.js.map +1 -1
- package/dist/uploads/resolveFileRef.js +2 -1
- package/dist/uploads/resolveFileRef.js.map +1 -1
- 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
|
-
* `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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 * `
|
|
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
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
67
|
+
to,
|
|
78
68
|
subject,
|
|
79
69
|
html,
|
|
80
70
|
...config.from ? { from: config.from } : {},
|
|
81
|
-
...
|
|
82
|
-
...
|
|
83
|
-
...
|
|
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
|
|
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
|
|
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
|