@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
@@ -1 +1 @@
1
- {"version":3,"file":"emailTeam.js","names":[],"sources":["../../../src/actions/builtin/emailTeam.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\n\ntype EmailTeamConfig = { to?: string; subject?: string; body?: unknown }\n\n/**\n * `subject` and `body` are email content and follow `localize`; the `to` address never does.\n * `editor` overrides the body field's Lexical/richText editor (from the plugin's `richText.editor` option).\n */\nexport const buildEmailTeam = (localize: boolean, editor?: RichTextField['editor']) =>\n\tdefineAction<EmailTeamConfig>({\n\t\ttype: 'emailTeam',\n\t\tlabel: keys.actionEmailTeam,\n\t\tconfig: [\n\t\t\t{ name: 'to', type: 'text', label: labelFor(keys.actionConfigTo) },\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tif (!config.to) {\n\t\t\t\tthrow new Error('emailTeam: missing \"to\" address')\n\t\t\t}\n\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('emailTeam: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolverFor(values))\n\t\t\tconst html = await renderBody(config.body)\n\n\t\t\tawait payload.sendEmail({ to: config.to, subject, html })\n\t\t},\n\t})\n\nexport const emailTeam = buildEmailTeam(true)\n"],"mappings":";;;;;;;;;;;AAcA,MAAa,kBAAkB,UAAmB,WACjD,aAA8B;CAC7B,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ;EACP;GAAE,MAAM;GAAM,MAAM;GAAQ,OAAO,SAAS,KAAK,cAAc;EAAE;EACjE;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,IACX,MAAM,IAAI,MAAM,mCAAiC;EAGlD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,wCAAwC;EAGzD,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,YAAY,MAAM,CAAC;EACrE,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;EAEzC,MAAM,QAAQ,UAAU;GAAE,IAAI,OAAO;GAAI;GAAS;EAAK,CAAC;CACzD;AACD,CAAC;AAEuB,eAAe,IAAI"}
1
+ {"version":3,"file":"emailTeam.js","names":[],"sources":["../../../src/actions/builtin/emailTeam.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\nimport { buildRecipientField, type RecipientsConfig, resolveRecipients } from '../emailRecipients'\nimport { buildFromField, type FromAddressesResolver } from '../fromAddresses'\n\ntype EmailTeamConfig = {\n\tto?: string[]\n\tfrom?: string\n\tcc?: string[]\n\tbcc?: string[]\n\treplyTo?: string[]\n\tsubject?: string\n\tbody?: unknown\n}\n\n/**\n * `to`, `replyTo`, `cc`, and `bcc` are recipient lists (`RecipientsSelect`, stored as `string[]`):\n * each entry is a literal email, a picked department address (when `departments` is set its options\n * back the select), or a `{{field}}` token resolved from the submission at send. `from` is a single\n * address that never localizes, added only when `fromAddresses` is set. `subject` and `body` are email\n * content; `editor` overrides the body's richText editor. `recipients` narrows the recipient fields'\n * behavior (free-typed emails, field tokens). Content and recipient fields carry `localized: true`\n * when `localize` is true. At send, every recipient list is interpolated, empties dropped, and joined\n * into the comma-separated string `payload.sendEmail` (nodemailer) accepts.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args thread plugin options (localize, editor, fromAddresses, departments, recipients)\nexport const buildEmailTeam = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver,\n\trecipients?: RecipientsConfig\n) => {\n\tconst endpoint = departments ? 'departments' : undefined\n\tconst recip = (name: string, labelKey: string) =>\n\t\tbuildRecipientField(name, labelKey, localize, { endpoint, recipients, width: '50%' })\n\treturn defineAction<EmailTeamConfig>({\n\t\ttype: 'emailTeam',\n\t\tlabel: keys.actionEmailTeam,\n\t\tconfig: [\n\t\t\t{\n\t\t\t\ttype: 'row',\n\t\t\t\tfields: [recip('to', keys.actionConfigTo), recip('replyTo', keys.actionConfigReplyTo)],\n\t\t\t},\n\t\t\t...(fromAddresses ? [buildFromField(fromAddresses)] : []),\n\t\t\t{\n\t\t\t\ttype: 'row',\n\t\t\t\tfields: [recip('cc', keys.actionConfigCc), recip('bcc', keys.actionConfigBcc)],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst to = resolveRecipients(config.to, resolve)\n\t\t\tif (!to) {\n\t\t\t\tthrow new Error('emailTeam: missing \"to\" address')\n\t\t\t}\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('emailTeam: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\t\t\tconst cc = resolveRecipients(config.cc, resolve)\n\t\t\tconst bcc = resolveRecipients(config.bcc, resolve)\n\t\t\tconst replyTo = resolveRecipients(config.replyTo, resolve)\n\n\t\t\t// `from` was validated at save time against `fromAddresses(req)`; not re-checked here\n\t\t\t// (the job's `req` may differ from the authoring admin's, and the config is\n\t\t\t// admin-authored, not visitor-controlled), so the stored value is forwarded verbatim.\n\t\t\tawait payload.sendEmail({\n\t\t\t\tto,\n\t\t\t\tsubject,\n\t\t\t\thtml,\n\t\t\t\t...(config.from ? { from: config.from } : {}),\n\t\t\t\t...(cc ? { cc } : {}),\n\t\t\t\t...(bcc ? { bcc } : {}),\n\t\t\t\t...(replyTo ? { replyTo } : {}),\n\t\t\t})\n\t\t},\n\t})\n}\n\nexport const emailTeam = buildEmailTeam(true)\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgCA,MAAa,kBACZ,UACA,QACA,eACA,aACA,eACI;CACJ,MAAM,WAAW,cAAc,gBAAgB,KAAA;CAC/C,MAAM,SAAS,MAAc,aAC5B,oBAAoB,MAAM,UAAU,UAAU;EAAE;EAAU;EAAY,OAAO;CAAM,CAAC;CACrF,OAAO,aAA8B;EACpC,MAAM;EACN,OAAO,KAAK;EACZ,QAAQ;GACP;IACC,MAAM;IACN,QAAQ,CAAC,MAAM,MAAM,KAAK,cAAc,GAAG,MAAM,WAAW,KAAK,mBAAmB,CAAC;GACtF;GACA,GAAI,gBAAgB,CAAC,eAAe,aAAa,CAAC,IAAI,CAAC;GACvD;IACC,MAAM;IACN,QAAQ,CAAC,MAAM,MAAM,KAAK,cAAc,GAAG,MAAM,OAAO,KAAK,eAAe,CAAC;GAC9E;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,mBAAmB;IACxC,GAAG,YAAY,QAAQ;GACxB;GACA;IACC,MAAM;IACN,MAAM;IACN,OAAO,SAAS,KAAK,gBAAgB;IACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;IACjE,GAAG,YAAY,QAAQ;IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAC5B;EACD;EACA,KAAK,OAAO,SAAS;GACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;GAEhD,MAAM,UAAU,YAAY,MAAM;GAClC,MAAM,KAAK,kBAAkB,OAAO,IAAI,OAAO;GAC/C,IAAI,CAAC,IACJ,MAAM,IAAI,MAAM,mCAAiC;GAElD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,wCAAwC;GAGzD,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;GACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;GACzC,MAAM,KAAK,kBAAkB,OAAO,IAAI,OAAO;GAC/C,MAAM,MAAM,kBAAkB,OAAO,KAAK,OAAO;GACjD,MAAM,UAAU,kBAAkB,OAAO,SAAS,OAAO;GAKzD,MAAM,QAAQ,UAAU;IACvB;IACA;IACA;IACA,GAAI,OAAO,OAAO,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;IAC3C,GAAI,KAAK,EAAE,GAAG,IAAI,CAAC;IACnB,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;IACrB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;GAC9B,CAAC;EACF;CACD,CAAC;AACF;AAEyB,eAAe,IAAI"}
@@ -1,14 +1,20 @@
1
1
  import { AnyActionDefinition } from "../defineAction.js";
2
+ import { FromAddressesResolver } from "../fromAddresses.js";
3
+ import { RecipientsConfig } from "../emailRecipients.js";
4
+ import { DepartmentEmailsResolver } from "../../email/departments.js";
2
5
  import { RichTextField } from "payload";
3
6
 
4
7
  //#region src/actions/builtin/index.d.ts
5
8
  /**
6
9
  * Built-in action definitions with content-bearing config fields (email subjects and rich text
7
10
  * bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides
8
- * the Lexical/richText editor on both action bodies (from the plugin's `richText.editor` option).
9
- * `signedWebhook` has no content fields and is shared static.
11
+ * the Lexical/richText editor on both action bodies (the plugin passes
12
+ * `richText.bodyEditor ?? richText.editor`). `fromAddresses`, when given (the plugin's
13
+ * `email.fromAddresses` option), adds a `from` select to both email actions. `departments`, when
14
+ * given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the
15
+ * host's resolved departments. `signedWebhook` has no content fields and is shared static.
10
16
  */
11
- declare const buildDefaultActionDefinitions: (localize: boolean, editor?: RichTextField["editor"]) => Record<string, AnyActionDefinition>;
17
+ declare const buildDefaultActionDefinitions: (localize: boolean, editor?: RichTextField["editor"], fromAddresses?: FromAddressesResolver, departments?: DepartmentEmailsResolver, recipients?: RecipientsConfig) => Record<string, AnyActionDefinition>;
12
18
  /**
13
19
  * Prebuilt with content localization on; with `localizeContent: false`, derive overrides from
14
20
  * `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.
@@ -5,12 +5,15 @@ import { signedWebhook } from "./signedWebhook.js";
5
5
  /**
6
6
  * Built-in action definitions with content-bearing config fields (email subjects and rich text
7
7
  * bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides
8
- * the Lexical/richText editor on both action bodies (from the plugin's `richText.editor` option).
9
- * `signedWebhook` has no content fields and is shared static.
8
+ * the Lexical/richText editor on both action bodies (the plugin passes
9
+ * `richText.bodyEditor ?? richText.editor`). `fromAddresses`, when given (the plugin's
10
+ * `email.fromAddresses` option), adds a `from` select to both email actions. `departments`, when
11
+ * given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the
12
+ * host's resolved departments. `signedWebhook` has no content fields and is shared static.
10
13
  */
11
- const buildDefaultActionDefinitions = (localize, editor) => ({
12
- emailTeam: buildEmailTeam(localize, editor),
13
- confirmation: buildConfirmation(localize, editor),
14
+ const buildDefaultActionDefinitions = (localize, editor, fromAddresses, departments, recipients) => ({
15
+ emailTeam: buildEmailTeam(localize, editor, fromAddresses, departments, recipients),
16
+ confirmation: buildConfirmation(localize, editor, fromAddresses, departments, recipients),
14
17
  signedWebhook
15
18
  });
16
19
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/actions/builtin/index.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { AnyActionDefinition } from '../defineAction'\nimport { buildConfirmation } from './confirmation'\nimport { buildEmailTeam } from './emailTeam'\nimport { signedWebhook } from './signedWebhook'\n\n/**\n * Built-in action definitions with content-bearing config fields (email subjects and rich text\n * bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides\n * the Lexical/richText editor on both action bodies (from the plugin's `richText.editor` option).\n * `signedWebhook` has no content fields and is shared static.\n */\nexport const buildDefaultActionDefinitions = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor']\n): Record<string, AnyActionDefinition> => ({\n\temailTeam: buildEmailTeam(localize, editor) as AnyActionDefinition,\n\tconfirmation: buildConfirmation(localize, editor) as AnyActionDefinition,\n\tsignedWebhook: signedWebhook as AnyActionDefinition,\n})\n\n/**\n * Prebuilt with content localization on; with `localizeContent: false`, derive overrides from\n * `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.\n */\nexport const defaultActionDefinitions: Record<string, AnyActionDefinition> =\n\tbuildDefaultActionDefinitions(true)\n"],"mappings":";;;;;;;;;;AAYA,MAAa,iCACZ,UACA,YAC0C;CAC1C,WAAW,eAAe,UAAU,MAAM;CAC1C,cAAc,kBAAkB,UAAU,MAAM;CACjC;AAChB;;;;;AAMA,MAAa,2BACZ,8BAA8B,IAAI"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/actions/builtin/index.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { DepartmentEmailsResolver } from '../../email/departments'\nimport type { AnyActionDefinition } from '../defineAction'\nimport type { RecipientsConfig } from '../emailRecipients'\nimport type { FromAddressesResolver } from '../fromAddresses'\nimport { buildConfirmation } from './confirmation'\nimport { buildEmailTeam } from './emailTeam'\nimport { signedWebhook } from './signedWebhook'\n\n/**\n * Built-in action definitions with content-bearing config fields (email subjects and rich text\n * bodies) carrying `localized: true` when `localize` is true. `editor`, when given, overrides\n * the Lexical/richText editor on both action bodies (the plugin passes\n * `richText.bodyEditor ?? richText.editor`). `fromAddresses`, when given (the plugin's\n * `email.fromAddresses` option), adds a `from` select to both email actions. `departments`, when\n * given (the plugin's `email.departments` option), turns the `emailTeam` `to` into a select over the\n * host's resolved departments. `signedWebhook` has no content fields and is shared static.\n */\n// biome-ignore lint/complexity/useMaxParams: positional args thread the plugin editor and email options (localize, editor, fromAddresses, departments, recipients)\nexport const buildDefaultActionDefinitions = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tfromAddresses?: FromAddressesResolver,\n\tdepartments?: DepartmentEmailsResolver,\n\trecipients?: RecipientsConfig\n): Record<string, AnyActionDefinition> => ({\n\temailTeam: buildEmailTeam(\n\t\tlocalize,\n\t\teditor,\n\t\tfromAddresses,\n\t\tdepartments,\n\t\trecipients\n\t) as AnyActionDefinition,\n\tconfirmation: buildConfirmation(\n\t\tlocalize,\n\t\teditor,\n\t\tfromAddresses,\n\t\tdepartments,\n\t\trecipients\n\t) as AnyActionDefinition,\n\tsignedWebhook: signedWebhook as AnyActionDefinition,\n})\n\n/**\n * Prebuilt with content localization on; with `localizeContent: false`, derive overrides from\n * `buildDefaultActionDefinitions(false)` instead so a spread does not reintroduce `localized`.\n */\nexport const defaultActionDefinitions: Record<string, AnyActionDefinition> =\n\tbuildDefaultActionDefinitions(true)\n"],"mappings":";;;;;;;;;;;;;AAmBA,MAAa,iCACZ,UACA,QACA,eACA,aACA,gBAC0C;CAC1C,WAAW,eACV,UACA,QACA,eACA,aACA,UACD;CACA,cAAc,kBACb,UACA,QACA,eACA,aACA,UACD;CACe;AAChB;;;;;AAMA,MAAa,2BACZ,8BAA8B,IAAI"}
@@ -1,5 +1,5 @@
1
- import { Translate } from "../fields/types.js";
2
1
  import { SubmissionDescriptor, SubmissionValue } from "../submissions/types.js";
2
+ import { Translate } from "../fields/types.js";
3
3
  import { Field, Payload, PayloadRequest } from "payload";
4
4
 
5
5
  //#region src/actions/defineAction.d.ts
@@ -0,0 +1,12 @@
1
+ import { PayloadRequest } from "payload";
2
+
3
+ //#region src/actions/emailRecipients.d.ts
4
+ /** Host-configurable behavior for the recipient fields (plugin option `email.recipients`). */
5
+ type RecipientsConfig = {
6
+ /** Allow free-typed emails (default true). */allowCustom?: boolean; /** Offer the form's own fields as recipient tokens (default true). */
7
+ fieldTokens?: boolean; /** Field types eligible as tokens (default `['email']`). */
8
+ tokenFieldTypes?: string[];
9
+ };
10
+ //#endregion
11
+ export { RecipientsConfig };
12
+ //# sourceMappingURL=emailRecipients.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { fieldNames, fieldNamesOfType } from "../fields/fieldNamesOfType.js";
2
+ import { localizedIf } from "../fields/localizedIf.js";
3
+ import { interpolate } from "../recall/interpolate.js";
4
+ import { keys } from "../translations/keys.js";
5
+ import { asTranslate, labelFor } from "../translations/server.js";
6
+ //#region src/actions/emailRecipients.ts
7
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8
+ const TOKEN_RE = /^\{\{\s*([\w.-]+)\s*\}\}$/;
9
+ /** A plausible email address (a permissive shape check, not full RFC validation). */
10
+ const isPlausibleEmail = (value) => EMAIL_RE.test(value.trim());
11
+ /** The field name inside a `{{name}}` recipient token, or undefined when the string is not a token. */
12
+ const parseFieldToken = (value) => {
13
+ const match = TOKEN_RE.exec(value.trim());
14
+ return match ? match[1] : void 0;
15
+ };
16
+ const toList = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : typeof value === "string" && value.length > 0 ? [value] : [];
17
+ /**
18
+ * Resolve a stored recipient value (a `string[]`, or a legacy single string) to the comma-separated
19
+ * list `payload.sendEmail` accepts: each entry is interpolated (a `{{field}}` token resolves from the
20
+ * submission, a plain email passes through), trimmed, empties dropped, joined with `, `.
21
+ */
22
+ const resolveRecipients = (value, resolve) => toList(value).map((entry) => interpolate(entry, resolve).trim()).filter((entry) => entry.length > 0).join(", ");
23
+ /**
24
+ * Field `validate` for a recipient list: unset is fine; otherwise every entry must be a valid email
25
+ * or a `{{field}}` token naming an existing field of an allowed token type (`tokenFieldTypes`, or any
26
+ * named field when empty). Reads the form's `fields` off `data`, like the confirmation `toField`.
27
+ */
28
+ const validateRecipients = (tokenFieldTypes) => (value, { data, req }) => {
29
+ const list = toList(value);
30
+ if (list.length === 0) return true;
31
+ const fields = data && typeof data === "object" ? data.fields : void 0;
32
+ const allowed = new Set(tokenFieldTypes && tokenFieldTypes.length > 0 ? fieldNamesOfType(fields, tokenFieldTypes) : fieldNames(fields));
33
+ for (const entry of list) {
34
+ const token = parseFieldToken(entry);
35
+ if (token) {
36
+ if (!allowed.has(token)) return asTranslate(req.t)(keys.validationRecipientUnknownField);
37
+ continue;
38
+ }
39
+ if (!isPlausibleEmail(entry)) return asTranslate(req.t)(keys.validationRecipientInvalid);
40
+ }
41
+ return true;
42
+ };
43
+ const RECIPIENTS_FIELD_REF = "@10x-media/form-builder/client#RecipientsSelect";
44
+ /**
45
+ * A `text hasMany` field rendered by `RecipientsSelect`, used for every email address list. `endpoint`
46
+ * (when set) supplies preset options (e.g. the host's departments); `recipients` narrows the field's
47
+ * behavior; `width` sets `admin.width` so a pair can share a row.
48
+ */
49
+ const buildRecipientField = (name, labelKey, localize, opts = {}) => ({
50
+ name,
51
+ type: "text",
52
+ hasMany: true,
53
+ label: labelFor(labelKey),
54
+ validate: validateRecipients(opts.recipients?.tokenFieldTypes ?? ["email"]),
55
+ ...localizedIf(localize),
56
+ admin: {
57
+ ...opts.width ? { width: opts.width } : {},
58
+ components: { Field: {
59
+ path: RECIPIENTS_FIELD_REF,
60
+ clientProps: {
61
+ ...opts.endpoint ? { endpoint: opts.endpoint } : {},
62
+ ...opts.recipients?.allowCustom === false ? { allowCustom: false } : {},
63
+ ...opts.recipients?.fieldTokens === false ? { fieldTokens: false } : {},
64
+ ...opts.recipients?.tokenFieldTypes ? { tokenFieldTypes: opts.recipients.tokenFieldTypes } : {}
65
+ }
66
+ } }
67
+ }
68
+ });
69
+ //#endregion
70
+ export { buildRecipientField, isPlausibleEmail, resolveRecipients };
71
+
72
+ //# sourceMappingURL=emailRecipients.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emailRecipients.js","names":[],"sources":["../../src/actions/emailRecipients.ts"],"sourcesContent":["import type { PayloadRequest, TextField } from 'payload'\nimport { fieldNames, fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport { localizedIf } from '../fields/localizedIf'\nimport { interpolate } from '../recall/interpolate'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelFor } from '../translations/server'\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst TOKEN_RE = /^\\{\\{\\s*([\\w.-]+)\\s*\\}\\}$/\n\n/** A plausible email address (a permissive shape check, not full RFC validation). */\nexport const isPlausibleEmail = (value: string): boolean => EMAIL_RE.test(value.trim())\n\n/** The field name inside a `{{name}}` recipient token, or undefined when the string is not a token. */\nexport const parseFieldToken = (value: string): string | undefined => {\n\tconst match = TOKEN_RE.exec(value.trim())\n\treturn match ? match[1] : undefined\n}\n\nexport const isFieldToken = (value: string): boolean => parseFieldToken(value) !== undefined\n\nconst toList = (value: unknown): string[] =>\n\tArray.isArray(value)\n\t\t? value.filter((entry): entry is string => typeof entry === 'string')\n\t\t: typeof value === 'string' && value.length > 0\n\t\t\t? [value]\n\t\t\t: []\n\n/**\n * Resolve a stored recipient value (a `string[]`, or a legacy single string) to the comma-separated\n * list `payload.sendEmail` accepts: each entry is interpolated (a `{{field}}` token resolves from the\n * submission, a plain email passes through), trimmed, empties dropped, joined with `, `.\n */\nexport const resolveRecipients = (value: unknown, resolve: (name: string) => string): string =>\n\ttoList(value)\n\t\t.map((entry) => interpolate(entry, resolve).trim())\n\t\t.filter((entry) => entry.length > 0)\n\t\t.join(', ')\n\n/**\n * Field `validate` for a recipient list: unset is fine; otherwise every entry must be a valid email\n * or a `{{field}}` token naming an existing field of an allowed token type (`tokenFieldTypes`, or any\n * named field when empty). Reads the form's `fields` off `data`, like the confirmation `toField`.\n */\nexport const validateRecipients =\n\t(tokenFieldTypes?: string[]) =>\n\t(value: unknown, { data, req }: { data?: unknown; req: PayloadRequest }): string | true => {\n\t\tconst list = toList(value)\n\t\tif (list.length === 0) {\n\t\t\treturn true\n\t\t}\n\t\tconst fields =\n\t\t\tdata && typeof data === 'object' ? (data as Record<string, unknown>).fields : undefined\n\t\tconst allowed = new Set(\n\t\t\ttokenFieldTypes && tokenFieldTypes.length > 0\n\t\t\t\t? fieldNamesOfType(fields, tokenFieldTypes)\n\t\t\t\t: fieldNames(fields)\n\t\t)\n\t\tfor (const entry of list) {\n\t\t\tconst token = parseFieldToken(entry)\n\t\t\tif (token) {\n\t\t\t\tif (!allowed.has(token)) {\n\t\t\t\t\treturn asTranslate(req.t)(keys.validationRecipientUnknownField)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (!isPlausibleEmail(entry)) {\n\t\t\t\treturn asTranslate(req.t)(keys.validationRecipientInvalid)\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n/** Host-configurable behavior for the recipient fields (plugin option `email.recipients`). */\nexport type RecipientsConfig = {\n\t/** Allow free-typed emails (default true). */\n\tallowCustom?: boolean\n\t/** Offer the form's own fields as recipient tokens (default true). */\n\tfieldTokens?: boolean\n\t/** Field types eligible as tokens (default `['email']`). */\n\ttokenFieldTypes?: string[]\n}\n\nconst RECIPIENTS_FIELD_REF = '@10x-media/form-builder/client#RecipientsSelect'\n\n/**\n * A `text hasMany` field rendered by `RecipientsSelect`, used for every email address list. `endpoint`\n * (when set) supplies preset options (e.g. the host's departments); `recipients` narrows the field's\n * behavior; `width` sets `admin.width` so a pair can share a row.\n */\n// biome-ignore lint/complexity/useMaxParams: the field identity (name, label, localize) plus its grouped options is the minimal surface\nexport const buildRecipientField = (\n\tname: string,\n\tlabelKey: string,\n\tlocalize: boolean,\n\topts: { endpoint?: string; recipients?: RecipientsConfig; width?: string } = {}\n): TextField => ({\n\tname,\n\ttype: 'text',\n\thasMany: true,\n\tlabel: labelFor(labelKey),\n\tvalidate: validateRecipients(opts.recipients?.tokenFieldTypes ?? ['email']),\n\t...localizedIf(localize),\n\tadmin: {\n\t\t...(opts.width ? { width: opts.width } : {}),\n\t\tcomponents: {\n\t\t\tField: {\n\t\t\t\tpath: RECIPIENTS_FIELD_REF,\n\t\t\t\tclientProps: {\n\t\t\t\t\t...(opts.endpoint ? { endpoint: opts.endpoint } : {}),\n\t\t\t\t\t...(opts.recipients?.allowCustom === false ? { allowCustom: false } : {}),\n\t\t\t\t\t...(opts.recipients?.fieldTokens === false ? { fieldTokens: false } : {}),\n\t\t\t\t\t...(opts.recipients?.tokenFieldTypes\n\t\t\t\t\t\t? { tokenFieldTypes: opts.recipients.tokenFieldTypes }\n\t\t\t\t\t\t: {}),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n})\n"],"mappings":";;;;;;AAOA,MAAM,WAAW;AACjB,MAAM,WAAW;;AAGjB,MAAa,oBAAoB,UAA2B,SAAS,KAAK,MAAM,KAAK,CAAC;;AAGtF,MAAa,mBAAmB,UAAsC;CACrE,MAAM,QAAQ,SAAS,KAAK,MAAM,KAAK,CAAC;CACxC,OAAO,QAAQ,MAAM,KAAK,KAAA;AAC3B;AAIA,MAAM,UAAU,UACf,MAAM,QAAQ,KAAK,IAChB,MAAM,QAAQ,UAA2B,OAAO,UAAU,QAAQ,IAClE,OAAO,UAAU,YAAY,MAAM,SAAS,IAC3C,CAAC,KAAK,IACN,CAAC;;;;;;AAON,MAAa,qBAAqB,OAAgB,YACjD,OAAO,KAAK,EACV,KAAK,UAAU,YAAY,OAAO,OAAO,EAAE,KAAK,CAAC,EACjD,QAAQ,UAAU,MAAM,SAAS,CAAC,EAClC,KAAK,IAAI;;;;;;AAOZ,MAAa,sBACX,qBACA,OAAgB,EAAE,MAAM,UAAkE;CAC1F,MAAM,OAAO,OAAO,KAAK;CACzB,IAAI,KAAK,WAAW,GACnB,OAAO;CAER,MAAM,SACL,QAAQ,OAAO,SAAS,WAAY,KAAiC,SAAS,KAAA;CAC/E,MAAM,UAAU,IAAI,IACnB,mBAAmB,gBAAgB,SAAS,IACzC,iBAAiB,QAAQ,eAAe,IACxC,WAAW,MAAM,CACrB;CACA,KAAK,MAAM,SAAS,MAAM;EACzB,MAAM,QAAQ,gBAAgB,KAAK;EACnC,IAAI,OAAO;GACV,IAAI,CAAC,QAAQ,IAAI,KAAK,GACrB,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,+BAA+B;GAE/D;EACD;EACA,IAAI,CAAC,iBAAiB,KAAK,GAC1B,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,0BAA0B;CAE3D;CACA,OAAO;AACR;AAYD,MAAM,uBAAuB;;;;;;AAQ7B,MAAa,uBACZ,MACA,UACA,UACA,OAA6E,CAAC,OAC9D;CAChB;CACA,MAAM;CACN,SAAS;CACT,OAAO,SAAS,QAAQ;CACxB,UAAU,mBAAmB,KAAK,YAAY,mBAAmB,CAAC,OAAO,CAAC;CAC1E,GAAG,YAAY,QAAQ;CACvB,OAAO;EACN,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;EAC1C,YAAY,EACX,OAAO;GACN,MAAM;GACN,aAAa;IACZ,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;IACnD,GAAI,KAAK,YAAY,gBAAgB,QAAQ,EAAE,aAAa,MAAM,IAAI,CAAC;IACvE,GAAI,KAAK,YAAY,gBAAgB,QAAQ,EAAE,aAAa,MAAM,IAAI,CAAC;IACvE,GAAI,KAAK,YAAY,kBAClB,EAAE,iBAAiB,KAAK,WAAW,gBAAgB,IACnD,CAAC;GACL;EACD,EACD;CACD;AACD"}
@@ -0,0 +1,21 @@
1
+ import { PayloadRequest } from "payload";
2
+
3
+ //#region src/actions/fromAddresses.d.ts
4
+ /** One selectable "from" address for the built-in email actions. */
5
+ type FromAddressOption = {
6
+ label: string;
7
+ value: string;
8
+ };
9
+ /**
10
+ * Host seam resolving the selectable `from` addresses for `emailTeam`/`confirmation`
11
+ * (plugin option `email.fromAddresses`). Multi-tenant hosts derive tenant scoping from `req`
12
+ * (host header, cookie, or auth context) and return only that tenant's allowed senders. `value`
13
+ * is the literal string handed to `payload.sendEmail`'s `from` (e.g. `'Name <addr@x.com>'` or a
14
+ * plain address). Absent keeps the email adapter's default sender and adds no `from` field at all.
15
+ */
16
+ type FromAddressesResolver = (args: {
17
+ req: PayloadRequest;
18
+ }) => Promise<FromAddressOption[]> | FromAddressOption[];
19
+ //#endregion
20
+ export { FromAddressOption, FromAddressesResolver };
21
+ //# sourceMappingURL=fromAddresses.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { keys } from "../translations/keys.js";
2
+ import { asTranslate, labelFor } from "../translations/server.js";
3
+ //#region src/actions/fromAddresses.ts
4
+ const FROM_FIELD_REF = "@10x-media/form-builder/client#EndpointOptionsSelect";
5
+ /**
6
+ * Validate for the `from` field, closed over the host resolver (mirrors the confirmation action's
7
+ * `toField` and poll's `resultsField`): unset is fine, otherwise the value must be one of the
8
+ * resolver's options for this request. A throwing resolver fails closed with a translated message
9
+ * rather than surfacing a raw error on save.
10
+ *
11
+ * Failing closed has an operational cost worth knowing: Payload runs this on every save, not only
12
+ * when `from` changed, so for as long as the resolver is down no form carrying an email action with
13
+ * a `from` set can be saved at all, including edits that never touch the address. That is the
14
+ * deliberate trade: failing open would persist a sender the host can no longer vouch for, and
15
+ * unlike `toField` and `resultsField` this seam depends on host infrastructure that can be down.
16
+ * A resolver reaching a flaky upstream should cache or fall back internally rather than throw.
17
+ */
18
+ const validateFromField = (resolver) => async (value, { req }) => {
19
+ if (typeof value !== "string" || value.length === 0) return true;
20
+ let options;
21
+ try {
22
+ options = await resolver({ req });
23
+ } catch {
24
+ return asTranslate(req.t)(keys.validationFromUnavailable);
25
+ }
26
+ return options.some((option) => option.value === value) ? true : asTranslate(req.t)(keys.validationFromUnknown);
27
+ };
28
+ /**
29
+ * The `from` select shared by `emailTeam` and `confirmation`: an `EndpointOptionsSelect` backed by
30
+ * the forms collection's `/:id/from-addresses` endpoint (registered only when `email.fromAddresses`
31
+ * is set). That route's document id goes unused server-side: the option set is request-scoped, not
32
+ * per-form, so this reuses the existing doc-scoped component as-is instead of adding an id-less mode.
33
+ */
34
+ const buildFromField = (resolver) => ({
35
+ name: "from",
36
+ type: "text",
37
+ label: labelFor(keys.actionConfigFrom),
38
+ validate: validateFromField(resolver),
39
+ admin: { components: { Field: {
40
+ path: FROM_FIELD_REF,
41
+ clientProps: {
42
+ endpoint: "from-addresses",
43
+ descriptionKey: keys.actionConfigFromDescription
44
+ }
45
+ } } }
46
+ });
47
+ /**
48
+ * Authorize and resolve the `GET /:id/from-addresses` request backing the `from` selects:
49
+ * authenticated callers get the host resolver's current options for this request; anonymous
50
+ * callers are always refused. The route id is unused (see `buildFromField`). Statuses mirror
51
+ * the poll-options endpoint: 403 unauthenticated, 503 when the resolver throws (fail closed).
52
+ */
53
+ const resolveFromAddressesRequest = async (args) => {
54
+ const { isAuthed, req, resolver } = args;
55
+ if (!isAuthed) return {
56
+ status: 403,
57
+ body: { errors: [{ message: "Forbidden" }] }
58
+ };
59
+ try {
60
+ return {
61
+ status: 200,
62
+ body: { options: await resolver({ req }) }
63
+ };
64
+ } catch {
65
+ return {
66
+ status: 503,
67
+ body: { errors: [{ message: "From addresses unavailable" }] }
68
+ };
69
+ }
70
+ };
71
+ //#endregion
72
+ export { buildFromField, resolveFromAddressesRequest };
73
+
74
+ //# sourceMappingURL=fromAddresses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fromAddresses.js","names":[],"sources":["../../src/actions/fromAddresses.ts"],"sourcesContent":["import type { PayloadRequest, TextField } from 'payload'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelFor } from '../translations/server'\n\n/** One selectable \"from\" address for the built-in email actions. */\nexport type FromAddressOption = { label: string; value: string }\n\n/**\n * Host seam resolving the selectable `from` addresses for `emailTeam`/`confirmation`\n * (plugin option `email.fromAddresses`). Multi-tenant hosts derive tenant scoping from `req`\n * (host header, cookie, or auth context) and return only that tenant's allowed senders. `value`\n * is the literal string handed to `payload.sendEmail`'s `from` (e.g. `'Name <addr@x.com>'` or a\n * plain address). Absent keeps the email adapter's default sender and adds no `from` field at all.\n */\nexport type FromAddressesResolver = (args: {\n\treq: PayloadRequest\n}) => Promise<FromAddressOption[]> | FromAddressOption[]\n\nconst FROM_FIELD_REF = '@10x-media/form-builder/client#EndpointOptionsSelect'\n\n/**\n * Validate for the `from` field, closed over the host resolver (mirrors the confirmation action's\n * `toField` and poll's `resultsField`): unset is fine, otherwise the value must be one of the\n * resolver's options for this request. A throwing resolver fails closed with a translated message\n * rather than surfacing a raw error on save.\n *\n * Failing closed has an operational cost worth knowing: Payload runs this on every save, not only\n * when `from` changed, so for as long as the resolver is down no form carrying an email action with\n * a `from` set can be saved at all, including edits that never touch the address. That is the\n * deliberate trade: failing open would persist a sender the host can no longer vouch for, and\n * unlike `toField` and `resultsField` this seam depends on host infrastructure that can be down.\n * A resolver reaching a flaky upstream should cache or fall back internally rather than throw.\n */\nexport const validateFromField =\n\t(resolver: FromAddressesResolver) =>\n\tasync (value: unknown, { req }: { req: PayloadRequest }): Promise<string | true> => {\n\t\tif (typeof value !== 'string' || value.length === 0) {\n\t\t\treturn true\n\t\t}\n\t\tlet options: FromAddressOption[]\n\t\ttry {\n\t\t\toptions = await resolver({ req })\n\t\t} catch {\n\t\t\treturn asTranslate(req.t)(keys.validationFromUnavailable)\n\t\t}\n\t\treturn options.some((option) => option.value === value)\n\t\t\t? true\n\t\t\t: asTranslate(req.t)(keys.validationFromUnknown)\n\t}\n\n/**\n * The `from` select shared by `emailTeam` and `confirmation`: an `EndpointOptionsSelect` backed by\n * the forms collection's `/:id/from-addresses` endpoint (registered only when `email.fromAddresses`\n * is set). That route's document id goes unused server-side: the option set is request-scoped, not\n * per-form, so this reuses the existing doc-scoped component as-is instead of adding an id-less mode.\n */\nexport const buildFromField = (resolver: FromAddressesResolver): TextField => ({\n\tname: 'from',\n\ttype: 'text',\n\tlabel: labelFor(keys.actionConfigFrom),\n\tvalidate: validateFromField(resolver),\n\tadmin: {\n\t\tcomponents: {\n\t\t\tField: {\n\t\t\t\tpath: FROM_FIELD_REF,\n\t\t\t\tclientProps: {\n\t\t\t\t\tendpoint: 'from-addresses',\n\t\t\t\t\tdescriptionKey: keys.actionConfigFromDescription,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n})\n\nexport type ResolveFromAddressesRequestArgs = {\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq: PayloadRequest\n\tresolver: FromAddressesResolver\n}\n\nexport type ResolveFromAddressesRequestResult = {\n\tstatus: number\n\tbody: { options: FromAddressOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/from-addresses` request backing the `from` selects:\n * authenticated callers get the host resolver's current options for this request; anonymous\n * callers are always refused. The route id is unused (see `buildFromField`). Statuses mirror\n * the poll-options endpoint: 403 unauthenticated, 503 when the resolver throws (fail closed).\n */\nexport const resolveFromAddressesRequest = async (\n\targs: ResolveFromAddressesRequestArgs\n): Promise<ResolveFromAddressesRequestResult> => {\n\tconst { isAuthed, req, resolver } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolver({ req })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'From addresses unavailable' }] } }\n\t}\n}\n"],"mappings":";;;AAkBA,MAAM,iBAAiB;;;;;;;;;;;;;;AAevB,MAAa,qBACX,aACD,OAAO,OAAgB,EAAE,UAA2D;CACnF,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAER,IAAI;CACJ,IAAI;EACH,UAAU,MAAM,SAAS,EAAE,IAAI,CAAC;CACjC,QAAQ;EACP,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,yBAAyB;CACzD;CACA,OAAO,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK,IACnD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,qBAAqB;AACjD;;;;;;;AAQD,MAAa,kBAAkB,cAAgD;CAC9E,MAAM;CACN,MAAM;CACN,OAAO,SAAS,KAAK,gBAAgB;CACrC,UAAU,kBAAkB,QAAQ;CACpC,OAAO,EACN,YAAY,EACX,OAAO;EACN,MAAM;EACN,aAAa;GACZ,UAAU;GACV,gBAAgB,KAAK;EACtB;CACD,EACD,EACD;AACD;;;;;;;AAoBA,MAAa,8BAA8B,OAC1C,SACgD;CAChD,MAAM,EAAE,UAAU,KAAK,aAAa;CACpC,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAEH,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAA,MADR,SAAS,EAAE,IAAI,CAAC,EACA;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,6BAA6B,CAAC,EAAE;EAAE;CACrF;AACD"}
@@ -14,14 +14,6 @@ const asDescriptors = (value) => Array.isArray(value) ? value : [];
14
14
  */
15
15
  const runActionsForSubmission = async (args) => {
16
16
  const { input, registry, payload, req, richText } = args;
17
- const form = await payload.findByID({
18
- collection: FORMS_SLUG,
19
- id: input.formId,
20
- depth: 0,
21
- overrideAccess: true,
22
- req
23
- }).catch(() => null);
24
- if (!form) return;
25
17
  const submission = await payload.findByID({
26
18
  collection: FORM_SUBMISSIONS_SLUG,
27
19
  id: input.submissionId,
@@ -31,6 +23,15 @@ const runActionsForSubmission = async (args) => {
31
23
  }).catch(() => null);
32
24
  if (!submission) return;
33
25
  const locale = typeof submission.locale === "string" ? submission.locale : req?.locale ?? "en";
26
+ const form = await payload.findByID({
27
+ collection: FORMS_SLUG,
28
+ id: input.formId,
29
+ depth: 0,
30
+ overrideAccess: true,
31
+ locale,
32
+ req
33
+ }).catch(() => null);
34
+ if (!form) return;
34
35
  const t = asFieldTranslate(req?.i18n?.t ?? ((key) => key));
35
36
  await runActions({
36
37
  actions: asActions(form.actions),
@@ -1 +1 @@
1
- {"version":3,"file":"task.js","names":[],"sources":["../../src/actions/task.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport type { ActionRegistry } from './registry'\nimport type { ActionInstance } from './runActions'\nimport { runActions } from './runActions'\n\nexport const ACTIONS_TASK_SLUG = 'form-builder-actions'\n\n/** Input the dispatch path enqueues; the handler re-loads everything else from the DB. */\nexport type ActionsTaskInput = { formId: number | string; submissionId: number | string }\n\nconst asActions = (value: unknown): ActionInstance[] =>\n\tArray.isArray(value) ? (value as ActionInstance[]) : []\n\nconst asValues = (value: unknown): SubmissionValue[] =>\n\tArray.isArray(value) ? (value as SubmissionValue[]) : []\n\nconst asDescriptors = (value: unknown): SubmissionDescriptor[] =>\n\tArray.isArray(value) ? (value as SubmissionDescriptor[]) : []\n\n/**\n * Load the form and submission by id and run the form's actions through the shared, failure-isolating\n * `runActions`. Tolerates a missing form or submission (the row may have been deleted between enqueue and\n * run) by returning early. Used by both the queued task handler and the inline fallback.\n */\nexport const runActionsForSubmission = async (args: {\n\tinput: ActionsTaskInput\n\tregistry: ActionRegistry\n\tpayload: Payload\n\treq?: PayloadRequest\n\trichText?: RichTextBodyOption\n}): Promise<void> => {\n\tconst { input, registry, payload, req, richText } = args\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: input.formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn\n\t}\n\tconst submission = await payload\n\t\t.findByID({\n\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\tid: input.submissionId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!submission) {\n\t\treturn\n\t}\n\n\tconst locale = typeof submission.locale === 'string' ? submission.locale : (req?.locale ?? 'en')\n\tconst t: Translate = asFieldTranslate(req?.i18n?.t ?? ((key: string) => key))\n\n\tawait runActions({\n\t\tactions: asActions(form.actions),\n\t\tregistry,\n\t\trichText,\n\t\tform: { id: form.id, title: typeof form.title === 'string' ? form.title : undefined },\n\t\tsubmissionId: submission.id,\n\t\tvalues: asValues(submission.values),\n\t\tdescriptors: asDescriptors(submission.descriptors),\n\t\tpayload,\n\t\treq,\n\t\tlocale,\n\t\tt,\n\t})\n}\n\n/** Native Payload jobs task that runs a submission's post-submit actions out of band. */\nexport const buildActionsTask = (\n\tregistry: ActionRegistry,\n\trichText?: RichTextBodyOption\n): TaskConfig =>\n\t({\n\t\tslug: ACTIONS_TASK_SLUG,\n\t\tinputSchema: [\n\t\t\t{ name: 'formId', type: 'text', required: true },\n\t\t\t{ name: 'submissionId', type: 'text', required: true },\n\t\t],\n\t\thandler: async ({ input, req }) => {\n\t\t\tawait runActionsForSubmission({\n\t\t\t\tinput: input as ActionsTaskInput,\n\t\t\t\tregistry,\n\t\t\t\tpayload: req.payload,\n\t\t\t\treq,\n\t\t\t\trichText,\n\t\t\t})\n\t\t\treturn { output: {} }\n\t\t},\n\t}) as TaskConfig\n\n/** Register the actions task on `config.jobs.tasks`, creating the jobs config if absent. */\nexport const registerActionsTask = (\n\tconfig: Config,\n\tregistry: ActionRegistry,\n\trichText?: RichTextBodyOption\n): void => {\n\tconfig.jobs ??= {}\n\tconfig.jobs.tasks ??= []\n\tconfig.jobs.tasks.push(buildActionsTask(registry, richText))\n}\n"],"mappings":";;;;;AAWA,MAAa,oBAAoB;AAKjC,MAAM,aAAa,UAClB,MAAM,QAAQ,KAAK,IAAK,QAA6B,CAAC;AAEvD,MAAM,YAAY,UACjB,MAAM,QAAQ,KAAK,IAAK,QAA8B,CAAC;AAExD,MAAM,iBAAiB,UACtB,MAAM,QAAQ,KAAK,IAAK,QAAmC,CAAC;;;;;;AAO7D,MAAa,0BAA0B,OAAO,SAMzB;CACpB,MAAM,EAAE,OAAO,UAAU,SAAS,KAAK,aAAa;CACpD,MAAM,OAAO,MAAM,QACjB,SAAS;EACT,YAAY;EACZ,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ;CAED,MAAM,aAAa,MAAM,QACvB,SAAS;EACT,YAAY;EACZ,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,YACJ;CAGD,MAAM,SAAS,OAAO,WAAW,WAAW,WAAW,WAAW,SAAU,KAAK,UAAU;CAC3F,MAAM,IAAe,iBAAiB,KAAK,MAAM,OAAO,QAAgB,IAAI;CAE5E,MAAM,WAAW;EAChB,SAAS,UAAU,KAAK,OAAO;EAC/B;EACA;EACA,MAAM;GAAE,IAAI,KAAK;GAAI,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;EAAU;EACpF,cAAc,WAAW;EACzB,QAAQ,SAAS,WAAW,MAAM;EAClC,aAAa,cAAc,WAAW,WAAW;EACjD;EACA;EACA;EACA;CACD,CAAC;AACF;;AAGA,MAAa,oBACZ,UACA,cAEC;CACA,MAAM;CACN,aAAa,CACZ;EAAE,MAAM;EAAU,MAAM;EAAQ,UAAU;CAAK,GAC/C;EAAE,MAAM;EAAgB,MAAM;EAAQ,UAAU;CAAK,CACtD;CACA,SAAS,OAAO,EAAE,OAAO,UAAU;EAClC,MAAM,wBAAwB;GACtB;GACP;GACA,SAAS,IAAI;GACb;GACA;EACD,CAAC;EACD,OAAO,EAAE,QAAQ,CAAC,EAAE;CACrB;AACD;;AAGD,MAAa,uBACZ,QACA,UACA,aACU;CACV,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,UAAU,CAAC;CACvB,OAAO,KAAK,MAAM,KAAK,iBAAiB,UAAU,QAAQ,CAAC;AAC5D"}
1
+ {"version":3,"file":"task.js","names":[],"sources":["../../src/actions/task.ts"],"sourcesContent":["import type { Config, Payload, PayloadRequest, TaskConfig, TypedLocale } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { Translate } from '../fields/types'\nimport type { SubmissionDescriptor, SubmissionValue } from '../submissions/types'\nimport { asFieldTranslate } from '../translations/server'\nimport type { RichTextBodyOption } from './body/serializeBody'\nimport type { ActionRegistry } from './registry'\nimport type { ActionInstance } from './runActions'\nimport { runActions } from './runActions'\n\nexport const ACTIONS_TASK_SLUG = 'form-builder-actions'\n\n/** Input the dispatch path enqueues; the handler re-loads everything else from the DB. */\nexport type ActionsTaskInput = { formId: number | string; submissionId: number | string }\n\nconst asActions = (value: unknown): ActionInstance[] =>\n\tArray.isArray(value) ? (value as ActionInstance[]) : []\n\nconst asValues = (value: unknown): SubmissionValue[] =>\n\tArray.isArray(value) ? (value as SubmissionValue[]) : []\n\nconst asDescriptors = (value: unknown): SubmissionDescriptor[] =>\n\tArray.isArray(value) ? (value as SubmissionDescriptor[]) : []\n\n/**\n * Load the form and submission by id and run the form's actions through the shared, failure-isolating\n * `runActions`. Tolerates a missing form or submission (the row may have been deleted between enqueue and\n * run) by returning early. Used by both the queued task handler and the inline fallback.\n */\nexport const runActionsForSubmission = async (args: {\n\tinput: ActionsTaskInput\n\tregistry: ActionRegistry\n\tpayload: Payload\n\treq?: PayloadRequest\n\trichText?: RichTextBodyOption\n}): Promise<void> => {\n\tconst { input, registry, payload, req, richText } = args\n\tconst submission = await payload\n\t\t.findByID({\n\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\tid: input.submissionId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!submission) {\n\t\treturn\n\t}\n\n\t// The submission's own stored locale (set from req.locale at submit) is authoritative, so the form\n\t// is loaded at it. A localized action config, notably the emailTeam `to`, then resolves to the\n\t// submission's locale even on the queued path, where the job runner's req may carry a different\n\t// (or no) locale than the visitor who submitted.\n\tconst locale = typeof submission.locale === 'string' ? submission.locale : (req?.locale ?? 'en')\n\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: FORMS_SLUG,\n\t\t\tid: input.formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\t// Cast: the stored locale is a plain string; a host's concrete locale union is unknowable from\n\t\t\t// the plugin, and an unrecognized code just falls back on read, so this narrows (zero runtime\n\t\t\t// delta) to satisfy a host whose `findByID` locale is a real union.\n\t\t\tlocale: locale as TypedLocale,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn\n\t}\n\n\tconst t: Translate = asFieldTranslate(req?.i18n?.t ?? ((key: string) => key))\n\n\tawait runActions({\n\t\tactions: asActions(form.actions),\n\t\tregistry,\n\t\trichText,\n\t\tform: { id: form.id, title: typeof form.title === 'string' ? form.title : undefined },\n\t\tsubmissionId: submission.id,\n\t\tvalues: asValues(submission.values),\n\t\tdescriptors: asDescriptors(submission.descriptors),\n\t\tpayload,\n\t\treq,\n\t\tlocale,\n\t\tt,\n\t})\n}\n\n/** Native Payload jobs task that runs a submission's post-submit actions out of band. */\nexport const buildActionsTask = (\n\tregistry: ActionRegistry,\n\trichText?: RichTextBodyOption\n): TaskConfig =>\n\t({\n\t\tslug: ACTIONS_TASK_SLUG,\n\t\tinputSchema: [\n\t\t\t{ name: 'formId', type: 'text', required: true },\n\t\t\t{ name: 'submissionId', type: 'text', required: true },\n\t\t],\n\t\thandler: async ({ input, req }) => {\n\t\t\tawait runActionsForSubmission({\n\t\t\t\tinput: input as ActionsTaskInput,\n\t\t\t\tregistry,\n\t\t\t\tpayload: req.payload,\n\t\t\t\treq,\n\t\t\t\trichText,\n\t\t\t})\n\t\t\treturn { output: {} }\n\t\t},\n\t}) as TaskConfig\n\n/** Register the actions task on `config.jobs.tasks`, creating the jobs config if absent. */\nexport const registerActionsTask = (\n\tconfig: Config,\n\tregistry: ActionRegistry,\n\trichText?: RichTextBodyOption\n): void => {\n\tconfig.jobs ??= {}\n\tconfig.jobs.tasks ??= []\n\tconfig.jobs.tasks.push(buildActionsTask(registry, richText))\n}\n"],"mappings":";;;;;AAWA,MAAa,oBAAoB;AAKjC,MAAM,aAAa,UAClB,MAAM,QAAQ,KAAK,IAAK,QAA6B,CAAC;AAEvD,MAAM,YAAY,UACjB,MAAM,QAAQ,KAAK,IAAK,QAA8B,CAAC;AAExD,MAAM,iBAAiB,UACtB,MAAM,QAAQ,KAAK,IAAK,QAAmC,CAAC;;;;;;AAO7D,MAAa,0BAA0B,OAAO,SAMzB;CACpB,MAAM,EAAE,OAAO,UAAU,SAAS,KAAK,aAAa;CACpD,MAAM,aAAa,MAAM,QACvB,SAAS;EACT,YAAY;EACZ,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,YACJ;CAOD,MAAM,SAAS,OAAO,WAAW,WAAW,WAAW,WAAW,SAAU,KAAK,UAAU;CAE3F,MAAM,OAAO,MAAM,QACjB,SAAS;EACT,YAAY;EACZ,IAAI,MAAM;EACV,OAAO;EACP,gBAAgB;EAIR;EACR;CACD,CAAC,EACA,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ;CAGD,MAAM,IAAe,iBAAiB,KAAK,MAAM,OAAO,QAAgB,IAAI;CAE5E,MAAM,WAAW;EAChB,SAAS,UAAU,KAAK,OAAO;EAC/B;EACA;EACA,MAAM;GAAE,IAAI,KAAK;GAAI,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;EAAU;EACpF,cAAc,WAAW;EACzB,QAAQ,SAAS,WAAW,MAAM;EAClC,aAAa,cAAc,WAAW,WAAW;EACjD;EACA;EACA;EACA;CACD,CAAC;AACF;;AAGA,MAAa,oBACZ,UACA,cAEC;CACA,MAAM;CACN,aAAa,CACZ;EAAE,MAAM;EAAU,MAAM;EAAQ,UAAU;CAAK,GAC/C;EAAE,MAAM;EAAgB,MAAM;EAAQ,UAAU;CAAK,CACtD;CACA,SAAS,OAAO,EAAE,OAAO,UAAU;EAClC,MAAM,wBAAwB;GACtB;GACP;GACA,SAAS,IAAI;GACb;GACA;EACD,CAAC;EACD,OAAO,EAAE,QAAQ,CAAC,EAAE;CACrB;AACD;;AAGD,MAAa,uBACZ,QACA,UACA,aACU;CACV,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,UAAU,CAAC;CACvB,OAAO,KAAK,MAAM,KAAK,iBAAiB,UAAU,QAAQ,CAAC;AAC5D"}
@@ -11,6 +11,15 @@ type AggregateFormResponsesArgs = {
11
11
  req?: PayloadRequest; /** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */
12
12
  maxSubmissions?: number; /** Page size for the submission scan. Default 500. */
13
13
  pageSize?: number;
14
+ /**
15
+ * Source-resolved options by field name (an option-source poll's results field). They replace
16
+ * the instance's authored options for bucket seeding, order, and labels, and mark the field
17
+ * enumerable for the default field selection.
18
+ */
19
+ resolvedOptions?: Record<string, {
20
+ value: string;
21
+ label: string;
22
+ }[]>;
14
23
  };
15
24
  /** True when a field declares non-empty options (a choice field safe to aggregate publicly). */
16
25
  declare const fieldHasOptions: (field: FormFieldInstance) => boolean;
@@ -1,22 +1,16 @@
1
+ import { isNamedField } from "../fields/fieldKey.js";
2
+ import { instanceOptionsOf } from "../fields/instanceOptions.js";
1
3
  import { aggregateRowsForFields } from "./aggregateRows.js";
2
4
  import { FORMS_SLUG } from "../collections/forms.js";
3
5
  import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
4
6
  //#region src/aggregation/aggregateResponses.ts
5
- const optionsOf = (field) => {
6
- if (!Array.isArray(field.options)) return;
7
- const options = field.options.filter((option) => typeof option?.value === "string").map((option) => ({
8
- value: String(option.value),
9
- label: option.label ?? String(option.value)
10
- }));
11
- return options.length > 0 ? options : void 0;
12
- };
13
7
  /** True when a field declares non-empty options (a choice field safe to aggregate publicly). */
14
- const fieldHasOptions = (field) => optionsOf(field) !== void 0;
15
- const metaFor = (field) => ({
8
+ const fieldHasOptions = (field) => instanceOptionsOf(field) !== void 0;
9
+ const metaFor = (field, resolved) => ({
16
10
  field: field.name,
17
11
  label: field.label ?? field.name,
18
12
  fieldType: field.blockType,
19
- options: optionsOf(field)
13
+ options: resolved && resolved.length > 0 ? resolved : instanceOptionsOf(field)
20
14
  });
21
15
  /**
22
16
  * Aggregate submission responses for a form. Loads the form for current labels + option order, then pages
@@ -25,7 +19,7 @@ const metaFor = (field) => ({
25
19
  * `truncated`. Returns one `FieldAggregation` per requested field (or per enumerable field by default).
26
20
  */
27
21
  const aggregateFormResponses = async (args) => {
28
- const { payload, formId, fields, status = "complete", req, maxSubmissions = 1e4, pageSize = 500 } = args;
22
+ const { payload, formId, fields, status = "complete", req, maxSubmissions = 1e4, pageSize = 500, resolvedOptions } = args;
29
23
  const form = await payload.findByID({
30
24
  collection: FORMS_SLUG,
31
25
  id: formId,
@@ -35,9 +29,9 @@ const aggregateFormResponses = async (args) => {
35
29
  }).catch(() => null);
36
30
  if (!form || !Array.isArray(form.fields)) return [];
37
31
  const instances = form.fields;
38
- const selected = fields ? instances.filter((field) => fields.includes(field.name)) : instances.filter((field) => optionsOf(field) !== void 0);
32
+ const selected = fields ? instances.filter((field) => isNamedField(field) && fields.includes(field.name)) : instances.filter((field) => isNamedField(field) && (instanceOptionsOf(field) !== void 0 || (resolvedOptions?.[field.name]?.length ?? 0) > 0));
39
33
  if (selected.length === 0) return [];
40
- const metas = selected.map(metaFor);
34
+ const metas = selected.map((field) => metaFor(field, resolvedOptions?.[field.name]));
41
35
  const where = { and: [{ form: { equals: formId } }, ...status === "all" ? [] : [{ status: { equals: status } }]] };
42
36
  const rows = [];
43
37
  let page = 1;
@@ -1 +1 @@
1
- {"version":3,"file":"aggregateResponses.js","names":[],"sources":["../../src/aggregation/aggregateResponses.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateRowsForFields } from './aggregateRows'\nimport type { AggregationRow, FieldAggregation, FieldMeta, SubmissionStatusFilter } from './types'\n\nexport type AggregateFormResponsesArgs = {\n\tpayload: Payload\n\tformId: number | string\n\t/** Field machine names to aggregate. Defaults to every field that declares options (enumerable). */\n\tfields?: string[]\n\t/** Submission status to count. Defaults to `complete`. */\n\tstatus?: SubmissionStatusFilter\n\treq?: PayloadRequest\n\t/** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */\n\tmaxSubmissions?: number\n\t/** Page size for the submission scan. Default 500. */\n\tpageSize?: number\n}\n\nconst optionsOf = (field: FormFieldInstance): { value: string; label: string }[] | undefined => {\n\tif (!Array.isArray(field.options)) {\n\t\treturn undefined\n\t}\n\tconst options = (field.options as Array<{ label?: string; value?: string }>)\n\t\t.filter((option) => typeof option?.value === 'string')\n\t\t.map((option) => ({ value: String(option.value), label: option.label ?? String(option.value) }))\n\treturn options.length > 0 ? options : undefined\n}\n\n/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */\nexport const fieldHasOptions = (field: FormFieldInstance): boolean => optionsOf(field) !== undefined\n\nconst metaFor = (field: FormFieldInstance): FieldMeta => ({\n\tfield: field.name,\n\tlabel: field.label ?? field.name,\n\tfieldType: field.blockType,\n\toptions: optionsOf(field),\n})\n\n/**\n * Aggregate submission responses for a form. Loads the form for current labels + option order, then pages\n * `payload.find` over its submissions (JSON `values` cannot be GROUP BY'd portably across Mongo + Postgres,\n * so we scan + reduce in JS, cross-DB-identically). Bounded by `maxSubmissions`; beyond it the result is\n * `truncated`. Returns one `FieldAggregation` per requested field (or per enumerable field by default).\n */\nexport const aggregateFormResponses = async (\n\targs: AggregateFormResponsesArgs\n): Promise<FieldAggregation[]> => {\n\tconst {\n\t\tpayload,\n\t\tformId,\n\t\tfields,\n\t\tstatus = 'complete',\n\t\treq,\n\t\tmaxSubmissions = 10000,\n\t\tpageSize = 500,\n\t} = args\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form || !Array.isArray(form.fields)) {\n\t\treturn []\n\t}\n\tconst instances = form.fields as FormFieldInstance[]\n\tconst selected = fields\n\t\t? instances.filter((field) => fields.includes(field.name))\n\t\t: instances.filter((field) => optionsOf(field) !== undefined)\n\tif (selected.length === 0) {\n\t\treturn []\n\t}\n\tconst metas = selected.map(metaFor)\n\n\tconst statusWhere = status === 'all' ? [] : [{ status: { equals: status } }]\n\tconst where = { and: [{ form: { equals: formId } }, ...statusWhere] }\n\n\tconst rows: AggregationRow[] = []\n\tlet page = 1\n\tlet truncated = false\n\tfor (;;) {\n\t\tconst result = await payload.find({\n\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\twhere,\n\t\t\tlimit: pageSize,\n\t\t\tpage,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t\tselect: { values: true, descriptors: true },\n\t\t})\n\t\tfor (const doc of result.docs as AggregationRow[]) {\n\t\t\t// Check before pushing so `truncated` means \"matching rows were left out\", not \"the cap was reached\n\t\t\t// exactly\": a form with precisely `maxSubmissions` responses is complete, not a sample.\n\t\t\tif (rows.length >= maxSubmissions) {\n\t\t\t\ttruncated = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trows.push({ values: doc.values, descriptors: doc.descriptors })\n\t\t}\n\t\tif (truncated || !result.hasNextPage) {\n\t\t\tbreak\n\t\t}\n\t\tpage += 1\n\t}\n\n\treturn aggregateRowsForFields(rows, metas, truncated)\n}\n\nexport type AggregateFieldResponsesArgs = Omit<AggregateFormResponsesArgs, 'fields'> & {\n\tfield: string\n}\n\n/** Single-field convenience over `aggregateFormResponses`. Returns the field's aggregation, or null if absent. */\nexport const aggregateFieldResponses = async (\n\targs: AggregateFieldResponsesArgs\n): Promise<FieldAggregation | null> => {\n\tconst { field, ...rest } = args\n\tconst [result] = await aggregateFormResponses({ ...rest, fields: [field] })\n\treturn result ?? null\n}\n"],"mappings":";;;;AAqBA,MAAM,aAAa,UAA6E;CAC/F,IAAI,CAAC,MAAM,QAAQ,MAAM,OAAO,GAC/B;CAED,MAAM,UAAW,MAAM,QACrB,QAAQ,WAAW,OAAO,QAAQ,UAAU,QAAQ,EACpD,KAAK,YAAY;EAAE,OAAO,OAAO,OAAO,KAAK;EAAG,OAAO,OAAO,SAAS,OAAO,OAAO,KAAK;CAAE,EAAE;CAChG,OAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;AACvC;;AAGA,MAAa,mBAAmB,UAAsC,UAAU,KAAK,MAAM,KAAA;AAE3F,MAAM,WAAW,WAAyC;CACzD,OAAO,MAAM;CACb,OAAO,MAAM,SAAS,MAAM;CAC5B,WAAW,MAAM;CACjB,SAAS,UAAU,KAAK;AACzB;;;;;;;AAQA,MAAa,yBAAyB,OACrC,SACiC;CACjC,MAAM,EACL,SACA,QACA,QACA,SAAS,YACT,KACA,iBAAiB,KACjB,WAAW,QACR;CACJ,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,QAAQ,KAAK,MAAM,GACtC,OAAO,CAAC;CAET,MAAM,YAAY,KAAK;CACvB,MAAM,WAAW,SACd,UAAU,QAAQ,UAAU,OAAO,SAAS,MAAM,IAAI,CAAC,IACvD,UAAU,QAAQ,UAAU,UAAU,KAAK,MAAM,KAAA,CAAS;CAC7D,IAAI,SAAS,WAAW,GACvB,OAAO,CAAC;CAET,MAAM,QAAQ,SAAS,IAAI,OAAO;CAGlC,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,OAAO,EAAE,GAAG,GADhC,WAAW,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,OAAO,EAAE,CAAC,CACT,EAAE;CAEpE,MAAM,OAAyB,CAAC;CAChC,IAAI,OAAO;CACX,IAAI,YAAY;CAChB,SAAS;EACR,MAAM,SAAS,MAAM,QAAQ,KAAK;GACjC,YAAY;GACZ;GACA,OAAO;GACP;GACA,OAAO;GACP,gBAAgB;GAChB;GACA,QAAQ;IAAE,QAAQ;IAAM,aAAa;GAAK;EAC3C,CAAC;EACD,KAAK,MAAM,OAAO,OAAO,MAA0B;GAGlD,IAAI,KAAK,UAAU,gBAAgB;IAClC,YAAY;IACZ;GACD;GACA,KAAK,KAAK;IAAE,QAAQ,IAAI;IAAQ,aAAa,IAAI;GAAY,CAAC;EAC/D;EACA,IAAI,aAAa,CAAC,OAAO,aACxB;EAED,QAAQ;CACT;CAEA,OAAO,uBAAuB,MAAM,OAAO,SAAS;AACrD;;AAOA,MAAa,0BAA0B,OACtC,SACsC;CACtC,MAAM,EAAE,OAAO,GAAG,SAAS;CAC3B,MAAM,CAAC,UAAU,MAAM,uBAAuB;EAAE,GAAG;EAAM,QAAQ,CAAC,KAAK;CAAE,CAAC;CAC1E,OAAO,UAAU;AAClB"}
1
+ {"version":3,"file":"aggregateResponses.js","names":[],"sources":["../../src/aggregation/aggregateResponses.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport { FORMS_SLUG } from '../collections/forms'\nimport { isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport { instanceOptionsOf } from '../fields/instanceOptions'\nimport type { FormFieldInstance } from '../submissions/types'\nimport { aggregateRowsForFields } from './aggregateRows'\nimport type { AggregationRow, FieldAggregation, FieldMeta, SubmissionStatusFilter } from './types'\n\nexport type AggregateFormResponsesArgs = {\n\tpayload: Payload\n\tformId: number | string\n\t/** Field machine names to aggregate. Defaults to every field that declares options (enumerable). */\n\tfields?: string[]\n\t/** Submission status to count. Defaults to `complete`. */\n\tstatus?: SubmissionStatusFilter\n\treq?: PayloadRequest\n\t/** Safety cap on submissions scanned; beyond it the result is flagged `truncated`. Default 10000. */\n\tmaxSubmissions?: number\n\t/** Page size for the submission scan. Default 500. */\n\tpageSize?: number\n\t/**\n\t * Source-resolved options by field name (an option-source poll's results field). They replace\n\t * the instance's authored options for bucket seeding, order, and labels, and mark the field\n\t * enumerable for the default field selection.\n\t */\n\tresolvedOptions?: Record<string, { value: string; label: string }[]>\n}\n\n/** True when a field declares non-empty options (a choice field safe to aggregate publicly). */\nexport const fieldHasOptions = (field: FormFieldInstance): boolean =>\n\tinstanceOptionsOf(field) !== undefined\n\nconst metaFor = (\n\tfield: NamedFormFieldInstance,\n\tresolved?: { value: string; label: string }[]\n): FieldMeta => ({\n\tfield: field.name,\n\tlabel: field.label ?? field.name,\n\tfieldType: field.blockType,\n\toptions: resolved && resolved.length > 0 ? resolved : instanceOptionsOf(field),\n})\n\n/**\n * Aggregate submission responses for a form. Loads the form for current labels + option order, then pages\n * `payload.find` over its submissions (JSON `values` cannot be GROUP BY'd portably across Mongo + Postgres,\n * so we scan + reduce in JS, cross-DB-identically). Bounded by `maxSubmissions`; beyond it the result is\n * `truncated`. Returns one `FieldAggregation` per requested field (or per enumerable field by default).\n */\nexport const aggregateFormResponses = async (\n\targs: AggregateFormResponsesArgs\n): Promise<FieldAggregation[]> => {\n\tconst {\n\t\tpayload,\n\t\tformId,\n\t\tfields,\n\t\tstatus = 'complete',\n\t\treq,\n\t\tmaxSubmissions = 10000,\n\t\tpageSize = 500,\n\t\tresolvedOptions,\n\t} = args\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tif (!form || !Array.isArray(form.fields)) {\n\t\treturn []\n\t}\n\tconst instances = form.fields as FormFieldInstance[]\n\tconst selected = fields\n\t\t? instances.filter(\n\t\t\t\t(field): field is NamedFormFieldInstance =>\n\t\t\t\t\tisNamedField(field) && fields.includes(field.name)\n\t\t\t)\n\t\t: instances.filter(\n\t\t\t\t(field): field is NamedFormFieldInstance =>\n\t\t\t\t\tisNamedField(field) &&\n\t\t\t\t\t(instanceOptionsOf(field) !== undefined ||\n\t\t\t\t\t\t(resolvedOptions?.[field.name]?.length ?? 0) > 0)\n\t\t\t)\n\tif (selected.length === 0) {\n\t\treturn []\n\t}\n\tconst metas = selected.map((field) => metaFor(field, resolvedOptions?.[field.name]))\n\n\tconst statusWhere = status === 'all' ? [] : [{ status: { equals: status } }]\n\tconst where = { and: [{ form: { equals: formId } }, ...statusWhere] }\n\n\tconst rows: AggregationRow[] = []\n\tlet page = 1\n\tlet truncated = false\n\tfor (;;) {\n\t\tconst result = await payload.find({\n\t\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\t\twhere,\n\t\t\tlimit: pageSize,\n\t\t\tpage,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t\tselect: { values: true, descriptors: true },\n\t\t})\n\t\tfor (const doc of result.docs as AggregationRow[]) {\n\t\t\t// Check before pushing so `truncated` means \"matching rows were left out\", not \"the cap was reached\n\t\t\t// exactly\": a form with precisely `maxSubmissions` responses is complete, not a sample.\n\t\t\tif (rows.length >= maxSubmissions) {\n\t\t\t\ttruncated = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trows.push({ values: doc.values, descriptors: doc.descriptors })\n\t\t}\n\t\tif (truncated || !result.hasNextPage) {\n\t\t\tbreak\n\t\t}\n\t\tpage += 1\n\t}\n\n\treturn aggregateRowsForFields(rows, metas, truncated)\n}\n\nexport type AggregateFieldResponsesArgs = Omit<AggregateFormResponsesArgs, 'fields'> & {\n\tfield: string\n}\n\n/** Single-field convenience over `aggregateFormResponses`. Returns the field's aggregation, or null if absent. */\nexport const aggregateFieldResponses = async (\n\targs: AggregateFieldResponsesArgs\n): Promise<FieldAggregation | null> => {\n\tconst { field, ...rest } = args\n\tconst [result] = await aggregateFormResponses({ ...rest, fields: [field] })\n\treturn result ?? null\n}\n"],"mappings":";;;;;;;AA8BA,MAAa,mBAAmB,UAC/B,kBAAkB,KAAK,MAAM,KAAA;AAE9B,MAAM,WACL,OACA,cACgB;CAChB,OAAO,MAAM;CACb,OAAO,MAAM,SAAS,MAAM;CAC5B,WAAW,MAAM;CACjB,SAAS,YAAY,SAAS,SAAS,IAAI,WAAW,kBAAkB,KAAK;AAC9E;;;;;;;AAQA,MAAa,yBAAyB,OACrC,SACiC;CACjC,MAAM,EACL,SACA,QACA,QACA,SAAS,YACT,KACA,iBAAiB,KACjB,WAAW,KACX,oBACG;CACJ,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,QAAQ,KAAK,MAAM,GACtC,OAAO,CAAC;CAET,MAAM,YAAY,KAAK;CACvB,MAAM,WAAW,SACd,UAAU,QACT,UACA,aAAa,KAAK,KAAK,OAAO,SAAS,MAAM,IAAI,CACnD,IACC,UAAU,QACT,UACA,aAAa,KAAK,MACjB,kBAAkB,KAAK,MAAM,KAAA,MAC5B,kBAAkB,MAAM,OAAO,UAAU,KAAK,EAClD;CACF,IAAI,SAAS,WAAW,GACvB,OAAO,CAAC;CAET,MAAM,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO,kBAAkB,MAAM,KAAK,CAAC;CAGnF,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,OAAO,EAAE,GAAG,GADhC,WAAW,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,OAAO,EAAE,CAAC,CACT,EAAE;CAEpE,MAAM,OAAyB,CAAC;CAChC,IAAI,OAAO;CACX,IAAI,YAAY;CAChB,SAAS;EACR,MAAM,SAAS,MAAM,QAAQ,KAAK;GACjC,YAAY;GACZ;GACA,OAAO;GACP;GACA,OAAO;GACP,gBAAgB;GAChB;GACA,QAAQ;IAAE,QAAQ;IAAM,aAAa;GAAK;EAC3C,CAAC;EACD,KAAK,MAAM,OAAO,OAAO,MAA0B;GAGlD,IAAI,KAAK,UAAU,gBAAgB;IAClC,YAAY;IACZ;GACD;GACA,KAAK,KAAK;IAAE,QAAQ,IAAI;IAAQ,aAAa,IAAI;GAAY,CAAC;EAC/D;EACA,IAAI,aAAa,CAAC,OAAO,aACxB;EAED,QAAQ;CACT;CAEA,OAAO,uBAAuB,MAAM,OAAO,SAAS;AACrD;;AAOA,MAAa,0BAA0B,OACtC,SACsC;CACtC,MAAM,EAAE,OAAO,GAAG,SAAS;CAC3B,MAAM,CAAC,UAAU,MAAM,uBAAuB;EAAE,GAAG;EAAM,QAAQ,CAAC,KAAK;CAAE,CAAC;CAC1E,OAAO,UAAU;AAClB"}
@@ -22,6 +22,13 @@ type ResolveResultsRequestArgs = {
22
22
  isAuthed: boolean;
23
23
  req?: PayloadRequest; /** Optional host seam for anonymous reads; absent means plugin-default gating only. */
24
24
  access?: FormResultsAccess;
25
+ /**
26
+ * Poll-eligible field types (registry-derived). When set, an anonymous read of an
27
+ * option-source poll also requires the results field's instance to be one of these types,
28
+ * so legacy or db-written docs pointing at a free-text field can never expose stored
29
+ * answers as leftover result buckets.
30
+ */
31
+ eligibleTypes?: readonly string[];
25
32
  };
26
33
  type ResolveResultsRequestResult = {
27
34
  status: number;
@@ -35,12 +42,18 @@ type ResolveResultsRequestResult = {
35
42
  };
36
43
  /**
37
44
  * Authorize and resolve a poll/survey results request. Authed callers may aggregate any field (or all
38
- * enumerable fields) and bypass the `access` seam. Anonymous callers are allowed only when the form's
39
- * poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`: any time; `afterClose`: only
40
- * once `closesAt` has passed), and the optional host `access` seam approves; and then only for the
41
- * configured `poll.resultsField`, and only if that field is enumerable (has options) so a misconfigured
42
- * `resultsField` pointing at a free-text or PII field can never be dumped publicly. Returns only
43
- * aggregate counts, never raw submissions.
45
+ * enumerable fields) and bypass the `access` seam; for a poll-enabled form they also get the results
46
+ * field's effective options injected so sourced/resolver-backed polls render labels (best-effort, a
47
+ * resolve failure degrades to raw values rather than blocking the trusted read). Anonymous callers are
48
+ * allowed only when the form's poll is enabled, the poll's `resultsVisibility` permits it (`afterVote`:
49
+ * any time; `afterClose`: only once `closesAt` has passed), and the optional host `access` seam
50
+ * approves; and then only for the configured `poll.resultsField`, and only if that field is enumerable
51
+ * so a misconfigured `resultsField` pointing at a free-text or PII field can never be dumped publicly. A
52
+ * static poll is enumerable through its authored options; a poll whose options come from an
53
+ * `optionSource` or the field type's own `resolveOptions` resolves them here (registry via
54
+ * `config.custom`, per-request cache), gated by `eligibleTypes` and enumerable only when resolution
55
+ * yields any, with the resolved options driving bucket order and labels. Resolution failure fails
56
+ * closed (503) on the anonymous path. Returns only aggregate counts, never raw submissions.
44
57
  */
45
58
  declare const resolveFormResultsRequest: (args: ResolveResultsRequestArgs) => Promise<ResolveResultsRequestResult>;
46
59
  //#endregion