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

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 (213) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/actions/buildActionBlocks.js +12 -9
  3. package/dist/actions/buildActionBlocks.js.map +1 -1
  4. package/dist/actions/builtin/confirmation.js +27 -79
  5. package/dist/actions/builtin/confirmation.js.map +1 -1
  6. package/dist/actions/builtin/emailAction.d.ts +17 -0
  7. package/dist/actions/builtin/emailAction.js +83 -0
  8. package/dist/actions/builtin/emailAction.js.map +1 -0
  9. package/dist/actions/builtin/emailTeam.js +14 -84
  10. package/dist/actions/builtin/emailTeam.js.map +1 -1
  11. package/dist/actions/builtin/index.d.ts +9 -11
  12. package/dist/actions/builtin/index.js +11 -11
  13. package/dist/actions/builtin/index.js.map +1 -1
  14. package/dist/actions/defineAction.d.ts +2 -2
  15. package/dist/actions/defineAction.js.map +1 -1
  16. package/dist/actions/emailRecipients.d.ts +12 -0
  17. package/dist/actions/emailRecipients.js +116 -0
  18. package/dist/actions/emailRecipients.js.map +1 -0
  19. package/dist/actions/registry.d.ts +2 -1
  20. package/dist/actions/registry.js +2 -6
  21. package/dist/actions/registry.js.map +1 -1
  22. package/dist/actions/task.js +5 -3
  23. package/dist/actions/task.js.map +1 -1
  24. package/dist/calc/evaluate.js +1 -2
  25. package/dist/calc/evaluate.js.map +1 -1
  26. package/dist/calc/normalizeCalc.js +1 -2
  27. package/dist/calc/normalizeCalc.js.map +1 -1
  28. package/dist/calc/types.js.map +1 -1
  29. package/dist/client/ConsentBlockLabel.d.ts +15 -0
  30. package/dist/client/ConsentBlockLabel.js +71 -0
  31. package/dist/client/ConsentBlockLabel.js.map +1 -0
  32. package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
  33. package/dist/client/ConsentSourceRowLabel.js +21 -0
  34. package/dist/client/ConsentSourceRowLabel.js.map +1 -0
  35. package/dist/client/EndpointOptionsSelect.d.ts +1 -0
  36. package/dist/client/EndpointOptionsSelect.js +4 -1
  37. package/dist/client/EndpointOptionsSelect.js.map +1 -1
  38. package/dist/client/FieldNameSelect.d.ts +1 -0
  39. package/dist/client/FieldNameSelect.js +4 -1
  40. package/dist/client/FieldNameSelect.js.map +1 -1
  41. package/dist/client/RecipientsSelect.d.ts +31 -0
  42. package/dist/client/RecipientsSelect.js +174 -0
  43. package/dist/client/RecipientsSelect.js.map +1 -0
  44. package/dist/client/admin.css +5 -0
  45. package/dist/collections/buttonFields.js +1 -1
  46. package/dist/collections/formSubmissions.js +4 -2
  47. package/dist/collections/formSubmissions.js.map +1 -1
  48. package/dist/collections/forms.js +46 -99
  49. package/dist/collections/forms.js.map +1 -1
  50. package/dist/collections/formsEndpoints.js +101 -0
  51. package/dist/collections/formsEndpoints.js.map +1 -0
  52. package/dist/conditions/evaluate.js +1 -1
  53. package/dist/conditions/evaluate.js.map +1 -1
  54. package/dist/conditions/fieldTypes.d.ts +3 -1
  55. package/dist/conditions/fieldTypes.js +4 -1
  56. package/dist/conditions/fieldTypes.js.map +1 -1
  57. package/dist/conditions/normalizeConditions.js +18 -5
  58. package/dist/conditions/normalizeConditions.js.map +1 -1
  59. package/dist/consent/captureConsent.d.ts +21 -5
  60. package/dist/consent/captureConsent.js +31 -7
  61. package/dist/consent/captureConsent.js.map +1 -1
  62. package/dist/consent/consentSourcesField.d.ts +8 -0
  63. package/dist/consent/consentSourcesField.js +13 -2
  64. package/dist/consent/consentSourcesField.js.map +1 -1
  65. package/dist/consent/resolveConsentEntries.d.ts +1 -1
  66. package/dist/consent/resolveConsentEntries.js +1 -4
  67. package/dist/consent/resolveConsentEntries.js.map +1 -1
  68. package/dist/consent/resolveConsentSourcesRequest.js +0 -1
  69. package/dist/consent/resolveConsentSourcesRequest.js.map +1 -1
  70. package/dist/consent/resolveConsentStatements.d.ts +1 -1
  71. package/dist/consent/resolveConsentStatements.js.map +1 -1
  72. package/dist/consent/types.d.ts +13 -1
  73. package/dist/email/departments.d.ts +13 -9
  74. package/dist/email/departments.js +7 -50
  75. package/dist/email/departments.js.map +1 -1
  76. package/dist/email/departmentsField.d.ts +4 -0
  77. package/dist/email/departmentsField.js +5 -1
  78. package/dist/email/departmentsField.js.map +1 -1
  79. package/dist/exports/client.d.ts +4 -1
  80. package/dist/exports/client.js +4 -1
  81. package/dist/exports/react.d.ts +3 -2
  82. package/dist/exports/react.js +4 -3
  83. package/dist/exports/rsc.js +1 -1
  84. package/dist/exports/types.d.ts +1 -1
  85. package/dist/fields/buildFieldBlocks.js +4 -4
  86. package/dist/fields/buildFieldBlocks.js.map +1 -1
  87. package/dist/fields/builtin/consent.js +6 -1
  88. package/dist/fields/builtin/consent.js.map +1 -1
  89. package/dist/fields/builtin/email.js +1 -1
  90. package/dist/fields/builtin/email.js.map +1 -1
  91. package/dist/fields/builtin/file.js +9 -3
  92. package/dist/fields/builtin/file.js.map +1 -1
  93. package/dist/fields/builtin/index.d.ts +1 -1
  94. package/dist/fields/builtin/index.js +3 -3
  95. package/dist/fields/builtin/index.js.map +1 -1
  96. package/dist/fields/builtin/message.js +1 -1
  97. package/dist/fields/builtin/repeater.js +1 -1
  98. package/dist/fields/builtin/select.js +1 -1
  99. package/dist/fields/registry.d.ts +2 -1
  100. package/dist/fields/registry.js +2 -6
  101. package/dist/fields/registry.js.map +1 -1
  102. package/dist/fields/sharedConfig.js +1 -1
  103. package/dist/fields/types.d.ts +9 -2
  104. package/dist/flow/normalizeFlow.js +7 -3
  105. package/dist/flow/normalizeFlow.js.map +1 -1
  106. package/dist/form/toFormDocument.d.ts +20 -3
  107. package/dist/form/toFormDocument.js +16 -4
  108. package/dist/form/toFormDocument.js.map +1 -1
  109. package/dist/form/types.d.ts +5 -5
  110. package/dist/index.d.ts +5 -193
  111. package/dist/index.js +18 -8
  112. package/dist/index.js.map +1 -1
  113. package/dist/options.d.ts +228 -0
  114. package/dist/plugin/applyRegistryConfig.d.ts +11 -0
  115. package/dist/plugin/applyRegistryConfig.js +20 -0
  116. package/dist/plugin/applyRegistryConfig.js.map +1 -0
  117. package/dist/plugin/registerCollections.js +3 -1
  118. package/dist/plugin/registerCollections.js.map +1 -1
  119. package/dist/plugin/uploadsCollection.d.ts +3 -0
  120. package/dist/plugin/uploadsCollection.js +10 -1
  121. package/dist/plugin/uploadsCollection.js.map +1 -1
  122. package/dist/poll/buildPollOptionSourceFields.js +2 -2
  123. package/dist/poll/buildPollOptionSourceFields.js.map +1 -1
  124. package/dist/poll/definePollOptionSource.d.ts +2 -2
  125. package/dist/poll/definePollOptionSource.js.map +1 -1
  126. package/dist/poll/pollTypeRegistry.js +2 -4
  127. package/dist/poll/pollTypeRegistry.js.map +1 -1
  128. package/dist/poll/registry.d.ts +2 -1
  129. package/dist/poll/registry.js +2 -6
  130. package/dist/poll/registry.js.map +1 -1
  131. package/dist/poll/resolvePollOptionsRequest.js +0 -1
  132. package/dist/poll/resolvePollOptionsRequest.js.map +1 -1
  133. package/dist/prefill/valuesFromSearchParams.js +2 -7
  134. package/dist/prefill/valuesFromSearchParams.js.map +1 -1
  135. package/dist/react/Form.d.ts +3 -1
  136. package/dist/react/Form.js +93 -155
  137. package/dist/react/Form.js.map +1 -1
  138. package/dist/react/FormContext.d.ts +5 -1
  139. package/dist/react/FormContext.js.map +1 -1
  140. package/dist/react/FormFields.d.ts +18 -0
  141. package/dist/react/FormFields.js +114 -0
  142. package/dist/react/FormFields.js.map +1 -0
  143. package/dist/react/FormLayout.d.ts +4 -2
  144. package/dist/react/FormLayout.js +3 -2
  145. package/dist/react/FormLayout.js.map +1 -1
  146. package/dist/react/Poll.js +13 -3
  147. package/dist/react/Poll.js.map +1 -1
  148. package/dist/react/presentation/DialogSurface.js +1 -4
  149. package/dist/react/presentation/DialogSurface.js.map +1 -1
  150. package/dist/react/renderers/calculation.js +1 -3
  151. package/dist/react/renderers/calculation.js.map +1 -1
  152. package/dist/react/renderers/checkbox.js +1 -3
  153. package/dist/react/renderers/checkbox.js.map +1 -1
  154. package/dist/react/renderers/consent.js +2 -3
  155. package/dist/react/renderers/consent.js.map +1 -1
  156. package/dist/react/renderers/date.js +1 -3
  157. package/dist/react/renderers/date.js.map +1 -1
  158. package/dist/react/renderers/email.js +1 -3
  159. package/dist/react/renderers/email.js.map +1 -1
  160. package/dist/react/renderers/file.js +6 -7
  161. package/dist/react/renderers/file.js.map +1 -1
  162. package/dist/react/renderers/number.js +1 -3
  163. package/dist/react/renderers/number.js.map +1 -1
  164. package/dist/react/renderers/region.js +1 -3
  165. package/dist/react/renderers/region.js.map +1 -1
  166. package/dist/react/renderers/repeater.js +20 -4
  167. package/dist/react/renderers/repeater.js.map +1 -1
  168. package/dist/react/renderers/select.js +1 -3
  169. package/dist/react/renderers/select.js.map +1 -1
  170. package/dist/react/renderers/text.js +1 -3
  171. package/dist/react/renderers/text.js.map +1 -1
  172. package/dist/react/renderers/textarea.js +1 -3
  173. package/dist/react/renderers/textarea.js.map +1 -1
  174. package/dist/spam/constants.js +7 -1
  175. package/dist/spam/constants.js.map +1 -1
  176. package/dist/spam/resolveSpam.js +2 -1
  177. package/dist/spam/resolveSpam.js.map +1 -1
  178. package/dist/spam/spamGuard.js +22 -6
  179. package/dist/spam/spamGuard.js.map +1 -1
  180. package/dist/spam/types.d.ts +17 -2
  181. package/dist/submissions/SubmissionAnswers.js.map +1 -1
  182. package/dist/submissions/SubmissionAnswersClient.js +33 -26
  183. package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
  184. package/dist/submissions/coerceBoolean.js +14 -0
  185. package/dist/submissions/coerceBoolean.js.map +1 -0
  186. package/dist/submissions/runSubmission.js +9 -8
  187. package/dist/submissions/runSubmission.js.map +1 -1
  188. package/dist/submissions/validateSubmission.js +4 -2
  189. package/dist/submissions/validateSubmission.js.map +1 -1
  190. package/dist/translations/de.js +14 -2
  191. package/dist/translations/de.js.map +1 -1
  192. package/dist/translations/en.js +14 -2
  193. package/dist/translations/en.js.map +1 -1
  194. package/dist/translations/keys.d.ts +14 -2
  195. package/dist/translations/keys.js +14 -2
  196. package/dist/translations/keys.js.map +1 -1
  197. package/dist/translations/server.js +8 -1
  198. package/dist/translations/server.js.map +1 -1
  199. package/dist/uploads/captureFileRef.d.ts +10 -3
  200. package/dist/uploads/captureFileRef.js +18 -9
  201. package/dist/uploads/captureFileRef.js.map +1 -1
  202. package/dist/uploads/resolveFileRef.js +2 -1
  203. package/dist/uploads/resolveFileRef.js.map +1 -1
  204. package/dist/validation/builtin/email.js +1 -1
  205. package/dist/validation/builtin/email.js.map +1 -1
  206. package/dist/validation/builtin/notAlreadySubmitted.js +1 -0
  207. package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
  208. package/dist/validation/emailPattern.js +10 -0
  209. package/dist/validation/emailPattern.js.map +1 -0
  210. package/dist/validation/registry.d.ts +2 -1
  211. package/dist/validation/registry.js +2 -9
  212. package/dist/validation/registry.js.map +1 -1
  213. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"buildPollOptionSourceFields.js","names":[],"sources":["../../src/poll/buildPollOptionSourceFields.ts"],"sourcesContent":["import type { Field } from 'payload'\nimport { keys } from '../translations/keys'\nimport { labelFor, labelForKey } from '../translations/server'\nimport type { PollOptionSourceRegistry } from './registry'\n\n/**\n * Builds the `optionSource` select and `sourceConfig` group for the forms poll group; returns\n * nothing when no sources are registered, so the fields only exist for hosts that opted in.\n * Fields inside `sourceConfig` are annotated with `admin.condition` so only the fields of the\n * currently selected source type are visible; like consent's `sourceConfig`, field names within\n * the group must be unique across all registered sources. Unlike consent's `source` select there\n * is no default: hand-authored options remain the baseline and a source is an explicit opt-in per\n * form (the select stays clearable).\n */\nexport const buildPollOptionSourceFields = (registry: PollOptionSourceRegistry): Field[] => {\n\tconst sources = [...registry.values()]\n\tif (sources.length === 0) {\n\t\treturn []\n\t}\n\n\tconst sourceConfigFields: Field[] = sources.flatMap((source) =>\n\t\t(source.config ?? []).map((field) => {\n\t\t\tconst existingAdmin =\n\t\t\t\ttypeof field.admin === 'object' && field.admin !== null ? field.admin : {}\n\t\t\treturn {\n\t\t\t\t...field,\n\t\t\t\tadmin: {\n\t\t\t\t\t...existingAdmin,\n\t\t\t\t\tcondition: (data: unknown) =>\n\t\t\t\t\t\t(data as { poll?: { optionSource?: string } })?.poll?.optionSource === source.type,\n\t\t\t\t},\n\t\t\t} as Field\n\t\t})\n\t)\n\n\treturn [\n\t\t{\n\t\t\tname: 'optionSource',\n\t\t\ttype: 'select',\n\t\t\tlabel: labelForKey(keys.pollOptionSource),\n\t\t\tadmin: {\n\t\t\t\tdescription: labelForKey(keys.pollOptionSourceDescription),\n\t\t\t},\n\t\t\toptions: sources.map((source) => ({ label: labelFor(source.label), value: source.type })),\n\t\t},\n\t\t{\n\t\t\tname: 'sourceConfig',\n\t\t\ttype: 'group',\n\t\t\tlabel: labelForKey(keys.pollSourceConfig),\n\t\t\tadmin: {\n\t\t\t\tcondition: (_data, siblingData) => Boolean(siblingData?.optionSource),\n\t\t\t},\n\t\t\tfields:\n\t\t\t\tsourceConfigFields.length > 0\n\t\t\t\t\t? sourceConfigFields\n\t\t\t\t\t: [{ name: '_placeholder', type: 'text', admin: { hidden: true } }],\n\t\t},\n\t]\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,+BAA+B,aAAgD;CAC3F,MAAM,UAAU,CAAC,GAAG,SAAS,OAAO,CAAC;CACrC,IAAI,QAAQ,WAAW,GACtB,OAAO,CAAC;CAGT,MAAM,qBAA8B,QAAQ,SAAS,YACnD,OAAO,UAAU,CAAC,GAAG,KAAK,UAAU;EACpC,MAAM,gBACL,OAAO,MAAM,UAAU,YAAY,MAAM,UAAU,OAAO,MAAM,QAAQ,CAAC;EAC1E,OAAO;GACN,GAAG;GACH,OAAO;IACN,GAAG;IACH,YAAY,SACV,MAA+C,MAAM,iBAAiB,OAAO;GAChF;EACD;CACD,CAAC,CACF;CAEA,OAAO,CACN;EACC,MAAM;EACN,MAAM;EACN,OAAO,YAAY,KAAK,gBAAgB;EACxC,OAAO,EACN,aAAa,YAAY,KAAK,2BAA2B,EAC1D;EACA,SAAS,QAAQ,KAAK,YAAY;GAAE,OAAO,SAAS,OAAO,KAAK;GAAG,OAAO,OAAO;EAAK,EAAE;CACzF,GACA;EACC,MAAM;EACN,MAAM;EACN,OAAO,YAAY,KAAK,gBAAgB;EACxC,OAAO,EACN,YAAY,OAAO,gBAAgB,QAAQ,aAAa,YAAY,EACrE;EACA,QACC,mBAAmB,SAAS,IACzB,qBACA,CAAC;GAAE,MAAM;GAAgB,MAAM;GAAQ,OAAO,EAAE,QAAQ,KAAK;EAAE,CAAC;CACrE,CACD;AACD"}
