@10x-media/form-builder 0.1.0-beta.2 → 0.1.0-beta.3

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 (156) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -2
  3. package/dist/actions/body/converters.d.ts +20 -0
  4. package/dist/actions/body/converters.js +57 -0
  5. package/dist/actions/body/converters.js.map +1 -0
  6. package/dist/actions/body/escapeHtml.d.ts +6 -0
  7. package/dist/actions/body/escapeHtml.js +14 -0
  8. package/dist/actions/body/escapeHtml.js.map +1 -0
  9. package/dist/actions/body/serializeBody.d.ts +35 -0
  10. package/dist/actions/body/serializeBody.js +76 -0
  11. package/dist/actions/body/serializeBody.js.map +1 -0
  12. package/dist/actions/body/serializeSlate.js +31 -0
  13. package/dist/actions/body/serializeSlate.js.map +1 -0
  14. package/dist/actions/body/wildcards.d.ts +10 -0
  15. package/dist/actions/body/wildcards.js +33 -0
  16. package/dist/actions/body/wildcards.js.map +1 -0
  17. package/dist/actions/builtin/confirmation.js +28 -12
  18. package/dist/actions/builtin/confirmation.js.map +1 -1
  19. package/dist/actions/builtin/emailTeam.js +7 -9
  20. package/dist/actions/builtin/emailTeam.js.map +1 -1
  21. package/dist/actions/builtin/signedWebhook.js +21 -3
  22. package/dist/actions/builtin/signedWebhook.js.map +1 -1
  23. package/dist/actions/defineAction.d.ts +2 -1
  24. package/dist/actions/defineAction.js.map +1 -1
  25. package/dist/actions/dispatch.js +2 -1
  26. package/dist/actions/dispatch.js.map +1 -1
  27. package/dist/actions/runActions.js +8 -1
  28. package/dist/actions/runActions.js.map +1 -1
  29. package/dist/actions/task.js +7 -5
  30. package/dist/actions/task.js.map +1 -1
  31. package/dist/calc/normalizeCalc.js +5 -17
  32. package/dist/calc/normalizeCalc.js.map +1 -1
  33. package/dist/calc/types.d.ts +8 -2
  34. package/dist/calc/types.js +22 -0
  35. package/dist/calc/types.js.map +1 -0
  36. package/dist/client/ByteSizeField.d.ts +21 -0
  37. package/dist/client/ByteSizeField.js +56 -0
  38. package/dist/client/ByteSizeField.js.map +1 -0
  39. package/dist/client/ConditionBuilder.js +2 -1
  40. package/dist/client/ConditionBuilder.js.map +1 -1
  41. package/dist/client/FieldNameSelect.d.ts +20 -0
  42. package/dist/client/FieldNameSelect.js +58 -0
  43. package/dist/client/FieldNameSelect.js.map +1 -0
  44. package/dist/client/FlowBuilder.d.ts +0 -1
  45. package/dist/client/FlowBuilder.js +4 -6
  46. package/dist/client/FlowBuilder.js.map +1 -1
  47. package/dist/client/FormConditionField.js +1 -4
  48. package/dist/client/FormConditionField.js.map +1 -1
  49. package/dist/client/admin.css +192 -0
  50. package/dist/client/toStaticLabel.js +13 -0
  51. package/dist/client/toStaticLabel.js.map +1 -0
  52. package/dist/collections/formSubmissions.js +5 -3
  53. package/dist/collections/formSubmissions.js.map +1 -1
  54. package/dist/collections/forms.js +74 -57
  55. package/dist/collections/forms.js.map +1 -1
  56. package/dist/exports/client.d.ts +3 -1
  57. package/dist/exports/client.js +3 -1
  58. package/dist/exports/react.d.ts +6 -1
  59. package/dist/exports/react.js +5 -1
  60. package/dist/fields/buildFieldBlocks.js +14 -18
  61. package/dist/fields/buildFieldBlocks.js.map +1 -1
  62. package/dist/fields/builtin/calculation.js +115 -2
  63. package/dist/fields/builtin/calculation.js.map +1 -1
  64. package/dist/fields/builtin/date.js +29 -0
  65. package/dist/fields/builtin/date.js.map +1 -0
  66. package/dist/fields/builtin/file.d.ts +15 -0
  67. package/dist/fields/builtin/file.js +79 -3
  68. package/dist/fields/builtin/file.js.map +1 -1
  69. package/dist/fields/builtin/index.d.ts +14 -0
  70. package/dist/fields/builtin/index.js +11 -2
  71. package/dist/fields/builtin/index.js.map +1 -1
  72. package/dist/fields/fieldNamesOfType.js +25 -0
  73. package/dist/fields/fieldNamesOfType.js.map +1 -0
  74. package/dist/fields/sharedConfig.js +30 -18
  75. package/dist/fields/sharedConfig.js.map +1 -1
  76. package/dist/index.d.ts +19 -2
  77. package/dist/index.js +14 -4
  78. package/dist/index.js.map +1 -1
  79. package/dist/plugin/registerCollections.js +3 -2
  80. package/dist/plugin/registerCollections.js.map +1 -1
  81. package/dist/react/Form.js +3 -3
  82. package/dist/react/Form.js.map +1 -1
  83. package/dist/react/Poll.js +1 -1
  84. package/dist/react/captcha/HcaptchaCaptcha.d.ts +28 -0
  85. package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
  86. package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
  87. package/dist/react/captcha/RecaptchaCaptcha.d.ts +41 -0
  88. package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
  89. package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
  90. package/dist/react/captcha/TurnstileCaptcha.d.ts +28 -0
  91. package/dist/react/captcha/TurnstileCaptcha.js +53 -0
  92. package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
  93. package/dist/react/captcha/types.d.ts +8 -0
  94. package/dist/react/captcha/useCaptchaScript.js +45 -0
  95. package/dist/react/captcha/useCaptchaScript.js.map +1 -0
  96. package/dist/react/captcha/vendors.js +8 -0
  97. package/dist/react/captcha/vendors.js.map +1 -0
  98. package/dist/react/primitives/Input.d.ts +1 -1
  99. package/dist/react/primitives/Input.js.map +1 -1
  100. package/dist/react/renderers/calculation.js +1 -1
  101. package/dist/react/renderers/checkbox.js +1 -1
  102. package/dist/react/renderers/consent.js +1 -1
  103. package/dist/react/renderers/date.js +37 -0
  104. package/dist/react/renderers/date.js.map +1 -0
  105. package/dist/react/renderers/email.js +1 -1
  106. package/dist/react/renderers/file.js +20 -7
  107. package/dist/react/renderers/file.js.map +1 -1
  108. package/dist/react/renderers/index.js +2 -0
  109. package/dist/react/renderers/index.js.map +1 -1
  110. package/dist/react/renderers/number.js +1 -1
  111. package/dist/react/renderers/repeater.js +1 -1
  112. package/dist/react/renderers/select.js +1 -1
  113. package/dist/react/renderers/text.js +1 -1
  114. package/dist/react/renderers/textarea.js +1 -1
  115. package/dist/spam/captcha.d.ts +3 -2
  116. package/dist/spam/captcha.js +3 -2
  117. package/dist/spam/captcha.js.map +1 -1
  118. package/dist/spam/clientIp.js +9 -0
  119. package/dist/spam/clientIp.js.map +1 -0
  120. package/dist/spam/identify.js +3 -6
  121. package/dist/spam/identify.js.map +1 -1
  122. package/dist/spam/providers/hcaptcha.d.ts +17 -0
  123. package/dist/spam/providers/hcaptcha.js +18 -0
  124. package/dist/spam/providers/hcaptcha.js.map +1 -0
  125. package/dist/spam/providers/recaptcha.d.ts +23 -0
  126. package/dist/spam/providers/recaptcha.js +23 -0
  127. package/dist/spam/providers/recaptcha.js.map +1 -0
  128. package/dist/spam/providers/siteverify.js +34 -0
  129. package/dist/spam/providers/siteverify.js.map +1 -0
  130. package/dist/spam/providers/turnstile.d.ts +17 -0
  131. package/dist/spam/providers/turnstile.js +18 -0
  132. package/dist/spam/providers/turnstile.js.map +1 -0
  133. package/dist/spam/spamGuard.js +2 -5
  134. package/dist/spam/spamGuard.js.map +1 -1
  135. package/dist/spam/types.d.ts +2 -2
  136. package/dist/translations/en.js +26 -4
  137. package/dist/translations/en.js.map +1 -1
  138. package/dist/translations/keys.d.ts +26 -4
  139. package/dist/translations/keys.js +26 -4
  140. package/dist/translations/keys.js.map +1 -1
  141. package/dist/uploads/formatBytes.d.ts +9 -0
  142. package/dist/uploads/formatBytes.js +25 -0
  143. package/dist/uploads/formatBytes.js.map +1 -0
  144. package/dist/uploads/resolveFileRef.js +1 -1
  145. package/dist/uploads/resolveFileRef.js.map +1 -1
  146. package/dist/validation/builtin/index.d.ts +14 -0
  147. package/dist/validation/builtin/index.js +12 -1
  148. package/dist/validation/builtin/index.js.map +1 -1
  149. package/dist/validation/builtin/maxDate.js +23 -0
  150. package/dist/validation/builtin/maxDate.js.map +1 -0
  151. package/dist/validation/builtin/minDate.js +23 -0
  152. package/dist/validation/builtin/minDate.js.map +1 -0
  153. package/dist/validation/builtin/validateDateBound.js +18 -0
  154. package/dist/validation/builtin/validateDateBound.js.map +1 -0
  155. package/package.json +3 -2
  156. package/styles/form-builder.css +0 -190
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @10x-media/form-builder
2
2
 
