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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +3 -3
  3. package/dist/actions/body/serializeBody.d.ts +8 -5
  4. package/dist/actions/body/serializeBody.js.map +1 -1
  5. package/dist/actions/body/textOfBody.d.ts +13 -0
  6. package/dist/actions/body/textOfBody.js +35 -0
  7. package/dist/actions/body/textOfBody.js.map +1 -0
  8. package/dist/actions/builtin/confirmation.js +81 -48
  9. package/dist/actions/builtin/confirmation.js.map +1 -1
  10. package/dist/actions/builtin/emailTeam.js +67 -38
  11. package/dist/actions/builtin/emailTeam.js.map +1 -1
  12. package/dist/actions/builtin/index.d.ts +9 -3
  13. package/dist/actions/builtin/index.js +8 -5
  14. package/dist/actions/builtin/index.js.map +1 -1
  15. package/dist/actions/defineAction.d.ts +1 -1
  16. package/dist/actions/emailRecipients.d.ts +12 -0
  17. package/dist/actions/emailRecipients.js +72 -0
  18. package/dist/actions/emailRecipients.js.map +1 -0
  19. package/dist/actions/fromAddresses.d.ts +21 -0
  20. package/dist/actions/fromAddresses.js +74 -0
  21. package/dist/actions/fromAddresses.js.map +1 -0
  22. package/dist/actions/task.js +9 -8
  23. package/dist/actions/task.js.map +1 -1
  24. package/dist/aggregation/aggregateResponses.d.ts +9 -0
  25. package/dist/aggregation/aggregateResponses.js +8 -14
  26. package/dist/aggregation/aggregateResponses.js.map +1 -1
  27. package/dist/aggregation/resolveResultsRequest.d.ts +19 -6
  28. package/dist/aggregation/resolveResultsRequest.js +58 -12
  29. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  30. package/dist/calc/computeCalcFields.js +2 -1
  31. package/dist/calc/computeCalcFields.js.map +1 -1
  32. package/dist/client/ByteSizeField.d.ts +8 -0
  33. package/dist/client/ByteSizeField.js +3 -1
  34. package/dist/client/ByteSizeField.js.map +1 -1
  35. package/dist/client/ClosePollButton.d.ts +15 -0
  36. package/dist/client/ClosePollButton.js +80 -0
  37. package/dist/client/ClosePollButton.js.map +1 -0
  38. package/dist/client/CondensedArray.d.ts +15 -0
  39. package/dist/client/CondensedArray.js +97 -0
  40. package/dist/client/CondensedArray.js.map +1 -0
  41. package/dist/client/ConsentBlockLabel.d.ts +15 -0
  42. package/dist/client/ConsentBlockLabel.js +71 -0
  43. package/dist/client/ConsentBlockLabel.js.map +1 -0
  44. package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
  45. package/dist/client/ConsentSourceRowLabel.js +21 -0
  46. package/dist/client/ConsentSourceRowLabel.js.map +1 -0
  47. package/dist/client/EndpointOptionsSelect.d.ts +46 -0
  48. package/dist/client/EndpointOptionsSelect.js +106 -0
  49. package/dist/client/EndpointOptionsSelect.js.map +1 -0
  50. package/dist/client/FieldBlockLabel.d.ts +18 -0
  51. package/dist/client/FieldBlockLabel.js +34 -0
  52. package/dist/client/FieldBlockLabel.js.map +1 -0
  53. package/dist/client/FieldNameSelect.d.ts +21 -4
  54. package/dist/client/FieldNameSelect.js +29 -15
  55. package/dist/client/FieldNameSelect.js.map +1 -1
  56. package/dist/client/FlowBuilder.d.ts +2 -1
  57. package/dist/client/FlowBuilder.js +63 -57
  58. package/dist/client/FlowBuilder.js.map +1 -1
  59. package/dist/client/RecipientsSelect.d.ts +31 -0
  60. package/dist/client/RecipientsSelect.js +174 -0
  61. package/dist/client/RecipientsSelect.js.map +1 -0
  62. package/dist/client/RuleDescription.d.ts +21 -0
  63. package/dist/client/RuleDescription.js +19 -0
  64. package/dist/client/RuleDescription.js.map +1 -0
  65. package/dist/client/admin.css +41 -0
  66. package/dist/client/endpointOptions.js +29 -0
  67. package/dist/client/endpointOptions.js.map +1 -0
  68. package/dist/client/flowAuthoring.js +114 -7
  69. package/dist/client/flowAuthoring.js.map +1 -1
  70. package/dist/client/synthesizeClientField.js +15 -8
  71. package/dist/client/synthesizeClientField.js.map +1 -1
  72. package/dist/collections/buttonFields.d.ts +34 -0
  73. package/dist/collections/buttonFields.js +35 -0
  74. package/dist/collections/buttonFields.js.map +1 -0
  75. package/dist/collections/cells/FieldCountCell.d.ts +15 -0
  76. package/dist/collections/cells/FieldCountCell.js +18 -0
  77. package/dist/collections/cells/FieldCountCell.js.map +1 -0
  78. package/dist/collections/cells/FlowStepsCell.d.ts +14 -0
  79. package/dist/collections/cells/FlowStepsCell.js +17 -0
  80. package/dist/collections/cells/FlowStepsCell.js.map +1 -0
  81. package/dist/collections/formSubmissions.js +23 -17
  82. package/dist/collections/formSubmissions.js.map +1 -1
  83. package/dist/collections/forms.js +323 -134
  84. package/dist/collections/forms.js.map +1 -1
  85. package/dist/collections/redirectFields.d.ts +29 -0
  86. package/dist/consent/applyConsentStatements.d.ts +14 -0
  87. package/dist/consent/applyConsentStatements.js +17 -0
  88. package/dist/consent/applyConsentStatements.js.map +1 -0
  89. package/dist/consent/captureConsent.d.ts +31 -11
  90. package/dist/consent/captureConsent.js +53 -13
  91. package/dist/consent/captureConsent.js.map +1 -1
  92. package/dist/consent/consentSourcesField.d.ts +62 -0
  93. package/dist/consent/consentSourcesField.js +86 -0
  94. package/dist/consent/consentSourcesField.js.map +1 -0
  95. package/dist/consent/resolveConsentEntries.d.ts +24 -0
  96. package/dist/consent/resolveConsentEntries.js +43 -0
  97. package/dist/consent/resolveConsentEntries.js.map +1 -0
  98. package/dist/consent/resolveConsentSourcesRequest.d.ts +37 -0
  99. package/dist/consent/resolveConsentSourcesRequest.js +55 -0
  100. package/dist/consent/resolveConsentSourcesRequest.js.map +1 -0
  101. package/dist/consent/resolveConsentStatements.d.ts +45 -0
  102. package/dist/consent/resolveConsentStatements.js +44 -0
  103. package/dist/consent/resolveConsentStatements.js.map +1 -0
  104. package/dist/consent/resolvePublishedVersionRef.d.ts +17 -11
  105. package/dist/consent/resolvePublishedVersionRef.js +13 -10
  106. package/dist/consent/resolvePublishedVersionRef.js.map +1 -1
  107. package/dist/consent/types.d.ts +53 -0
  108. package/dist/email/departments.d.ts +59 -0
  109. package/dist/email/departments.js +87 -0
  110. package/dist/email/departments.js.map +1 -0
  111. package/dist/email/departmentsField.d.ts +53 -0
  112. package/dist/email/departmentsField.js +73 -0
  113. package/dist/email/departmentsField.js.map +1 -0
  114. package/dist/exports/client.d.ts +11 -1
  115. package/dist/exports/client.js +11 -1
  116. package/dist/exports/react.d.ts +8 -3
  117. package/dist/exports/react.js +6 -2
  118. package/dist/exports/rsc.d.ts +3 -1
  119. package/dist/exports/rsc.js +4 -2
  120. package/dist/exports/types.d.ts +16 -8
  121. package/dist/fields/buildFieldBlocks.js +28 -13
  122. package/dist/fields/buildFieldBlocks.js.map +1 -1
  123. package/dist/fields/builtin/calculation.js +7 -0
  124. package/dist/fields/builtin/calculation.js.map +1 -1
  125. package/dist/fields/builtin/checkbox.js +8 -0
  126. package/dist/fields/builtin/checkbox.js.map +1 -1
  127. package/dist/fields/builtin/consent.js +32 -21
  128. package/dist/fields/builtin/consent.js.map +1 -1
  129. package/dist/fields/builtin/country.d.ts +13 -0
  130. package/dist/fields/builtin/country.js +30 -0
  131. package/dist/fields/builtin/country.js.map +1 -0
  132. package/dist/fields/builtin/date.js +2 -0
  133. package/dist/fields/builtin/date.js.map +1 -1
  134. package/dist/fields/builtin/file.js +14 -7
  135. package/dist/fields/builtin/file.js.map +1 -1
  136. package/dist/fields/builtin/index.d.ts +8 -6
  137. package/dist/fields/builtin/index.js +12 -11
  138. package/dist/fields/builtin/index.js.map +1 -1
  139. package/dist/fields/builtin/message.js +8 -4
  140. package/dist/fields/builtin/message.js.map +1 -1
  141. package/dist/fields/builtin/repeater.js +1 -0
  142. package/dist/fields/builtin/repeater.js.map +1 -1
  143. package/dist/fields/builtin/select.js +1 -0
  144. package/dist/fields/builtin/select.js.map +1 -1
  145. package/dist/fields/builtin/state.d.ts +13 -0
  146. package/dist/fields/builtin/state.js +30 -0
  147. package/dist/fields/builtin/state.js.map +1 -0
  148. package/dist/fields/data/regions.d.ts +21 -0
  149. package/dist/fields/data/regions.js +1238 -0
  150. package/dist/fields/data/regions.js.map +1 -0
  151. package/dist/fields/fieldKey.d.ts +15 -0
  152. package/dist/fields/fieldKey.js +16 -0
  153. package/dist/fields/fieldKey.js.map +1 -0
  154. package/dist/fields/fieldNamesOfType.js +13 -8
  155. package/dist/fields/fieldNamesOfType.js.map +1 -1
  156. package/dist/fields/instanceOptions.js +20 -0
  157. package/dist/fields/instanceOptions.js.map +1 -0
  158. package/dist/fields/registry.d.ts +1 -0
  159. package/dist/fields/registry.js +21 -2
  160. package/dist/fields/registry.js.map +1 -1
  161. package/dist/fields/sharedConfig.js +46 -9
  162. package/dist/fields/sharedConfig.js.map +1 -1
  163. package/dist/fields/types.d.ts +63 -5
  164. package/dist/flow/engine.d.ts +6 -4
  165. package/dist/flow/engine.js +11 -6
  166. package/dist/flow/engine.js.map +1 -1
  167. package/dist/flow/normalizeFlow.js +16 -7
  168. package/dist/flow/normalizeFlow.js.map +1 -1
  169. package/dist/flow/types.d.ts +12 -3
  170. package/dist/flow/types.js +11 -0
  171. package/dist/flow/types.js.map +1 -0
  172. package/dist/form/pollState.d.ts +4 -4
  173. package/dist/form/pollState.js.map +1 -1
  174. package/dist/form/toFormDocument.d.ts +54 -12
  175. package/dist/form/toFormDocument.js +56 -8
  176. package/dist/form/toFormDocument.js.map +1 -1
  177. package/dist/form/types.d.ts +31 -14
  178. package/dist/index.d.ts +153 -29
  179. package/dist/index.js +47 -15
  180. package/dist/index.js.map +1 -1
  181. package/dist/plugin/customState.js +26 -0
  182. package/dist/plugin/customState.js.map +1 -0
  183. package/dist/plugin/registerCollections.js +24 -13
  184. package/dist/plugin/registerCollections.js.map +1 -1
  185. package/dist/plugin/uploadsCollection.d.ts +5 -0
  186. package/dist/plugin/uploadsCollection.js +10 -1
  187. package/dist/plugin/uploadsCollection.js.map +1 -1
  188. package/dist/poll/applyPollOptions.js +3 -2
  189. package/dist/poll/applyPollOptions.js.map +1 -1
  190. package/dist/poll/buildPollOptionSourceFields.js +2 -5
  191. package/dist/poll/buildPollOptionSourceFields.js.map +1 -1
  192. package/dist/poll/closeJob.d.ts +52 -0
  193. package/dist/poll/closeJob.js +119 -0
  194. package/dist/poll/closeJob.js.map +1 -0
  195. package/dist/poll/definePollOptionSource.d.ts +10 -1
  196. package/dist/poll/definePollOptionSource.js.map +1 -1
  197. package/dist/poll/definePollType.d.ts +34 -0
  198. package/dist/poll/definePollType.js +6 -0
  199. package/dist/poll/definePollType.js.map +1 -0
  200. package/dist/poll/effectivePollOptions.d.ts +38 -0
  201. package/dist/poll/effectivePollOptions.js +64 -0
  202. package/dist/poll/effectivePollOptions.js.map +1 -0
  203. package/dist/poll/mostVoted.d.ts +21 -0
  204. package/dist/poll/mostVoted.js +32 -0
  205. package/dist/poll/mostVoted.js.map +1 -0
  206. package/dist/poll/outcomeBeforeChange.js +91 -0
  207. package/dist/poll/outcomeBeforeChange.js.map +1 -0
  208. package/dist/poll/outcomeFields.d.ts +38 -0
  209. package/dist/poll/outcomeFields.js +52 -0
  210. package/dist/poll/outcomeFields.js.map +1 -0
  211. package/dist/poll/pollTypeRegistry.d.ts +34 -0
  212. package/dist/poll/pollTypeRegistry.js +71 -0
  213. package/dist/poll/pollTypeRegistry.js.map +1 -0
  214. package/dist/poll/resolvePollCloseRequest.js +105 -0
  215. package/dist/poll/resolvePollCloseRequest.js.map +1 -0
  216. package/dist/poll/resolvePollOptions.d.ts +2 -1
  217. package/dist/poll/resolvePollOptions.js +5 -17
  218. package/dist/poll/resolvePollOptions.js.map +1 -1
  219. package/dist/poll/resolvePollOptionsRequest.js +51 -0
  220. package/dist/poll/resolvePollOptionsRequest.js.map +1 -0
  221. package/dist/poll/resolvePollOutcome.d.ts +22 -9
  222. package/dist/poll/resolvePollOutcome.js +54 -14
  223. package/dist/poll/resolvePollOutcome.js.map +1 -1
  224. package/dist/poll/resultsField.js +21 -0
  225. package/dist/poll/resultsField.js.map +1 -0
  226. package/dist/prefill/valuesFromSearchParams.js +2 -1
  227. package/dist/prefill/valuesFromSearchParams.js.map +1 -1
  228. package/dist/react/Form.d.ts +8 -8
  229. package/dist/react/Form.js +74 -62
  230. package/dist/react/Form.js.map +1 -1
  231. package/dist/react/FormContext.d.ts +6 -4
  232. package/dist/react/FormContext.js.map +1 -1
  233. package/dist/react/FormControls.d.ts +3 -3
  234. package/dist/react/FormControls.js +3 -3
  235. package/dist/react/FormControls.js.map +1 -1
  236. package/dist/react/FormResults.d.ts +5 -5
  237. package/dist/react/FormResults.js +6 -6
  238. package/dist/react/FormResults.js.map +1 -1
  239. package/dist/react/Poll.d.ts +2 -2
  240. package/dist/react/Poll.js +5 -5
  241. package/dist/react/Poll.js.map +1 -1
  242. package/dist/react/contract.d.ts +2 -3
  243. package/dist/react/contract.js.map +1 -1
  244. package/dist/react/primitives/Checkbox.d.ts +4 -2
  245. package/dist/react/primitives/Checkbox.js +2 -1
  246. package/dist/react/primitives/Checkbox.js.map +1 -1
  247. package/dist/react/primitives/FieldShell.d.ts +2 -4
  248. package/dist/react/primitives/FieldShell.js +12 -19
  249. package/dist/react/primitives/FieldShell.js.map +1 -1
  250. package/dist/react/renderers/calculation.js +1 -2
  251. package/dist/react/renderers/calculation.js.map +1 -1
  252. package/dist/react/renderers/checkbox.js +1 -2
  253. package/dist/react/renderers/checkbox.js.map +1 -1
  254. package/dist/react/renderers/consent.js +78 -34
  255. package/dist/react/renderers/consent.js.map +1 -1
  256. package/dist/react/renderers/date.js +1 -3
  257. package/dist/react/renderers/date.js.map +1 -1
  258. package/dist/react/renderers/email.js +1 -2
  259. package/dist/react/renderers/email.js.map +1 -1
  260. package/dist/react/renderers/file.js +1 -2
  261. package/dist/react/renderers/file.js.map +1 -1
  262. package/dist/react/renderers/index.js +3 -0
  263. package/dist/react/renderers/index.js.map +1 -1
  264. package/dist/react/renderers/number.js +1 -2
  265. package/dist/react/renderers/number.js.map +1 -1
  266. package/dist/react/renderers/region.js +45 -0
  267. package/dist/react/renderers/region.js.map +1 -0
  268. package/dist/react/renderers/repeater.js +7 -10
  269. package/dist/react/renderers/repeater.js.map +1 -1
  270. package/dist/react/renderers/select.js +1 -2
  271. package/dist/react/renderers/select.js.map +1 -1
  272. package/dist/react/renderers/text.js +1 -2
  273. package/dist/react/renderers/text.js.map +1 -1
  274. package/dist/react/renderers/textarea.js +1 -2
  275. package/dist/react/renderers/textarea.js.map +1 -1
  276. package/dist/react/state.d.ts +0 -3
  277. package/dist/react/state.js +7 -11
  278. package/dist/react/state.js.map +1 -1
  279. package/dist/react/useField.d.ts +0 -1
  280. package/dist/react/useField.js +0 -1
  281. package/dist/react/useField.js.map +1 -1
  282. package/dist/react/useFormState.d.ts +1 -1
  283. package/dist/react/useFormState.js +1 -1
  284. package/dist/react/useFormState.js.map +1 -1
  285. package/dist/react/validateField.js +3 -6
  286. package/dist/react/validateField.js.map +1 -1
  287. package/dist/recall/interpolate.d.ts +4 -3
  288. package/dist/recall/interpolate.js +5 -4
  289. package/dist/recall/interpolate.js.map +1 -1
  290. package/dist/recall/resolver.d.ts +1 -1
  291. package/dist/recall/resolver.js +14 -2
  292. package/dist/recall/resolver.js.map +1 -1
  293. package/dist/spam/captcha.d.ts +1 -1
  294. package/dist/spam/captcha.js +1 -1
  295. package/dist/spam/captcha.js.map +1 -1
  296. package/dist/submissions/SubmissionAnswers.js +6 -2
  297. package/dist/submissions/SubmissionAnswers.js.map +1 -1
  298. package/dist/submissions/SubmissionAnswersClient.js +100 -64
  299. package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
  300. package/dist/submissions/createSubmission.d.ts +36 -0
  301. package/dist/submissions/createSubmission.js +26 -0
  302. package/dist/submissions/createSubmission.js.map +1 -0
  303. package/dist/submissions/runSubmission.d.ts +10 -0
  304. package/dist/submissions/runSubmission.js +31 -19
  305. package/dist/submissions/runSubmission.js.map +1 -1
  306. package/dist/submissions/types.d.ts +11 -4
  307. package/dist/submissions/validateSubmission.js +39 -23
  308. package/dist/submissions/validateSubmission.js.map +1 -1
  309. package/dist/submissions/votedCookie.js +3 -3
  310. package/dist/submissions/votedCookie.js.map +1 -1
  311. package/dist/translations/de.js +104 -26
  312. package/dist/translations/de.js.map +1 -1
  313. package/dist/translations/en.js +105 -27
  314. package/dist/translations/en.js.map +1 -1
  315. package/dist/translations/keys.d.ts +99 -21
  316. package/dist/translations/keys.js +100 -22
  317. package/dist/translations/keys.js.map +1 -1
  318. package/dist/uploads/resolveFileRef.js +2 -1
  319. package/dist/uploads/resolveFileRef.js.map +1 -1
  320. package/dist/validation/buildRuleBlocks.js +18 -17
  321. package/dist/validation/buildRuleBlocks.js.map +1 -1
  322. package/dist/validation/builtin/email.js +1 -0
  323. package/dist/validation/builtin/email.js.map +1 -1
  324. package/dist/validation/builtin/matchesField.js +3 -7
  325. package/dist/validation/builtin/matchesField.js.map +1 -1
  326. package/dist/validation/builtin/max.js +1 -0
  327. package/dist/validation/builtin/max.js.map +1 -1
  328. package/dist/validation/builtin/maxDate.js +1 -0
  329. package/dist/validation/builtin/maxDate.js.map +1 -1
  330. package/dist/validation/builtin/maxLength.js +1 -0
  331. package/dist/validation/builtin/maxLength.js.map +1 -1
  332. package/dist/validation/builtin/min.js +1 -0
  333. package/dist/validation/builtin/min.js.map +1 -1
  334. package/dist/validation/builtin/minDate.js +1 -0
  335. package/dist/validation/builtin/minDate.js.map +1 -1
  336. package/dist/validation/builtin/minLength.js +1 -0
  337. package/dist/validation/builtin/minLength.js.map +1 -1
  338. package/dist/validation/builtin/notAlreadySubmitted.js +1 -0
  339. package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
  340. package/dist/validation/builtin/oneOf.js +1 -0
  341. package/dist/validation/builtin/oneOf.js.map +1 -1
  342. package/dist/validation/builtin/pattern.js +11 -2
  343. package/dist/validation/builtin/pattern.js.map +1 -1
  344. package/dist/validation/builtin/url.js +1 -0
  345. package/dist/validation/builtin/url.js.map +1 -1
  346. package/dist/validation/builtin/validateRegexParams.js +44 -0
  347. package/dist/validation/builtin/validateRegexParams.js.map +1 -0
  348. package/dist/validation/fieldTargetParam.d.ts +22 -0
  349. package/dist/validation/fieldTargetParam.js +38 -0
  350. package/dist/validation/fieldTargetParam.js.map +1 -0
  351. package/dist/validation/runValidation.js +8 -29
  352. package/dist/validation/runValidation.js.map +1 -1
  353. package/dist/validation/types.d.ts +5 -11
  354. package/package.json +6 -6
  355. package/dist/consent/buildConsentSourceConfig.js +0 -49
  356. package/dist/consent/buildConsentSourceConfig.js.map +0 -1
  357. package/dist/consent/builtin/index.d.ts +0 -17
  358. package/dist/consent/builtin/index.js +0 -21
  359. package/dist/consent/builtin/index.js.map +0 -1
  360. package/dist/consent/builtin/pageReference.js +0 -75
  361. package/dist/consent/builtin/pageReference.js.map +0 -1
  362. package/dist/consent/builtin/static.js +0 -48
  363. package/dist/consent/builtin/static.js.map +0 -1
  364. package/dist/consent/defineConsentSource.d.ts +0 -35
  365. package/dist/consent/defineConsentSource.js +0 -6
  366. package/dist/consent/defineConsentSource.js.map +0 -1
  367. package/dist/consent/registry.d.ts +0 -16
  368. package/dist/consent/registry.js +0 -16
  369. package/dist/consent/registry.js.map +0 -1
  370. package/dist/consent/resolveConsentLinks.d.ts +0 -19
  371. package/dist/consent/resolveConsentLinks.js +0 -26
  372. package/dist/consent/resolveConsentLinks.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,117 @@
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
+
32
+ ## 0.1.0-beta.5
33
+
34
+ ### Minor Changes
35
+
36
+ - Finish localizing the flow builder: the remaining transition, hint, empty-state, and fallback-title strings now go through the translation system (en and de).
37
+
38
+ Ship shadcn `country` and `state` field renderers so the shadcn registry covers every built-in field type, and export `COUNTRIES` and `US_STATES` from `@10x-media/form-builder/react` for them. A parity test now asserts the registry renderer set matches the package's `defaultRenderers`, so a new field type cannot ship without its shadcn renderer. The country and state option labels are intentionally left unlocalized (the stored value is the stable code).
39
+
40
+ - Feedback round: host-owned consent sources with live statements, bare message blocks, form-level button labels, sequential step flow, poll field eligibility and outcome selection, and per-request sender addresses, plus a field-config cleanup pass (drop dead shared fields, source poll options from a field, admin polish). Also: consent sources identified by their own row id, a two-checkbox form header driving conditional Flow/Poll tabs, poll outcomes that support ties with extensible auto-close strategies, fully localized and department-routable email, concise list-view columns, and response messages with the same recall fidelity as action emails. This pass also redesigns the submission answers view around native Payload fields, adds a manual poll-close action with self-managing `mostVoted` outcomes, clarifies field labels in the flow builder, and adds a server-side `createSubmission` helper alongside a documented submit contract. Validation is simplified to a binary pass-or-fail (severities and warnings are gone), every rule now opens with a plain-language description, `matchesField` and custom rules target another field through a validated picker, `country` and `state` become opt-in field types, and the redirect response gains a field-composition seam.
41
+
42
+ **Breaking:**
43
+
44
+ - **Display settings are gone; the form's title is exposed instead.** The `display` group (`showTitle`/`title`/`intro`), its Display tab, `FormDocument.display`, and `FormDisplaySettings` are all removed, and `<Form>` no longer renders a heading or intro above the fields. Whether a form's title appears is the host's call: `FormDocument.title` carries the document title (the collection's existing required `useAsTitle` field) for a page to render, or not. Authored `display` data is orphaned on next save.
45
+ - **`message` is a bare, nameless block.** The block is its `content` rich text alone: no name, label, width, required, placeholder, description, validation, or `visibleWhen`. `FormFieldDefinition` gains `bare`, `FormFieldInstance.name` becomes optional, and instances are addressed by key (a name, else Payload's hidden block row id), so flow step assignment, render keys, and the builder's picker all keep working. Stored named message rows keep rendering, but resaving a form through the admin drops the name, so a flow referencing a legacy message by name needs that message re-assigned to its step. Bare blocks are excluded from repeater `subFields`, whose row plumbing is name-keyed. A bare definition must declare `value: 'none'`; the plugin throws at boot otherwise.
46
+ - **`response.submitLabel` is removed in favour of a `buttons` group.** Every form now carries `buttons` at the bottom of its Fields tab: `submitLabel` at full width, then `prevLabel` and `nextLabel` side by side in a row (previous first). Store the label at `buttons.submitLabel`; `FormResponseSettings` no longer carries `submitLabel` and `<Form>` ignores legacy stored values. The `<Form>`/`<FormControls>` label prop and the `buttons.fields` seam key are `prevLabel`/`defaultFields.prev` (the `renderBack` prop, `backButtonClassName`, `BackButtonRenderProps`, and the `fb-form__back` class keep the `back` name; only the label concept moved to `prev`).
47
+ - **A flow step's unset `next` falls through instead of ending the form.** `next` now has three states: absent falls through to the next step in array order (the last step ends the form), `null` is an explicit end of form regardless of position, and a step id is a jump. Matching transitions still win over all three. Flows relying on an unset `next` to end the form mid-array must set that step's next to "End of form" (`next: null`). Newly added steps previously carried no `next` and were silently terminal, so a fresh two-step flow showed Submit on step one and never reached step two. `__end__` is reserved as the builder's sentinel and rejected as a step id.
48
+ - **Consent references sources you own, not fields on the form.** A consent field now stores one thing, a source `key`. The statement, its policy page, and its link live on a `consentSourcesField()` you place on any collection or global you own, read back through a `consent.sources` resolver. The old machinery is removed wholesale: `defineConsentSource`, the consent-source registry, the `static` and `pageReference` built-ins, `resolveConsentLinks`, `buildConsentSourceConfig`, the per-source `sourceConfig` group, the `consentSources` plugin option, and the consent block's own `statement` and `optional` fields. Without a `consent.sources` resolver the `consent` field type is not registered at all, mirroring `uploads: false` removing `file`. The statement is a live reference resolved per request in the visitor's locale via `resolveConsentStatements()` (root + `/rsc`) and injected by `toFormDocument(doc, { consentStatements })`, so correcting a sentence corrects every form pointing at it, with nothing to re-save. Proof is id-based: `ConsentProof` is now `{ agreed, source, page?: { relationTo, id }, versionRef?, at }` (the old `ref` slug is gone); `versionRef` is the published version document's own id, captured only when the page's collection has drafts enabled (a collection with versions but no drafts, or none at all, records no `versionRef` rather than a misleading null). `required` now actually enforces (the field gained an intrinsic validator, since a missing consent coerces to `false`, a present value the shared required guard skipped), and `label`/`placeholder` drop off the block via `omitShared` because the statement is the visible text and the accessible name. Authored statements and source config from the previous shape are orphaned. New exports: `consentSourcesField`, `resolveConsentStatements`, the `ConsentSourceEntry`/`ConsentSourcePage`/`ConsentSourcesResolver` types, and `sanitizeUrl` (now also from `./react`).
49
+ - **`poll.resultsField` is a select over eligible fields.** It was free text. It now lists the form's named instances of field types declaring `pollEligible` (built-in: `select`), and server validation enforces the same rule on save, so a free-text or PII field can never be stored while the poll is enabled.
50
+ - **Some field types no longer store shared basics they never read.** `FormFieldDefinition.omitShared` lets a type leave chosen shared config fields (`label`, `placeholder`, `description`, `width`, `required`) unauthored while keeping the rest of the chrome; `name` is never omittable. Applied where the renderer proves the field dead: `checkbox`, `file`, `repeater`, and `date` no longer store a `placeholder` (browsers ignore one on a date input), and `calculation`, being server-derived and read-only, stores neither `placeholder` nor `required`. Those columns are dropped, orphaning any authored values. `OmittableSharedField` is exported.
51
+ - **`richText.editor` is now plugin-wide.** It previously overrode the action body fields only; it is now the default editor for every plugin rich text field (message content, response message, action bodies). The new `richText.bodyEditor` is the action-body-specific override and falls back to `editor`. A host that set `editor` to give email authors a different toolbar should rename it to `bodyEditor` to keep that scope. Consent statements are unaffected: they live on your `consentSourcesField()`, which carries its own `editor` option.
52
+ - **A consent source is identified by its own row id, not a hand-authored `key`.** `consentSourcesField()`'s rows no longer have a `key`; the array row's own auto-assigned id is the stable reference a consent field stores and a proof records, so renaming or reordering a row can never orphan either. The row's name, shown when picking a source and used as the policy-link text, is now called `name` (was `label`). Row-level descriptions were trimmed to the array itself and the page field, relabeled "Statement source" and described as: set it if you want a submission to save a reference to your policy. A `consent.sources` resolver returning the previous `{ key, label, ... }` shape must return `{ id, name, ... }` instead; existing consent field values referencing an old `key` no longer resolve to a source.
53
+ - **The form header gained two independent checkboxes, and the `buttons` group is gone.** `multistep` and `pollEnabled` are new top-level checkboxes: `multistep` reveals a **Flow** tab (previously always visible) and drives the client's step navigation, `pollEnabled` reveals a new **Poll** tab and replaces the previous nested `poll.enabled` (the poll group's own fields are otherwise unchanged). The **Buttons** group is gone: `submitLabel` now sits directly at the bottom of the Fields tab, and `nextLabel`/`prevLabel` sit in a row at the bottom of the Flow tab (shown once the flow has a step); all three are still reassembled into `FormDocument.buttons` the same way for `<Form>`. The `buttons.fields` composition seam now receives and returns a `{ submit, prev, next }` map keyed by slot instead of a single field array. A field a host adds alongside a default label no longer rides along automatically into `FormDocument.buttons`: `buttons`, like `poll`, is now a fixed allowlist on `toFormDocument` (previously it, like `response` still is, passed the whole group through), so surfacing a custom button field to the client now needs merging it into `toFormDocument`'s output by hand. `FormDocument` gains `multistep: boolean` and `pollEnabled: boolean`.
54
+ - **A poll outcome records a set of winners, not one.** `poll.outcome.winningValue` is `poll.outcome.winningValues`, an array: the admin winner picker is a multi-select, and a tie records every tied value instead of forcing a single pick. `resolvePollOutcome`'s `winningValue` argument is `winningValues`, also an array. `definePollOptionSource.resolveOutcome` may now return a single value, an array (a tie), or `undefined`.
55
+ - **The form title is localized.** `title` (the collection's `useAsTitle` field) now follows `localizeContent` like the rest of a form's visitor-facing text, required only in the default locale so other locales fall back to it instead of forcing a translation everywhere.
56
+ - **A poll's outcome type defaults to `mostVoted`, not `manual`, and the winner picker is hidden for every auto-resolving type, not just `mostVoted`.** A freshly enabled poll now resolves itself at close with no admin action required; picking `manual` still opts back into a hand-set winner. `poll.outcome.winningValues` shows only for `manual` now; it used to show for anything except `mostVoted`, so a `source` poll (or a host `definePollType` strategy) that previously let an editor hand-override its winner no longer offers that picker at all. A poll that already has an explicit `type` stored is unaffected; only one whose `type` was never saved picks up the new default.
57
+ - **Enabling a poll now requires a vote field.** With `pollEnabled` on and no `poll.resultsField` set, saving auto-picks the form's one eligible field (a named `select`, an opted-in `country`/`state`, or a custom `pollEligible` instance), asks you to choose when there are several, and refuses the save when there are none, unless the poll is driven by an option source or the `source` outcome type, which need no on-form field. Previously an enabled poll with no vote field saved without complaint and simply had nothing to aggregate.
58
+ - **Validation is binary; `severity` and `warnings` are gone.** A rule either blocks or passes, so the per-rule `severity` select, the `{ message, severity }` result form, `ValidationSeverity`, and the entire non-blocking `warnings` pathway are removed: the `FieldRendererProps.warnings` prop, the form state's `warnings`, `FieldShell`'s warning region, and `ValidateFieldResult.warnings`. A rule's `validate` now returns `true` (pass) or a `string` (a blocking error); every failure blocks submission. A custom renderer that read `warnings` should drop it, and a rule returning `{ message, severity: 'warning' }` now blocks like any other error.
59
+ - **`matchesField`'s target is a picker validated on save.** Its `field` param is authored by choosing from the form's own fields instead of typing a name (the value is still a stored field name, so no data migration), and it is checked on save: a target that no longer resolves to a field is a save error rather than a rule that silently never fires. The same picker and check are available to custom rules through the exported `fieldTargetParam`.
60
+
61
+ **Features:**
62
+
63
+ - Host-owned consent sources: `consentSourcesField()` is an array (`name`, rich text `statement`, optional polymorphic `page`) you place wherever your policies live, and `consent.sources` is the async `req`-scoped resolver that reads it back. The statement is authored rich text serialized through the same escaping and URL-sanitizing pipeline as action bodies; the policy link (a source's `name` plus a resolver-provided `url`) renders beside it, and the checkbox takes its accessible name from the statement flattened to its visible words. That flattener, `textOfBody`, is exported (root and `./react`) and handles rich text, legacy Slate, or a plain string. Multi-tenancy needs no plugin support: place the field on a tenant-scoped document and scope the resolver by `req`.
64
+ - Form-level button labels: `submitLabel`/`nextLabel`/`prevLabel`, localized following `localizeContent`, each resolved as `<Form>` prop, then the stored value, then the translated default. `buildSubmitLabelField`, `buildNextLabelField`, `buildPrevLabelField`, and `buildDefaultButtonFields` (keyed `submit`/`next`/`prev`) are exported, and `useFormContext()` now exposes the `form` document so custom chrome can read them.
65
+ - Sequential step fall-through: the flow builder's Default next select gains "Next step in order" as the default for an unset `next`, "End of form" maps to an explicit `null`, `normalizeFlow` preserves `null` as distinct from absent, and deleting a step cascades, clearing every `next` and transition aimed at it.
66
+ - Poll field eligibility: `pollEligible` on a field definition marks a type choosable as a poll's results field. The results endpoint gates anonymous reads on the same rule, serves option-source polls anonymously (buckets seeded in resolved order with the source's labels), fails closed with a 503 when a source throws, and refuses with a 403 while resolution is empty.
67
+ - A field type can source its own poll options: `FormFieldDefinition.resolveOptions` lets a `pollEligible` type produce its poll choices from the configured instance itself (for example a `hasMany` relationship to the voteable records) rather than from authored options or a shared source. The one resolution backs submission validation, the admin winner select, and results labeling alike; it is consulted after an `optionSource` and before the instance's authored `options`. `resolveEffectivePollOptions` (root and `/rsc`) and `ResolveFieldOptionsArgs` are exported.
68
+ - Poll outcome selection: `poll.outcome.winningValues` is admin-editable through a multi-select backed by an authenticated `GET /api/forms/:id/poll-options`, with `resolvedAt` server-stamped and access-locked (set, changed, or cleared by a hook; direct writes dropped). The group is composable through the `poll.outcomeFields` option, which receives the two default fields (`winningValues`, `resolvedAt`) and returns the group's fields verbatim, so a host can swap the winner picker for its own component; membership validation runs server-side regardless, so a swap cannot bypass it. `definePollOptionSource` gains a `resolveOutcome` seam, so `resolvePollOutcome` runs in auto mode with `winningValues` omitted and returns the recorded set either way. `resolveEffectivePollOptions` is the single option set backing the admin select, the endpoint, outcome validation, and a field type's `resolveOptions`, so what the admin offers and what the server accepts cannot drift. `buildWinningValuesField`, `buildResolvedAtField`, `buildDefaultOutcomeFields`, and `OutcomeFieldsOverride` are exported.
69
+ - Per-request sender addresses: `email.fromAddresses`, an async `req`-scoped resolver, gives both email actions a From select backed by an authenticated `GET /api/forms/:id/from-addresses`, validated for membership on save and passed through to `payload.sendEmail`. Intended for multi-tenant hosts scoping senders to the tenant derived from `req`. Absent (the default) adds no field and keeps the adapter's default sender.
70
+ - Admin polish: select option rows label their two fields `Label`/`Value` (were `Option label`/`Option value`) beneath an array already titled Option/Options; the flow step title's hardcoded, untranslatable placeholder is gone and the remaining English strings in the flow builder are translated; and `poll.outcome` and `response.redirect`, the two groups nested inside another group, render flush via `admin.hideGutter`.
71
+ - Extensible poll outcome types: `definePollType` registers a named outcome strategy (`resolveOutcome`), chosen per form via the poll's `type` select and configured plugin-wide via `poll.types`. Three ship and are always available: `manual` (default; an admin always sets the winner), `mostVoted` (auto-resolves to the top-voted value(s) once there are responses, a tie recording every co-winner, declining to decide on a truncated sample), and `source` (delegates to the poll's option source's own `resolveOutcome`). A poll with a `closesAt` and a non-`manual` type schedules its own resolution as a native Payload job (`waitUntil: closesAt`, re-enqueued on every save so the newest close date always wins); this needs a wired job runner (`config.jobs.autoRun`, a worker, or a serverless cron) to actually run. As a safety net with no runner, a closed, unresolved, non-manual poll also resolves the next time its results are read, by an admin or through the public endpoint alike.
72
+ - Department-routed email: the new `email.departments` plugin option turns `emailTeam`'s **To** into a select over host-managed addresses (an authenticated endpoint, validated on save, fails closed like `email.fromAddresses`). `departmentsField()` is the array a host places anywhere (a settings global, a tenant document) to hold the addresses, rendered as condensed flat rows with no per-row collapsible, and `resolveDepartmentOptions` builds the resolver's return value from it with a reusable current-locale -> default-locale -> next-available fallback. Independent of departments, **To** is now always localized like the subject and body, so a submission routes to the address stored for its own locale; the queued (job) dispatch path loads the form at the submission's own stored locale for the same reason.
73
+ - Concise, localized list columns: the `forms` list shows a field count ("6 Fields") and a step count ("2 steps", or none with no flow) instead of Payload's raw block/JSON summaries, plus a curated default column set (title, fields, flow, poll, updated at); `form-submissions` gets a curated default set too (form, status, locale, created at).
74
+ - Response messages interpolate recall tokens: a form's success message (`response.message`) now supports the same `{{field}}` / `{{field|fallback}}` / `{{field||fallback}}` tokens as an action body, resolved against the submission's own formatted answers (option labels, Yes/No, localized dates) and field descriptors, matching what `emailTeam`'s body already gave an admin.
75
+ - The submission answers view now renders with native Payload field components (the same `TextInput`/`FieldLabel`/`Pill` building blocks Payload's own fields use) instead of bespoke markup, and honors each field's authored width so the admin layout matches the widths editors set on the form. Consent renders as an Agreed/Declined `Pill` beside its source link and capture timestamp; `locale` and the submission `meta` (received-at time, IP/user agent when captured, spam signal) are folded into a single Submission-details section instead of separate top-level fields. `showSubmissionRawFields` now also gates `locale` and `meta`, alongside the existing `values`/`descriptors`/`consent`. `SubmissionDescriptor` gains `width`, and `SubmissionWidth` (`full`/`half`/`third`/`twoThirds`) is exported.
76
+ - A poll with no scheduled `closesAt` gets a manual **Close poll now** button in the admin, backed by an authenticated `POST /api/forms/:id/close`: it stamps `closesAt` to now and, for the `mostVoted`/`source` outcome types, resolves the winner the same way a scheduled auto-close would. A `manual` poll must already have a winner recorded, closing refuses (400) otherwise, and closing an already-closed poll is refused the same way. A `mostVoted` poll now manages its own outcome end to end: its `winningValues` field is hidden from the outcome group entirely (the strategy decides the winner, so there is nothing to hand-pick), whether it resolves on schedule or via this new button. The outcome `type` select reads more plainly, now with a description of what it does ("How the winning option is decided when the poll closes."): its options are `Set the winner manually`, `Most-voted option wins`, and `Winner from the option source` (the last hidden from the select until an option source is registered).
77
+ - The flow builder's field picker and unassigned-fields list now label each field with its authored `label` (falling back to the machine name) instead of the raw name, and label a bare message block with a short snippet of its authored content (falling back to its translated type label, numbered when the form has more than one) instead of a generic placeholder.
78
+ - A server-side `createSubmission(payload, { form, values, req? })` (root export) wraps `payload.create` for the `form-submissions` collection, giving server code (a script, a queued job, another collection's hook) a typed, documented entry point instead of hand-rolling the collection slug and body shape. It runs the exact pipeline the browser and REST paths run: server-authoritative validation, spam metadata when `req` is threaded through, descriptor snapshotting, consent capture, and post-submit action dispatch are all `form-submissions` collection hooks, not a route handler. Access is unchanged (`create: () => true`; `read` logged-in only; `update` disabled), so a raw REST `POST` and `createSubmission` reach the identical pipeline `submitForm` does.
79
+ - Admin and docs polish: a new Options API conventions section documents the plugin's few configuration shapes (opt-in flags, resolver-gated features, composition seams, registry seams), so which pattern a given option follows is no longer a guess. A field block's collapsed row header now shows the field's own label once set, falling back to its translated type name, instead of Payload's default block-name placeholder that reads "Untitled" until filled in (`FieldBlockLabel`, exported from `./client`). Two field descriptions duplicating their own field's obvious purpose are gone: the previous/next button-label fields' "Shown on multi-step forms only" and the department-routed **To** field's "The department this submission is routed to. Each locale keeps its own address."
80
+ - Save-through poll close and reopen: the Poll tab's button now saves the whole document through the admin's own submit instead of posting to a dedicated endpoint, so a `manual` poll's winner, even one picked in the same sitting, persists together with the close in one request rather than needing a prior save. The same button now also reopens: once closed, clicking it clears `closesAt` and the recorded `winningValues` together, for every outcome type. The `POST /:id/close` endpoint keeps its previous behavior unchanged for programmatic callers; only the admin button stopped using it.
81
+ - Email actions gain **Cc**, **Bcc**, and **Reply-to**: `emailTeam` and `confirmation` both carry all three, localized like `subject` and interpolated with the same recall tokens, each accepting a comma-separated address list forwarded to `payload.sendEmail` as-is.
82
+ - Internal-reference redirects: the new `redirectRelationships` plugin option (a `CollectionSlug[]`, named after `@payloadcms/plugin-form-builder`'s option of the same purpose) adds a polymorphic `reference` field to `response.redirect`, so an author can point a successful submit at an internal document instead of, or alongside, a URL. `toFormDocument` passes the raw `{ relationTo, value }` pair through unresolved; resolving it to a URL is the host's job, since the plugin has no notion of your routing.
83
+ - Two opt-in field types, `country` and `state`: fixed-option pickers (every ISO 3166-1 country; the 50 US states plus DC) that store the code and `format` it to the English name, poll-eligible like `select`. They are not registered by default; a host enables them with `fields: { country: countryField, state: stateField }` (both exported). Their renderers are always registered, so opting in needs no frontend wiring.
84
+ - Every validation rule opens with a plain-language description. Each rule's config block leads with a short, localized sentence explaining what makes it fail, so an editor never has to infer a rule's mechanics from its name (`matchesField`, for instance, states that it fails when the two answers differ). `ValidationRuleDefinition.description` carries it as a localizable key; the built-ins ship English and German.
85
+ - `fieldTargetParam(name, opts?)` is a reusable rule/action param that targets another field on the same form: a text value authored by the field-name picker, with a save-time existence check so a removed target is caught. `matchesField` uses it, and it is exported for custom rules that need to point at another field.
86
+ - A composition seam for the redirect response, `response.redirect.fields`: it hands you the group's default fields (the `url` text field, plus the `reference` relationship when `redirectRelationships` is set) and takes your array back, so you can drop in a custom link field, swap `url` for your own picker, reorder, or filter, mirroring `buttons.fields` and `poll.outcomeFields`. `RedirectFieldsOverride`, `ResponseOption`, and `RedirectOption` are exported. The plugin's built-in redirect still reads `redirect.url` / `redirect.reference`, so a host that replaces `url` owns resolving its own field to a destination.
87
+
88
+ **Fixes:**
89
+
90
+ - **A required checkbox accepted an explicit "unchecked".** The engine's required guard fires on `isEmpty(value)`, and `false` is a present value, so an untouched box (never sent) was rejected while a checked-then-unchecked one (sent as `false`) passed. `checkbox` (and `consent`) gained the intrinsic validator they lacked rather than widening the global `isEmpty`, which would ripple to every boolean. The one `runValidation` engine backs both client and server, so the fix binds them alike.
91
+ - **Admins saw raw stored values for sourced polls.** The authenticated results endpoint now injects the results field's effective options (source-resolved, `resolveOptions`-backed, or authored) too, so a sourced or resolver-backed poll renders labels in the admin instead of raw values, best-effort: a resolve failure there degrades to raw values rather than blocking the trusted read (the anonymous path still fails closed).
92
+ - The `pattern` rule's params are validated at authoring time, so an uncompilable expression or flag set is a save error instead of a rule that silently never fires. `pattern` compiles against its sibling `flags`, since flags decide validity, and an invalid flag is left to the `flags` field so one typo reports once. The submit-time fail-open stays as the safety net for rules stored before the guard existed.
93
+ - The unused-translation-key guard could never fail. It listed key-enumerating files by name and so missed `de.ts`, which then entered the usage corpus and, keying every entry as `keys.<name>`, matched all keys on its own, leaving `unused` permanently empty and the guard passing unconditionally. The corpus now excludes `src/translations/` by directory rather than by naming the files inside it, so no future locale can rejoin it and re-blind the scan however it happens to be written; nothing in that directory consumes a `keys.X` reference, so a key used only from there was never a used key anyway. Unreferenced keys are now reported, and no orphans exist once the scan works.
94
+ - A defaulted admin select was clearable with a silent consequence: a cleared submission `status` (which defaults to `complete`, and aggregation counts `complete`) dropped the submission out of every poll result with nothing in the UI saying so. It is now `isClearable: false`, matching the precedent on `width`, `response.type`, and `poll.resultsVisibility`.
95
+ - The shadcn registry's consent field rendered no required marker, making a required consent the only labelled field in the registry giving the visitor no sign it was mandatory until submit failed.
96
+ - Three fields with a custom select component silently rendered no description. A custom `Field` replaces Payload's whole default render, and a function `admin.description` is dropped from the client field before it ever gets there, so the text was configured but unreachable: the confirmation action's recipient field ("The email field on this form the confirmation is sent to.") and the file field's maximum size both lost theirs in earlier releases, and the poll's results field lost the sentence naming the rule it exists to enforce ("Use a choice field, never a free-text or PII field."). All three now pass a translated description key to their component, which renders it.
97
+ - The condition builder's value input for a `select` or `date` operand threw and broke the builder. Payload's native `Select`/`Date` leaf field components read `field.admin.placeholder` unconditionally, and the client field the builder synthesizes for a leaf condition carried no `admin` object at all; every synthesized leaf (`text` and `number` too, for consistency) now carries an empty `admin: {}`.
98
+ - Closing a `manual` poll with no winner recorded used to round-trip to the server and fail with a toast error after the click. The Close button now disables itself first, with an inline hint ("Select a winning value first."), so the invalid click is prevented rather than attempted and rejected.
99
+ - The poll's vote-field select was labelled "Results field," easy to misread as a display setting rather than the question being voted on. It is now "Vote field," with its description explaining the new auto-fill behavior.
100
+ - The email actions' **Cc** and **Bcc** labels are now cased **CC** and **BCC** (both locales), matching how the headers are conventionally written.
101
+ - The shadcn `form-results` registry snapshot was rebuilt. It still shipped the single-winner `winningValue?: string` component after the source moved to `winningValues?: string[]` for tie support, so a project pulling the registry got a component that could not highlight a tie; `registry:build` now matches the source.
102
+
103
+ **Notes:**
104
+
105
+ - No migrations, as ever pre-1.0: the removed `display` group, `response.submitLabel`, the consent `statement`/`optional` fields, the per-source `sourceConfig`, the dropped `placeholder`/`required` columns, and the form title's move to localized storage are orphaned rather than dropped. Write your own if you carry data.
106
+ - The From select and the poll winner select are both document-scoped, so each stays empty until the form's first save: their endpoints need a document id to resolve against. Save the form, then pick. The consent-sources select is the same.
107
+ - `email.fromAddresses` fails closed, and Payload runs the validate on every save rather than only when `from` changed, so while the resolver is throwing, no form whose email action has a From set can be saved at all, including edits that never touch the address. Failing open would persist a sender the host can no longer vouch for; a resolver reaching a flaky upstream should cache or fall back internally instead of throwing.
108
+ - Internal, no changeset of its own: the shared test harness gained `warmVersionCollections`, initializing drafts-enabled collections so consent version-capture is exercisable in integration tests. It is tooling (`@10x-media/test-harness`), not published.
109
+ - The plugin now registers `form-submissions` before `forms` (Payload lists collections in registration order within a nav group), so the shared "Forms" sidebar group reads alphabetically. Ordering the rest of a host's own sidebar against it stays the host's config concern; Payload has no global nav-sort option.
110
+
111
+ ### Patch Changes
112
+
113
+ - Correct `payload` and `@payloadcms/ui` peer ranges to `^3.83.0`. The plugin uses `definePlugin`, which shipped in Payload 3.83.0, so 3.82.x installs satisfied the old range but failed at import.
114
+
3
115
  ## 0.1.0-beta.4
4
116
 
5
117
  ### Minor Changes
package/README.md CHANGED
@@ -8,8 +8,8 @@ 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, 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
- - **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.
11
+ - **Fields**: text, textarea, email, number, date, select, checkbox, file, consent, calculation, repeater (dynamic row lists with per-row sub-field validation), and message, authored as Payload blocks; `country` and `state` are opt-in, and custom types via `defineFormField` are never second-class.
12
+ - **Validation**: declarative per-field rules with plain-language descriptions, custom messages, 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.
15
15
  - **Headless rendering**: `<Form>` with progressive validation and lifecycle events, accessible primitives, an optional layout grid; style via CSS hooks, a shadcn registry, or your own renderers.
@@ -17,7 +17,7 @@ Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-pl
17
17
  - **Recall and prefill**: pipe answers into labels and messages; URL prefill; hidden context fields.
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
- - **Consent** with proof by reference and policy-version capture; **file uploads** into your own upload collection with server-enforced MIME/size; **polls** with a gated public results endpoint.
20
+ - **Consent** referencing statements you own, resolved live per request, with proof stored as document ids and published versions; **file uploads** into your own upload collection with server-enforced MIME/size; **polls** with a gated public results endpoint.
21
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` and `form-submissions` with extra fields, hooks, and access rules using an explicit spread API that guarantees plugin-critical hooks always run.
@@ -27,16 +27,19 @@ type SerializeBodyArgs = {
27
27
  req?: PayloadRequest;
28
28
  };
29
29
  /**
30
- * Customizes how action bodies are authored and rendered. `converters` spread over the default
31
- * Lexical node converters; `serialize` replaces the whole pipeline (for non-HTML channels like
32
- * chat or plain text, or to hand the body plus the submitted `form`/`req` off to a renderer like
33
- * react-email). `editor` overrides the Lexical/richText editor on both the emailTeam and
34
- * confirmation action body fields (see `buildDefaultActionDefinitions`).
30
+ * Customizes how the plugin's rich text is authored and rendered. `converters` spread over the
31
+ * default Lexical node converters; `serialize` replaces the whole action-body pipeline (for
32
+ * non-HTML channels like chat or plain text, or to hand the body plus the submitted `form`/`req`
33
+ * off to a renderer like react-email). `editor` is the default Lexical/richText editor for every
34
+ * plugin-authored richText field: message content, consent statement, the response message, and
35
+ * the action body fields. `bodyEditor` overrides the action body fields specifically (emailTeam
36
+ * and confirmation), falling back to `editor` when absent.
35
37
  */
36
38
  type RichTextBodyOption = {
37
39
  converters?: Record<string, BodyConverter>;
38
40
  serialize?: (args: SerializeBodyArgs) => Promise<string> | string;
39
41
  editor?: RichTextField['editor'];
42
+ bodyEditor?: RichTextField['editor'];
40
43
  };
41
44
  /**
42
45
  * Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is
@@ -1 +1 @@
1
- {"version":3,"file":"serializeBody.js","names":[],"sources":["../../../src/actions/body/serializeBody.ts"],"sourcesContent":["import type { PayloadRequest, RichTextField } from 'payload'\nimport { 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/** Minimal form identity threaded alongside a rendered body (e.g. per-tenant template lookups). */\ntype SerializeBodyForm = { id: number | string; title?: string }\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/**\n * Args a custom `richText.serialize` replacement receives per rendered body. Always populated by\n * `makeRenderBody` (the action-body pipeline): `form` and `req` enable per-tenant template\n * lookups or handing the body off to a renderer like react-email.\n */\nexport type SerializeBodyArgs = {\n\tbody: unknown\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tform: SerializeBodyForm\n\treq?: PayloadRequest\n}\n\n/**\n * Customizes how action bodies are authored and rendered. `converters` spread over the default\n * Lexical node converters; `serialize` replaces the whole pipeline (for non-HTML channels like\n * chat or plain text, or to hand the body plus the submitted `form`/`req` off to a renderer like\n * react-email). `editor` overrides the Lexical/richText editor on both the emailTeam and\n * confirmation action body fields (see `buildDefaultActionDefinitions`).\n */\nexport type RichTextBodyOption = {\n\tconverters?: Record<string, BodyConverter>\n\tserialize?: (args: SerializeBodyArgs) => Promise<string> | string\n\teditor?: RichTextField['editor']\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\tform: SerializeBodyForm\n\t\treq?: PayloadRequest\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\tform: args.form,\n\t\t\t\treq: args.req,\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":";;;;;;;AA8CA,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,SAOD,OAAO,SAAmC;CACzC,IAAI,KAAK,UAAU,WAClB,OAAO,MAAM,KAAK,SAAS,UAAU;EACpC;EACA,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,MAAM,KAAK;EACX,KAAK,KAAK;CACX,CAAC;CAEF,OAAO,cAAc,MAAM;EAC1B,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,YAAY,KAAK,UAAU;CAC5B,CAAC;AACF"}
1
+ {"version":3,"file":"serializeBody.js","names":[],"sources":["../../../src/actions/body/serializeBody.ts"],"sourcesContent":["import type { PayloadRequest, RichTextField } from 'payload'\nimport { 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/** Minimal form identity threaded alongside a rendered body (e.g. per-tenant template lookups). */\ntype SerializeBodyForm = { id: number | string; title?: string }\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/**\n * Args a custom `richText.serialize` replacement receives per rendered body. Always populated by\n * `makeRenderBody` (the action-body pipeline): `form` and `req` enable per-tenant template\n * lookups or handing the body off to a renderer like react-email.\n */\nexport type SerializeBodyArgs = {\n\tbody: unknown\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tform: SerializeBodyForm\n\treq?: PayloadRequest\n}\n\n/**\n * Customizes how the plugin's rich text is authored and rendered. `converters` spread over the\n * default Lexical node converters; `serialize` replaces the whole action-body pipeline (for\n * non-HTML channels like chat or plain text, or to hand the body plus the submitted `form`/`req`\n * off to a renderer like react-email). `editor` is the default Lexical/richText editor for every\n * plugin-authored richText field: message content, consent statement, the response message, and\n * the action body fields. `bodyEditor` overrides the action body fields specifically (emailTeam\n * and confirmation), falling back to `editor` when absent.\n */\nexport type RichTextBodyOption = {\n\tconverters?: Record<string, BodyConverter>\n\tserialize?: (args: SerializeBodyArgs) => Promise<string> | string\n\teditor?: RichTextField['editor']\n\tbodyEditor?: RichTextField['editor']\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\tform: SerializeBodyForm\n\t\treq?: PayloadRequest\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\tform: args.form,\n\t\t\t\treq: args.req,\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":";;;;;;;AAiDA,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,SAOD,OAAO,SAAmC;CACzC,IAAI,KAAK,UAAU,WAClB,OAAO,MAAM,KAAK,SAAS,UAAU;EACpC;EACA,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,MAAM,KAAK;EACX,KAAK,KAAK;CACX,CAAC;CAEF,OAAO,cAAc,MAAM;EAC1B,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,YAAY,KAAK,UAAU;CAC5B,CAAC;AACF"}
@@ -0,0 +1,13 @@
1
+ //#region src/actions/body/textOfBody.d.ts
2
+ /**
3
+ * Plain-text content of a body (Lexical rich text, legacy Slate, or a legacy string): leaf text
4
+ * nodes concatenated in document order, block-level nodes (paragraph, heading, list item, quote)
5
+ * joined by single spaces, whitespace collapsed, so multi-block content flattens to a readable
6
+ * sentence. For accessible names and previews, where formatting and links collapse to their
7
+ * visible words. A plain string passes through untouched; unrecognized shapes yield `''`;
8
+ * never throws.
9
+ */
10
+ declare const textOfBody: (body: unknown) => string;
11
+ //#endregion
12
+ export { textOfBody };
13
+ //# sourceMappingURL=textOfBody.d.ts.map
@@ -0,0 +1,35 @@
1
+ //#region src/actions/body/textOfBody.ts
2
+ const isNode = (value) => value != null && typeof value === "object" && !Array.isArray(value);
3
+ /** Children-bearing block-level node types whose text reads as a separate run. */
4
+ const BLOCK_TYPES = new Set([
5
+ "paragraph",
6
+ "heading",
7
+ "listitem",
8
+ "quote"
9
+ ]);
10
+ const textOfNode = (node) => {
11
+ if (!isNode(node)) return "";
12
+ if (typeof node.text === "string") return node.text;
13
+ const children = Array.isArray(node.children) ? node.children.map(textOfNode).join("") : "";
14
+ if (children !== "" && typeof node.type === "string" && BLOCK_TYPES.has(node.type)) return ` ${children} `;
15
+ return children;
16
+ };
17
+ /**
18
+ * Plain-text content of a body (Lexical rich text, legacy Slate, or a legacy string): leaf text
19
+ * nodes concatenated in document order, block-level nodes (paragraph, heading, list item, quote)
20
+ * joined by single spaces, whitespace collapsed, so multi-block content flattens to a readable
21
+ * sentence. For accessible names and previews, where formatting and links collapse to their
22
+ * visible words. A plain string passes through untouched; unrecognized shapes yield `''`;
23
+ * never throws.
24
+ */
25
+ const textOfBody = (body) => {
26
+ if (typeof body === "string") return body;
27
+ const root = isNode(body) ? body.root : void 0;
28
+ const nodes = Array.isArray(body) ? body : isNode(root) && Array.isArray(root.children) ? root.children : null;
29
+ if (!nodes) return "";
30
+ return nodes.map(textOfNode).join("").replace(/\s+/g, " ").trim();
31
+ };
32
+ //#endregion
33
+ export { textOfBody };
34
+
35
+ //# sourceMappingURL=textOfBody.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textOfBody.js","names":[],"sources":["../../../src/actions/body/textOfBody.ts"],"sourcesContent":["type BodyNode = Record<string, unknown>\n\nconst isNode = (value: unknown): value is BodyNode =>\n\tvalue != null && typeof value === 'object' && !Array.isArray(value)\n\n/** Children-bearing block-level node types whose text reads as a separate run. */\nconst BLOCK_TYPES = new Set(['paragraph', 'heading', 'listitem', 'quote'])\n\nconst textOfNode = (node: unknown): string => {\n\tif (!isNode(node)) {\n\t\treturn ''\n\t}\n\tif (typeof node.text === 'string') {\n\t\treturn node.text\n\t}\n\tconst children = Array.isArray(node.children) ? node.children.map(textOfNode).join('') : ''\n\tif (children !== '' && typeof node.type === 'string' && BLOCK_TYPES.has(node.type)) {\n\t\treturn ` ${children} `\n\t}\n\treturn children\n}\n\n/**\n * Plain-text content of a body (Lexical rich text, legacy Slate, or a legacy string): leaf text\n * nodes concatenated in document order, block-level nodes (paragraph, heading, list item, quote)\n * joined by single spaces, whitespace collapsed, so multi-block content flattens to a readable\n * sentence. For accessible names and previews, where formatting and links collapse to their\n * visible words. A plain string passes through untouched; unrecognized shapes yield `''`;\n * never throws.\n */\nexport const textOfBody = (body: unknown): string => {\n\tif (typeof body === 'string') {\n\t\treturn body\n\t}\n\tconst root = isNode(body) ? body.root : undefined\n\tconst nodes = Array.isArray(body)\n\t\t? body\n\t\t: isNode(root) && Array.isArray(root.children)\n\t\t\t? root.children\n\t\t\t: null\n\tif (!nodes) {\n\t\treturn ''\n\t}\n\treturn nodes.map(textOfNode).join('').replace(/\\s+/g, ' ').trim()\n}\n"],"mappings":";AAEA,MAAM,UAAU,UACf,SAAS,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;;AAGnE,MAAM,cAAc,IAAI,IAAI;CAAC;CAAa;CAAW;CAAY;AAAO,CAAC;AAEzE,MAAM,cAAc,SAA0B;CAC7C,IAAI,CAAC,OAAO,IAAI,GACf,OAAO;CAER,IAAI,OAAO,KAAK,SAAS,UACxB,OAAO,KAAK;CAEb,MAAM,WAAW,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,SAAS,IAAI,UAAU,EAAE,KAAK,EAAE,IAAI;CACzF,IAAI,aAAa,MAAM,OAAO,KAAK,SAAS,YAAY,YAAY,IAAI,KAAK,IAAI,GAChF,OAAO,IAAI,SAAS;CAErB,OAAO;AACR;;;;;;;;;AAUA,MAAa,cAAc,SAA0B;CACpD,IAAI,OAAO,SAAS,UACnB,OAAO;CAER,MAAM,OAAO,OAAO,IAAI,IAAI,KAAK,OAAO,KAAA;CACxC,MAAM,QAAQ,MAAM,QAAQ,IAAI,IAC7B,OACA,OAAO,IAAI,KAAK,MAAM,QAAQ,KAAK,QAAQ,IAC1C,KAAK,WACL;CACJ,IAAI,CAAC,OACJ,OAAO;CAER,OAAO,MAAM,IAAI,UAAU,EAAE,KAAK,EAAE,EAAE,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACjE"}
@@ -5,6 +5,8 @@ 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";
9
+ import { buildFromField } from "../fromAddresses.js";
8
10
  //#region src/actions/builtin/confirmation.ts
9
11
  const TO_FIELD_REF = "@10x-media/form-builder/client#FieldNameSelect";
10
12
  /**
@@ -16,57 +18,88 @@ const validateToField = (value, { data, req }) => {
16
18
  return fieldNamesOfType(data && typeof data === "object" ? data.fields : void 0, ["email"]).includes(value) ? true : asTranslate(req.t)(keys.validationEmailFieldUnknown);
17
19
  };
18
20
  /**
19
- * `subject` and `body` are email content and follow `localize`; `toField` is an identifier and never does.
20
- * `editor` overrides the body field's Lexical/richText editor (from the plugin's `richText.editor` option).
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`.
21
27
  */
22
- const buildConfirmation = (localize, editor) => defineAction({
23
- type: "confirmation",
24
- label: keys.actionConfirmation,
25
- config: [
26
- {
27
- name: "toField",
28
- type: "text",
29
- label: labelFor(keys.actionConfigToField),
30
- admin: {
31
- description: labelFor(keys.actionConfigToFieldDescription),
32
- components: { Field: {
33
- path: TO_FIELD_REF,
34
- clientProps: { types: ["email"] }
35
- } }
36
- },
37
- validate: validateToField
38
- },
39
- {
40
- name: "subject",
41
- type: "text",
42
- label: labelFor(keys.actionConfigSubject),
43
- ...localizedIf(localize)
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: {
42
+ path: TO_FIELD_REF,
43
+ clientProps: {
44
+ types: ["email"],
45
+ descriptionKey: keys.actionConfigToFieldDescription
46
+ }
47
+ } }
44
48
  },
45
- {
46
- name: "body",
47
- type: "richText",
48
- label: labelFor(keys.actionConfigBody),
49
- admin: { description: labelFor(keys.actionConfigBodyDescription) },
50
- ...localizedIf(localize),
51
- ...editor ? { editor } : {}
49
+ validate: validateToField
50
+ };
51
+ return defineAction({
52
+ type: "confirmation",
53
+ label: keys.actionConfirmation,
54
+ config: [
55
+ {
56
+ type: "row",
57
+ fields: [toField, recip("replyTo", keys.actionConfigReplyTo)]
58
+ },
59
+ ...fromAddresses ? [buildFromField(fromAddresses)] : [],
60
+ {
61
+ type: "row",
62
+ fields: [recip("cc", keys.actionConfigCc), recip("bcc", keys.actionConfigBcc)]
63
+ },
64
+ {
65
+ name: "subject",
66
+ type: "text",
67
+ label: labelFor(keys.actionConfigSubject),
68
+ ...localizedIf(localize)
69
+ },
70
+ {
71
+ name: "body",
72
+ type: "richText",
73
+ label: labelFor(keys.actionConfigBody),
74
+ admin: { description: labelFor(keys.actionConfigBodyDescription) },
75
+ ...localizedIf(localize),
76
+ ...editor ? { editor } : {}
77
+ }
78
+ ],
79
+ run: async (args) => {
80
+ const { config, values, payload, renderBody } = args;
81
+ if (!config.toField) return;
82
+ const resolve = resolverFor(values);
83
+ const to = resolve(config.toField);
84
+ if (!to) return;
85
+ if (typeof payload.sendEmail !== "function") throw new Error("confirmation: no email adapter configured");
86
+ const subject = interpolate(config.subject ?? "", resolve);
87
+ const html = await renderBody(config.body);
88
+ const cc = resolveRecipients(config.cc, resolve);
89
+ const bcc = resolveRecipients(config.bcc, resolve);
90
+ const replyTo = resolveRecipients(config.replyTo, resolve);
91
+ await payload.sendEmail({
92
+ to,
93
+ subject,
94
+ html,
95
+ ...config.from ? { from: config.from } : {},
96
+ ...cc ? { cc } : {},
97
+ ...bcc ? { bcc } : {},
98
+ ...replyTo ? { replyTo } : {}
99
+ });
52
100
  }
53
- ],
54
- run: async (args) => {
55
- const { config, values, payload, renderBody } = args;
56
- if (!config.toField) return;
57
- const resolve = resolverFor(values);
58
- const to = resolve(config.toField);
59
- if (!to) return;
60
- if (typeof payload.sendEmail !== "function") throw new Error("confirmation: no email adapter configured");
61
- const subject = interpolate(config.subject ?? "", resolve);
62
- const html = await renderBody(config.body);
63
- await payload.sendEmail({
64
- to,
65
- subject,
66
- html
67
- });
68
- }
69
- });
101
+ });
102
+ };
70
103
  buildConfirmation(true);
71
104
  //#endregion
72
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'\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\n/**\n * `subject` and `body` are email content and follow `localize`; `toField` is an identifier and never does.\n * `editor` overrides the body field's Lexical/richText editor (from the plugin's `richText.editor` option).\n */\nexport const buildConfirmation = (localize: boolean, editor?: RichTextField['editor']) =>\n\tdefineAction<ConfirmationConfig>({\n\t\ttype: 'confirmation',\n\t\tlabel: keys.actionConfirmation,\n\t\tconfig: [\n\t\t\t{\n\t\t\t\tname: 'toField',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigToField),\n\t\t\t\tadmin: {\n\t\t\t\t\tdescription: labelFor(keys.actionConfigToFieldDescription),\n\t\t\t\t\tcomponents: { Field: { path: TO_FIELD_REF, clientProps: { types: ['email'] } } },\n\t\t\t\t},\n\t\t\t\tvalidate: validateToField,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tif (!config.toField) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst to = resolve(config.toField)\n\n\t\t\tif (!to) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('confirmation: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\n\t\t\tawait payload.sendEmail({ to, subject, html })\n\t\t},\n\t})\n\nexport const confirmation = buildConfirmation(true)\n"],"mappings":";;;;;;;;AAWA,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;;;;;AAMA,MAAa,qBAAqB,UAAmB,WACpD,aAAiC;CAChC,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;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,mBAAmB;GACxC,GAAG,YAAY,QAAQ;EACxB;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,gBAAgB;GACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;GACjE,GAAG,YAAY,QAAQ;GACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC5B;CACD;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;EAEhD,IAAI,CAAC,OAAO,SACX;EAGD,MAAM,UAAU,YAAY,MAAM;EAClC,MAAM,KAAK,QAAQ,OAAO,OAAO;EAEjC,IAAI,CAAC,IACJ;EAGD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,2CAA2C;EAG5D,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;EACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;EAEzC,MAAM,QAAQ,UAAU;GAAE;GAAI;GAAS;EAAK,CAAC;CAC9C;AACD,CAAC;AAE0B,kBAAkB,IAAI"}
1
+ {"version":3,"file":"confirmation.js","names":[],"sources":["../../../src/actions/builtin/confirmation.ts"],"sourcesContent":["import type { Field, PayloadRequest, RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport { fieldNamesOfType } from '../../fields/fieldNamesOfType'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { asTranslate, labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\nimport { buildRecipientField, type RecipientsConfig, resolveRecipients } from '../emailRecipients'\nimport { buildFromField, type FromAddressesResolver } from '../fromAddresses'\n\ntype ConfirmationConfig = {\n\ttoField?: string\n\tfrom?: string\n\tcc?: string[]\n\tbcc?: string[]\n\treplyTo?: string[]\n\tsubject?: string\n\tbody?: unknown\n}\n\nconst TO_FIELD_REF = '@10x-media/form-builder/client#FieldNameSelect'\n\n/**\n * Validates the confirmation action's `toField`: unset is fine, otherwise it must name an\n * existing `email`-type field on the form. Exported for unit testing.\n */\nexport const validateToField = (\n\tvalue: unknown,\n\t{ data, req }: { data?: unknown; req: PayloadRequest }\n): string | true => {\n\tif (typeof value !== 'string' || value.length === 0) {\n\t\treturn true\n\t}\n\tconst fields =\n\t\tdata && typeof data === 'object' ? (data as Record<string, unknown>).fields : undefined\n\treturn fieldNamesOfType(fields, ['email']).includes(value)\n\t\t? true\n\t\t: asTranslate(req.t)(keys.validationEmailFieldUnknown)\n}\n\n/**\n * `toField` names the form's own email field the confirmation is sent to (a `FieldNameSelect`, not a\n * recipient list). `replyTo`, `cc`, and `bcc` are recipient lists (`RecipientsSelect`, `string[]`):\n * literal emails, picked department addresses (when `departments` is set), or `{{field}}` tokens\n * resolved from the submission at send. `from` is a single address, added only when `fromAddresses`\n * is set. `subject`/`body` are content; `editor` overrides the body editor; `recipients` narrows the\n * recipient fields' behavior. Content and recipient fields carry `localized: true` when `localize`.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args thread plugin options (localize, editor, fromAddresses, departments, recipients)\nexport const buildConfirmation = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver,\n\trecipients?: RecipientsConfig\n) => {\n\tconst endpoint = departments ? 'departments' : undefined\n\tconst recip = (name: string, labelKey: string) =>\n\t\tbuildRecipientField(name, labelKey, localize, { endpoint, recipients, width: '50%' })\n\tconst toField: Field = {\n\t\tname: 'toField',\n\t\ttype: 'text',\n\t\tlabel: labelFor(keys.actionConfigToField),\n\t\tadmin: {\n\t\t\twidth: '50%',\n\t\t\tcomponents: {\n\t\t\t\tField: {\n\t\t\t\t\tpath: TO_FIELD_REF,\n\t\t\t\t\tclientProps: { types: ['email'], descriptionKey: keys.actionConfigToFieldDescription },\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tvalidate: validateToField,\n\t}\n\treturn defineAction<ConfirmationConfig>({\n\t\ttype: 'confirmation',\n\t\tlabel: keys.actionConfirmation,\n\t\tconfig: [\n\t\t\t{ type: 'row', fields: [toField, recip('replyTo', keys.actionConfigReplyTo)] },\n\t\t\t...(fromAddresses ? [buildFromField(fromAddresses)] : []),\n\t\t\t{\n\t\t\t\ttype: 'row',\n\t\t\t\tfields: [recip('cc', keys.actionConfigCc), recip('bcc', keys.actionConfigBcc)],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tif (!config.toField) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst to = resolve(config.toField)\n\n\t\t\tif (!to) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('confirmation: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\t\t\tconst cc = resolveRecipients(config.cc, resolve)\n\t\t\tconst bcc = resolveRecipients(config.bcc, resolve)\n\t\t\tconst replyTo = resolveRecipients(config.replyTo, resolve)\n\n\t\t\t// `from` was validated at save time against `fromAddresses(req)`; not re-checked here\n\t\t\t// (the job's `req` may differ from the authoring admin's, and the config is\n\t\t\t// admin-authored, not visitor-controlled), so the stored value is forwarded verbatim.\n\t\t\tawait payload.sendEmail({\n\t\t\t\tto,\n\t\t\t\tsubject,\n\t\t\t\thtml,\n\t\t\t\t...(config.from ? { from: config.from } : {}),\n\t\t\t\t...(cc ? { cc } : {}),\n\t\t\t\t...(bcc ? { bcc } : {}),\n\t\t\t\t...(replyTo ? { replyTo } : {}),\n\t\t\t})\n\t\t},\n\t})\n}\n\nexport const confirmation = buildConfirmation(true)\n"],"mappings":";;;;;;;;;;AAsBA,MAAM,eAAe;;;;;AAMrB,MAAa,mBACZ,OACA,EAAE,MAAM,UACW;CACnB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAIR,OAAO,iBADN,QAAQ,OAAO,SAAS,WAAY,KAAiC,SAAS,KAAA,GAC/C,CAAC,OAAO,CAAC,EAAE,SAAS,KAAK,IACtD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,2BAA2B;AACvD;;;;;;;;;AAWA,MAAa,qBACZ,UACA,QACA,eACA,aACA,eACI;CACJ,MAAM,WAAW,cAAc,gBAAgB,KAAA;CAC/C,MAAM,SAAS,MAAc,aAC5B,oBAAoB,MAAM,UAAU,UAAU;EAAE;EAAU;EAAY,OAAO;CAAM,CAAC;CACrF,MAAM,UAAiB;EACtB,MAAM;EACN,MAAM;EACN,OAAO,SAAS,KAAK,mBAAmB;EACxC,OAAO;GACN,OAAO;GACP,YAAY,EACX,OAAO;IACN,MAAM;IACN,aAAa;KAAE,OAAO,CAAC,OAAO;KAAG,gBAAgB,KAAK;IAA+B;GACtF,EACD;EACD;EACA,UAAU;CACX;CACA,OAAO,aAAiC;EACvC,MAAM;EACN,OAAO,KAAK;EACZ,QAAQ;GACP;IAAE,MAAM;IAAO,QAAQ,CAAC,SAAS,MAAM,WAAW,KAAK,mBAAmB,CAAC;GAAE;GAC7E,GAAI,gBAAgB,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC;GACvD;IACC,MAAM;IACN,QAAQ,CAAC,MAAM,MAAM,KAAK,cAAc,GAAG,MAAM,OAAO,KAAK,eAAe,CAAC;GAC9E;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,gBAAgB;IACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;IACjE,GAAG,YAAY,QAAQ;IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAC5B;EACD;EACA,KAAK,OAAO,SAAS;GACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;GAEhD,IAAI,CAAC,OAAO,SACX;GAGD,MAAM,UAAU,YAAY,MAAM;GAClC,MAAM,KAAK,QAAQ,OAAO,OAAO;GAEjC,IAAI,CAAC,IACJ;GAGD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,2CAA2C;GAG5D,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;GACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;GACzC,MAAM,KAAK,kBAAkB,OAAO,IAAI,OAAO;GAC/C,MAAM,MAAM,kBAAkB,OAAO,KAAK,OAAO;GACjD,MAAM,UAAU,kBAAkB,OAAO,SAAS,OAAO;GAKzD,MAAM,QAAQ,UAAU;IACvB;IACA;IACA;IACA,GAAI,OAAO,OAAO,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;IAC3C,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC;IACnB,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;IACrB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;GAC9B,CAAC;EACF;CACD,CAAC;AACF;AAE4B,kBAAkB,IAAI"}
@@ -4,48 +4,77 @@ 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";
8
+ import { buildFromField } from "../fromAddresses.js";
7
9
  //#region src/actions/builtin/emailTeam.ts