1
+ {"version":3,"file":"buildPollOptionSourceFields.js","names":[],"sources":["../../src/poll/buildPollOptionSourceFields.ts"],"sourcesContent":["import type { Field } from 'payload'\nimport { keys } from '../translations/keys'\nimport { labelForKey, resolveDefinitionLabel } from '../translations/server'\nimport type { PollOptionSourceRegistry } from './registry'\n\n/**\n * Builds the `optionSource` select and `sourceConfig` group for the forms poll group; returns\n * nothing when no sources are registered, so the fields only exist for hosts that opted in.\n * Fields inside `sourceConfig` are annotated with `admin.condition` so only the fields of the\n * currently selected source type are visible; like consent's `sourceConfig`, field names within\n * the group must be unique across all registered sources. Unlike consent's `source` select there\n * is no default: hand-authored options remain the baseline and a source is an explicit opt-in per\n * form (the select stays clearable).\n */\nexport const buildPollOptionSourceFields = (registry: PollOptionSourceRegistry): Field[] => {\n\tconst sources = [...registry.values()]\n\tif (sources.length === 0) {\n\t\treturn []\n\t}\n\n\tconst sourceConfigFields: Field[] = sources.flatMap((source) =>\n\t\t(source.config ?? []).map((field) => {\n\t\t\tconst existingAdmin =\n\t\t\t\ttypeof field.admin === 'object' && field.admin !== null ? field.admin : {}\n\t\t\treturn {\n\t\t\t\t...field,\n\t\t\t\tadmin: {\n\t\t\t\t\t...existingAdmin,\n\t\t\t\t\tcondition: (data: unknown) =>\n\t\t\t\t\t\t(data as { poll?: { optionSource?: string } })?.poll?.optionSource === source.type,\n\t\t\t\t},\n\t\t\t} as Field\n\t\t})\n\t)\n\n\treturn [\n\t\t{\n\t\t\tname: 'optionSource',\n\t\t\ttype: 'select',\n\t\t\tlabel: labelForKey(keys.pollOptionSource),\n\t\t\tadmin: {\n\t\t\t\tdescription: labelForKey(keys.pollOptionSourceDescription),\n\t\t\t},\n\t\t\toptions: sources.map((source) => ({\n\t\t\t\tlabel: resolveDefinitionLabel(source.label),\n\t\t\t\tvalue: source.type,\n\t\t\t})),\n\t\t},\n\t\t{\n\t\t\tname: 'sourceConfig',\n\t\t\ttype: 'group',\n\t\t\tlabel: labelForKey(keys.pollSourceConfig),\n\t\t\tadmin: {\n\t\t\t\tcondition: (_data, siblingData) => Boolean(siblingData?.optionSource),\n\t\t\t},\n\t\t\tfields:\n\t\t\t\tsourceConfigFields.length > 0\n\t\t\t\t\t? sourceConfigFields\n\t\t\t\t\t: [{ name: '_placeholder', type: 'text', admin: { hidden: true } }],\n\t\t},\n\t]\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,+BAA+B,aAAgD;CAC3F,MAAM,UAAU,CAAC,GAAG,SAAS,OAAO,CAAC;CACrC,IAAI,QAAQ,WAAW,GACtB,OAAO,CAAC;CAGT,MAAM,qBAA8B,QAAQ,SAAS,YACnD,OAAO,UAAU,CAAC,GAAG,KAAK,UAAU;EACpC,MAAM,gBACL,OAAO,MAAM,UAAU,YAAY,MAAM,UAAU,OAAO,MAAM,QAAQ,CAAC;EAC1E,OAAO;GACN,GAAG;GACH,OAAO;IACN,GAAG;IACH,YAAY,SACV,MAA+C,MAAM,iBAAiB,OAAO;GAChF;EACD;CACD,CAAC,CACF;CAEA,OAAO,CACN;EACC,MAAM;EACN,MAAM;EACN,OAAO,YAAY,KAAK,gBAAgB;EACxC,OAAO,EACN,aAAa,YAAY,KAAK,2BAA2B,EAC1D;EACA,SAAS,QAAQ,KAAK,YAAY;GACjC,OAAO,uBAAuB,OAAO,KAAK;GAC1C,OAAO,OAAO;EACf,EAAE;CACH,GACA;EACC,MAAM;EACN,MAAM;EACN,OAAO,YAAY,KAAK,gBAAgB;EACxC,OAAO,EACN,YAAY,OAAO,gBAAgB,QAAQ,aAAa,YAAY,EACrE;EACA,QACC,mBAAmB,SAAS,IACzB,qBACA,CAAC;GAAE,MAAM;GAAgB,MAAM;GAAQ,OAAO,EAAE,QAAQ,KAAK;EAAE,CAAC;CACrE,CACD;AACD"}
@@ -22,8 +22,8 @@ type PollOptionResolveArgs<TConfig extends Record<string, unknown> = Record<stri
22
22
  * the resolved values are the only accepted answers (fail closed).
23
23
  */
24
24
  type PollOptionSource<TConfig extends Record<string, unknown> = Record<string, unknown>> = {
25
- type: string;
26
- label: string;
25
+ type: string; /** i18n-key or literal (resolved like a field label), or a per-locale record. */
26
+ label: string | Record<string, string>;
27
27
  config?: Field[];
28
28
  resolve: (args: PollOptionResolveArgs<TConfig>) => Promise<PollOption[]> | PollOption[];
29
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"definePollOptionSource.js","names":[],"sources":["../../src/poll/definePollOptionSource.ts"],"sourcesContent":["import type { Field, Payload, PayloadRequest } from 'payload'\n\n/** One selectable poll choice. `value` is the host's stable identifier contract: it is stored in submissions and later matched against `outcome.winningValues`, so it must never change once votes exist. */\nexport type PollOption = { label: string; value: string }\n\nexport type PollOptionResolveArgs<\n\tTConfig extends Record<string, unknown> = Record<string, unknown>,\n> = {\n\tconfig: TConfig\n\tform: { id: number | string; title?: string }\n\tpayload: Payload\n\treq?: PayloadRequest\n}\n\n/**\n * A poll option source type, authored once by the host: `config` is the admin `Field[]` an author\n * fills per form (e.g. an event id); `resolve` maps that config to the current options from host\n * domain data. Called at render time (via `resolvePollOptions`) and again at submission time, where\n * the resolved values are the only accepted answers (fail closed).\n */\nexport type PollOptionSource<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\ttype: string\n\tlabel: string\n\tconfig?: Field[]\n\tresolve: (args: PollOptionResolveArgs<TConfig>) => Promise<PollOption[]> | PollOption[]\n\t/**\n\t * Optional outcome resolver: return the winning option value(s) from domain data (e.g. the\n\t * recorded race result), a `string[]` when the result is a tie, or `undefined` while the outcome\n\t * is not yet decidable. Backs the built-in `source` poll type, so a form whose `poll.type` is\n\t * `source` auto-resolves through it (via the scheduled close job, a results-read, or an explicit\n\t * `resolvePollOutcome({ payload, formId })`); every returned value is validated against the poll's\n\t * resolved options before it is recorded, so each must be one of the values `resolve` yields.\n\t */\n\tresolveOutcome?: (\n\t\targs: PollOptionResolveArgs<TConfig>\n\t) => Promise<string | string[] | undefined> | string | string[] | undefined\n}\n\n/** Erased shape stored in the registry; config re-narrows per matched type at resolution. */\nexport type AnyPollOptionSource = PollOptionSource<Record<string, unknown>>\n\nexport const definePollOptionSource = <\n\tTConfig extends Record<string, unknown> = Record<string, unknown>,\n>(\n\tsource: PollOptionSource<TConfig>\n): PollOptionSource<TConfig> => source\n"],"mappings":";AAyCA,MAAa,0BAGZ,WAC+B"}
1
+ {"version":3,"file":"definePollOptionSource.js","names":[],"sources":["../../src/poll/definePollOptionSource.ts"],"sourcesContent":["import type { Field, Payload, PayloadRequest } from 'payload'\n\n/** One selectable poll choice. `value` is the host's stable identifier contract: it is stored in submissions and later matched against `outcome.winningValues`, so it must never change once votes exist. */\nexport type PollOption = { label: string; value: string }\n\nexport type PollOptionResolveArgs<\n\tTConfig extends Record<string, unknown> = Record<string, unknown>,\n> = {\n\tconfig: TConfig\n\tform: { id: number | string; title?: string }\n\tpayload: Payload\n\treq?: PayloadRequest\n}\n\n/**\n * A poll option source type, authored once by the host: `config` is the admin `Field[]` an author\n * fills per form (e.g. an event id); `resolve` maps that config to the current options from host\n * domain data. Called at render time (via `resolvePollOptions`) and again at submission time, where\n * the resolved values are the only accepted answers (fail closed).\n */\nexport type PollOptionSource<TConfig extends Record<string, unknown> = Record<string, unknown>> = {\n\ttype: string\n\t/** i18n-key or literal (resolved like a field label), or a per-locale record. */\n\tlabel: string | Record<string, string>\n\tconfig?: Field[]\n\tresolve: (args: PollOptionResolveArgs<TConfig>) => Promise<PollOption[]> | PollOption[]\n\t/**\n\t * Optional outcome resolver: return the winning option value(s) from domain data (e.g. the\n\t * recorded race result), a `string[]` when the result is a tie, or `undefined` while the outcome\n\t * is not yet decidable. Backs the built-in `source` poll type, so a form whose `poll.type` is\n\t * `source` auto-resolves through it (via the scheduled close job, a results-read, or an explicit\n\t * `resolvePollOutcome({ payload, formId })`); every returned value is validated against the poll's\n\t * resolved options before it is recorded, so each must be one of the values `resolve` yields.\n\t */\n\tresolveOutcome?: (\n\t\targs: PollOptionResolveArgs<TConfig>\n\t) => Promise<string | string[] | undefined> | string | string[] | undefined\n}\n\n/** Erased shape stored in the registry; config re-narrows per matched type at resolution. */\nexport type AnyPollOptionSource = PollOptionSource<Record<string, unknown>>\n\nexport const definePollOptionSource = <\n\tTConfig extends Record<string, unknown> = Record<string, unknown>,\n>(\n\tsource: PollOptionSource<TConfig>\n): PollOptionSource<TConfig> => source\n"],"mappings":";AA0CA,MAAa,0BAGZ,WAC+B"}
@@ -1,4 +1,5 @@
1
1
  import { keys } from "../translations/keys.js";
2
+ import { applyRegistryConfig } from "../plugin/applyRegistryConfig.js";
2
3
  import { customStateOf, stashCustomState } from "../plugin/customState.js";
3
4
  import { definePollType } from "./definePollType.js";
4
5
  import { mostVotedStrategy } from "./mostVoted.js";
@@ -56,10 +57,7 @@ const resolvePollTypes = (config) => {
56
57
  [sourceStrategy.type, sourceStrategy]
57
58
  ]);