3
+ ## 0.1.0-beta.3
4
+
5
+ ### Minor Changes
6
+
7
+ - Bundled captcha adapters and widget components. `turnstileProvider`, `recaptchaProvider` (v2 + v3 with `minScore`), and `hcaptchaProvider` verify tokens server-side with fail-closed semantics (network errors, timeouts, and non-2xx responses reject the submission). Matching headless `TurnstileCaptcha`, `RecaptchaCaptcha`, and `HcaptchaCaptcha` components on the `/react` export load each vendor script once, report tokens through `onToken` for `<Form captchaToken>`, clear on expiry or error, and expose `reset` (plus on-demand `execute` refresh for reCAPTCHA v3) via a ref handle.
8
+
9
+ - Feedback round: `date` field type (native date input, real-calendar validation) with `minDate`/`maxDate` rules; rich text notification bodies for `emailTeam`/`confirmation` using the project's configured editor, serialized with escaping, sanitized links, `{{ name|fallback }}`/`{{*}}`/`{{*:table}}` tokens, a plugin `richText { converters, serialize }` option for custom nodes and non-HTML channels, and `renderBody` on action run args; the confirmation recipient is a select over the form's email fields, validated server-side on save; `signedWebhook` validates its URL as absolute http(s) and describes its config fields; calculation expressions get an admin description, Monaco JSON-schema autocomplete, and save-time validation of the expression tree; the file field's `mimeTypes` is a curated select and `maxSize` live-previews as a human-readable size, with a client-side size pre-check and hint line on the renderer; flow transitions resolve against effective values so calculation fields can drive branching; the forms document (Fields/Flow/Actions) and every field block (Field/Validation/Advanced) use tabbed admin layouts, and admin components ship their own CSS (no `styles.css` import needed for the admin). New exports: `serializeBody`, `defaultBodyConverters`, `sanitizeUrl`, `escapeHtml`, `renderAllValues`, `renderAllValuesTable`, `fileMimeTypeOptions`, `formatBytes`, `defaultFieldDefinitionsByType`, `defaultValidationRulesByType`, and the `ByteSizeField`/`FieldNameSelect` client components.
10
+
11
+ **Soft compat:** existing plain-string action bodies keep working through the send path (interpolated exactly as before), though the admin editor shows them as empty rich text until re-entered. File fields keep enforcing stored free-text MIME values, but values outside the curated list need re-selecting the next time that field is edited; submitted data is untouched.
12
+
13
+ **Postgres migration note:** on `@payloadcms/db-postgres`, two `forms` column types change: action `body` goes `varchar` to `jsonb` (pre-existing string bodies are not valid JSON, so cast with `USING to_jsonb(body)` when writing the migration), and `mimeTypes` moves from a text array to an enum-backed select. Mongo needs no migration. The project config must also set an `editor` (rich text bodies inherit it; Payload throws `MissingEditorProp` without one).
14
+
15
+ ### Patch Changes
16
+
17
+ - The registry (shadcn-style) file field now matches the built-in file renderer: a client-side max-size pre-check rejects oversized files before uploading, and an accepted-types/max-size hint line renders under the input.
18
+
19
+ `minDate`/`maxDate` rule bounds are now validated as real `YYYY-MM-DD` calendar dates in the admin UI, so a malformed bound (e.g. `abc` or `2024-02-30`) is rejected at config time instead of silently breaking the rule's comparisons.
20
+
3
21
  ## 0.1.0-beta.2
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -8,7 +8,7 @@ Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-pl
8
8
 
9
9
  ## Features
10
10
 
11
- - **Fields**: text, textarea, email, number, select, checkbox, file, consent, calculation, and repeater (dynamic row lists with per-row sub-field validation), authored as Payload blocks; custom types via `defineFormField` are never second-class.
11
+ - **Fields**: text, textarea, email, number, date, select, checkbox, file, consent, calculation, and repeater (dynamic row lists with per-row sub-field validation), authored as Payload blocks; custom types via `defineFormField` are never second-class.
12
12
  - **Validation**: declarative per-field rules, custom messages and severities, cross-field and async server-only rules, a Standard Schema escape hatch (zod, valibot, ...), one server-authoritative engine.
13
13
  - **Conditional logic**: `visibleWhen` / `validateWhen` in Payload's `Where` shape, one isomorphic engine on client and server.
14
14
  - **Multi-step flows** with conditional branching, authored in the admin flow builder.
@@ -18,7 +18,7 @@ Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-pl
18
18
  - **Calculations**: a safe no-eval expression engine for totals and quiz scores, recomputed on the server.
19
19
  - **Post-submit actions**: email, confirmation, signed webhook, and custom actions, queued via Payload jobs with a bounded inline fallback.
20
20
  - **Consent** with proof by reference and policy-version capture; **file uploads** with server-enforced MIME/size; **polls** with a gated public results endpoint.
21
- - **Spam protection** on by default: honeypot, per-identity rate limiting, a captcha seam, upload-ownership scoping, privacy-first metadata.
21
+ - **Spam protection** on by default: honeypot, per-identity rate limiting, bundled captcha adapters (Turnstile, reCAPTCHA, hCaptcha), upload-ownership scoping, privacy-first metadata.
22
22
  - **Accessibility** verified by automated axe checks; **typed translations** via `@10x-media/form-builder/i18n`.
23
23
  - **Collection overrides**: extend `forms`, `form-submissions`, and `form-uploads` with extra fields, hooks, and access rules using an explicit spread API that guarantees plugin-critical hooks always run.
24
24
 