8
10
  /**
9
- * `subject` and `body` are email content and follow `localize`; the `to` address never does.
10
- * `editor` overrides the body field's Lexical/richText editor (from the plugin's `richText.editor` option).
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.
11
19
  */
12
- const buildEmailTeam = (localize, editor) => defineAction({
13
- type: "emailTeam",
14
- label: keys.actionEmailTeam,
15
- config: [
16
- {
17
- name: "to",
18
- type: "text",
19
- label: labelFor(keys.actionConfigTo)
20
- },
21
- {
22
- name: "subject",
23
- type: "text",
24
- label: labelFor(keys.actionConfigSubject),
25
- ...localizedIf(localize)
26
- },
27
- {
28
- name: "body",
29
- type: "richText",
30
- label: labelFor(keys.actionConfigBody),
31
- admin: { description: labelFor(keys.actionConfigBodyDescription) },
32
- ...localizedIf(localize),
33
- ...editor ? { editor } : {}
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
+ });
27
+ return defineAction({
28
+ type: "emailTeam",
29
+ label: keys.actionEmailTeam,
30
+ config: [
31
+ {
32
+ type: "row",
33
+ fields: [recip("to", keys.actionConfigTo), recip("replyTo", keys.actionConfigReplyTo)]
34
+ },
35
+ ...fromAddresses ? [buildFromField(fromAddresses)] : [],
36
+ {
37
+ type: "row",
38
+ fields: [recip("cc", keys.actionConfigCc), recip("bcc", keys.actionConfigBcc)]
39
+ },
40
+ {
41
+ name: "subject",
42
+ type: "text",
43
+ label: labelFor(keys.actionConfigSubject),
44
+ ...localizedIf(localize)
45
+ },
46
+ {
47
+ name: "body",
48
+ type: "richText",
49
+ label: labelFor(keys.actionConfigBody),
50
+ admin: { description: labelFor(keys.actionConfigBodyDescription) },
51
+ ...localizedIf(localize),
52
+ ...editor ? { editor } : {}
53
+ }
54
+ ],
55
+ run: async (args) => {
56
+ const { config, values, payload, renderBody } = args;
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");
61
+ const subject = interpolate(config.subject ?? "", resolve);
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);
66
+ await payload.sendEmail({
67
+ to,
68
+ subject,
69
+ html,
70
+ ...config.from ? { from: config.from } : {},
71
+ ...cc ? { cc } : {},
72
+ ...bcc ? { bcc } : {},
73
+ ...replyTo ? { replyTo } : {}
74
+ });
34
75
  }
35
- ],
36
- run: async (args) => {
37
- const { config, values, payload, renderBody } = args;
38
- if (!config.to) throw new Error("emailTeam: missing \"to\" address");
39
- if (typeof payload.sendEmail !== "function") throw new Error("emailTeam: no email adapter configured");
40
- const subject = interpolate(config.subject ?? "", resolverFor(values));
41
- const html = await renderBody(config.body);
42
- await payload.sendEmail({
43
- to: config.to,
44
- subject,
45
- html
46
- });
47
- }
48
- });
76
+ });
77
+ };
49
78
  buildEmailTeam(true);
50
79
  //#endregion
51
80
  export { buildEmailTeam };