58
59
  if (Array.isArray(config)) for (const strategy of config) registry.set(strategy.type, strategy);
59
- else if (config) for (const [type, strategy] of Object.entries(config)) registry.set(type, {
60
- ...strategy,
61
- type
62
- });
60
+ else if (config) applyRegistryConfig(registry, config);
63
61
  return registry;
64
62
  };
65
63
  /** Store the resolved registry on the config so `resolvePollOutcome` can reach it via `payload.config`. */
@@ -1 +1 @@
1
- {"version":3,"file":"pollTypeRegistry.js","names":[],"sources":["../../src/poll/pollTypeRegistry.ts"],"sourcesContent":["import type { Payload } from 'payload'\nimport { customStateOf, stashCustomState } from '../plugin/customState'\nimport { keys } from '../translations/keys'\nimport { definePollType, type PollOutcomeStrategy } from './definePollType'\nimport { mostVotedStrategy } from './mostVoted'\nimport { pollOptionSourcesOf } from './resolvePollOptions'\n\nexport type PollTypeRegistry = Map<string, PollOutcomeStrategy>\n\n/**\n * Host-supplied outcome strategies: a keyed record (the key forces `type`, like the field-type\n * registry) or a plain array. There are no boolean toggles because the three built-ins are always\n * registered; a host entry with a built-in's `type` replaces it.\n */\nexport type PollTypesConfig = Record<string, PollOutcomeStrategy> | PollOutcomeStrategy[]\n\n/** The default: an admin records `winningValues` by hand, so there is nothing to auto-resolve. */\nexport const manualStrategy: PollOutcomeStrategy = definePollType({\n\ttype: 'manual',\n\tlabel: keys.pollTypeManual,\n\tresolveOutcome: () => undefined,\n})\n\n/** One winner set as a deduped array of non-empty strings, or `undefined` when the source yields none. */\nconst normalizeSourceOutcome = (value: string | string[] | undefined): string[] | undefined => {\n\tif (value === undefined) {\n\t\treturn undefined\n\t}\n\tconst cleaned = [\n\t\t...new Set(\n\t\t\t(Array.isArray(value) ? value : [value]).filter(\n\t\t\t\t(entry): entry is string => typeof entry === 'string' && entry.length > 0\n\t\t\t)\n\t\t),\n\t]\n\treturn cleaned.length > 0 ? cleaned : undefined\n}\n\n/**\n * Delegate to the poll's configured option source `resolveOutcome` (the same domain resolver that\n * feeds explicit `resolvePollOutcome` auto mode before this task shipped). Returns `undefined` when\n * the poll has no source, the source is unregistered, or it does not implement `resolveOutcome`, so a\n * misconfigured poll never throws from the close job or the read fallback; a source that resolves to\n * no decision (its `resolveOutcome` returns `undefined`) also yields `undefined`.\n */\nexport const sourceStrategy: PollOutcomeStrategy = definePollType({\n\ttype: 'source',\n\tlabel: keys.pollTypeSource,\n\tresolveOutcome: async ({ payload, req, form, poll }) => {\n\t\tconst optionSource =\n\t\t\ttypeof poll.optionSource === 'string' && poll.optionSource.length > 0\n\t\t\t\t? poll.optionSource\n\t\t\t\t: undefined\n\t\tif (!optionSource) {\n\t\t\treturn undefined\n\t\t}\n\t\tconst source = pollOptionSourcesOf(payload).get(optionSource)\n\t\tif (!source?.resolveOutcome) {\n\t\t\treturn undefined\n\t\t}\n\t\tconst config =\n\t\t\tpoll.sourceConfig != null && typeof poll.sourceConfig === 'object'\n\t\t\t\t? (poll.sourceConfig as Record<string, unknown>)\n\t\t\t\t: {}\n\t\treturn normalizeSourceOutcome(\n\t\t\tawait source.resolveOutcome({\n\t\t\t\tconfig,\n\t\t\t\tform: { id: form.id, title: typeof form.title === 'string' ? form.title : undefined },\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t})\n\t\t)\n\t},\n})\n\n/**\n * The active poll-type registry: the three always-registered built-ins (`manual`, `mostVoted`,\n * `source`) plus any host strategies from the plugin `poll.types` option. A host entry keyed by (or\n * carrying a `type` of) a built-in slug replaces that built-in. Mirrors the field-type registry: a\n * record key forces the strategy's `type` so a keyed override cannot rename the slot.\n */\nexport const resolvePollTypes = (config?: PollTypesConfig): PollTypeRegistry => {\n\tconst registry: PollTypeRegistry = new Map([\n\t\t[manualStrategy.type, manualStrategy],\n\t\t[mostVotedStrategy.type, mostVotedStrategy],\n\t\t[sourceStrategy.type, sourceStrategy],\n\t])\n\tif (Array.isArray(config)) {\n\t\tfor (const strategy of config) {\n\t\t\tregistry.set(strategy.type, strategy)\n\t\t}\n\t} else if (config) {\n\t\tfor (const [type, strategy] of Object.entries(config)) {\n\t\t\tregistry.set(type, { ...strategy, type })\n\t\t}\n\t}\n\treturn registry\n}\n\ntype PollTypesCustomState = { pollTypes: PollTypeRegistry }\n\n/** Store the resolved registry on the config so `resolvePollOutcome` can reach it via `payload.config`. */\nexport const stashPollTypes = (\n\tcustom: Record<string, unknown> | undefined,\n\tregistry: PollTypeRegistry\n): Record<string, unknown> =>\n\tstashCustomState<PollTypesCustomState>(custom, { pollTypes: registry })\n\nexport const pollTypesOf = (payload: Payload): PollTypeRegistry =>\n\tcustomStateOf<PollTypesCustomState>(payload).pollTypes ?? new Map()\n"],"mappings":";;;;;;;AAiBA,MAAa,iBAAsC,eAAe;CACjE,MAAM;CACN,OAAO,KAAK;CACZ,sBAAsB,KAAA;AACvB,CAAC;;AAGD,MAAM,0BAA0B,UAA+D;CAC9F,IAAI,UAAU,KAAA,GACb;CAED,MAAM,UAAU,CACf,GAAG,IAAI,KACL,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAAG,QACvC,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CACzE,CACD,CACD;CACA,OAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;AACvC;;;;;;;;AASA,MAAa,iBAAsC,eAAe;CACjE,MAAM;CACN,OAAO,KAAK;CACZ,gBAAgB,OAAO,EAAE,SAAS,KAAK,MAAM,WAAW;EACvD,MAAM,eACL,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjE,KAAK,eACL,KAAA;EACJ,IAAI,CAAC,cACJ;EAED,MAAM,SAAS,oBAAoB,OAAO,EAAE,IAAI,YAAY;EAC5D,IAAI,CAAC,QAAQ,gBACZ;EAED,MAAM,SACL,KAAK,gBAAgB,QAAQ,OAAO,KAAK,iBAAiB,WACtD,KAAK,eACN,CAAC;EACL,OAAO,uBACN,MAAM,OAAO,eAAe;GAC3B;GACA,MAAM;IAAE,IAAI,KAAK;IAAI,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;GAAU;GACpF;GACA;EACD,CAAC,CACF;CACD;AACD,CAAC;;;;;;;AAQD,MAAa,oBAAoB,WAA+C;CAC/E,MAAM,WAA6B,IAAI,IAAI;EAC1C,CAAC,eAAe,MAAM,cAAc;EACpC,CAAC,kBAAkB,MAAM,iBAAiB;EAC1C,CAAC,eAAe,MAAM,cAAc;CACrC,CAAC;CACD,IAAI,MAAM,QAAQ,MAAM,GACvB,KAAK,MAAM,YAAY,QACtB,SAAS,IAAI,SAAS,MAAM,QAAQ;MAE/B,IAAI,QACV,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,MAAM,GACnD,SAAS,IAAI,MAAM;EAAE,GAAG;EAAU;CAAK,CAAC;CAG1C,OAAO;AACR;;AAKA,MAAa,kBACZ,QACA,aAEA,iBAAuC,QAAQ,EAAE,WAAW,SAAS,CAAC;AAEvE,MAAa,eAAe,YAC3B,cAAoC,OAAO,EAAE,6BAAa,IAAI,IAAI"}
1
+ {"version":3,"file":"pollTypeRegistry.js","names":[],"sources":["../../src/poll/pollTypeRegistry.ts"],"sourcesContent":["import type { Payload } from 'payload'\nimport { applyRegistryConfig } from '../plugin/applyRegistryConfig'\nimport { customStateOf, stashCustomState } from '../plugin/customState'\nimport { keys } from '../translations/keys'\nimport { definePollType, type PollOutcomeStrategy } from './definePollType'\nimport { mostVotedStrategy } from './mostVoted'\nimport { pollOptionSourcesOf } from './resolvePollOptions'\n\nexport type PollTypeRegistry = Map<string, PollOutcomeStrategy>\n\n/**\n * Host-supplied outcome strategies: a keyed record (the key forces `type`, like the field-type\n * registry) or a plain array. There are no boolean toggles because the three built-ins are always\n * registered; a host entry with a built-in's `type` replaces it.\n */\nexport type PollTypesConfig = Record<string, PollOutcomeStrategy> | PollOutcomeStrategy[]\n\n/** The default: an admin records `winningValues` by hand, so there is nothing to auto-resolve. */\nexport const manualStrategy: PollOutcomeStrategy = definePollType({\n\ttype: 'manual',\n\tlabel: keys.pollTypeManual,\n\tresolveOutcome: () => undefined,\n})\n\n/** One winner set as a deduped array of non-empty strings, or `undefined` when the source yields none. */\nconst normalizeSourceOutcome = (value: string | string[] | undefined): string[] | undefined => {\n\tif (value === undefined) {\n\t\treturn undefined\n\t}\n\tconst cleaned = [\n\t\t...new Set(\n\t\t\t(Array.isArray(value) ? value : [value]).filter(\n\t\t\t\t(entry): entry is string => typeof entry === 'string' && entry.length > 0\n\t\t\t)\n\t\t),\n\t]\n\treturn cleaned.length > 0 ? cleaned : undefined\n}\n\n/**\n * Delegate to the poll's configured option source `resolveOutcome` (the same domain resolver that\n * feeds explicit `resolvePollOutcome` auto mode before this task shipped). Returns `undefined` when\n * the poll has no source, the source is unregistered, or it does not implement `resolveOutcome`, so a\n * misconfigured poll never throws from the close job or the read fallback; a source that resolves to\n * no decision (its `resolveOutcome` returns `undefined`) also yields `undefined`.\n */\nexport const sourceStrategy: PollOutcomeStrategy = definePollType({\n\ttype: 'source',\n\tlabel: keys.pollTypeSource,\n\tresolveOutcome: async ({ payload, req, form, poll }) => {\n\t\tconst optionSource =\n\t\t\ttypeof poll.optionSource === 'string' && poll.optionSource.length > 0\n\t\t\t\t? poll.optionSource\n\t\t\t\t: undefined\n\t\tif (!optionSource) {\n\t\t\treturn undefined\n\t\t}\n\t\tconst source = pollOptionSourcesOf(payload).get(optionSource)\n\t\tif (!source?.resolveOutcome) {\n\t\t\treturn undefined\n\t\t}\n\t\tconst config =\n\t\t\tpoll.sourceConfig != null && typeof poll.sourceConfig === 'object'\n\t\t\t\t? (poll.sourceConfig as Record<string, unknown>)\n\t\t\t\t: {}\n\t\treturn normalizeSourceOutcome(\n\t\t\tawait source.resolveOutcome({\n\t\t\t\tconfig,\n\t\t\t\tform: { id: form.id, title: typeof form.title === 'string' ? form.title : undefined },\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t})\n\t\t)\n\t},\n})\n\n/**\n * The active poll-type registry: the three always-registered built-ins (`manual`, `mostVoted`,\n * `source`) plus any host strategies from the plugin `poll.types` option. A host entry keyed by (or\n * carrying a `type` of) a built-in slug replaces that built-in. Mirrors the field-type registry: a\n * record key forces the strategy's `type` so a keyed override cannot rename the slot.\n */\nexport const resolvePollTypes = (config?: PollTypesConfig): PollTypeRegistry => {\n\tconst registry: PollTypeRegistry = new Map([\n\t\t[manualStrategy.type, manualStrategy],\n\t\t[mostVotedStrategy.type, mostVotedStrategy],\n\t\t[sourceStrategy.type, sourceStrategy],\n\t])\n\tif (Array.isArray(config)) {\n\t\tfor (const strategy of config) {\n\t\t\tregistry.set(strategy.type, strategy)\n\t\t}\n\t} else if (config) {\n\t\t// A keyed record shares the sibling registries' merge (the key forces `type`). There are no\n\t\t// boolean toggles here because the three built-ins are always registered, so those branches\n\t\t// are inert for this always-strategy config.\n\t\tapplyRegistryConfig(registry, config)\n\t}\n\treturn registry\n}\n\ntype PollTypesCustomState = { pollTypes: PollTypeRegistry }\n\n/** Store the resolved registry on the config so `resolvePollOutcome` can reach it via `payload.config`. */\nexport const stashPollTypes = (\n\tcustom: Record<string, unknown> | undefined,\n\tregistry: PollTypeRegistry\n): Record<string, unknown> =>\n\tstashCustomState<PollTypesCustomState>(custom, { pollTypes: registry })\n\nexport const pollTypesOf = (payload: Payload): PollTypeRegistry =>\n\tcustomStateOf<PollTypesCustomState>(payload).pollTypes ?? new Map()\n"],"mappings":";;;;;;;;AAkBA,MAAa,iBAAsC,eAAe;CACjE,MAAM;CACN,OAAO,KAAK;CACZ,sBAAsB,KAAA;AACvB,CAAC;;AAGD,MAAM,0BAA0B,UAA+D;CAC9F,IAAI,UAAU,KAAA,GACb;CAED,MAAM,UAAU,CACf,GAAG,IAAI,KACL,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAAG,QACvC,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CACzE,CACD,CACD;CACA,OAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;AACvC;;;;;;;;AASA,MAAa,iBAAsC,eAAe;CACjE,MAAM;CACN,OAAO,KAAK;CACZ,gBAAgB,OAAO,EAAE,SAAS,KAAK,MAAM,WAAW;EACvD,MAAM,eACL,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IACjE,KAAK,eACL,KAAA;EACJ,IAAI,CAAC,cACJ;EAED,MAAM,SAAS,oBAAoB,OAAO,EAAE,IAAI,YAAY;EAC5D,IAAI,CAAC,QAAQ,gBACZ;EAED,MAAM,SACL,KAAK,gBAAgB,QAAQ,OAAO,KAAK,iBAAiB,WACtD,KAAK,eACN,CAAC;EACL,OAAO,uBACN,MAAM,OAAO,eAAe;GAC3B;GACA,MAAM;IAAE,IAAI,KAAK;IAAI,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;GAAU;GACpF;GACA;EACD,CAAC,CACF;CACD;AACD,CAAC;;;;;;;AAQD,MAAa,oBAAoB,WAA+C;CAC/E,MAAM,WAA6B,IAAI,IAAI;EAC1C,CAAC,eAAe,MAAM,cAAc;EACpC,CAAC,kBAAkB,MAAM,iBAAiB;EAC1C,CAAC,eAAe,MAAM,cAAc;CACrC,CAAC;CACD,IAAI,MAAM,QAAQ,MAAM,GACvB,KAAK,MAAM,YAAY,QACtB,SAAS,IAAI,SAAS,MAAM,QAAQ;MAE/B,IAAI,QAIV,oBAAoB,UAAU,MAAM;CAErC,OAAO;AACR;;AAKA,MAAa,kBACZ,QACA,aAEA,iBAAuC,QAAQ,EAAE,WAAW,SAAS,CAAC;AAEvE,MAAa,eAAe,YAC3B,cAAoC,OAAO,EAAE,6BAAa,IAAI,IAAI"}
@@ -1,10 +1,11 @@
1
1
  import { AnyPollOptionSource } from "./definePollOptionSource.js";