@@ -0,0 +1,20 @@
1
+ //#region src/actions/body/converters.d.ts
2
+ /** Text pipelines a converter can apply to author-typed content. */
3
+ type BodyRender = {
4
+ /** Full text pipeline: HTML-escape, then expand `{{*}}`/`{{*:table}}` and recall tokens with escaped values. */text: (raw: string) => string; /** Recall-token interpolation only, unescaped; for URL-like values that get escaped separately. */
5
+ interpolate: (raw: string) => string;
6
+ };
7
+ type BodyConverterArgs = {
8
+ node: Record<string, unknown>; /** The node's already-serialized children. */
9
+ children: string;
10
+ render: BodyRender;
11
+ };
12
+ /** Serializes one Lexical node type to an HTML string. Keyed by `node.type` in the registry. */
13
+ type BodyConverter = (args: BodyConverterArgs) => string;
14
+ /** Allow http(s)/mailto/tel and scheme-less (relative) URLs; anything else becomes `#`. */
15
+ declare const sanitizeUrl: (url: string) => string;
16
+ /** Default Lexical-to-HTML converter registry; consumers spread overrides per node type. */
17
+ declare const defaultBodyConverters: Record<string, BodyConverter>;
18
+ //#endregion
19
+ export { BodyConverter, BodyConverterArgs, BodyRender, defaultBodyConverters, sanitizeUrl };
20
+ //# sourceMappingURL=converters.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { escapeHtml } from "./escapeHtml.js";
2
+ //#region src/actions/body/converters.ts
3
+ const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
4
+ const SAFE_SCHEME = /^(https?|mailto|tel):/i;
5
+ /** Allow http(s)/mailto/tel and scheme-less (relative) URLs; anything else becomes `#`. */
6
+ const sanitizeUrl = (url) => {
7
+ const probe = url.replace(/\s/g, "");
8
+ if (probe === "") return "#";
9
+ if (SCHEME.test(probe)) return SAFE_SCHEME.test(probe) ? url.trim() : "#";
10
+ return url.trim();
11
+ };
12
+ const BOLD = 1;
13
+ const ITALIC = 2;
14
+ const STRIKETHROUGH = 4;
15
+ const UNDERLINE = 8;
16
+ const CODE = 16;
17
+ const text = ({ node, render }) => {
18
+ let html = render.text(typeof node.text === "string" ? node.text : "");
19
+ const format = typeof node.format === "number" ? node.format : 0;
20
+ if (format & CODE) html = `<code>${html}</code>`;
21
+ if (format & BOLD) html = `<strong>${html}</strong>`;
22
+ if (format & ITALIC) html = `<em>${html}</em>`;
23
+ if (format & STRIKETHROUGH) html = `<s>${html}</s>`;
24
+ if (format & UNDERLINE) html = `<u>${html}</u>`;
25
+ return html;
26
+ };
27
+ const link = ({ node, children, render }) => {
28
+ const fields = node.fields ?? {};
29
+ const raw = typeof fields.url === "string" ? fields.url : typeof node.url === "string" ? node.url : "";
30
+ return `<a href="${escapeHtml(sanitizeUrl(render.interpolate(raw)))}"${fields.newTab === true ? " rel=\"noopener noreferrer\" target=\"_blank\"" : ""}>${children}</a>`;
31
+ };
32
+ const HEADING_TAG = /^h[1-6]$/;
33
+ const heading = ({ node, children }) => {
34
+ const tag = typeof node.tag === "string" && HEADING_TAG.test(node.tag) ? node.tag : "h2";
35
+ return `<${tag}>${children}</${tag}>`;
36
+ };
37
+ const list = ({ node, children }) => {
38
+ const tag = node.tag === "ol" || node.listType === "number" ? "ol" : "ul";
39
+ return `<${tag}>${children}</${tag}>`;
40
+ };
41
+ /** Default Lexical-to-HTML converter registry; consumers spread overrides per node type. */
42
+ const defaultBodyConverters = {
43
+ autolink: link,
44
+ heading,
45
+ horizontalrule: () => "<hr />",
46
+ linebreak: () => "<br />",
47
+ link,
48
+ list,
49
+ listitem: ({ children }) => `<li>${children}</li>`,
50
+ paragraph: ({ children }) => `<p>${children}</p>`,
51
+ quote: ({ children }) => `<blockquote>${children}</blockquote>`,
52
+ text
53
+ };
54
+ //#endregion
55
+ export { defaultBodyConverters, sanitizeUrl };
56
+
57
+ //# sourceMappingURL=converters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converters.js","names":[],"sources":["../../../src/actions/body/converters.ts"],"sourcesContent":["import { escapeHtml } from './escapeHtml'\n\n/** Text pipelines a converter can apply to author-typed content. */\nexport type BodyRender = {\n\t/** Full text pipeline: HTML-escape, then expand `{{*}}`/`{{*:table}}` and recall tokens with escaped values. */\n\ttext: (raw: string) => string\n\t/** Recall-token interpolation only, unescaped; for URL-like values that get escaped separately. */\n\tinterpolate: (raw: string) => string\n}\n\nexport type BodyConverterArgs = {\n\tnode: Record<string, unknown>\n\t/** The node's already-serialized children. */\n\tchildren: string\n\trender: BodyRender\n}\n\n/** Serializes one Lexical node type to an HTML string. Keyed by `node.type` in the registry. */\nexport type BodyConverter = (args: BodyConverterArgs) => string\n\nconst SCHEME = /^[a-z][a-z0-9+.-]*:/i\nconst SAFE_SCHEME = /^(https?|mailto|tel):/i\n\n/** Allow http(s)/mailto/tel and scheme-less (relative) URLs; anything else becomes `#`. */\nexport const sanitizeUrl = (url: string): string => {\n\tconst probe = url.replace(/\\s/g, '')\n\tif (probe === '') {\n\t\treturn '#'\n\t}\n\tif (SCHEME.test(probe)) {\n\t\treturn SAFE_SCHEME.test(probe) ? url.trim() : '#'\n\t}\n\treturn url.trim()\n}\n\nconst BOLD = 1\nconst ITALIC = 2\nconst STRIKETHROUGH = 4\nconst UNDERLINE = 8\nconst CODE = 16\n\nconst text: BodyConverter = ({ node, render }) => {\n\tlet html = render.text(typeof node.text === 'string' ? node.text : '')\n\tconst format = typeof node.format === 'number' ? node.format : 0\n\tif (format & CODE) {\n\t\thtml = `<code>${html}</code>`\n\t}\n\tif (format & BOLD) {\n\t\thtml = `<strong>${html}</strong>`\n\t}\n\tif (format & ITALIC) {\n\t\thtml = `<em>${html}</em>`\n\t}\n\tif (format & STRIKETHROUGH) {\n\t\thtml = `<s>${html}</s>`\n\t}\n\tif (format & UNDERLINE) {\n\t\thtml = `<u>${html}</u>`\n\t}\n\treturn html\n}\n\nconst link: BodyConverter = ({ node, children, render }) => {\n\tconst fields = (node.fields ?? {}) as Record<string, unknown>\n\tconst raw =\n\t\ttypeof fields.url === 'string' ? fields.url : typeof node.url === 'string' ? node.url : ''\n\tconst href = escapeHtml(sanitizeUrl(render.interpolate(raw)))\n\tconst newTab = fields.newTab === true\n\treturn `<a href=\"${href}\"${newTab ? ' rel=\"noopener noreferrer\" target=\"_blank\"' : ''}>${children}</a>`\n}\n\nconst HEADING_TAG = /^h[1-6]$/\n\nconst heading: BodyConverter = ({ node, children }) => {\n\tconst tag = typeof node.tag === 'string' && HEADING_TAG.test(node.tag) ? node.tag : 'h2'\n\treturn `<${tag}>${children}</${tag}>`\n}\n\nconst list: BodyConverter = ({ node, children }) => {\n\tconst tag = node.tag === 'ol' || node.listType === 'number' ? 'ol' : 'ul'\n\treturn `<${tag}>${children}</${tag}>`\n}\n\n/** Default Lexical-to-HTML converter registry; consumers spread overrides per node type. */\nexport const defaultBodyConverters: Record<string, BodyConverter> = {\n\tautolink: link,\n\theading,\n\thorizontalrule: () => '<hr />',\n\tlinebreak: () => '<br />',\n\tlink,\n\tlist,\n\tlistitem: ({ children }) => `<li>${children}</li>`,\n\tparagraph: ({ children }) => `<p>${children}</p>`,\n\tquote: ({ children }) => `<blockquote>${children}</blockquote>`,\n\ttext,\n}\n"],"mappings":";;AAoBA,MAAM,SAAS;AACf,MAAM,cAAc;;AAGpB,MAAa,eAAe,QAAwB;CACnD,MAAM,QAAQ,IAAI,QAAQ,OAAO,EAAE;CACnC,IAAI,UAAU,IACb,OAAO;CAER,IAAI,OAAO,KAAK,KAAK,GACpB,OAAO,YAAY,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI;CAE/C,OAAO,IAAI,KAAK;AACjB;AAEA,MAAM,OAAO;AACb,MAAM,SAAS;AACf,MAAM,gBAAgB;AACtB,MAAM,YAAY;AAClB,MAAM,OAAO;AAEb,MAAM,QAAuB,EAAE,MAAM,aAAa;CACjD,IAAI,OAAO,OAAO,KAAK,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,EAAE;CACrE,MAAM,SAAS,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS;CAC/D,IAAI,SAAS,MACZ,OAAO,SAAS,KAAK;CAEtB,IAAI,SAAS,MACZ,OAAO,WAAW,KAAK;CAExB,IAAI,SAAS,QACZ,OAAO,OAAO,KAAK;CAEpB,IAAI,SAAS,eACZ,OAAO,MAAM,KAAK;CAEnB,IAAI,SAAS,WACZ,OAAO,MAAM,KAAK;CAEnB,OAAO;AACR;AAEA,MAAM,QAAuB,EAAE,MAAM,UAAU,aAAa;CAC3D,MAAM,SAAU,KAAK,UAAU,CAAC;CAChC,MAAM,MACL,OAAO,OAAO,QAAQ,WAAW,OAAO,MAAM,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;CAGzF,OAAO,YAFM,WAAW,YAAY,OAAO,YAAY,GAAG,CAAC,CAErC,EAAE,GADT,OAAO,WAAW,OACG,mDAA+C,GAAG,GAAG,SAAS;AACnG;AAEA,MAAM,cAAc;AAEpB,MAAM,WAA0B,EAAE,MAAM,eAAe;CACtD,MAAM,MAAM,OAAO,KAAK,QAAQ,YAAY,YAAY,KAAK,KAAK,GAAG,IAAI,KAAK,MAAM;CACpF,OAAO,IAAI,IAAI,GAAG,SAAS,IAAI,IAAI;AACpC;AAEA,MAAM,QAAuB,EAAE,MAAM,eAAe;CACnD,MAAM,MAAM,KAAK,QAAQ,QAAQ,KAAK,aAAa,WAAW,OAAO;CACrE,OAAO,IAAI,IAAI,GAAG,SAAS,IAAI,IAAI;AACpC;;AAGA,MAAa,wBAAuD;CACnE,UAAU;CACV;CACA,sBAAsB;CACtB,iBAAiB;CACjB;CACA;CACA,WAAW,EAAE,eAAe,OAAO,SAAS;CAC5C,YAAY,EAAE,eAAe,MAAM,SAAS;CAC5C,QAAQ,EAAE,eAAe,eAAe,SAAS;CACjD;AACD"}
@@ -0,0 +1,6 @@
1
+ //#region src/actions/body/escapeHtml.d.ts
2
+ /** Escape `&`, `<`, `>`, `"`, `'` for safe embedding in HTML text or attribute values. */
3
+ declare const escapeHtml: (value: string) => string;
4
+ //#endregion
5
+ export { escapeHtml };
6
+ //# sourceMappingURL=escapeHtml.d.ts.map
@@ -0,0 +1,14 @@
1
+ //#region src/actions/body/escapeHtml.ts
2
+ const REPLACEMENTS = {
3
+ "&": "&amp;",
4
+ "<": "&lt;",
5
+ ">": "&gt;",
6
+ "\"": "&quot;",
7
+ "'": "&#39;"
8
+ };
9
+ /** Escape `&`, `<`, `>`, `"`, `'` for safe embedding in HTML text or attribute values. */
10
+ const escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => REPLACEMENTS[char] ?? char);
11
+ //#endregion
12
+ export { escapeHtml };
13
+
14
+ //# sourceMappingURL=escapeHtml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escapeHtml.js","names":[],"sources":["../../../src/actions/body/escapeHtml.ts"],"sourcesContent":["const REPLACEMENTS: Record<string, string> = {\n\t'&': '&amp;',\n\t'<': '&lt;',\n\t'>': '&gt;',\n\t'\"': '&quot;',\n\t\"'\": '&#39;',\n}\n\n/** Escape `&`, `<`, `>`, `\"`, `'` for safe embedding in HTML text or attribute values. */\nexport const escapeHtml = (value: string): string =>\n\tvalue.replace(/[&<>\"']/g, (char) => REPLACEMENTS[char] ?? char)\n"],"mappings":";AAAA,MAAM,eAAuC;CAC5C,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAK;CACL,KAAK;AACN;;AAGA,MAAa,cAAc,UAC1B,MAAM,QAAQ,aAAa,SAAS,aAAa,SAAS,IAAI"}
@@ -0,0 +1,35 @@
1
+ import { SubmissionDescriptor, SubmissionValue } from "../../submissions/types.js";
2
+ import { BodyConverter } from "./converters.js";
3
+
4
+ //#region src/actions/body/serializeBody.d.ts
5
+ /** Submission data plus optional converter overrides available while serializing a body. */
6
+ type BodyContext = {
7
+ values: SubmissionValue[];
8
+ descriptors: SubmissionDescriptor[];
9
+ converters?: Record<string, BodyConverter>;
10
+ };
11
+ /** Args a custom `richText.serialize` replacement receives per rendered body. */
12
+ type SerializeBodyArgs = {
13
+ body: unknown;
14
+ values: SubmissionValue[];
15
+ descriptors: SubmissionDescriptor[];
16
+ };
17
+ /**
18
+ * Customizes how action bodies become channel-ready strings. `converters` spread over the
19
+ * defaults per Lexical node type; `serialize` replaces the whole pipeline (for non-HTML
20
+ * channels like chat or plain text).
21
+ */
22
+ type RichTextBodyOption = {
23
+ converters?: Record<string, BodyConverter>;
24
+ serialize?: (args: SerializeBodyArgs) => Promise<string> | string;
25
+ };
26
+ /**
27
+ * Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is
28
+ * (pre-richText behavior, no escaping); a Lexical state walks the converter registry; a Slate
29
+ * array uses the minimal legacy serializer; anything else yields `''`. Rendered text is
30
+ * HTML-escaped and supports `{{ name|fallback }}`, `{{*}}`, and `{{*:table}}` tokens.
31
+ */
32
+ declare const serializeBody: (body: unknown, ctx: BodyContext) => string;
33
+ //#endregion
34
+ export { BodyContext, RichTextBodyOption, SerializeBodyArgs, serializeBody };
35
+ //# sourceMappingURL=serializeBody.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { interpolate } from "../../recall/interpolate.js";
2
+ import { escapeHtml } from "./escapeHtml.js";
3
+ import { defaultBodyConverters } from "./converters.js";
4
+ import { serializeSlate } from "./serializeSlate.js";
5
+ import { renderAllValues, renderAllValuesTable } from "./wildcards.js";
6
+ //#region src/actions/body/serializeBody.ts
7
+ /** Recall resolver over submission values: field name to stringified value, `''` when absent. */
8
+ const resolverFor = (values) => (name) => {
9
+ const entry = values.find((value) => value.field === name);
10
+ return entry == null ? "" : String(entry.value ?? "");
11
+ };
12
+ const renderFor = (ctx) => {
13
+ const resolve = resolverFor(ctx.values);
14
+ const htmlResolve = (name) => {
15
+ if (name === "*") return renderAllValues(ctx.values, ctx.descriptors);
16
+ if (name === "*:table") return renderAllValuesTable(ctx.values, ctx.descriptors);
17
+ return escapeHtml(resolve(name));
18
+ };
19
+ return {
20
+ text: (raw) => interpolate(escapeHtml(raw), htmlResolve),
21
+ interpolate: (raw) => interpolate(raw, resolve)
22
+ };
23
+ };
24
+ const serializeNodes = (nodes, converters, render) => nodes.map((node) => {
25
+ if (node == null || typeof node !== "object") return "";
26
+ const lexicalNode = node;
27
+ const children = Array.isArray(lexicalNode.children) ? serializeNodes(lexicalNode.children, converters, render) : "";
28
+ const converter = typeof lexicalNode.type === "string" ? converters[lexicalNode.type] : void 0;
29
+ return converter ? converter({
30
+ node: lexicalNode,
31
+ children,
32
+ render
33
+ }) : children;
34
+ }).join("");
35
+ const lexicalRootOf = (body) => {
36
+ if (body == null || typeof body !== "object" || Array.isArray(body)) return null;
37
+ const root = body.root;
38
+ return root != null && typeof root === "object" ? root : null;
39
+ };
40
+ /**
41
+ * Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is
42
+ * (pre-richText behavior, no escaping); a Lexical state walks the converter registry; a Slate
43
+ * array uses the minimal legacy serializer; anything else yields `''`. Rendered text is
44
+ * HTML-escaped and supports `{{ name|fallback }}`, `{{*}}`, and `{{*:table}}` tokens.
45
+ */
46
+ const serializeBody = (body, ctx) => {
47
+ if (typeof body === "string") return interpolate(body, resolverFor(ctx.values));
48
+ const render = renderFor(ctx);
49
+ if (Array.isArray(body)) return serializeSlate(body, render);
50
+ const root = lexicalRootOf(body);
51
+ if (root) {
52
+ const converters = {
53
+ ...defaultBodyConverters,
54
+ ...ctx.converters ?? {}
55
+ };
56
+ return serializeNodes(Array.isArray(root.children) ? root.children : [], converters, render);
57
+ }
58
+ return "";
59
+ };
60
+ /** Build the `renderBody` passed to actions, honoring a plugin-level `richText` customization. */
61
+ const makeRenderBody = (args) => async (body) => {
62
+ if (args.richText?.serialize) return await args.richText.serialize({
63
+ body,
64
+ values: args.values,
65
+ descriptors: args.descriptors
66
+ });
67
+ return serializeBody(body, {
68
+ values: args.values,
69
+ descriptors: args.descriptors,
70
+ converters: args.richText?.converters
71
+ });
72
+ };
73
+ //#endregion
74
+ export { makeRenderBody, resolverFor, serializeBody };
75
+
76
+ //# sourceMappingURL=serializeBody.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializeBody.js","names":[],"sources":["../../../src/actions/body/serializeBody.ts"],"sourcesContent":["import { interpolate } from '../../recall/interpolate'\nimport type { SubmissionDescriptor, SubmissionValue } from '../../submissions/types'\nimport type { BodyConverter, BodyRender } from './converters'\nimport { defaultBodyConverters } from './converters'\nimport { escapeHtml } from './escapeHtml'\nimport { serializeSlate } from './serializeSlate'\nimport { renderAllValues, renderAllValuesTable } from './wildcards'\n\n/** Submission data plus optional converter overrides available while serializing a body. */\nexport type BodyContext = {\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tconverters?: Record<string, BodyConverter>\n}\n\n/** Args a custom `richText.serialize` replacement receives per rendered body. */\nexport type SerializeBodyArgs = {\n\tbody: unknown\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n}\n\n/**\n * Customizes how action bodies become channel-ready strings. `converters` spread over the\n * defaults per Lexical node type; `serialize` replaces the whole pipeline (for non-HTML\n * channels like chat or plain text).\n */\nexport type RichTextBodyOption = {\n\tconverters?: Record<string, BodyConverter>\n\tserialize?: (args: SerializeBodyArgs) => Promise<string> | string\n}\n\n/** Recall resolver over submission values: field name to stringified value, `''` when absent. */\nexport const resolverFor =\n\t(values: SubmissionValue[]) =>\n\t(name: string): string => {\n\t\tconst entry = values.find((value) => value.field === name)\n\t\treturn entry == null ? '' : String(entry.value ?? '')\n\t}\n\nconst renderFor = (ctx: BodyContext): BodyRender => {\n\tconst resolve = resolverFor(ctx.values)\n\tconst htmlResolve = (name: string): string => {\n\t\tif (name === '*') {\n\t\t\treturn renderAllValues(ctx.values, ctx.descriptors)\n\t\t}\n\t\tif (name === '*:table') {\n\t\t\treturn renderAllValuesTable(ctx.values, ctx.descriptors)\n\t\t}\n\t\treturn escapeHtml(resolve(name))\n\t}\n\treturn {\n\t\ttext: (raw) => interpolate(escapeHtml(raw), htmlResolve),\n\t\tinterpolate: (raw) => interpolate(raw, resolve),\n\t}\n}\n\nconst serializeNodes = (\n\tnodes: unknown[],\n\tconverters: Record<string, BodyConverter>,\n\trender: BodyRender\n): string =>\n\tnodes\n\t\t.map((node) => {\n\t\t\tif (node == null || typeof node !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst lexicalNode = node as Record<string, unknown>\n\t\t\tconst children = Array.isArray(lexicalNode.children)\n\t\t\t\t? serializeNodes(lexicalNode.children, converters, render)\n\t\t\t\t: ''\n\t\t\tconst converter =\n\t\t\t\ttypeof lexicalNode.type === 'string' ? converters[lexicalNode.type] : undefined\n\t\t\treturn converter ? converter({ node: lexicalNode, children, render }) : children\n\t\t})\n\t\t.join('')\n\nconst lexicalRootOf = (body: unknown): Record<string, unknown> | null => {\n\tif (body == null || typeof body !== 'object' || Array.isArray(body)) {\n\t\treturn null\n\t}\n\tconst root = (body as { root?: unknown }).root\n\treturn root != null && typeof root === 'object' ? (root as Record<string, unknown>) : null\n}\n\n/**\n * Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is\n * (pre-richText behavior, no escaping); a Lexical state walks the converter registry; a Slate\n * array uses the minimal legacy serializer; anything else yields `''`. Rendered text is\n * HTML-escaped and supports `{{ name|fallback }}`, `{{*}}`, and `{{*:table}}` tokens.\n */\nexport const serializeBody = (body: unknown, ctx: BodyContext): string => {\n\tif (typeof body === 'string') {\n\t\treturn interpolate(body, resolverFor(ctx.values))\n\t}\n\tconst render = renderFor(ctx)\n\tif (Array.isArray(body)) {\n\t\treturn serializeSlate(body, render)\n\t}\n\tconst root = lexicalRootOf(body)\n\tif (root) {\n\t\tconst converters = { ...defaultBodyConverters, ...(ctx.converters ?? {}) }\n\t\treturn serializeNodes(Array.isArray(root.children) ? root.children : [], converters, render)\n\t}\n\treturn ''\n}\n\n/** Build the `renderBody` passed to actions, honoring a plugin-level `richText` customization. */\nexport const makeRenderBody =\n\t(args: {\n\t\tvalues: SubmissionValue[]\n\t\tdescriptors: SubmissionDescriptor[]\n\t\trichText?: RichTextBodyOption\n\t}) =>\n\tasync (body: unknown): Promise<string> => {\n\t\tif (args.richText?.serialize) {\n\t\t\treturn await args.richText.serialize({\n\t\t\t\tbody,\n\t\t\t\tvalues: args.values,\n\t\t\t\tdescriptors: args.descriptors,\n\t\t\t})\n\t\t}\n\t\treturn serializeBody(body, {\n\t\t\tvalues: args.values,\n\t\t\tdescriptors: args.descriptors,\n\t\t\tconverters: args.richText?.converters,\n\t\t})\n\t}\n"],"mappings":";;;;;;;AAiCA,MAAa,eACX,YACA,SAAyB;CACzB,MAAM,QAAQ,OAAO,MAAM,UAAU,MAAM,UAAU,IAAI;CACzD,OAAO,SAAS,OAAO,KAAK,OAAO,MAAM,SAAS,EAAE;AACrD;AAED,MAAM,aAAa,QAAiC;CACnD,MAAM,UAAU,YAAY,IAAI,MAAM;CACtC,MAAM,eAAe,SAAyB;EAC7C,IAAI,SAAS,KACZ,OAAO,gBAAgB,IAAI,QAAQ,IAAI,WAAW;EAEnD,IAAI,SAAS,WACZ,OAAO,qBAAqB,IAAI,QAAQ,IAAI,WAAW;EAExD,OAAO,WAAW,QAAQ,IAAI,CAAC;CAChC;CACA,OAAO;EACN,OAAO,QAAQ,YAAY,WAAW,GAAG,GAAG,WAAW;EACvD,cAAc,QAAQ,YAAY,KAAK,OAAO;CAC/C;AACD;AAEA,MAAM,kBACL,OACA,YACA,WAEA,MACE,KAAK,SAAS;CACd,IAAI,QAAQ,QAAQ,OAAO,SAAS,UACnC,OAAO;CAER,MAAM,cAAc;CACpB,MAAM,WAAW,MAAM,QAAQ,YAAY,QAAQ,IAChD,eAAe,YAAY,UAAU,YAAY,MAAM,IACvD;CACH,MAAM,YACL,OAAO,YAAY,SAAS,WAAW,WAAW,YAAY,QAAQ,KAAA;CACvE,OAAO,YAAY,UAAU;EAAE,MAAM;EAAa;EAAU;CAAO,CAAC,IAAI;AACzE,CAAC,EACA,KAAK,EAAE;AAEV,MAAM,iBAAiB,SAAkD;CACxE,IAAI,QAAQ,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GACjE,OAAO;CAER,MAAM,OAAQ,KAA4B;CAC1C,OAAO,QAAQ,QAAQ,OAAO,SAAS,WAAY,OAAmC;AACvF;;;;;;;AAQA,MAAa,iBAAiB,MAAe,QAA6B;CACzE,IAAI,OAAO,SAAS,UACnB,OAAO,YAAY,MAAM,YAAY,IAAI,MAAM,CAAC;CAEjD,MAAM,SAAS,UAAU,GAAG;CAC5B,IAAI,MAAM,QAAQ,IAAI,GACrB,OAAO,eAAe,MAAM,MAAM;CAEnC,MAAM,OAAO,cAAc,IAAI;CAC/B,IAAI,MAAM;EACT,MAAM,aAAa;GAAE,GAAG;GAAuB,GAAI,IAAI,cAAc,CAAC;EAAG;EACzE,OAAO,eAAe,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC,GAAG,YAAY,MAAM;CAC5F;CACA,OAAO;AACR;;AAGA,MAAa,kBACX,SAKD,OAAO,SAAmC;CACzC,IAAI,KAAK,UAAU,WAClB,OAAO,MAAM,KAAK,SAAS,UAAU;EACpC;EACA,QAAQ,KAAK;EACb,aAAa,KAAK;CACnB,CAAC;CAEF,OAAO,cAAc,MAAM;EAC1B,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,YAAY,KAAK,UAAU;CAC5B,CAAC;AACF"}
@@ -0,0 +1,31 @@
1
+ import { escapeHtml } from "./escapeHtml.js";
2
+ import { sanitizeUrl } from "./converters.js";
3
+ //#region src/actions/body/serializeSlate.ts
4
+ const leaf = (node, render) => {
5
+ let html = render.text(typeof node.text === "string" ? node.text : "");
6
+ if (node.code === true) html = `<code>${html}</code>`;
7
+ if (node.bold === true) html = `<strong>${html}</strong>`;
8
+ if (node.italic === true) html = `<em>${html}</em>`;
9
+ if (node.underline === true) html = `<u>${html}</u>`;
10
+ return html;
11
+ };
12
+ const serializeNode = (node, render) => {
13
+ if (node == null || typeof node !== "object") return "";
14
+ const slateNode = node;
15
+ if (typeof slateNode.text === "string") return leaf(slateNode, render);
16
+ const children = Array.isArray(slateNode.children) ? slateNode.children.map((child) => serializeNode(child, render)).join("") : "";
17
+ if (slateNode.type === "link") {
18
+ const raw = typeof slateNode.url === "string" ? slateNode.url : "";
19
+ return `<a href="${escapeHtml(sanitizeUrl(render.interpolate(raw)))}">${children}</a>`;
20
+ }
21
+ return `<p>${children}</p>`;
22
+ };
23
+ /**
24
+ * Minimal legacy Slate serializer: paragraph-ish elements, bold/italic/underline/code marks, links.
25
+ * Strikethrough is intentionally unsupported for legacy Slate bodies.
26
+ */
27
+ const serializeSlate = (nodes, render) => nodes.map((node) => serializeNode(node, render)).join("");
28
+ //#endregion
29
+ export { serializeSlate };
30
+
31
+ //# sourceMappingURL=serializeSlate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializeSlate.js","names":[],"sources":["../../../src/actions/body/serializeSlate.ts"],"sourcesContent":["import type { BodyRender } from './converters'\nimport { sanitizeUrl } from './converters'\nimport { escapeHtml } from './escapeHtml'\n\ntype SlateNode = Record<string, unknown>\n\nconst leaf = (node: SlateNode, render: BodyRender): string => {\n\tlet html = render.text(typeof node.text === 'string' ? node.text : '')\n\tif (node.code === true) {\n\t\thtml = `<code>${html}</code>`\n\t}\n\tif (node.bold === true) {\n\t\thtml = `<strong>${html}</strong>`\n\t}\n\tif (node.italic === true) {\n\t\thtml = `<em>${html}</em>`\n\t}\n\tif (node.underline === true) {\n\t\thtml = `<u>${html}</u>`\n\t}\n\treturn html\n}\n\nconst serializeNode = (node: unknown, render: BodyRender): string => {\n\tif (node == null || typeof node !== 'object') {\n\t\treturn ''\n\t}\n\tconst slateNode = node as SlateNode\n\tif (typeof slateNode.text === 'string') {\n\t\treturn leaf(slateNode, render)\n\t}\n\tconst children = Array.isArray(slateNode.children)\n\t\t? slateNode.children.map((child) => serializeNode(child, render)).join('')\n\t\t: ''\n\tif (slateNode.type === 'link') {\n\t\tconst raw = typeof slateNode.url === 'string' ? slateNode.url : ''\n\t\tconst href = escapeHtml(sanitizeUrl(render.interpolate(raw)))\n\t\treturn `<a href=\"${href}\">${children}</a>`\n\t}\n\treturn `<p>${children}</p>`\n}\n\n/**\n * Minimal legacy Slate serializer: paragraph-ish elements, bold/italic/underline/code marks, links.\n * Strikethrough is intentionally unsupported for legacy Slate bodies.\n */\nexport const serializeSlate = (nodes: unknown[], render: BodyRender): string =>\n\tnodes.map((node) => serializeNode(node, render)).join('')\n"],"mappings":";;;AAMA,MAAM,QAAQ,MAAiB,WAA+B;CAC7D,IAAI,OAAO,OAAO,KAAK,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,EAAE;CACrE,IAAI,KAAK,SAAS,MACjB,OAAO,SAAS,KAAK;CAEtB,IAAI,KAAK,SAAS,MACjB,OAAO,WAAW,KAAK;CAExB,IAAI,KAAK,WAAW,MACnB,OAAO,OAAO,KAAK;CAEpB,IAAI,KAAK,cAAc,MACtB,OAAO,MAAM,KAAK;CAEnB,OAAO;AACR;AAEA,MAAM,iBAAiB,MAAe,WAA+B;CACpE,IAAI,QAAQ,QAAQ,OAAO,SAAS,UACnC,OAAO;CAER,MAAM,YAAY;CAClB,IAAI,OAAO,UAAU,SAAS,UAC7B,OAAO,KAAK,WAAW,MAAM;CAE9B,MAAM,WAAW,MAAM,QAAQ,UAAU,QAAQ,IAC9C,UAAU,SAAS,KAAK,UAAU,cAAc,OAAO,MAAM,CAAC,EAAE,KAAK,EAAE,IACvE;CACH,IAAI,UAAU,SAAS,QAAQ;EAC9B,MAAM,MAAM,OAAO,UAAU,QAAQ,WAAW,UAAU,MAAM;EAEhE,OAAO,YADM,WAAW,YAAY,OAAO,YAAY,GAAG,CAAC,CACrC,EAAE,IAAI,SAAS;CACtC;CACA,OAAO,MAAM,SAAS;AACvB;;;;;AAMA,MAAa,kBAAkB,OAAkB,WAChD,MAAM,KAAK,SAAS,cAAc,MAAM,MAAM,CAAC,EAAE,KAAK,EAAE"}
@@ -0,0 +1,10 @@
1
+ import { SubmissionDescriptor, SubmissionValue } from "../../submissions/types.js";
2
+
3
+ //#region src/actions/body/wildcards.d.ts
4
+ /** All answers as HTML-escaped `Label: value` lines joined with `<br />`. */
5
+ declare const renderAllValues: (values: SubmissionValue[], descriptors: SubmissionDescriptor[]) => string;
6
+ /** All answers as a simple HTML-escaped two-column table. */
7
+ declare const renderAllValuesTable: (values: SubmissionValue[], descriptors: SubmissionDescriptor[]) => string;
8
+ //#endregion
9
+ export { renderAllValues, renderAllValuesTable };
10
+ //# sourceMappingURL=wildcards.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { escapeHtml } from "./escapeHtml.js";
2
+ //#region src/actions/body/wildcards.ts
3
+ const displayValue = (value, descriptor) => {
4
+ if (value == null) return "";
5
+ if (Array.isArray(value)) return value.map((entry) => displayValue(entry, descriptor)).join(", ");
6
+ const raw = typeof value === "object" ? JSON.stringify(value) : String(value);
7
+ return descriptor?.optionLabels?.[raw] ?? raw;
8
+ };
9
+ /**
10
+ * Descriptors are the submit-time snapshot of real form fields, so when present they gate which
11
+ * values render (internal keys like the honeypot never get a descriptor). Legacy rows without
12
+ * descriptors fall back to rendering every value under its field name.
13
+ */
14
+ const rowsOf = (values, descriptors) => {
15
+ const byField = new Map(descriptors.map((descriptor) => [descriptor.field, descriptor]));
16
+ return (byField.size > 0 ? values.filter((value) => byField.has(value.field)) : values).map((entry) => {
17
+ const descriptor = byField.get(entry.field);
18
+ return {
19
+ label: descriptor?.label ?? entry.field,
20
+ value: displayValue(entry.value, descriptor)
21
+ };
22
+ });
23
+ };
24
+ /** All answers as HTML-escaped `Label: value` lines joined with `<br />`. */
25
+ const renderAllValues = (values, descriptors) => rowsOf(values, descriptors).map((row) => `${escapeHtml(row.label)}: ${escapeHtml(row.value)}`).join("<br />");
26
+ /** All answers as a simple HTML-escaped two-column table. */
27
+ const renderAllValuesTable = (values, descriptors) => {
28
+ return `<table><tbody>${rowsOf(values, descriptors).map((row) => `<tr><td>${escapeHtml(row.label)}</td><td>${escapeHtml(row.value)}</td></tr>`).join("")}</tbody></table>`;
29
+ };
30
+ //#endregion
31
+ export { renderAllValues, renderAllValuesTable };
32
+
33
+ //# sourceMappingURL=wildcards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wildcards.js","names":[],"sources":["../../../src/actions/body/wildcards.ts"],"sourcesContent":["import type { SubmissionDescriptor, SubmissionValue } from '../../submissions/types'\nimport { escapeHtml } from './escapeHtml'\n\ntype Row = { label: string; value: string }\n\nconst displayValue = (value: unknown, descriptor?: SubmissionDescriptor): string => {\n\tif (value == null) {\n\t\treturn ''\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn value.map((entry) => displayValue(entry, descriptor)).join(', ')\n\t}\n\tconst raw = typeof value === 'object' ? JSON.stringify(value) : String(value)\n\treturn descriptor?.optionLabels?.[raw] ?? raw\n}\n\n/**\n * Descriptors are the submit-time snapshot of real form fields, so when present they gate which\n * values render (internal keys like the honeypot never get a descriptor). Legacy rows without\n * descriptors fall back to rendering every value under its field name.\n */\nconst rowsOf = (values: SubmissionValue[], descriptors: SubmissionDescriptor[]): Row[] => {\n\tconst byField = new Map(descriptors.map((descriptor) => [descriptor.field, descriptor]))\n\tconst included = byField.size > 0 ? values.filter((value) => byField.has(value.field)) : values\n\treturn included.map((entry) => {\n\t\tconst descriptor = byField.get(entry.field)\n\t\treturn { label: descriptor?.label ?? entry.field, value: displayValue(entry.value, descriptor) }\n\t})\n}\n\n/** All answers as HTML-escaped `Label: value` lines joined with `<br />`. */\nexport const renderAllValues = (\n\tvalues: SubmissionValue[],\n\tdescriptors: SubmissionDescriptor[]\n): string =>\n\trowsOf(values, descriptors)\n\t\t.map((row) => `${escapeHtml(row.label)}: ${escapeHtml(row.value)}`)\n\t\t.join('<br />')\n\n/** All answers as a simple HTML-escaped two-column table. */\nexport const renderAllValuesTable = (\n\tvalues: SubmissionValue[],\n\tdescriptors: SubmissionDescriptor[]\n): string => {\n\tconst rows = rowsOf(values, descriptors)\n\t\t.map((row) => `<tr><td>${escapeHtml(row.label)}</td><td>${escapeHtml(row.value)}</td></tr>`)\n\t\t.join('')\n\treturn `<table><tbody>${rows}</tbody></table>`\n}\n"],"mappings":";;AAKA,MAAM,gBAAgB,OAAgB,eAA8C;CACnF,IAAI,SAAS,MACZ,OAAO;CAER,IAAI,MAAM,QAAQ,KAAK,GACtB,OAAO,MAAM,KAAK,UAAU,aAAa,OAAO,UAAU,CAAC,EAAE,KAAK,IAAI;CAEvE,MAAM,MAAM,OAAO,UAAU,WAAW,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK;CAC5E,OAAO,YAAY,eAAe,QAAQ;AAC3C;;;;;;AAOA,MAAM,UAAU,QAA2B,gBAA+C;CACzF,MAAM,UAAU,IAAI,IAAI,YAAY,KAAK,eAAe,CAAC,WAAW,OAAO,UAAU,CAAC,CAAC;CAEvF,QADiB,QAAQ,OAAO,IAAI,OAAO,QAAQ,UAAU,QAAQ,IAAI,MAAM,KAAK,CAAC,IAAI,QACzE,KAAK,UAAU;EAC9B,MAAM,aAAa,QAAQ,IAAI,MAAM,KAAK;EAC1C,OAAO;GAAE,OAAO,YAAY,SAAS,MAAM;GAAO,OAAO,aAAa,MAAM,OAAO,UAAU;EAAE;CAChG,CAAC;AACF;;AAGA,MAAa,mBACZ,QACA,gBAEA,OAAO,QAAQ,WAAW,EACxB,KAAK,QAAQ,GAAG,WAAW,IAAI,KAAK,EAAE,IAAI,WAAW,IAAI,KAAK,GAAG,EACjE,KAAK,QAAQ;;AAGhB,MAAa,wBACZ,QACA,gBACY;CAIZ,OAAO,iBAHM,OAAO,QAAQ,WAAW,EACrC,KAAK,QAAQ,WAAW,WAAW,IAAI,KAAK,EAAE,WAAW,WAAW,IAAI,KAAK,EAAE,WAAW,EAC1F,KAAK,EACoB,EAAE;AAC9B"}
@@ -1,8 +1,19 @@
1
+ import { fieldNamesOfType } from "../../fields/fieldNamesOfType.js";
1
2
  import { interpolate } from "../../recall/interpolate.js";