2
+ import { RegistryConfig } from "../plugin/applyRegistryConfig.js";
2
3
 
3
4
  //#region src/poll/registry.d.ts
4
5
  type PollOptionSourceRegistry = Map<string, AnyPollOptionSource>;
5
6
  /** `false` removes a source, `true` keeps it (no-op: there are no built-ins), a definition adds or replaces one. */
6
7
  type PollOptionSourceOption = boolean | AnyPollOptionSource;
7
- type PollOptionSourcesConfig = Record<string, PollOptionSourceOption>;
8
+ type PollOptionSourcesConfig = RegistryConfig<AnyPollOptionSource>;
8
9
  /**
9
10
  * Resolve the active poll option-source registry from a consumer map. Unlike the consent and
10
11
  * action registries there are no built-in defaults: the registry starts empty and only host
@@ -1,15 +1,11 @@
1
+ import { applyRegistryConfig } from "../plugin/applyRegistryConfig.js";
1
2
  //#region src/poll/registry.ts
2
3
  /**
3
4
  * Resolve the active poll option-source registry from a consumer map. Unlike the consent and
4
5
  * action registries there are no built-in defaults: the registry starts empty and only host
5
6
  * definitions populate it, so `true`/`false` entries are accepted but change nothing.
6
7
  */
7
- const resolvePollOptionSources = (config = {}) => {
8
- const registry = /* @__PURE__ */ new Map();
9
- for (const [type, option] of Object.entries(config)) if (option === false) registry.delete(type);
10
- else if (option === true) {} else registry.set(type, option);
11
- return registry;
12
- };
8
+ const resolvePollOptionSources = (config = {}) => applyRegistryConfig(/* @__PURE__ */ new Map(), config);
13
9
  //#endregion
14
10
  export { resolvePollOptionSources };
15
11
 
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","names":[],"sources":["../../src/poll/registry.ts"],"sourcesContent":["import type { AnyPollOptionSource } from './definePollOptionSource'\n\nexport type PollOptionSourceRegistry = Map<string, AnyPollOptionSource>\n\n/** `false` removes a source, `true` keeps it (no-op: there are no built-ins), a definition adds or replaces one. */\nexport type PollOptionSourceOption = boolean | AnyPollOptionSource\n\nexport type PollOptionSourcesConfig = Record<string, PollOptionSourceOption>\n\n/**\n * Resolve the active poll option-source registry from a consumer map. Unlike the consent and\n * action registries there are no built-in defaults: the registry starts empty and only host\n * definitions populate it, so `true`/`false` entries are accepted but change nothing.\n */\nexport const resolvePollOptionSources = (\n\tconfig: PollOptionSourcesConfig = {}\n): PollOptionSourceRegistry => {\n\tconst registry: PollOptionSourceRegistry = new Map()\n\tfor (const [type, option] of Object.entries(config)) {\n\t\tif (option === false) {\n\t\t\tregistry.delete(type)\n\t\t} else if (option === true) {\n\t\t\t// keep the default; no-op because no built-in poll option sources exist\n\t\t} else {\n\t\t\tregistry.set(type, option)\n\t\t}\n\t}\n\treturn registry\n}\n"],"mappings":";;;;;;AAcA,MAAa,4BACZ,SAAkC,CAAC,MACL;CAC9B,MAAM,2BAAqC,IAAI,IAAI;CACnD,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,MAAM,GACjD,IAAI,WAAW,OACd,SAAS,OAAO,IAAI;MACd,IAAI,WAAW,MAAM,CAE5B,OACC,SAAS,IAAI,MAAM,MAAM;CAG3B,OAAO;AACR"}
1
+ {"version":3,"file":"registry.js","names":[],"sources":["../../src/poll/registry.ts"],"sourcesContent":["import { applyRegistryConfig, type RegistryConfig } from '../plugin/applyRegistryConfig'\nimport type { AnyPollOptionSource } from './definePollOptionSource'\n\nexport type PollOptionSourceRegistry = Map<string, AnyPollOptionSource>\n\n/** `false` removes a source, `true` keeps it (no-op: there are no built-ins), a definition adds or replaces one. */\nexport type PollOptionSourceOption = boolean | AnyPollOptionSource\n\nexport type PollOptionSourcesConfig = RegistryConfig<AnyPollOptionSource>\n\n/**\n * Resolve the active poll option-source registry from a consumer map. Unlike the consent and\n * action registries there are no built-in defaults: the registry starts empty and only host\n * definitions populate it, so `true`/`false` entries are accepted but change nothing.\n */\nexport const resolvePollOptionSources = (\n\tconfig: PollOptionSourcesConfig = {}\n): PollOptionSourceRegistry => applyRegistryConfig(new Map<string, AnyPollOptionSource>(), config)\n"],"mappings":";;;;;;;AAeA,MAAa,4BACZ,SAAkC,CAAC,MACL,oCAAoB,IAAI,IAAiC,GAAG,MAAM"}
@@ -22,7 +22,6 @@ const resolvePollOptionsRequest = async (args) => {
22
22
  collection: FORMS_SLUG,
23
23
  id: formId,
24
24
  depth: 0,
25
- overrideAccess: true,
26
25
  req
27
26
  }).catch(() => null);
28
27
  if (!form) return {
@@ -1 +1 @@
1
- {"version":3,"file":"resolvePollOptionsRequest.js","names":[],"sources":["../../src/poll/resolvePollOptionsRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { PollOption } from './definePollOptionSource'\nimport { resolveEffectivePollOptions } from './effectivePollOptions'\n\nexport type ResolvePollOptionsRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq?: PayloadRequest\n}\n\nexport type ResolvePollOptionsRequestResult = {\n\tstatus: number\n\tbody: { options: PollOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/poll-options` request backing the admin winner select:\n * authenticated callers get the poll's effective options (source-resolved or authored, exactly the\n * set outcome validation accepts), anonymous callers are always refused. Statuses mirror the\n * results endpoint: 400 missing id, 403 unauthenticated, 404 unknown form, 503 when the option\n * source fails (fail closed).\n */\nexport const resolvePollOptionsRequest = async (\n\targs: ResolvePollOptionsRequestArgs\n): Promise<ResolvePollOptionsRequestResult> => {\n\tconst { payload, formId, isAuthed, req } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\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) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolveEffectivePollOptions({ payload, req, form })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'Poll options unavailable' }] } }\n\t}\n}\n"],"mappings":";;;;;;;;;;AAyBA,MAAa,4BAA4B,OACxC,SAC8C;CAC9C,MAAM,EAAE,SAAS,QAAQ,UAAU,QAAQ;CAC3C,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAE1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,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,4BAA4B;IAAE;IAAS;IAAK;GAAK,CAAC,EAClC;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,2BAA2B,CAAC,EAAE;EAAE;CACnF;AACD"}
1
+ {"version":3,"file":"resolvePollOptionsRequest.js","names":[],"sources":["../../src/poll/resolvePollOptionsRequest.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORMS_SLUG } from '../collections/forms'\nimport type { PollOption } from './definePollOptionSource'\nimport { resolveEffectivePollOptions } from './effectivePollOptions'\n\nexport type ResolvePollOptionsRequestArgs = {\n\tpayload: Payload\n\tformId: number | string | undefined\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq?: PayloadRequest\n}\n\nexport type ResolvePollOptionsRequestResult = {\n\tstatus: number\n\tbody: { options: PollOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/poll-options` request backing the admin winner select:\n * authenticated callers get the poll's effective options (source-resolved or authored, exactly the\n * set outcome validation accepts), anonymous callers are always refused. Statuses mirror the\n * results endpoint: 400 missing id, 403 unauthenticated, 404 unknown form, 503 when the option\n * source fails (fail closed).\n */\nexport const resolvePollOptionsRequest = async (\n\targs: ResolvePollOptionsRequestArgs\n): Promise<ResolvePollOptionsRequestResult> => {\n\tconst { payload, formId, isAuthed, req } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\tif (formId == null) {\n\t\treturn { status: 400, body: { errors: [{ message: 'Missing form id' }] } }\n\t}\n\t// Load under the caller's own read access (no overrideAccess): a tenant-scoped `forms.access.read`\n\t// then refuses a cross-tenant form id (null -> 404), so an authed user cannot read another tenant's\n\t// poll options.\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, req })\n\t\t.catch(() => null)\n\tif (!form) {\n\t\treturn { status: 404, body: { errors: [{ message: 'Not found' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolveEffectivePollOptions({ payload, req, form })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'Poll options unavailable' }] } }\n\t}\n}\n"],"mappings":";;;;;;;;;;AAyBA,MAAa,4BAA4B,OACxC,SAC8C;CAC9C,MAAM,EAAE,SAAS,QAAQ,UAAU,QAAQ;CAC3C,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI,UAAU,MACb,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,kBAAkB,CAAC,EAAE;CAAE;CAK1E,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG;CAAI,CAAC,EAC9D,YAAY,IAAI;CAClB,IAAI,CAAC,MACJ,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,4BAA4B;IAAE;IAAS;IAAK;GAAK,CAAC,EAClC;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,2BAA2B,CAAC,EAAE;EAAE;CACnF;AACD"}
@@ -1,18 +1,13 @@
1
1
  import { isNamedField } from "../fields/fieldKey.js";
2
+ import { isTruthyString } from "../submissions/coerceBoolean.js";
2
3
  //#region src/prefill/valuesFromSearchParams.ts
3
- const TRUTHY = new Set([
4
- "true",
5
- "1",
6
- "on",
7
- "yes"
8
- ]);
9
4
  const coerce = (kind, raw, all) => {
10
5
  switch (kind) {
11
6
  case "number": {
12
7
  const n = Number(raw);
13
8
  return Number.isNaN(n) ? void 0 : n;
14
9
  }
15
- case "boolean": return TRUTHY.has(raw.toLowerCase());
10
+ case "boolean": return isTruthyString(raw);
16
11
  case "text[]": return all;
17
12
  default: return raw;
18
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"valuesFromSearchParams.js","names":[],"sources":["../../src/prefill/valuesFromSearchParams.ts"],"sourcesContent":["import { isNamedField } from '../fields/fieldKey'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport type { FormFieldValueKind } from '../fields/types'\nimport type { FormFieldInstance } from '../submissions/types'\n\nexport type PrefillOptions = {\n\t/** Map a query-param name to a field name. Unmapped params use their own name. */\n\tmap?: Record<string, string>\n\t/** If set, only these field names may be prefilled. */\n\tallow?: string[]\n\t/** These field names may never be prefilled. */\n\tdeny?: string[]\n}\n\nconst TRUTHY = new Set(['true', '1', 'on', 'yes'])\n\nconst coerce = (kind: FormFieldValueKind, raw: string, all: string[]): unknown => {\n\tswitch (kind) {\n\t\tcase 'number': {\n\t\t\tconst n = Number(raw)\n\t\t\treturn Number.isNaN(n) ? undefined : n\n\t\t}\n\t\tcase 'boolean':\n\t\t\treturn TRUTHY.has(raw.toLowerCase())\n\t\tcase 'text[]':\n\t\t\treturn all\n\t\tdefault:\n\t\t\treturn raw\n\t}\n}\n\n/**\n * Map URL/query params to typed initial values for KNOWN fields only, coerced by each field's value\n * kind. Unknown, denied, or un-allowed params are ignored; invalid numbers are dropped. Prefilled\n * values are never trusted -- they still validate on submit. Pass the result to `<Form initialValues>`.\n */\n// biome-ignore lint/complexity/useMaxParams: four positional args are the minimal surface (params, fields, registry, options)\nexport const valuesFromSearchParams = (\n\tparams: URLSearchParams | Record<string, string>,\n\tfields: FormFieldInstance[],\n\tregistry: FieldTypeRegistry,\n\toptions: PrefillOptions = {}\n): Record<string, unknown> => {\n\tconst isSearch = params instanceof URLSearchParams\n\tconst paramKeys = isSearch ? [...new Set(params.keys())] : Object.keys(params)\n\tconst byName = new Map(fields.filter(isNamedField).map((field) => [field.name, field]))\n\tconst result: Record<string, unknown> = {}\n\n\tfor (const param of paramKeys) {\n\t\tconst fieldName = options.map?.[param] ?? param\n\n\t\tif (options.allow && !options.allow.includes(fieldName)) {\n\t\t\tcontinue\n\t\t}\n\t\tif (options.deny?.includes(fieldName)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tconst field = byName.get(fieldName)\n\t\tconst definition = field && registry.get(field.blockType)\n\t\t// 'none'-kind (display-only) fields carry no value, so a param under their name is ignored.\n\t\tif (!field || !definition || definition.value === 'none') {\n\t\t\tcontinue\n\t\t}\n\n\t\tconst all = isSearch ? params.getAll(param) : [params[param] as string]\n\t\tconst value = coerce(definition.value, all[0] ?? '', all)\n\t\tif (value !== undefined) {\n\t\t\tresult[fieldName] = value\n\t\t}\n\t}\n\n\treturn result\n}\n"],"mappings":";;AAcA,MAAM,SAAS,IAAI,IAAI;CAAC;CAAQ;CAAK;CAAM;AAAK,CAAC;AAEjD,MAAM,UAAU,MAA0B,KAAa,QAA2B;CACjF,QAAQ,MAAR;EACC,KAAK,UAAU;GACd,MAAM,IAAI,OAAO,GAAG;GACpB,OAAO,OAAO,MAAM,CAAC,IAAI,KAAA,IAAY;EACtC;EACA,KAAK,WACJ,OAAO,OAAO,IAAI,IAAI,YAAY,CAAC;EACpC,KAAK,UACJ,OAAO;EACR,SACC,OAAO;CACT;AACD;;;;;;AAQA,MAAa,0BACZ,QACA,QACA,UACA,UAA0B,CAAC,MACE;CAC7B,MAAM,WAAW,kBAAkB;CACnC,MAAM,YAAY,WAAW,CAAC,GAAG,IAAI,IAAI,OAAO,KAAK,CAAC,CAAC,IAAI,OAAO,KAAK,MAAM;CAC7E,MAAM,SAAS,IAAI,IAAI,OAAO,OAAO,YAAY,EAAE,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACtF,MAAM,SAAkC,CAAC;CAEzC,KAAK,MAAM,SAAS,WAAW;EAC9B,MAAM,YAAY,QAAQ,MAAM,UAAU;EAE1C,IAAI,QAAQ,SAAS,CAAC,QAAQ,MAAM,SAAS,SAAS,GACrD;EAED,IAAI,QAAQ,MAAM,SAAS,SAAS,GACnC;EAGD,MAAM,QAAQ,OAAO,IAAI,SAAS;EAClC,MAAM,aAAa,SAAS,SAAS,IAAI,MAAM,SAAS;EAExD,IAAI,CAAC,SAAS,CAAC,cAAc,WAAW,UAAU,QACjD;EAGD,MAAM,MAAM,WAAW,OAAO,OAAO,KAAK,IAAI,CAAC,OAAO,MAAgB;EACtE,MAAM,QAAQ,OAAO,WAAW,OAAO,IAAI,MAAM,IAAI,GAAG;EACxD,IAAI,UAAU,KAAA,GACb,OAAO,aAAa;CAEtB;CAEA,OAAO;AACR"}
1
+ {"version":3,"file":"valuesFromSearchParams.js","names":[],"sources":["../../src/prefill/valuesFromSearchParams.ts"],"sourcesContent":["import { isNamedField } from '../fields/fieldKey'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport type { FormFieldValueKind } from '../fields/types'\nimport { isTruthyString } from '../submissions/coerceBoolean'\nimport type { FormFieldInstance } from '../submissions/types'\n\nexport type PrefillOptions = {\n\t/** Map a query-param name to a field name. Unmapped params use their own name. */\n\tmap?: Record<string, string>\n\t/** If set, only these field names may be prefilled. */\n\tallow?: string[]\n\t/** These field names may never be prefilled. */\n\tdeny?: string[]\n}\n\nconst coerce = (kind: FormFieldValueKind, raw: string, all: string[]): unknown => {\n\tswitch (kind) {\n\t\tcase 'number': {\n\t\t\tconst n = Number(raw)\n\t\t\treturn Number.isNaN(n) ? undefined : n\n\t\t}\n\t\tcase 'boolean':\n\t\t\treturn isTruthyString(raw)\n\t\tcase 'text[]':\n\t\t\treturn all\n\t\tdefault:\n\t\t\treturn raw\n\t}\n}\n\n/**\n * Map URL/query params to typed initial values for KNOWN fields only, coerced by each field's value\n * kind. Unknown, denied, or un-allowed params are ignored; invalid numbers are dropped. Prefilled\n * values are never trusted -- they still validate on submit. Pass the result to `<Form initialValues>`.\n */\n// biome-ignore lint/complexity/useMaxParams: four positional args are the minimal surface (params, fields, registry, options)\nexport const valuesFromSearchParams = (\n\tparams: URLSearchParams | Record<string, string>,\n\tfields: FormFieldInstance[],\n\tregistry: FieldTypeRegistry,\n\toptions: PrefillOptions = {}\n): Record<string, unknown> => {\n\tconst isSearch = params instanceof URLSearchParams\n\tconst paramKeys = isSearch ? [...new Set(params.keys())] : Object.keys(params)\n\tconst byName = new Map(fields.filter(isNamedField).map((field) => [field.name, field]))\n\tconst result: Record<string, unknown> = {}\n\n\tfor (const param of paramKeys) {\n\t\tconst fieldName = options.map?.[param] ?? param\n\n\t\tif (options.allow && !options.allow.includes(fieldName)) {\n\t\t\tcontinue\n\t\t}\n\t\tif (options.deny?.includes(fieldName)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tconst field = byName.get(fieldName)\n\t\tconst definition = field && registry.get(field.blockType)\n\t\t// 'none'-kind (display-only) fields carry no value, so a param under their name is ignored.\n\t\tif (!field || !definition || definition.value === 'none') {\n\t\t\tcontinue\n\t\t}\n\n\t\tconst all = isSearch ? params.getAll(param) : [params[param] as string]\n\t\tconst value = coerce(definition.value, all[0] ?? '', all)\n\t\tif (value !== undefined) {\n\t\t\tresult[fieldName] = value\n\t\t}\n\t}\n\n\treturn result\n}\n"],"mappings":";;;AAeA,MAAM,UAAU,MAA0B,KAAa,QAA2B;CACjF,QAAQ,MAAR;EACC,KAAK,UAAU;GACd,MAAM,IAAI,OAAO,GAAG;GACpB,OAAO,OAAO,MAAM,CAAC,IAAI,KAAA,IAAY;EACtC;EACA,KAAK,WACJ,OAAO,eAAe,GAAG;EAC1B,KAAK,UACJ,OAAO;EACR,SACC,OAAO;CACT;AACD;;;;;;AAQA,MAAa,0BACZ,QACA,QACA,UACA,UAA0B,CAAC,MACE;CAC7B,MAAM,WAAW,kBAAkB;CACnC,MAAM,YAAY,WAAW,CAAC,GAAG,IAAI,IAAI,OAAO,KAAK,CAAC,CAAC,IAAI,OAAO,KAAK,MAAM;CAC7E,MAAM,SAAS,IAAI,IAAI,OAAO,OAAO,YAAY,EAAE,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACtF,MAAM,SAAkC,CAAC;CAEzC,KAAK,MAAM,SAAS,WAAW;EAC9B,MAAM,YAAY,QAAQ,MAAM,UAAU;EAE1C,IAAI,QAAQ,SAAS,CAAC,QAAQ,MAAM,SAAS,SAAS,GACrD;EAED,IAAI,QAAQ,MAAM,SAAS,SAAS,GACnC;EAGD,MAAM,QAAQ,OAAO,IAAI,SAAS;EAClC,MAAM,aAAa,SAAS,SAAS,IAAI,MAAM,SAAS;EAExD,IAAI,CAAC,SAAS,CAAC,cAAc,WAAW,UAAU,QACjD;EAGD,MAAM,MAAM,WAAW,OAAO,OAAO,KAAK,IAAI,CAAC,OAAO,MAAgB;EACtE,MAAM,QAAQ,OAAO,WAAW,OAAO,IAAI,MAAM,IAAI,GAAG;EACxD,IAAI,UAAU,KAAA,GACb,OAAO,aAAa;CAEtB;CAEA,OAAO;AACR"}
@@ -43,7 +43,8 @@ type FormProps = {
43
43
  name?: string;
44
44
  }; /** A token from your captcha widget; verified server-side when a captcha provider is configured. */
45
45
  captchaToken?: string; /** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */
46
- children?: ReactNode; /** Additional CSS class names applied to the root `<form>` element (and the success node). */
46
+ children?: ReactNode; /** Chrome rendered inside the form, above the fields, in default mode (e.g. `<FormSteps />`). */
47
+ header?: ReactNode; /** Additional CSS class names applied to the root `<form>` element (and the success node). */
47
48
  className?: string; /** Replace the default submit button entirely. Receives the resolved label and submitting state. */
48
49
  renderSubmit?: (props: SubmitButtonRenderProps) => ReactNode; /** Replace the default "Next" button in multi-step forms. */
49
50
  renderNext?: (props: NextButtonRenderProps) => ReactNode; /** Replace the default "Back" button in multi-step forms. */
@@ -79,6 +80,7 @@ declare const Form: ({
79
80
  honeypot,
80
81
  captchaToken,
81
82
  children,
83
+ header,
82
84
  className,
83
85
  renderSubmit,
84
86
  renderNext,
@@ -1,9 +1,9 @@
1
1
  "use client";
2
- import { interpolate } from "../recall/interpolate.js";
3
2
  import { keys } from "../translations/keys.js";
3
+ import { interpolate } from "../recall/interpolate.js";
4
4
  import { serializeBody } from "../actions/body/serializeBody.js";
5
5
  import { noopEventSink } from "../events/noopSink.js";
6
- import { CAPTCHA_TOKEN_KEY } from "../spam/constants.js";
6
+ import { CAPTCHA_TOKEN_KEY, HONEYPOT_VALUE_KEY } from "../spam/constants.js";
7
7
  import { fieldKey, isNamedField } from "../fields/fieldKey.js";
8
8
  import { calcExpressionOf, computeCalcFields } from "../calc/computeCalcFields.js";
9
9
  import { evaluateCondition } from "../conditions/evaluate.js";
@@ -16,82 +16,24 @@ import { makeTranslate } from "../translations/makeTranslate.js";
16
16
  import { emitFormEvent } from "./events.js";
17
17
  import { FormContext } from "./FormContext.js";
18
18
  import { FormControls } from "./FormControls.js";
19
- import { FormLayout, widthProps } from "./FormLayout.js";
19
+ import { FormFields } from "./FormFields.js";
20
20
  import { Honeypot } from "./Honeypot.js";
21
21
  import { defaultPresentations } from "./presentation/presentations.js";
22
22
  import { resolvePresentations } from "./presentation/registry.js";
23
- import { applyRecall } from "./recall.js";
24
23
  import { resolveRenderers } from "./registry.js";
25
- import { useField } from "./useField.js";
26
24
  import { defaultRenderers } from "./renderers/index.js";
27
25
  import { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from "./resolveForm.js";
28
26
  import { formReducer, initialFormState, seedFieldValues } from "./state.js";
29
27
  import { submitForm } from "./submitForm.js";
30
28
  import { validateFieldValue } from "./validateField.js";
31
29
  import { jsx, jsxs } from "react/jsx-runtime";
32
- import { createElement, useCallback, useEffect, useId, useMemo, useReducer, useRef, useState } from "react";
30
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
33
31
  //#region src/react/Form.tsx
34
32
  const isEmpty = (value) => value == null || value === "" || Array.isArray(value) && value.length === 0;
35
33
  /** A stored button label counts only when it is a non-empty string; anything else falls through. */
36
34
  const storedLabel = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
37
- const FIELD_WIDTHS = new Set([
38
- "full",
39
- "half",
40
- "third",
41
- "twoThirds"
42
- ]);
43
- const FieldHost = ({ field, renderer, locale, t }) => {
44
- const id = useId();
45
- const { value, errors, setValue, onBlur } = useField(field.name);
46
- return createElement(renderer, {
47
- field,
48
- id,
49
- name: field.name,
50
- value,
51
- onChange: setValue,
52
- onBlur,
53
- errors,
54
- required: Boolean(field.required),
55
- locale,
56
- t
57
- });
58
- };
59
- /** Hosts a derived (calc) field: read-only, value supplied from `effectiveValues`, never bound via `useField`. */
60
- const CalcFieldHost = ({ field, renderer, value, locale, t }) => {
61
- return createElement(renderer, {
62
- field,
63
- id: useId(),
64
- name: field.name,
65
- value,
66
- onChange: () => {},
67
- onBlur: () => {},
68
- errors: [],
69
- required: false,
70
- disabled: true,
71
- locale,
72
- t
73
- });
74
- };
75
- /**
76
- * Hosts a nameless (bare) display block, e.g. a message: no `useField` binding at all. The
77
- * renderer reads only the instance and the form context; `name` is the row key for consistency.
78
- */
79
- const StaticFieldHost = ({ field, renderer, locale, t }) => {
80
- return createElement(renderer, {
81
- field,
82
- id: useId(),
83
- name: fieldKey(field),
84
- value: void 0,
85
- onChange: () => {},
86
- onBlur: () => {},
87
- errors: [],
88
- required: false,
89
- locale,
90
- t
91
- });
92
- };
93
35
  /** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */
94
- const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, events, t, locale = "en", layout, submitLabel, nextLabel, prevLabel, closeLabel = "Close", successMessage = "Thank you.", presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, children, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName }) => {
36
+ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, events, t, locale = "en", layout, submitLabel, nextLabel, prevLabel, closeLabel, successMessage, presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, children, header, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName }) => {
95
37
  const honeypotName = honeypot === false ? null : honeypot?.name ?? "website";
96
38
  const honeypotRef = useRef(null);
97
39
  const registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes]);
@@ -101,6 +43,8 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
101
43
  const activePresentation = typeof presentation === "object" ? presentation : presentationRegistry.get(presentation ?? "page") ?? presentationRegistry.get("page") ?? defaultPresentationDescriptors.page;
102
44
  const fieldsByName = useMemo(() => new Map(form.fields.filter(isNamedField).map((field) => [field.name, field])), [form.fields]);
103
45
  const translate = useMemo(() => t ?? makeTranslate(en), [t]);
46
+ const resolvedCloseLabel = closeLabel ?? translate(keys.formClose);
47
+ const resolvedSuccessMessage = successMessage ?? translate(keys.formSuccess);
104
48
  const docButtons = form.buttons;
105
49
  const labels = useMemo(() => ({
106
50
  prev: prevLabel ?? storedLabel(docButtons?.prevLabel) ?? translate(keys.formBack),
@@ -141,6 +85,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
141
85
  const startedRef = useRef(false);
142
86
  const submittedRef = useRef(false);
143
87
  const submittingRef = useRef(false);
88
+ const advancingRef = useRef(false);
144
89
  const flowRef = useRef(flow);
145
90
  flowRef.current = flow;
146
91
  const dispatch = useCallback((action) => {
@@ -211,46 +156,86 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
211
156
  value: recall(field.name)
212
157
  }));
213
158
  const stepVisible = (flow && currentStepId ? stepFieldNames(flow, currentStepId) : []).map((key) => visible.find((field) => fieldKey(field) === key)).filter((field) => Boolean(field));
159
+ const validateRepeaterSubFields = async (repeaters) => {
160
+ const errors = {};
161
+ for (const field of repeaters.filter((f) => f.blockType === "repeater" && isNamedField(f))) {
162
+ const rows = Array.isArray(effectiveValues[field.name]) ? effectiveValues[field.name] : [];
163
+ const subFields = (Array.isArray(field.subFields) ? field.subFields : []).filter(isNamedField);
164
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
165
+ const row = rows[rowIndex] ?? {};
166
+ for (const subField of subFields) {
167
+ if (!evaluateCondition(subField.visibleWhen, row)) continue;
168
+ if (!evaluateCondition(subField.validateWhen, row)) continue;
169
+ const subResult = await validateFieldValue({
170
+ field: subField,
171
+ value: row[subField.name],
172
+ registry,
173
+ ruleRegistry,
174
+ answers: row,
175
+ locale,
176
+ t: translate
177
+ });
178
+ const compositeKey = `${field.name}[${rowIndex}].${subField.name}`;
179
+ if (subResult.errors.length > 0) errors[compositeKey] = subResult.errors;
180
+ }
181
+ }
182
+ }
183
+ return errors;
184
+ };
214
185
  const goNext = async () => {
215
- if (!flow || !currentStepId) return;
216
- const results = await Promise.all(stepVisible.filter((field) => !calcExpressionOf(field)).filter((field) => registry.get(field.blockType)?.value !== "none").filter(isNamedField).filter((field) => evaluateCondition(field.validateWhen, effectiveValues)).map(async (field) => ({
217
- field,
218
- ...await validateFieldValue({
186
+ if (!flow || !currentStepId || advancingRef.current) return;
187
+ advancingRef.current = true;
188
+ try {
189
+ const results = await Promise.all(stepVisible.filter((field) => !calcExpressionOf(field)).filter((field) => registry.get(field.blockType)?.value !== "none").filter(isNamedField).filter((field) => evaluateCondition(field.validateWhen, effectiveValues)).map(async (field) => ({
219
190
  field,
220
- value: effectiveValues[field.name],
221
- registry,
222
- ruleRegistry,
223
- answers: effectiveValues,
224
- locale,
225
- t: translate
226
- })
227
- })));
228
- let hasError = false;
229
- for (const result of results) {
230
- rawDispatch({
231
- type: "TOUCH",
232
- name: result.field.name
191
+ ...await validateFieldValue({
192
+ field,
193
+ value: effectiveValues[field.name],
194
+ registry,
195
+ ruleRegistry,
196
+ answers: effectiveValues,
197
+ locale,
198
+ t: translate
199
+ })
200
+ })));
201
+ let hasError = false;
202
+ for (const result of results) {
203
+ rawDispatch({
204
+ type: "TOUCH",
205
+ name: result.field.name
206
+ });
207
+ rawDispatch({
208
+ type: "SET_FIELD_ISSUES",
209
+ name: result.field.name,
210
+ errors: result.errors
211
+ });
212
+ if (result.errors.length > 0) hasError = true;
213
+ }
214
+ const repeaterErrors = await validateRepeaterSubFields(stepVisible);
215
+ for (const [compositeKey, errs] of Object.entries(repeaterErrors)) {
216
+ rawDispatch({
217
+ type: "SET_FIELD_ISSUES",
218
+ name: compositeKey,
219
+ errors: errs
220
+ });
221
+ hasError = true;
222
+ }
223
+ if (hasError) return;
224
+ const next = resolveNextStepId(flow, currentStepId, effectiveValues);
225
+ if (!next) return;
226
+ emitFormEvent(sinkRef.current, formIdRef.current, {
227
+ type: "step.completed",
228
+ stepId: currentStepId
233
229
  });
234
- rawDispatch({
235
- type: "SET_FIELD_ISSUES",
236
- name: result.field.name,
237
- errors: result.errors
230
+ setHistory((prev) => [...prev, currentStepId]);
231
+ setCurrentStepId(next);
232
+ emitFormEvent(sinkRef.current, formIdRef.current, {
233
+ type: "step.viewed",
234
+ stepId: next
238
235
  });
239
- if (result.errors.length > 0) hasError = true;
236
+ } finally {
237
+ advancingRef.current = false;
240
238
  }
241
- if (hasError) return;
242
- const next = resolveNextStepId(flow, currentStepId, effectiveValues);
243
- if (!next) return;
244
- emitFormEvent(sinkRef.current, formIdRef.current, {
245
- type: "step.completed",
246
- stepId: currentStepId
247
- });
248
- setHistory((prev) => [...prev, currentStepId]);
249
- setCurrentStepId(next);
250
- emitFormEvent(sinkRef.current, formIdRef.current, {
251
- type: "step.viewed",
252
- stepId: next
253
- });
254
239
  };
255
240
  const goBack = () => {
256
241
  const prev = history[history.length - 1];
@@ -306,28 +291,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
306
291
  message: firstError
307
292
  });
308
293
  }
309
- for (const field of visible.filter((f) => f.blockType === "repeater" && isNamedField(f))) {
310
- const rows = Array.isArray(effectiveValues[field.name]) ? effectiveValues[field.name] : [];
311
- const subFields = (Array.isArray(field.subFields) ? field.subFields : []).filter(isNamedField);
312
- for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
313
- const row = rows[rowIndex] ?? {};
314
- for (const subField of subFields) {
315
- if (!evaluateCondition(subField.visibleWhen, row)) continue;
316
- if (!evaluateCondition(subField.validateWhen, row)) continue;
317
- const subResult = await validateFieldValue({
318
- field: subField,
319
- value: row[subField.name],
320
- registry,
321
- ruleRegistry,
322
- answers: row,
323
- locale,
324
- t: translate
325
- });
326
- const compositeKey = `${field.name}[${rowIndex}].${subField.name}`;
327
- if (subResult.errors.length > 0) errors[compositeKey] = subResult.errors;
328
- }
329
- }
330
- }
294
+ Object.assign(errors, await validateRepeaterSubFields(visible));
331
295
  rawDispatch({
332
296
  type: "SET_ALL_ISSUES",
333
297
  errors
@@ -341,7 +305,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
341
305
  if (honeypotName) {
342
306
  const decoy = honeypotRef.current?.value ?? "";
343
307
  if (decoy !== "") values.push({
344
- field: honeypotName,
308
+ field: HONEYPOT_VALUE_KEY,
345
309
  value: decoy
346
310
  });
347
311
  }
@@ -374,7 +338,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
374
338
  type: "SET_ALL_ISSUES",
375
339
  errors: result.fieldErrors
376
340
  });
377
- const message = result.message ?? "Submission failed";
341
+ const message = result.message ?? translate(keys.formSubmitFailed);
378
342
  rawDispatch({
379
343
  type: "SUBMIT_ERROR",
380
344
  message
@@ -410,7 +374,9 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
410
374
  rendererRegistry,
411
375
  labels,
412
376
  t: translate,
413
- effectiveValues
377
+ effectiveValues,
378
+ recall,
379
+ renderedFields: (flow ? stepVisible : visible).filter((field) => field.hidden !== true && field.calcDisplay !== false)
414
380
  };
415
381
  const PresentationWrapper = activePresentation.Wrapper;
416
382
  const wrap = (content) => PresentationWrapper ? /* @__PURE__ */ jsx(PresentationWrapper, {
@@ -418,7 +384,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
418
384
  open: true,
419
385
  onClose: handleClose,
420
386
  title,
421
- closeLabel,
387
+ closeLabel: resolvedCloseLabel,
422
388
  children: content
423
389
  }) : content;
424
390
  if (children !== void 0) return /* @__PURE__ */ jsx(FormContext.Provider, {
@@ -454,11 +420,10 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
454
420
  className: cn("fb-form__success", className),
455
421
  "data-fb-presentation": activePresentation.name,
456
422
  "data-fb-density": activePresentation.density,
457
- children: interpolate(successMessage, recall)
423
+ children: interpolate(resolvedSuccessMessage, recall)
458
424
  }))
459
425
  });
460
426
  }
461
- const rendered = (flow ? stepVisible : visible).filter((field) => field.hidden !== true && field.calcDisplay !== false);
462
427
  return /* @__PURE__ */ jsx(FormContext.Provider, {
463
428
  value: contextValue,
464
429
  children: wrap(/* @__PURE__ */ jsxs("form", {
@@ -472,35 +437,8 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
472
437
  name: honeypotName,
473
438
  inputRef: honeypotRef
474
439
  }) : null,
475
- /* @__PURE__ */ jsx(FormLayout, {
476
- enabled: layout !== false,
477
- children: rendered.map((field) => {
478
- const renderer = rendererRegistry.get(field.blockType);
479
- if (!renderer) return null;
480
- const width = typeof field.width === "string" && FIELD_WIDTHS.has(field.width) ? field.width : void 0;
481
- const recalledField = applyRecall(field, recall);
482
- return /* @__PURE__ */ jsx("div", {
483
- ...widthProps(width),
484
- children: !isNamedField(recalledField) ? /* @__PURE__ */ jsx(StaticFieldHost, {
485
- field: recalledField,
486
- renderer,
487
- locale,
488
- t: translate
489
- }) : calcExpressionOf(recalledField) ? /* @__PURE__ */ jsx(CalcFieldHost, {
490
- field: recalledField,
491
- renderer,
492
- value: effectiveValues[recalledField.name],
493
- locale,
494
- t: translate
495
- }) : /* @__PURE__ */ jsx(FieldHost, {
496
- field: recalledField,
497
- renderer,
498
- locale,
499
- t: translate
500
- })
501
- }, fieldKey(field));
502
- })
503
- }),
440
+ header,
441
+ /* @__PURE__ */ jsx(FormFields, { layout }),
504
442
  state.submitError ? /* @__PURE__ */ jsx("p", {
505
443
  role: "alert",
506
444
  className: "fb-form__submit-error",