2
3
  import { keys } from "../../translations/keys.js";
3
- import { labelFor } from "../../translations/server.js";
4
+ import { asTranslate, labelFor } from "../../translations/server.js";
5
+ import { resolverFor } from "../body/serializeBody.js";
4
6
  import { defineAction } from "../defineAction.js";
5
7
  //#region src/actions/builtin/confirmation.ts
8
+ const TO_FIELD_REF = "@10x-media/form-builder/client#FieldNameSelect";
9
+ /**
10
+ * Validates the confirmation action's `toField`: unset is fine, otherwise it must name an
11
+ * existing `email`-type field on the form. Exported for unit testing.
12
+ */
13
+ const validateToField = (value, { data, req }) => {
14
+ if (typeof value !== "string" || value.length === 0) return true;
15
+ return fieldNamesOfType(data && typeof data === "object" ? data.fields : void 0, ["email"]).includes(value) ? true : asTranslate(req.t)(keys.validationEmailFieldUnknown);
16
+ };
6
17
  const confirmation = defineAction({
7
18
  type: "confirmation",
8
19
  label: keys.actionConfirmation,
@@ -10,7 +21,15 @@ const confirmation = defineAction({
10
21
  {
11
22
  name: "toField",
12
23
  type: "text",
13
- label: labelFor(keys.actionConfigToField)
24
+ label: labelFor(keys.actionConfigToField),
25
+ admin: {
26
+ description: labelFor(keys.actionConfigToFieldDescription),
27
+ components: { Field: {
28
+ path: TO_FIELD_REF,
29
+ clientProps: { types: ["email"] }
30
+ } }
31
+ },
32
+ validate: validateToField
14
33
  },
15
34
  {
16
35
  name: "subject",
@@ -19,23 +38,20 @@ const confirmation = defineAction({
19
38
  },
20
39
  {
21
40
  name: "body",
22
- type: "textarea",
23
- label: labelFor(keys.actionConfigBody)
41
+ type: "richText",
42
+ label: labelFor(keys.actionConfigBody),
43
+ admin: { description: labelFor(keys.actionConfigBodyDescription) }
24
44
  }
25
45
  ],
26
46
  run: async (args) => {
27
- const { config, values, payload } = args;
47
+ const { config, values, payload, renderBody } = args;
28
48
  if (!config.toField) return;
29
- const entry = values.find((v) => v.field === config.toField);
30
- const to = entry == null ? "" : String(entry.value ?? "");
49
+ const resolve = resolverFor(values);
50
+ const to = resolve(config.toField);
31
51
  if (!to) return;
32
52
  if (typeof payload.sendEmail !== "function") throw new Error("confirmation: no email adapter configured");
33
- const resolve = (name) => {
34
- const found = values.find((v) => v.field === name);
35
- return found == null ? "" : String(found.value ?? "");
36
- };
37
53
  const subject = interpolate(config.subject ?? "", resolve);
38
- const html = interpolate(config.body ?? "", resolve);
54
+ const html = await renderBody(config.body);
39
55
  await payload.sendEmail({
40
56
  to,
41
57
  subject,
@@ -1 +1 @@
1
- {"version":3,"file":"confirmation.js","names":[],"sources":["../../../src/actions/builtin/confirmation.ts"],"sourcesContent":["import { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { defineAction } from '../defineAction'\n\ntype ConfirmationConfig = { toField?: string; subject?: string; body?: string }\n\nexport const confirmation = defineAction<ConfirmationConfig>({\n\ttype: 'confirmation',\n\tlabel: keys.actionConfirmation,\n\tconfig: [\n\t\t{ name: 'toField', type: 'text', label: labelFor(keys.actionConfigToField) },\n\t\t{ name: 'subject', type: 'text', label: labelFor(keys.actionConfigSubject) },\n\t\t{ name: 'body', type: 'textarea', label: labelFor(keys.actionConfigBody) },\n\t],\n\trun: async (args) => {\n\t\tconst { config, values, payload } = args\n\n\t\tif (!config.toField) {\n\t\t\treturn\n\t\t}\n\n\t\tconst entry = values.find((v) => v.field === config.toField)\n\t\tconst to = entry == null ? '' : String(entry.value ?? '')\n\n\t\tif (!to) {\n\t\t\treturn\n\t\t}\n\n\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\tthrow new Error('confirmation: no email adapter configured')\n\t\t}\n\n\t\tconst resolve = (name: string) => {\n\t\t\tconst found = values.find((v) => v.field === name)\n\t\t\treturn found == null ? '' : String(found.value ?? '')\n\t\t}\n\n\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\tconst html = interpolate(config.body ?? '', resolve)\n\n\t\tawait payload.sendEmail({ to, subject, html })\n\t},\n})\n"],"mappings":";;;;;AAOA,MAAa,eAAe,aAAiC;CAC5D,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ;EACP;GAAE,MAAM;GAAW,MAAM;GAAQ,OAAO,SAAS,KAAK,mBAAmB;EAAE;EAC3E;GAAE,MAAM;GAAW,MAAM;GAAQ,OAAO,SAAS,KAAK,mBAAmB;EAAE;EAC3E;GAAE,MAAM;GAAQ,MAAM;GAAY,OAAO,SAAS,KAAK,gBAAgB;EAAE;CAC1E;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,QAAQ,YAAY;EAEpC,IAAI,CAAC,OAAO,SACX;EAGD,MAAM,QAAQ,OAAO,MAAM,MAAM,EAAE,UAAU,OAAO,OAAO;EAC3D,MAAM,KAAK,SAAS,OAAO,KAAK,OAAO,MAAM,SAAS,EAAE;EAExD,IAAI,CAAC,IACJ;EAGD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,2CAA2C;EAG5D,MAAM,WAAW,SAAiB;GACjC,MAAM,QAAQ,OAAO,MAAM,MAAM,EAAE,UAAU,IAAI;GACjD,OAAO,SAAS,OAAO,KAAK,OAAO,MAAM,SAAS,EAAE;EACrD;EAEA,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;EACzD,MAAM,OAAO,YAAY,OAAO,QAAQ,IAAI,OAAO;EAEnD,MAAM,QAAQ,UAAU;GAAE;GAAI;GAAS;EAAK,CAAC;CAC9C;AACD,CAAC"}
1
+ {"version":3,"file":"confirmation.js","names":[],"sources":["../../../src/actions/builtin/confirmation.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\nimport { fieldNamesOfType } from '../../fields/fieldNamesOfType'\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'\n\ntype ConfirmationConfig = { toField?: string; subject?: string; body?: unknown }\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\nexport const confirmation = defineAction<ConfirmationConfig>({\n\ttype: 'confirmation',\n\tlabel: keys.actionConfirmation,\n\tconfig: [\n\t\t{\n\t\t\tname: 'toField',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelFor(keys.actionConfigToField),\n\t\t\tadmin: {\n\t\t\t\tdescription: labelFor(keys.actionConfigToFieldDescription),\n\t\t\t\tcomponents: { Field: { path: TO_FIELD_REF, clientProps: { types: ['email'] } } },\n\t\t\t},\n\t\t\tvalidate: validateToField,\n\t\t},\n\t\t{ name: 'subject', type: 'text', label: labelFor(keys.actionConfigSubject) },\n\t\t{\n\t\t\tname: 'body',\n\t\t\ttype: 'richText',\n\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t},\n\t],\n\trun: async (args) => {\n\t\tconst { config, values, payload, renderBody } = args\n\n\t\tif (!config.toField) {\n\t\t\treturn\n\t\t}\n\n\t\tconst resolve = resolverFor(values)\n\t\tconst to = resolve(config.toField)\n\n\t\tif (!to) {\n\t\t\treturn\n\t\t}\n\n\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\tthrow new Error('confirmation: no email adapter configured')\n\t\t}\n\n\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\tconst html = await renderBody(config.body)\n\n\t\tawait payload.sendEmail({ to, subject, html })\n\t},\n})\n"],"mappings":";;;;;;;AAUA,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;AAEA,MAAa,eAAe,aAAiC;CAC5D,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ;EACP;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,mBAAmB;GACxC,OAAO;IACN,aAAa,SAAS,KAAK,8BAA8B;IACzD,YAAY,EAAE,OAAO;KAAE,MAAM;KAAc,aAAa,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,EAAE;GAChF;GACA,UAAU;EACX;EACA;GAAE,MAAM;GAAW,MAAM;GAAQ,OAAO,SAAS,KAAK,mBAAmB;EAAE;EAC3E;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,gBAAgB;GACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;EAClE;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;EAEzC,MAAM,QAAQ,UAAU;GAAE;GAAI;GAAS;EAAK,CAAC;CAC9C;AACD,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { interpolate } from "../../recall/interpolate.js";
2
2
  import { keys } from "../../translations/keys.js";
3
3
  import { labelFor } from "../../translations/server.js";
4
+ import { resolverFor } from "../body/serializeBody.js";
4
5
  import { defineAction } from "../defineAction.js";
5
6
  //#region src/actions/builtin/emailTeam.ts
6
7
  const emailTeam = defineAction({
@@ -19,20 +20,17 @@ const emailTeam = defineAction({
19
20
  },
20
21
  {
21
22
  name: "body",
22
- type: "textarea",
23
- label: labelFor(keys.actionConfigBody)
23
+ type: "richText",
24
+ label: labelFor(keys.actionConfigBody),
25
+ admin: { description: labelFor(keys.actionConfigBodyDescription) }
24
26
  }
25
27
  ],
26
28
  run: async (args) => {
27
- const { config, values, payload } = args;
29
+ const { config, values, payload, renderBody } = args;
28
30
  if (!config.to) throw new Error("emailTeam: missing \"to\" address");
29
31
  if (typeof payload.sendEmail !== "function") throw new Error("emailTeam: no email adapter configured");
30
- const resolve = (name) => {
31
- const entry = values.find((v) => v.field === name);
32
- return entry == null ? "" : String(entry.value ?? "");
33
- };
34
- const subject = interpolate(config.subject ?? "", resolve);
35
- const html = interpolate(config.body ?? "", resolve);
32
+ const subject = interpolate(config.subject ?? "", resolverFor(values));
33
+ const html = await renderBody(config.body);
36
34
  await payload.sendEmail({
37
35
  to: config.to,
38
36
  subject,
@@ -1 +1 @@
1
- {"version":3,"file":"emailTeam.js","names":[],"sources":["../../../src/actions/builtin/emailTeam.ts"],"sourcesContent":["import { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { defineAction } from '../defineAction'\n\ntype EmailTeamConfig = { to?: string; subject?: string; body?: string }\n\nexport const emailTeam = defineAction<EmailTeamConfig>({\n\ttype: 'emailTeam',\n\tlabel: keys.actionEmailTeam,\n\tconfig: [\n\t\t{ name: 'to', type: 'text', label: labelFor(keys.actionConfigTo) },\n\t\t{ name: 'subject', type: 'text', label: labelFor(keys.actionConfigSubject) },\n\t\t{ name: 'body', type: 'textarea', label: labelFor(keys.actionConfigBody) },\n\t],\n\trun: async (args) => {\n\t\tconst { config, values, payload } = args\n\n\t\tif (!config.to) {\n\t\t\tthrow new Error('emailTeam: missing \"to\" address')\n\t\t}\n\n\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\tthrow new Error('emailTeam: no email adapter configured')\n\t\t}\n\n\t\tconst resolve = (name: string) => {\n\t\t\tconst entry = values.find((v) => v.field === name)\n\t\t\treturn entry == null ? '' : String(entry.value ?? '')\n\t\t}\n\n\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\tconst html = interpolate(config.body ?? '', resolve)\n\n\t\tawait payload.sendEmail({ to: config.to, subject, html })\n\t},\n})\n"],"mappings":";;;;;AAOA,MAAa,YAAY,aAA8B;CACtD,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ;EACP;GAAE,MAAM;GAAM,MAAM;GAAQ,OAAO,SAAS,KAAK,cAAc;EAAE;EACjE;GAAE,MAAM;GAAW,MAAM;GAAQ,OAAO,SAAS,KAAK,mBAAmB;EAAE;EAC3E;GAAE,MAAM;GAAQ,MAAM;GAAY,OAAO,SAAS,KAAK,gBAAgB;EAAE;CAC1E;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,QAAQ,YAAY;EAEpC,IAAI,CAAC,OAAO,IACX,MAAM,IAAI,MAAM,mCAAiC;EAGlD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,wCAAwC;EAGzD,MAAM,WAAW,SAAiB;GACjC,MAAM,QAAQ,OAAO,MAAM,MAAM,EAAE,UAAU,IAAI;GACjD,OAAO,SAAS,OAAO,KAAK,OAAO,MAAM,SAAS,EAAE;EACrD;EAEA,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;EACzD,MAAM,OAAO,YAAY,OAAO,QAAQ,IAAI,OAAO;EAEnD,MAAM,QAAQ,UAAU;GAAE,IAAI,OAAO;GAAI;GAAS;EAAK,CAAC;CACzD;AACD,CAAC"}
1
+ {"version":3,"file":"emailTeam.js","names":[],"sources":["../../../src/actions/builtin/emailTeam.ts"],"sourcesContent":["import { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\n\ntype EmailTeamConfig = { to?: string; subject?: string; body?: unknown }\n\nexport const emailTeam = defineAction<EmailTeamConfig>({\n\ttype: 'emailTeam',\n\tlabel: keys.actionEmailTeam,\n\tconfig: [\n\t\t{ name: 'to', type: 'text', label: labelFor(keys.actionConfigTo) },\n\t\t{ name: 'subject', type: 'text', label: labelFor(keys.actionConfigSubject) },\n\t\t{\n\t\t\tname: 'body',\n\t\t\ttype: 'richText',\n\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t},\n\t],\n\trun: async (args) => {\n\t\tconst { config, values, payload, renderBody } = args\n\n\t\tif (!config.to) {\n\t\t\tthrow new Error('emailTeam: missing \"to\" address')\n\t\t}\n\n\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\tthrow new Error('emailTeam: no email adapter configured')\n\t\t}\n\n\t\tconst subject = interpolate(config.subject ?? '', resolverFor(values))\n\t\tconst html = await renderBody(config.body)\n\n\t\tawait payload.sendEmail({ to: config.to, subject, html })\n\t},\n})\n"],"mappings":";;;;;;AAQA,MAAa,YAAY,aAA8B;CACtD,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ;EACP;GAAE,MAAM;GAAM,MAAM;GAAQ,OAAO,SAAS,KAAK,cAAc;EAAE;EACjE;GAAE,MAAM;GAAW,MAAM;GAAQ,OAAO,SAAS,KAAK,mBAAmB;EAAE;EAC3E;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,gBAAgB;GACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;EAClE;CACD;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;EAEhD,IAAI,CAAC,OAAO,IACX,MAAM,IAAI,MAAM,mCAAiC;EAGlD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,wCAAwC;EAGzD,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,YAAY,MAAM,CAAC;EACrE,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;EAEzC,MAAM,QAAQ,UAAU;GAAE,IAAI,OAAO;GAAI;GAAS;EAAK,CAAC;CACzD;AACD,CAAC"}
@@ -1,19 +1,37 @@
1
1
  import { keys } from "../../translations/keys.js";
2
- import { labelFor } from "../../translations/server.js";
2
+ import { asTranslate, labelFor } from "../../translations/server.js";
3
3
  import { defineAction } from "../defineAction.js";
4
4
  import { SIGNATURE_HEADER, signPayload } from "../sign.js";
5
5
  //#region src/actions/builtin/signedWebhook.ts
6
+ /**
7
+ * Validates the signed-webhook action's `url`: unset is fine (the runtime throws on a missing
8
+ * URL when the action actually runs), otherwise it must parse as an absolute http(s) URL.
9
+ * Exported for unit testing.
10
+ */
11
+ const validateUrl = (value, { req }) => {
12
+ if (typeof value !== "string" || value.length === 0) return true;
13
+ let parsed;
14
+ try {
15
+ parsed = new URL(value);
16
+ } catch {
17
+ return asTranslate(req.t)(keys.validationUrlInvalid);
18
+ }
19
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? true : asTranslate(req.t)(keys.validationUrlInvalid);
20
+ };
6
21
  const signedWebhook = defineAction({
7
22
  type: "signedWebhook",
8
23
  label: keys.actionSignedWebhook,
9
24
  config: [{
10
25
  name: "url",
11
26
  type: "text",
12
- label: labelFor(keys.actionConfigUrl)
27
+ label: labelFor(keys.actionConfigUrl),
28
+ admin: { description: labelFor(keys.actionConfigUrlDescription) },
29
+ validate: validateUrl
13
30
  }, {
14
31
  name: "secret",
15
32
  type: "text",
16
- label: labelFor(keys.actionConfigSecret)
33
+ label: labelFor(keys.actionConfigSecret),
34
+ admin: { description: labelFor(keys.actionConfigSecretDescription) }
17
35
  }],
18
36
  run: async (args) => {
19
37
  const { config, form, submissionId, values } = args;
@@ -1 +1 @@
1
- {"version":3,"file":"signedWebhook.js","names":[],"sources":["../../../src/actions/builtin/signedWebhook.ts"],"sourcesContent":["import { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { defineAction } from '../defineAction'\nimport { SIGNATURE_HEADER, signPayload } from '../sign'\n\ntype SignedWebhookConfig = { url?: string; secret?: string }\n\nexport const signedWebhook = defineAction<SignedWebhookConfig>({\n\ttype: 'signedWebhook',\n\tlabel: keys.actionSignedWebhook,\n\tconfig: [\n\t\t{ name: 'url', type: 'text', label: labelFor(keys.actionConfigUrl) },\n\t\t{ name: 'secret', type: 'text', label: labelFor(keys.actionConfigSecret) },\n\t],\n\trun: async (args) => {\n\t\tconst { config, form, submissionId, values } = args\n\n\t\tif (!config.url) {\n\t\t\tthrow new Error('signedWebhook: missing \"url\"')\n\t\t}\n\t\tif (!config.secret) {\n\t\t\tthrow new Error('signedWebhook: missing \"secret\"')\n\t\t}\n\n\t\tconst body = JSON.stringify({ formId: form.id, submissionId, values })\n\t\tconst controller = new AbortController()\n\t\tconst timer = setTimeout(() => controller.abort(), 10_000)\n\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await fetch(config.url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t[SIGNATURE_HEADER]: signPayload(body, config.secret),\n\t\t\t\t},\n\t\t\t\tbody,\n\t\t\t\tsignal: controller.signal,\n\t\t\t})\n\t\t} finally {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`signedWebhook: server responded ${response.status}`)\n\t\t}\n\t},\n})\n"],"mappings":";;;;;AAOA,MAAa,gBAAgB,aAAkC;CAC9D,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ,CACP;EAAE,MAAM;EAAO,MAAM;EAAQ,OAAO,SAAS,KAAK,eAAe;CAAE,GACnE;EAAE,MAAM;EAAU,MAAM;EAAQ,OAAO,SAAS,KAAK,kBAAkB;CAAE,CAC1E;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,MAAM,cAAc,WAAW;EAE/C,IAAI,CAAC,OAAO,KACX,MAAM,IAAI,MAAM,gCAA8B;EAE/C,IAAI,CAAC,OAAO,QACX,MAAM,IAAI,MAAM,mCAAiC;EAGlD,MAAM,OAAO,KAAK,UAAU;GAAE,QAAQ,KAAK;GAAI;GAAc;EAAO,CAAC;EACrE,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,QAAQ,iBAAiB,WAAW,MAAM,GAAG,GAAM;EAEzD,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,MAAM,OAAO,KAAK;IAClC,QAAQ;IACR,SAAS;KACR,gBAAgB;MACf,mBAAmB,YAAY,MAAM,OAAO,MAAM;IACpD;IACA;IACA,QAAQ,WAAW;GACpB,CAAC;EACF,UAAU;GACT,aAAa,KAAK;EACnB;EAEA,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,mCAAmC,SAAS,QAAQ;CAEtE;AACD,CAAC"}
1
+ {"version":3,"file":"signedWebhook.js","names":[],"sources":["../../../src/actions/builtin/signedWebhook.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\nimport { keys } from '../../translations/keys'\nimport { asTranslate, labelFor } from '../../translations/server'\nimport { defineAction } from '../defineAction'\nimport { SIGNATURE_HEADER, signPayload } from '../sign'\n\ntype SignedWebhookConfig = { url?: string; secret?: string }\n\n/**\n * Validates the signed-webhook action's `url`: unset is fine (the runtime throws on a missing\n * URL when the action actually runs), otherwise it must parse as an absolute http(s) URL.\n * Exported for unit testing.\n */\nexport const validateUrl = (\n\tvalue: unknown,\n\t{ req }: { data?: unknown; req: PayloadRequest }\n): string | true => {\n\tif (typeof value !== 'string' || value.length === 0) {\n\t\treturn true\n\t}\n\tlet parsed: URL\n\ttry {\n\t\tparsed = new URL(value)\n\t} catch {\n\t\treturn asTranslate(req.t)(keys.validationUrlInvalid)\n\t}\n\treturn parsed.protocol === 'http:' || parsed.protocol === 'https:'\n\t\t? true\n\t\t: asTranslate(req.t)(keys.validationUrlInvalid)\n}\n\nexport const signedWebhook = defineAction<SignedWebhookConfig>({\n\ttype: 'signedWebhook',\n\tlabel: keys.actionSignedWebhook,\n\tconfig: [\n\t\t{\n\t\t\tname: 'url',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelFor(keys.actionConfigUrl),\n\t\t\tadmin: { description: labelFor(keys.actionConfigUrlDescription) },\n\t\t\tvalidate: validateUrl,\n\t\t},\n\t\t{\n\t\t\tname: 'secret',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelFor(keys.actionConfigSecret),\n\t\t\tadmin: { description: labelFor(keys.actionConfigSecretDescription) },\n\t\t},\n\t],\n\trun: async (args) => {\n\t\tconst { config, form, submissionId, values } = args\n\n\t\tif (!config.url) {\n\t\t\tthrow new Error('signedWebhook: missing \"url\"')\n\t\t}\n\t\tif (!config.secret) {\n\t\t\tthrow new Error('signedWebhook: missing \"secret\"')\n\t\t}\n\n\t\tconst body = JSON.stringify({ formId: form.id, submissionId, values })\n\t\tconst controller = new AbortController()\n\t\tconst timer = setTimeout(() => controller.abort(), 10_000)\n\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await fetch(config.url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t[SIGNATURE_HEADER]: signPayload(body, config.secret),\n\t\t\t\t},\n\t\t\t\tbody,\n\t\t\t\tsignal: controller.signal,\n\t\t\t})\n\t\t} finally {\n\t\t\tclearTimeout(timer)\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`signedWebhook: server responded ${response.status}`)\n\t\t}\n\t},\n})\n"],"mappings":";;;;;;;;;;AAaA,MAAa,eACZ,OACA,EAAE,UACiB;CACnB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAER,IAAI;CACJ,IAAI;EACH,SAAS,IAAI,IAAI,KAAK;CACvB,QAAQ;EACP,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,oBAAoB;CACpD;CACA,OAAO,OAAO,aAAa,WAAW,OAAO,aAAa,WACvD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,oBAAoB;AAChD;AAEA,MAAa,gBAAgB,aAAkC;CAC9D,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ,CACP;EACC,MAAM;EACN,MAAM;EACN,OAAO,SAAS,KAAK,eAAe;EACpC,OAAO,EAAE,aAAa,SAAS,KAAK,0BAA0B,EAAE;EAChE,UAAU;CACX,GACA;EACC,MAAM;EACN,MAAM;EACN,OAAO,SAAS,KAAK,kBAAkB;EACvC,OAAO,EAAE,aAAa,SAAS,KAAK,6BAA6B,EAAE;CACpE,CACD;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,MAAM,cAAc,WAAW;EAE/C,IAAI,CAAC,OAAO,KACX,MAAM,IAAI,MAAM,gCAA8B;EAE/C,IAAI,CAAC,OAAO,QACX,MAAM,IAAI,MAAM,mCAAiC;EAGlD,MAAM,OAAO,KAAK,UAAU;GAAE,QAAQ,KAAK;GAAI;GAAc;EAAO,CAAC;EACrE,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,QAAQ,iBAAiB,WAAW,MAAM,GAAG,GAAM;EAEzD,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,MAAM,OAAO,KAAK;IAClC,QAAQ;IACR,SAAS;KACR,gBAAgB;MACf,mBAAmB,YAAY,MAAM,OAAO,MAAM;IACpD;IACA;IACA,QAAQ,WAAW;GACpB,CAAC;EACF,UAAU;GACT,aAAa,KAAK;EACnB;EAEA,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,mCAAmC,SAAS,QAAQ;CAEtE;AACD,CAAC"}
@@ -16,7 +16,8 @@ type ActionRunArgs<TConfig extends Record<string, unknown> = Record<string, unkn
16
16
  payload: Payload;
17
17
  req?: PayloadRequest;
18
18
  locale: string;
19
- t: Translate;
19
+ t: Translate; /** Serialize a rich text (or legacy string) body config into channel-ready HTML. */
20
+ renderBody: (body: unknown) => Promise<string>;
20
21
  };
21
22
  /**
22
23
  * A post-submit action type, authored once: `config` is the admin `Field[]` for authoring;
@@ -1 +1 @@
1
- {"version":3,"file":"defineAction.js","names":[],"sources":["../../src/actions/defineAction.ts"],"sourcesContent":["import type { Field, Payload, PayloadRequest } from 'payload'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\n\n/** Context passed to an action's `run` when a submission completes. */\nexport type ActionRunArgs<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\tform: { id: number | string; title?: string }\n\tsubmissionId: number | string\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tconfig: TConfig\n\tpayload: Payload\n\treq?: PayloadRequest\n\tlocale: string\n\tt: Translate\n}\n\n/**\n * A post-submit action type, authored once: `config` is the admin `Field[]` for authoring;\n * `run` executes when a submission completes. Built-ins use this same primitive.\n */\nexport type ActionDefinition<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\ttype: string\n\tlabel: string\n\tconfig?: Field[]\n\trun: (args: ActionRunArgs<TConfig>) => Promise<void> | void\n}\n\n/** Erased shape stored in the registry; config re-narrows per matched type at execution. */\nexport type AnyActionDefinition = ActionDefinition<Record<string, unknown>>\n\nexport const defineAction = <TConfig extends Record<string, unknown> = Record<string, unknown>>(\n\tdefinition: ActionDefinition<TConfig>\n): ActionDefinition<TConfig> => definition\n"],"mappings":";AA+BA,MAAa,gBACZ,eAC+B"}
1
+ {"version":3,"file":"defineAction.js","names":[],"sources":["../../src/actions/defineAction.ts"],"sourcesContent":["import type { Field, Payload, PayloadRequest } from 'payload'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\n\n/** Context passed to an action's `run` when a submission completes. */\nexport type ActionRunArgs<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\tform: { id: number | string; title?: string }\n\tsubmissionId: number | string\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tconfig: TConfig\n\tpayload: Payload\n\treq?: PayloadRequest\n\tlocale: string\n\tt: Translate\n\t/** Serialize a rich text (or legacy string) body config into channel-ready HTML. */\n\trenderBody: (body: unknown) => Promise<string>\n}\n\n/**\n * A post-submit action type, authored once: `config` is the admin `Field[]` for authoring;\n * `run` executes when a submission completes. Built-ins use this same primitive.\n */\nexport type ActionDefinition<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\ttype: string\n\tlabel: string\n\tconfig?: Field[]\n\trun: (args: ActionRunArgs<TConfig>) => Promise<void> | void\n}\n\n/** Erased shape stored in the registry; config re-narrows per matched type at execution. */\nexport type AnyActionDefinition = ActionDefinition<Record<string, unknown>>\n\nexport const defineAction = <TConfig extends Record<string, unknown> = Record<string, unknown>>(\n\tdefinition: ActionDefinition<TConfig>\n): ActionDefinition<TConfig> => definition\n"],"mappings":";AAiCA,MAAa,gBACZ,eAC+B"}