@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,3 +1,4 @@
1
+ import { applyRegistryConfig } from "../plugin/applyRegistryConfig.js";
1
2
  import { customStateOf, stashCustomState } from "../plugin/customState.js";
2
3
  //#region src/fields/registry.ts
3
4
  const buildRegistry = (definitions) => {
@@ -20,12 +21,7 @@ const assertBareContract = (definition) => {
20
21
  * definition must honor the bare contract; a violation throws at plugin boot.
21
22
  */
22
23
  const resolveFieldTypes = (defaults, config = {}) => {
23
- const registry = buildRegistry(defaults);
24
- for (const [type, option] of Object.entries(config)) if (option === false) registry.delete(type);
25
- else if (option === true) {} else registry.set(type, {
26
- ...option,
27
- type
28
- });
24
+ const registry = applyRegistryConfig(buildRegistry(defaults), config);
29
25
  for (const definition of registry.values()) assertBareContract(definition);
30
26
  return registry;
31
27
  };
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","names":[],"sources":["../../src/fields/registry.ts"],"sourcesContent":["import type { Payload } from 'payload'\nimport { customStateOf, stashCustomState } from '../plugin/customState'\nimport type { AnyFormFieldDefinition } from './types'\n\nexport type FieldTypeRegistry = Map<string, AnyFormFieldDefinition>\n\n/** Per-type opt-in: `false` removes a built-in, `true` keeps it, an object adds a new type or replaces one. */\nexport type FieldTypeOption = boolean | AnyFormFieldDefinition\n\nexport type FieldTypesConfig = Record<string, FieldTypeOption>\n\nexport const buildRegistry = (definitions: AnyFormFieldDefinition[]): FieldTypeRegistry => {\n\tconst registry: FieldTypeRegistry = new Map()\n\tfor (const definition of definitions) {\n\t\tregistry.set(definition.type, definition)\n\t}\n\treturn registry\n}\n\n/**\n * A bare definition without `value: 'none'` would no-op silently: its instances are nameless,\n * so `validate`/`schema` never run and no value is ever stored. Failing the boot makes the\n * misconfiguration impossible to ship.\n */\nconst assertBareContract = (definition: AnyFormFieldDefinition): void => {\n\tif (definition.bare === true && definition.value !== 'none') {\n\t\tthrow new Error(\n\t\t\t`form-builder: bare field types must be display-only (value: 'none'); '${definition.type}' declares value '${definition.value}'`\n\t\t)\n\t}\n}\n\n/**\n * Resolve the active field-type registry from the built-in defaults and the plugin `fields` option.\n * `false` removes a type, `true` keeps the default, an object adds a new type or replaces an existing\n * one (its `type` is forced to the config key so an override cannot rename the slot). Every resolved\n * definition must honor the bare contract; a violation throws at plugin boot.\n */\nexport const resolveFieldTypes = (\n\tdefaults: AnyFormFieldDefinition[],\n\tconfig: FieldTypesConfig = {}\n): FieldTypeRegistry => {\n\tconst registry = buildRegistry(defaults)\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; a no-op when no default exists for this key\n\t\t} else {\n\t\t\tregistry.set(type, { ...option, type })\n\t\t}\n\t}\n\tfor (const definition of registry.values()) {\n\t\tassertBareContract(definition)\n\t}\n\treturn registry\n}\n\ntype FieldTypesCustomState = { fieldTypes: FieldTypeRegistry }\n\n/**\n * Park the resolved field-type registry on the config so `resolveEffectivePollOptions` can look up\n * a poll results field's definition (and its `resolveOptions`) at request time via `payload.config`,\n * the same way poll option sources and consent sources are stashed for root-level helpers.\n */\nexport const stashFieldTypes = (\n\tcustom: Record<string, unknown> | undefined,\n\tregistry: FieldTypeRegistry\n): Record<string, unknown> =>\n\tstashCustomState<FieldTypesCustomState>(custom, { fieldTypes: registry })\n\n/** The field-type registry stashed at boot, or an empty registry when the plugin never booted. */\nexport const fieldTypesOf = (payload: Payload): FieldTypeRegistry =>\n\tcustomStateOf<FieldTypesCustomState>(payload).fieldTypes ?? new Map()\n"],"mappings":";;AAWA,MAAa,iBAAiB,gBAA6D;CAC1F,MAAM,2BAA8B,IAAI,IAAI;CAC5C,KAAK,MAAM,cAAc,aACxB,SAAS,IAAI,WAAW,MAAM,UAAU;CAEzC,OAAO;AACR;;;;;;AAOA,MAAM,sBAAsB,eAA6C;CACxE,IAAI,WAAW,SAAS,QAAQ,WAAW,UAAU,QACpD,MAAM,IAAI,MACT,yEAAyE,WAAW,KAAK,oBAAoB,WAAW,MAAM,EAC/H;AAEF;;;;;;;AAQA,MAAa,qBACZ,UACA,SAA2B,CAAC,MACL;CACvB,MAAM,WAAW,cAAc,QAAQ;CACvC,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,MAAM,GACjD,IAAI,WAAW,OACd,SAAS,OAAO,IAAI;MACd,IAAI,WAAW,MAAM,CAE5B,OACC,SAAS,IAAI,MAAM;EAAE,GAAG;EAAQ;CAAK,CAAC;CAGxC,KAAK,MAAM,cAAc,SAAS,OAAO,GACxC,mBAAmB,UAAU;CAE9B,OAAO;AACR;;;;;;AASA,MAAa,mBACZ,QACA,aAEA,iBAAwC,QAAQ,EAAE,YAAY,SAAS,CAAC;;AAGzE,MAAa,gBAAgB,YAC5B,cAAqC,OAAO,EAAE,8BAAc,IAAI,IAAI"}
1
+ {"version":3,"file":"registry.js","names":[],"sources":["../../src/fields/registry.ts"],"sourcesContent":["import type { Payload } from 'payload'\nimport { applyRegistryConfig, type RegistryConfig } from '../plugin/applyRegistryConfig'\nimport { customStateOf, stashCustomState } from '../plugin/customState'\nimport type { AnyFormFieldDefinition } from './types'\n\nexport type FieldTypeRegistry = Map<string, AnyFormFieldDefinition>\n\n/** Per-type opt-in: `false` removes a built-in, `true` keeps it, an object adds a new type or replaces one. */\nexport type FieldTypeOption = boolean | AnyFormFieldDefinition\n\nexport type FieldTypesConfig = RegistryConfig<AnyFormFieldDefinition>\n\nexport const buildRegistry = (definitions: AnyFormFieldDefinition[]): FieldTypeRegistry => {\n\tconst registry: FieldTypeRegistry = new Map()\n\tfor (const definition of definitions) {\n\t\tregistry.set(definition.type, definition)\n\t}\n\treturn registry\n}\n\n/**\n * A bare definition without `value: 'none'` would no-op silently: its instances are nameless,\n * so `validate`/`schema` never run and no value is ever stored. Failing the boot makes the\n * misconfiguration impossible to ship.\n */\nconst assertBareContract = (definition: AnyFormFieldDefinition): void => {\n\tif (definition.bare === true && definition.value !== 'none') {\n\t\tthrow new Error(\n\t\t\t`form-builder: bare field types must be display-only (value: 'none'); '${definition.type}' declares value '${definition.value}'`\n\t\t)\n\t}\n}\n\n/**\n * Resolve the active field-type registry from the built-in defaults and the plugin `fields` option.\n * `false` removes a type, `true` keeps the default, an object adds a new type or replaces an existing\n * one (its `type` is forced to the config key so an override cannot rename the slot). Every resolved\n * definition must honor the bare contract; a violation throws at plugin boot.\n */\nexport const resolveFieldTypes = (\n\tdefaults: AnyFormFieldDefinition[],\n\tconfig: FieldTypesConfig = {}\n): FieldTypeRegistry => {\n\tconst registry = applyRegistryConfig(buildRegistry(defaults), config)\n\tfor (const definition of registry.values()) {\n\t\tassertBareContract(definition)\n\t}\n\treturn registry\n}\n\ntype FieldTypesCustomState = { fieldTypes: FieldTypeRegistry }\n\n/**\n * Park the resolved field-type registry on the config so `resolveEffectivePollOptions` can look up\n * a poll results field's definition (and its `resolveOptions`) at request time via `payload.config`,\n * the same way poll option sources and consent sources are stashed for root-level helpers.\n */\nexport const stashFieldTypes = (\n\tcustom: Record<string, unknown> | undefined,\n\tregistry: FieldTypeRegistry\n): Record<string, unknown> =>\n\tstashCustomState<FieldTypesCustomState>(custom, { fieldTypes: registry })\n\n/** The field-type registry stashed at boot, or an empty registry when the plugin never booted. */\nexport const fieldTypesOf = (payload: Payload): FieldTypeRegistry =>\n\tcustomStateOf<FieldTypesCustomState>(payload).fieldTypes ?? new Map()\n"],"mappings":";;;AAYA,MAAa,iBAAiB,gBAA6D;CAC1F,MAAM,2BAA8B,IAAI,IAAI;CAC5C,KAAK,MAAM,cAAc,aACxB,SAAS,IAAI,WAAW,MAAM,UAAU;CAEzC,OAAO;AACR;;;;;;AAOA,MAAM,sBAAsB,eAA6C;CACxE,IAAI,WAAW,SAAS,QAAQ,WAAW,UAAU,QACpD,MAAM,IAAI,MACT,yEAAyE,WAAW,KAAK,oBAAoB,WAAW,MAAM,EAC/H;AAEF;;;;;;;AAQA,MAAa,qBACZ,UACA,SAA2B,CAAC,MACL;CACvB,MAAM,WAAW,oBAAoB,cAAc,QAAQ,GAAG,MAAM;CACpE,KAAK,MAAM,cAAc,SAAS,OAAO,GACxC,mBAAmB,UAAU;CAE9B,OAAO;AACR;;;;;;AASA,MAAa,mBACZ,QACA,aAEA,iBAAwC,QAAQ,EAAE,YAAY,SAAS,CAAC;;AAGzE,MAAa,gBAAgB,YAC5B,cAAqC,OAAO,EAAE,8BAAc,IAAI,IAAI"}
@@ -1,6 +1,6 @@
1
- import { localizedIf } from "./localizedIf.js";
2
1
  import { keys } from "../translations/keys.js";
3
2
  import { labelFor } from "../translations/server.js";
3
+ import { localizedIf } from "./localizedIf.js";
4
4
  //#region src/fields/sharedConfig.ts
5
5
  const CONDITION_FIELD_REF = "@10x-media/form-builder/client#FormConditionField";
6
6
  const conditionField = (name, labelKey, conditionTypes) => ({
@@ -72,7 +72,13 @@ type FormFieldDefinition<K extends FormFieldValueKind = FormFieldValueKind, TCon
72
72
  config?: Field[];
73
73
  validate?: FormFieldValidate<K, TConfig>;
74
74
  format?: FormFieldFormat<K, TConfig>;
75
- Field?: string; /** Optional Standard Schema validator (Zod/Valibot/etc.), run by the engine after the intrinsic validator. */
75
+ Field?: string;
76
+ /**
77
+ * Import-map ref for a custom block row Label, replacing the default `FieldBlockLabel` for this
78
+ * type's add-field block. Receives the same `{ typeLabelKey }` clientProps. The `consent` type
79
+ * uses this to resolve and show the referenced source's name.
80
+ */
81
+ blockLabel?: string; /** Optional Standard Schema validator (Zod/Valibot/etc.), run by the engine after the intrinsic validator. */
76
82
  schema?: StandardSchemaV1;
77
83
  icon?: string;
78
84
  group?: string; /** How this field appears in the condition builder. Defaults from `value` (see `defaultConditionType`). */
@@ -130,7 +136,8 @@ type AnyFormFieldDefinition = {
130
136
  config?: Field[];
131
137
  validate?: AnyFormFieldValidate;
132
138
  format?: AnyFormFieldFormat;
133
- Field?: string;
139
+ Field?: string; /** See `FormFieldDefinition.blockLabel`: import-map ref for a custom block row Label. */
140
+ blockLabel?: string;
134
141
  schema?: StandardSchemaV1;
135
142
  icon?: string;
136
143
  group?: string; /** How this field appears in the condition builder. Defaults from `value` (see `defaultConditionType`). */
@@ -14,8 +14,12 @@ const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
14
14
  * per step off `useFormStep()`, so an empty step is not necessarily an empty page, and dropping
15
15
  * one would strand the `next` and transition targets that point at it. The visitor gets a page with
16
16
  * only navigation on it, which is visible to the author in the flow builder and recoverable there.
17
+ *
18
+ * When `normalizeWhen` is provided, each transition's `when` is laundered through it (the same field
19
+ * condition normalization); a transition whose `when` strips to nothing (an operand field was deleted)
20
+ * is dropped rather than kept as an always-true route that would force navigation.
17
21
  */
18
- const normalizeFlow = (raw, fieldKeys) => {
22
+ const normalizeFlow = (raw, fieldKeys, normalizeWhen) => {
19
23
  if (!isRecord(raw) || !Array.isArray(raw.steps)) return;
20
24
  const knownFields = new Set(fieldKeys);
21
25
  const rawSteps = raw.steps.filter((s) => isRecord(s) && typeof s.id === "string");
@@ -30,9 +34,9 @@ const normalizeFlow = (raw, fieldKeys) => {
30
34
  return true;
31
35
  }) : [];
32
36
  const transitions = (Array.isArray(s.transitions) ? s.transitions : []).filter((t) => isRecord(t) && typeof t.to === "string" && knownIds.has(t.to) && isRecord(t.when)).map((t) => ({
33
- when: t.when,
37
+ when: normalizeWhen ? normalizeWhen(t.when) : t.when,
34
38
  to: t.to
35
- }));
39
+ })).filter((t) => t.when !== void 0);
36
40
  const next = s.next === null ? null : typeof s.next === "string" && knownIds.has(s.next) ? s.next : void 0;
37
41
  const step = {
38
42
  id,
@@ -1 +1 @@
1
- {"version":3,"file":"normalizeFlow.js","names":[],"sources":["../../src/flow/normalizeFlow.ts"],"sourcesContent":["import type { Where } from 'payload'\nimport type { FlowStep, FormFlow } from './types'\n\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n\tv !== null && typeof v === 'object' && !Array.isArray(v)\n\n/**\n * Pure guard that normalizes a raw (possibly untrusted) flow value against the form's current\n * field keys (machine names for named fields, block row ids for bare blocks; see `fieldKey`).\n * Returns `undefined` when the flow is absent, empty, or resolves to fewer than two steps\n * (a single step is an ordinary form). A field assigned to multiple steps keeps only its\n * first occurrence in step order. `next: null` (explicit end of form) is preserved as distinct\n * from an absent `next` (sequential fall-through); a `next` pointing at an unknown step is\n * dropped to absent.\n *\n * A step is kept even when it ends up with no fields, which happens when every field it named was\n * deleted from the form. Dropping it would be wrong twice over: a host can render its own content\n * per step off `useFormStep()`, so an empty step is not necessarily an empty page, and dropping\n * one would strand the `next` and transition targets that point at it. The visitor gets a page with\n * only navigation on it, which is visible to the author in the flow builder and recoverable there.\n */\nexport const normalizeFlow = (raw: unknown, fieldKeys: string[]): FormFlow | undefined => {\n\tif (!isRecord(raw) || !Array.isArray(raw.steps)) {\n\t\treturn undefined\n\t}\n\n\tconst knownFields = new Set(fieldKeys)\n\n\tconst rawSteps = raw.steps.filter(\n\t\t(s): s is Record<string, unknown> => isRecord(s) && typeof s.id === 'string'\n\t)\n\n\tconst knownIds = new Set(rawSteps.map((s) => s.id as string))\n\n\tif (knownIds.size < 2) {\n\t\treturn undefined\n\t}\n\n\tconst seenFields = new Set<string>()\n\n\tconst steps: FlowStep[] = rawSteps.map((s) => {\n\t\tconst id = s.id as string\n\n\t\tconst fields = Array.isArray(s.fields)\n\t\t\t? s.fields.filter((f): f is string => {\n\t\t\t\t\tif (typeof f !== 'string' || !knownFields.has(f) || seenFields.has(f)) return false\n\t\t\t\t\tseenFields.add(f)\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t: []\n\n\t\tconst rawTransitions = Array.isArray(s.transitions) ? s.transitions : []\n\t\tconst transitions = rawTransitions\n\t\t\t.filter(\n\t\t\t\t(t): t is Record<string, unknown> =>\n\t\t\t\t\tisRecord(t) &&\n\t\t\t\t\ttypeof t.to === 'string' &&\n\t\t\t\t\tknownIds.has(t.to as string) &&\n\t\t\t\t\tisRecord(t.when)\n\t\t\t)\n\t\t\t.map((t) => ({ when: t.when as Where, to: t.to as string }))\n\n\t\tconst next =\n\t\t\ts.next === null\n\t\t\t\t? null\n\t\t\t\t: typeof s.next === 'string' && knownIds.has(s.next)\n\t\t\t\t\t? (s.next as string)\n\t\t\t\t\t: undefined\n\n\t\tconst step: FlowStep = { id, fields }\n\t\tif (typeof s.title === 'string') step.title = s.title\n\t\tif (transitions.length > 0) step.transitions = transitions\n\t\tif (next !== undefined) step.next = next\n\n\t\treturn step\n\t})\n\n\treturn { steps }\n}\n"],"mappings":";AAGA,MAAM,YAAY,MACjB,MAAM,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;;;;;;;;;;;;;;;;AAiBxD,MAAa,iBAAiB,KAAc,cAA8C;CACzF,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,MAAM,QAAQ,IAAI,KAAK,GAC7C;CAGD,MAAM,cAAc,IAAI,IAAI,SAAS;CAErC,MAAM,WAAW,IAAI,MAAM,QACzB,MAAoC,SAAS,CAAC,KAAK,OAAO,EAAE,OAAO,QACrE;CAEA,MAAM,WAAW,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,EAAY,CAAC;CAE5D,IAAI,SAAS,OAAO,GACnB;CAGD,MAAM,6BAAa,IAAI,IAAY;CAuCnC,OAAO,EAAE,OArCiB,SAAS,KAAK,MAAM;EAC7C,MAAM,KAAK,EAAE;EAEb,MAAM,SAAS,MAAM,QAAQ,EAAE,MAAM,IAClC,EAAE,OAAO,QAAQ,MAAmB;GACpC,IAAI,OAAO,MAAM,YAAY,CAAC,YAAY,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,GAAG,OAAO;GAC9E,WAAW,IAAI,CAAC;GAChB,OAAO;EACR,CAAC,IACA,CAAC;EAGJ,MAAM,eADiB,MAAM,QAAQ,EAAE,WAAW,IAAI,EAAE,cAAc,CAAC,GAErE,QACC,MACA,SAAS,CAAC,KACV,OAAO,EAAE,OAAO,YAChB,SAAS,IAAI,EAAE,EAAY,KAC3B,SAAS,EAAE,IAAI,CACjB,EACC,KAAK,OAAO;GAAE,MAAM,EAAE;GAAe,IAAI,EAAE;EAAa,EAAE;EAE5D,MAAM,OACL,EAAE,SAAS,OACR,OACA,OAAO,EAAE,SAAS,YAAY,SAAS,IAAI,EAAE,IAAI,IAC/C,EAAE,OACH,KAAA;EAEL,MAAM,OAAiB;GAAE;GAAI;EAAO;EACpC,IAAI,OAAO,EAAE,UAAU,UAAU,KAAK,QAAQ,EAAE;EAChD,IAAI,YAAY,SAAS,GAAG,KAAK,cAAc;EAC/C,IAAI,SAAS,KAAA,GAAW,KAAK,OAAO;EAEpC,OAAO;CACR,CAEa,EAAE;AAChB"}
1
+ {"version":3,"file":"normalizeFlow.js","names":[],"sources":["../../src/flow/normalizeFlow.ts"],"sourcesContent":["import type { Where } from 'payload'\nimport type { FlowStep, FormFlow } from './types'\n\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n\tv !== null && typeof v === 'object' && !Array.isArray(v)\n\n/**\n * Pure guard that normalizes a raw (possibly untrusted) flow value against the form's current\n * field keys (machine names for named fields, block row ids for bare blocks; see `fieldKey`).\n * Returns `undefined` when the flow is absent, empty, or resolves to fewer than two steps\n * (a single step is an ordinary form). A field assigned to multiple steps keeps only its\n * first occurrence in step order. `next: null` (explicit end of form) is preserved as distinct\n * from an absent `next` (sequential fall-through); a `next` pointing at an unknown step is\n * dropped to absent.\n *\n * A step is kept even when it ends up with no fields, which happens when every field it named was\n * deleted from the form. Dropping it would be wrong twice over: a host can render its own content\n * per step off `useFormStep()`, so an empty step is not necessarily an empty page, and dropping\n * one would strand the `next` and transition targets that point at it. The visitor gets a page with\n * only navigation on it, which is visible to the author in the flow builder and recoverable there.\n *\n * When `normalizeWhen` is provided, each transition's `when` is laundered through it (the same field\n * condition normalization); a transition whose `when` strips to nothing (an operand field was deleted)\n * is dropped rather than kept as an always-true route that would force navigation.\n */\nexport const normalizeFlow = (\n\traw: unknown,\n\tfieldKeys: string[],\n\tnormalizeWhen?: (raw: unknown) => Where | undefined\n): FormFlow | undefined => {\n\tif (!isRecord(raw) || !Array.isArray(raw.steps)) {\n\t\treturn undefined\n\t}\n\n\tconst knownFields = new Set(fieldKeys)\n\n\tconst rawSteps = raw.steps.filter(\n\t\t(s): s is Record<string, unknown> => isRecord(s) && typeof s.id === 'string'\n\t)\n\n\tconst knownIds = new Set(rawSteps.map((s) => s.id as string))\n\n\tif (knownIds.size < 2) {\n\t\treturn undefined\n\t}\n\n\tconst seenFields = new Set<string>()\n\n\tconst steps: FlowStep[] = rawSteps.map((s) => {\n\t\tconst id = s.id as string\n\n\t\tconst fields = Array.isArray(s.fields)\n\t\t\t? s.fields.filter((f): f is string => {\n\t\t\t\t\tif (typeof f !== 'string' || !knownFields.has(f) || seenFields.has(f)) return false\n\t\t\t\t\tseenFields.add(f)\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t: []\n\n\t\tconst rawTransitions = Array.isArray(s.transitions) ? s.transitions : []\n\t\tconst transitions = rawTransitions\n\t\t\t.filter(\n\t\t\t\t(t): t is Record<string, unknown> =>\n\t\t\t\t\tisRecord(t) &&\n\t\t\t\t\ttypeof t.to === 'string' &&\n\t\t\t\t\tknownIds.has(t.to as string) &&\n\t\t\t\t\tisRecord(t.when)\n\t\t\t)\n\t\t\t.map((t) => ({\n\t\t\t\twhen: normalizeWhen ? normalizeWhen(t.when) : (t.when as Where),\n\t\t\t\tto: t.to as string,\n\t\t\t}))\n\t\t\t.filter((t): t is { when: Where; to: string } => t.when !== undefined)\n\n\t\tconst next =\n\t\t\ts.next === null\n\t\t\t\t? null\n\t\t\t\t: typeof s.next === 'string' && knownIds.has(s.next)\n\t\t\t\t\t? (s.next as string)\n\t\t\t\t\t: undefined\n\n\t\tconst step: FlowStep = { id, fields }\n\t\tif (typeof s.title === 'string') step.title = s.title\n\t\tif (transitions.length > 0) step.transitions = transitions\n\t\tif (next !== undefined) step.next = next\n\n\t\treturn step\n\t})\n\n\treturn { steps }\n}\n"],"mappings":";AAGA,MAAM,YAAY,MACjB,MAAM,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;AAqBxD,MAAa,iBACZ,KACA,WACA,kBAC0B;CAC1B,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC,MAAM,QAAQ,IAAI,KAAK,GAC7C;CAGD,MAAM,cAAc,IAAI,IAAI,SAAS;CAErC,MAAM,WAAW,IAAI,MAAM,QACzB,MAAoC,SAAS,CAAC,KAAK,OAAO,EAAE,OAAO,QACrE;CAEA,MAAM,WAAW,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,EAAY,CAAC;CAE5D,IAAI,SAAS,OAAO,GACnB;CAGD,MAAM,6BAAa,IAAI,IAAY;CA2CnC,OAAO,EAAE,OAzCiB,SAAS,KAAK,MAAM;EAC7C,MAAM,KAAK,EAAE;EAEb,MAAM,SAAS,MAAM,QAAQ,EAAE,MAAM,IAClC,EAAE,OAAO,QAAQ,MAAmB;GACpC,IAAI,OAAO,MAAM,YAAY,CAAC,YAAY,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,GAAG,OAAO;GAC9E,WAAW,IAAI,CAAC;GAChB,OAAO;EACR,CAAC,IACA,CAAC;EAGJ,MAAM,eADiB,MAAM,QAAQ,EAAE,WAAW,IAAI,EAAE,cAAc,CAAC,GAErE,QACC,MACA,SAAS,CAAC,KACV,OAAO,EAAE,OAAO,YAChB,SAAS,IAAI,EAAE,EAAY,KAC3B,SAAS,EAAE,IAAI,CACjB,EACC,KAAK,OAAO;GACZ,MAAM,gBAAgB,cAAc,EAAE,IAAI,IAAK,EAAE;GACjD,IAAI,EAAE;EACP,EAAE,EACD,QAAQ,MAAwC,EAAE,SAAS,KAAA,CAAS;EAEtE,MAAM,OACL,EAAE,SAAS,OACR,OACA,OAAO,EAAE,SAAS,YAAY,SAAS,IAAI,EAAE,IAAI,IAC/C,EAAE,OACH,KAAA;EAEL,MAAM,OAAiB;GAAE;GAAI;EAAO;EACpC,IAAI,OAAO,EAAE,UAAU,UAAU,KAAK,QAAQ,EAAE;EAChD,IAAI,YAAY,SAAS,GAAG,KAAK,cAAc;EAC/C,IAAI,SAAS,KAAA,GAAW,KAAK,OAAO;EAEpC,OAAO;CACR,CAEa,EAAE;AAChB"}
@@ -16,6 +16,17 @@ type ToFormDocumentOptions = {
16
16
  * consent fields render with no statement at all.
17
17
  */
18
18
  consentStatements?: ConsentStatements;
19
+ /**
20
+ * Resolve an internal redirect `reference` to a same-site URL. Called only when
21
+ * `response.type === 'redirect'`, a `reference` is present, and no `redirect.url` is set. Keep it
22
+ * sync: operate on the already-loaded (populated) reference, since resolving a Doc to a URL is host
23
+ * routing knowledge the plugin cannot supply. The result is written to `redirect.url`, so `<Form>`
24
+ * navigates with no client change.
25
+ */
26
+ resolveRedirect?: (reference: {
27
+ relationTo: string;
28
+ value: number | string | object;
29
+ }) => string | null | undefined;
19
30
  };
20
31
  /**
21
32
  * Narrows a Payload-generated form document (from `getPayload().findByID()` or `fetch`) to
@@ -26,8 +37,8 @@ type ToFormDocumentOptions = {
26
37
  * - `flow` is stored as opaque JSON; typed as `FormFlow | undefined`
27
38
  * - `response` may be null; coerced to `undefined`, otherwise passed through wholesale, which
28
39
  * includes `response.redirect.reference` (present only when the plugin's `redirectRelationships`
29
- * option is set): read the form at `depth: 0` to get Payload's raw `{ relationTo, value }` pair
30
- * rather than a populated document, since the host is the one who resolves it to a URL
40
+ * option is set): `value` may be a bare id (`depth: 0`) or a populated document, and the host
41
+ * resolves it to a URL either by writing `redirect.url` or by passing `options.resolveRedirect`
31
42
  * - the button labels (`submitLabel`/`prevLabel`/`nextLabel`) live at the document root now (there
32
43
  * is no `buttons` group); the non-empty ones are reassembled into `FormDocument.buttons`
33
44
  * - `title` may be null; coerced to `undefined`
@@ -61,7 +72,7 @@ declare function toFormDocument(form: {
61
72
  url?: string | null;
62
73
  reference?: {
63
74
  relationTo?: string | null;
64
- value?: number | string | null;
75
+ value?: number | string | object | null;
65
76
  } | null;
66
77
  } | null;
67
78
  } | null;
@@ -79,6 +90,12 @@ declare function toFormDocument(form: {
79
90
  winningValues?: (string | null)[] | null;
80
91
  } | null;
81
92
  } | null;
93
+ /**
94
+ * Consent statements resolved onto the doc by the plugin's forms `afterRead` hook (when
95
+ * `consent.sources` is set), so consent renders on any fetch path (modal/client, not just RSC).
96
+ * An explicit `options.consentStatements` takes precedence when both are present.
97
+ */
98
+ consentStatements?: ConsentStatements;
82
99
  }, options?: ToFormDocumentOptions): FormDocument;
83
100
  //#endregion
84
101
  export { ToFormDocumentOptions, toFormDocument };
@@ -26,8 +26,8 @@ const buttonSettingsOf = (form) => {
26
26
  * - `flow` is stored as opaque JSON; typed as `FormFlow | undefined`
27
27
  * - `response` may be null; coerced to `undefined`, otherwise passed through wholesale, which
28
28
  * includes `response.redirect.reference` (present only when the plugin's `redirectRelationships`
29
- * option is set): read the form at `depth: 0` to get Payload's raw `{ relationTo, value }` pair
30
- * rather than a populated document, since the host is the one who resolves it to a URL
29
+ * option is set): `value` may be a bare id (`depth: 0`) or a populated document, and the host
30
+ * resolves it to a URL either by writing `redirect.url` or by passing `options.resolveRedirect`
31
31
  * - the button labels (`submitLabel`/`prevLabel`/`nextLabel`) live at the document root now (there
32
32
  * is no `buttons` group); the non-empty ones are reassembled into `FormDocument.buttons`
33
33
  * - `title` may be null; coerced to `undefined`
@@ -56,12 +56,24 @@ function toFormDocument(form, options) {
56
56
  } : void 0;
57
57
  let fields = form.fields ?? [];
58
58
  if (options?.pollOptions) fields = applyPollOptions(fields, form.poll?.resultsField, options.pollOptions);
59
- if (options?.consentStatements) fields = applyConsentStatements(fields, options.consentStatements);
59
+ const consentStatements = options?.consentStatements ?? form.consentStatements;
60
+ if (consentStatements) fields = applyConsentStatements(fields, consentStatements);
61
+ let response = form.response ?? void 0;
62
+ if (options?.resolveRedirect && response?.type === "redirect" && response.redirect?.reference && !response.redirect.url) {
63
+ const url = options.resolveRedirect(response.redirect.reference);
64
+ if (url) response = {
65
+ ...response,
66
+ redirect: {
67
+ ...response.redirect,
68
+ url
69
+ }
70
+ };
71
+ }
60
72
  return {
61
73
  id: form.id,
62
74
  fields,
63
75
  flow: form.flow,
64
- response: form.response ?? void 0,
76
+ response,
65
77
  buttons: buttonSettingsOf(form),
66
78
  title: form.title ?? void 0,
67
79
  multistep: form.multistep === true,
@@ -1 +1 @@
1
- {"version":3,"file":"toFormDocument.js","names":[],"sources":["../../src/form/toFormDocument.ts"],"sourcesContent":["import { applyConsentStatements } from '../consent/applyConsentStatements'\nimport type { ConsentStatements } from '../consent/resolveConsentStatements'\nimport type { FormFlow } from '../flow/types'\nimport { applyPollOptions } from '../poll/applyPollOptions'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport type { FormFieldInstance } from '../submissions/types'\nimport type { FormButtonSettings, FormDocument, FormResponseSettings } from './types'\n\nexport type ToFormDocumentOptions = {\n\t/**\n\t * Source-resolved poll options (from `resolvePollOptions`), injected as the options of the\n\t * field instance named by the form's `poll.resultsField` so the client renders the host's\n\t * current choices instead of hand-authored ones.\n\t */\n\tpollOptions?: PollOption[]\n\t/**\n\t * Source-resolved consent statements (from `resolveConsentStatements`), injected into the\n\t * consent field instances they name. A form stores only a source id, so without these its\n\t * consent fields render with no statement at all.\n\t */\n\tconsentStatements?: ConsentStatements\n}\n\n/**\n * Reassemble the client button labels from the document's top-level `submitLabel`/`prevLabel`/\n * `nextLabel` fields, keeping only non-empty strings (mirroring the client's own `storedLabel`\n * fallback). Returns `undefined` when the author set no label, so the client uses its defaults.\n */\nconst buttonSettingsOf = (form: {\n\tsubmitLabel?: string | null\n\tprevLabel?: string | null\n\tnextLabel?: string | null\n}): FormButtonSettings | undefined => {\n\tconst label = (value: unknown): string | undefined =>\n\t\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\tconst settings: FormButtonSettings = {}\n\tconst submitLabel = label(form.submitLabel)\n\tconst prevLabel = label(form.prevLabel)\n\tconst nextLabel = label(form.nextLabel)\n\tif (submitLabel !== undefined) settings.submitLabel = submitLabel\n\tif (prevLabel !== undefined) settings.prevLabel = prevLabel\n\tif (nextLabel !== undefined) settings.nextLabel = nextLabel\n\treturn Object.keys(settings).length > 0 ? settings : undefined\n}\n\n/**\n * Narrows a Payload-generated form document (from `getPayload().findByID()` or `fetch`) to\n * `FormDocument` without an unsafe `as` cast. Handles the small structural mismatches between\n * the auto-generated collection types and what `<Form>` expects:\n * - `fields` may be a typed blocks-union array or null; normalized to `FormFieldInstance[]`\n * (nameless bare rows, e.g. message blocks, pass through with their row `id` intact)\n * - `flow` is stored as opaque JSON; typed as `FormFlow | undefined`\n * - `response` may be null; coerced to `undefined`, otherwise passed through wholesale, which\n * includes `response.redirect.reference` (present only when the plugin's `redirectRelationships`\n * option is set): read the form at `depth: 0` to get Payload's raw `{ relationTo, value }` pair\n * rather than a populated document, since the host is the one who resolves it to a URL\n * - the button labels (`submitLabel`/`prevLabel`/`nextLabel`) live at the document root now (there\n * is no `buttons` group); the non-empty ones are reassembled into `FormDocument.buttons`\n * - `title` may be null; coerced to `undefined`\n * - `multistep`/`pollEnabled` are the two top-level form-type flags, coerced to strict booleans\n * - `poll` may be null; coerced to `undefined` (`resultsField`, `optionSource`, and\n * `sourceConfig` are dropped: server-side only; `outcome` passes through `winningValues` only)\n *\n * Unknown keys survive on `response` but not on `poll`, which is deliberate rather than incidental.\n * `response` is a visitor-facing group a host is meant to extend (`overrides.forms.fields`) and read\n * back off `FormDocument` in custom chrome, so it is cast wholesale. `poll` is plugin-owned config\n * that is split between\n * client-safe lifecycle state and server-only members, so it is rebuilt from an allowlist: a\n * passthrough would ship whatever a host put in `sourceConfig` (domain config, ids, credentials)\n * to every anonymous visitor. Adding a client-visible poll member means naming it here.\n *\n * Pure and framework-agnostic (no 'use client'): safe to call from a Server Component or any\n * other server-side code before handing the result to the client `<Form>`.\n */\nexport function toFormDocument(\n\tform: {\n\t\tid: number | string\n\t\tfields?: { blockType: string; name?: string; [key: string]: unknown }[] | null\n\t\tflow?: unknown\n\t\tresponse?: {\n\t\t\ttype?: string | null\n\t\t\tmessage?: unknown\n\t\t\tredirect?: {\n\t\t\t\turl?: string | null\n\t\t\t\treference?: { relationTo?: string | null; value?: number | string | null } | null\n\t\t\t} | null\n\t\t} | null\n\t\tsubmitLabel?: string | null\n\t\tprevLabel?: string | null\n\t\tnextLabel?: string | null\n\t\ttitle?: string | null\n\t\tmultistep?: boolean | null\n\t\tpollEnabled?: boolean | null\n\t\tpoll?: {\n\t\t\tresultsField?: string | null\n\t\t\tresultsVisibility?: string | null\n\t\t\tclosesAt?: string | null\n\t\t\toutcome?: { winningValues?: (string | null)[] | null } | null\n\t\t} | null\n\t},\n\toptions?: ToFormDocumentOptions\n): FormDocument {\n\tconst rawWinningValues = form.poll?.outcome?.winningValues\n\tconst winningValues = Array.isArray(rawWinningValues)\n\t\t? rawWinningValues.filter(\n\t\t\t\t(value): value is string => typeof value === 'string' && value.length > 0\n\t\t\t)\n\t\t: []\n\tconst poll = form.poll\n\t\t? {\n\t\t\t\tresultsVisibility: (form.poll.resultsVisibility ?? undefined) as\n\t\t\t\t\t| 'afterVote'\n\t\t\t\t\t| 'afterClose'\n\t\t\t\t\t| undefined,\n\t\t\t\tclosesAt: form.poll.closesAt ?? undefined,\n\t\t\t\t...(winningValues.length > 0 ? { outcome: { winningValues } } : {}),\n\t\t\t}\n\t\t: undefined\n\tlet fields = (form.fields ?? []) as FormFieldInstance[]\n\tif (options?.pollOptions) {\n\t\tfields = applyPollOptions(fields, form.poll?.resultsField, options.pollOptions)\n\t}\n\tif (options?.consentStatements) {\n\t\tfields = applyConsentStatements(fields, options.consentStatements)\n\t}\n\treturn {\n\t\tid: form.id,\n\t\tfields,\n\t\tflow: form.flow as FormFlow | undefined,\n\t\tresponse: (form.response as FormResponseSettings | null | undefined) ?? undefined,\n\t\tbuttons: buttonSettingsOf(form),\n\t\ttitle: form.title ?? undefined,\n\t\tmultistep: form.multistep === true,\n\t\tpollEnabled: form.pollEnabled === true,\n\t\tpoll,\n\t}\n}\n"],"mappings":";;;;;;;;AA4BA,MAAM,oBAAoB,SAIY;CACrC,MAAM,SAAS,UACd,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA;CACzD,MAAM,WAA+B,CAAC;CACtC,MAAM,cAAc,MAAM,KAAK,WAAW;CAC1C,MAAM,YAAY,MAAM,KAAK,SAAS;CACtC,MAAM,YAAY,MAAM,KAAK,SAAS;CACtC,IAAI,gBAAgB,KAAA,GAAW,SAAS,cAAc;CACtD,IAAI,cAAc,KAAA,GAAW,SAAS,YAAY;CAClD,IAAI,cAAc,KAAA,GAAW,SAAS,YAAY;CAClD,OAAO,OAAO,KAAK,QAAQ,EAAE,SAAS,IAAI,WAAW,KAAA;AACtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,eACf,MAyBA,SACe;CACf,MAAM,mBAAmB,KAAK,MAAM,SAAS;CAC7C,MAAM,gBAAgB,MAAM,QAAQ,gBAAgB,IACjD,iBAAiB,QAChB,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CACzE,IACC,CAAC;CACJ,MAAM,OAAO,KAAK,OACf;EACA,mBAAoB,KAAK,KAAK,qBAAqB,KAAA;EAInD,UAAU,KAAK,KAAK,YAAY,KAAA;EAChC,GAAI,cAAc,SAAS,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,CAAC;CAClE,IACC,KAAA;CACH,IAAI,SAAU,KAAK,UAAU,CAAC;CAC9B,IAAI,SAAS,aACZ,SAAS,iBAAiB,QAAQ,KAAK,MAAM,cAAc,QAAQ,WAAW;CAE/E,IAAI,SAAS,mBACZ,SAAS,uBAAuB,QAAQ,QAAQ,iBAAiB;CAElE,OAAO;EACN,IAAI,KAAK;EACT;EACA,MAAM,KAAK;EACX,UAAW,KAAK,YAAwD,KAAA;EACxE,SAAS,iBAAiB,IAAI;EAC9B,OAAO,KAAK,SAAS,KAAA;EACrB,WAAW,KAAK,cAAc;EAC9B,aAAa,KAAK,gBAAgB;EAClC;CACD;AACD"}
1
+ {"version":3,"file":"toFormDocument.js","names":[],"sources":["../../src/form/toFormDocument.ts"],"sourcesContent":["import { applyConsentStatements } from '../consent/applyConsentStatements'\nimport type { ConsentStatements } from '../consent/resolveConsentStatements'\nimport type { FormFlow } from '../flow/types'\nimport { applyPollOptions } from '../poll/applyPollOptions'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport type { FormFieldInstance } from '../submissions/types'\nimport type { FormButtonSettings, FormDocument, FormResponseSettings } from './types'\n\nexport type ToFormDocumentOptions = {\n\t/**\n\t * Source-resolved poll options (from `resolvePollOptions`), injected as the options of the\n\t * field instance named by the form's `poll.resultsField` so the client renders the host's\n\t * current choices instead of hand-authored ones.\n\t */\n\tpollOptions?: PollOption[]\n\t/**\n\t * Source-resolved consent statements (from `resolveConsentStatements`), injected into the\n\t * consent field instances they name. A form stores only a source id, so without these its\n\t * consent fields render with no statement at all.\n\t */\n\tconsentStatements?: ConsentStatements\n\t/**\n\t * Resolve an internal redirect `reference` to a same-site URL. Called only when\n\t * `response.type === 'redirect'`, a `reference` is present, and no `redirect.url` is set. Keep it\n\t * sync: operate on the already-loaded (populated) reference, since resolving a Doc to a URL is host\n\t * routing knowledge the plugin cannot supply. The result is written to `redirect.url`, so `<Form>`\n\t * navigates with no client change.\n\t */\n\tresolveRedirect?: (reference: {\n\t\trelationTo: string\n\t\tvalue: number | string | object\n\t}) => string | null | undefined\n}\n\n/**\n * Reassemble the client button labels from the document's top-level `submitLabel`/`prevLabel`/\n * `nextLabel` fields, keeping only non-empty strings (mirroring the client's own `storedLabel`\n * fallback). Returns `undefined` when the author set no label, so the client uses its defaults.\n */\nconst buttonSettingsOf = (form: {\n\tsubmitLabel?: string | null\n\tprevLabel?: string | null\n\tnextLabel?: string | null\n}): FormButtonSettings | undefined => {\n\tconst label = (value: unknown): string | undefined =>\n\t\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\tconst settings: FormButtonSettings = {}\n\tconst submitLabel = label(form.submitLabel)\n\tconst prevLabel = label(form.prevLabel)\n\tconst nextLabel = label(form.nextLabel)\n\tif (submitLabel !== undefined) settings.submitLabel = submitLabel\n\tif (prevLabel !== undefined) settings.prevLabel = prevLabel\n\tif (nextLabel !== undefined) settings.nextLabel = nextLabel\n\treturn Object.keys(settings).length > 0 ? settings : undefined\n}\n\n/**\n * Narrows a Payload-generated form document (from `getPayload().findByID()` or `fetch`) to\n * `FormDocument` without an unsafe `as` cast. Handles the small structural mismatches between\n * the auto-generated collection types and what `<Form>` expects:\n * - `fields` may be a typed blocks-union array or null; normalized to `FormFieldInstance[]`\n * (nameless bare rows, e.g. message blocks, pass through with their row `id` intact)\n * - `flow` is stored as opaque JSON; typed as `FormFlow | undefined`\n * - `response` may be null; coerced to `undefined`, otherwise passed through wholesale, which\n * includes `response.redirect.reference` (present only when the plugin's `redirectRelationships`\n * option is set): `value` may be a bare id (`depth: 0`) or a populated document, and the host\n * resolves it to a URL either by writing `redirect.url` or by passing `options.resolveRedirect`\n * - the button labels (`submitLabel`/`prevLabel`/`nextLabel`) live at the document root now (there\n * is no `buttons` group); the non-empty ones are reassembled into `FormDocument.buttons`\n * - `title` may be null; coerced to `undefined`\n * - `multistep`/`pollEnabled` are the two top-level form-type flags, coerced to strict booleans\n * - `poll` may be null; coerced to `undefined` (`resultsField`, `optionSource`, and\n * `sourceConfig` are dropped: server-side only; `outcome` passes through `winningValues` only)\n *\n * Unknown keys survive on `response` but not on `poll`, which is deliberate rather than incidental.\n * `response` is a visitor-facing group a host is meant to extend (`overrides.forms.fields`) and read\n * back off `FormDocument` in custom chrome, so it is cast wholesale. `poll` is plugin-owned config\n * that is split between\n * client-safe lifecycle state and server-only members, so it is rebuilt from an allowlist: a\n * passthrough would ship whatever a host put in `sourceConfig` (domain config, ids, credentials)\n * to every anonymous visitor. Adding a client-visible poll member means naming it here.\n *\n * Pure and framework-agnostic (no 'use client'): safe to call from a Server Component or any\n * other server-side code before handing the result to the client `<Form>`.\n */\nexport function toFormDocument(\n\tform: {\n\t\tid: number | string\n\t\tfields?: { blockType: string; name?: string; [key: string]: unknown }[] | null\n\t\tflow?: unknown\n\t\tresponse?: {\n\t\t\ttype?: string | null\n\t\t\tmessage?: unknown\n\t\t\tredirect?: {\n\t\t\t\turl?: string | null\n\t\t\t\treference?: { relationTo?: string | null; value?: number | string | object | null } | null\n\t\t\t} | null\n\t\t} | null\n\t\tsubmitLabel?: string | null\n\t\tprevLabel?: string | null\n\t\tnextLabel?: string | null\n\t\ttitle?: string | null\n\t\tmultistep?: boolean | null\n\t\tpollEnabled?: boolean | null\n\t\tpoll?: {\n\t\t\tresultsField?: string | null\n\t\t\tresultsVisibility?: string | null\n\t\t\tclosesAt?: string | null\n\t\t\toutcome?: { winningValues?: (string | null)[] | null } | null\n\t\t} | null\n\t\t/**\n\t\t * Consent statements resolved onto the doc by the plugin's forms `afterRead` hook (when\n\t\t * `consent.sources` is set), so consent renders on any fetch path (modal/client, not just RSC).\n\t\t * An explicit `options.consentStatements` takes precedence when both are present.\n\t\t */\n\t\tconsentStatements?: ConsentStatements\n\t},\n\toptions?: ToFormDocumentOptions\n): FormDocument {\n\tconst rawWinningValues = form.poll?.outcome?.winningValues\n\tconst winningValues = Array.isArray(rawWinningValues)\n\t\t? rawWinningValues.filter(\n\t\t\t\t(value): value is string => typeof value === 'string' && value.length > 0\n\t\t\t)\n\t\t: []\n\tconst poll = form.poll\n\t\t? {\n\t\t\t\tresultsVisibility: (form.poll.resultsVisibility ?? undefined) as\n\t\t\t\t\t| 'afterVote'\n\t\t\t\t\t| 'afterClose'\n\t\t\t\t\t| undefined,\n\t\t\t\tclosesAt: form.poll.closesAt ?? undefined,\n\t\t\t\t...(winningValues.length > 0 ? { outcome: { winningValues } } : {}),\n\t\t\t}\n\t\t: undefined\n\tlet fields = (form.fields ?? []) as FormFieldInstance[]\n\tif (options?.pollOptions) {\n\t\tfields = applyPollOptions(fields, form.poll?.resultsField, options.pollOptions)\n\t}\n\tconst consentStatements = options?.consentStatements ?? form.consentStatements\n\tif (consentStatements) {\n\t\tfields = applyConsentStatements(fields, consentStatements)\n\t}\n\tlet response = (form.response as FormResponseSettings | null | undefined) ?? undefined\n\t// A host that passes `resolveRedirect` gets internal references turned into `redirect.url` (all\n\t// `<Form>` navigates by), but only when the author set no explicit url. Non-mutating: clone the group.\n\tif (\n\t\toptions?.resolveRedirect &&\n\t\tresponse?.type === 'redirect' &&\n\t\tresponse.redirect?.reference &&\n\t\t!response.redirect.url\n\t) {\n\t\tconst url = options.resolveRedirect(response.redirect.reference)\n\t\tif (url) {\n\t\t\tresponse = { ...response, redirect: { ...response.redirect, url } }\n\t\t}\n\t}\n\treturn {\n\t\tid: form.id,\n\t\tfields,\n\t\tflow: form.flow as FormFlow | undefined,\n\t\tresponse,\n\t\tbuttons: buttonSettingsOf(form),\n\t\ttitle: form.title ?? undefined,\n\t\tmultistep: form.multistep === true,\n\t\tpollEnabled: form.pollEnabled === true,\n\t\tpoll,\n\t}\n}\n"],"mappings":";;;;;;;;AAuCA,MAAM,oBAAoB,SAIY;CACrC,MAAM,SAAS,UACd,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA;CACzD,MAAM,WAA+B,CAAC;CACtC,MAAM,cAAc,MAAM,KAAK,WAAW;CAC1C,MAAM,YAAY,MAAM,KAAK,SAAS;CACtC,MAAM,YAAY,MAAM,KAAK,SAAS;CACtC,IAAI,gBAAgB,KAAA,GAAW,SAAS,cAAc;CACtD,IAAI,cAAc,KAAA,GAAW,SAAS,YAAY;CAClD,IAAI,cAAc,KAAA,GAAW,SAAS,YAAY;CAClD,OAAO,OAAO,KAAK,QAAQ,EAAE,SAAS,IAAI,WAAW,KAAA;AACtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,eACf,MA+BA,SACe;CACf,MAAM,mBAAmB,KAAK,MAAM,SAAS;CAC7C,MAAM,gBAAgB,MAAM,QAAQ,gBAAgB,IACjD,iBAAiB,QAChB,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CACzE,IACC,CAAC;CACJ,MAAM,OAAO,KAAK,OACf;EACA,mBAAoB,KAAK,KAAK,qBAAqB,KAAA;EAInD,UAAU,KAAK,KAAK,YAAY,KAAA;EAChC,GAAI,cAAc,SAAS,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,CAAC;CAClE,IACC,KAAA;CACH,IAAI,SAAU,KAAK,UAAU,CAAC;CAC9B,IAAI,SAAS,aACZ,SAAS,iBAAiB,QAAQ,KAAK,MAAM,cAAc,QAAQ,WAAW;CAE/E,MAAM,oBAAoB,SAAS,qBAAqB,KAAK;CAC7D,IAAI,mBACH,SAAS,uBAAuB,QAAQ,iBAAiB;CAE1D,IAAI,WAAY,KAAK,YAAwD,KAAA;CAG7E,IACC,SAAS,mBACT,UAAU,SAAS,cACnB,SAAS,UAAU,aACnB,CAAC,SAAS,SAAS,KAClB;EACD,MAAM,MAAM,QAAQ,gBAAgB,SAAS,SAAS,SAAS;EAC/D,IAAI,KACH,WAAW;GAAE,GAAG;GAAU,UAAU;IAAE,GAAG,SAAS;IAAU;GAAI;EAAE;CAEpE;CACA,OAAO;EACN,IAAI,KAAK;EACT;EACA,MAAM,KAAK;EACX;EACA,SAAS,iBAAiB,IAAI;EAC9B,OAAO,KAAK,SAAS,KAAA;EACrB,WAAW,KAAK,cAAc;EAC9B,aAAa,KAAK,gBAAgB;EAClC;CACD;AACD"}
@@ -10,14 +10,14 @@ type FormResponseSettings = {
10
10
  url?: string | null;
11
11
  /**
12
12
  * Present when the plugin's `redirectRelationships` option is set and the author picked an
13
- * internal document. Mirrors Payload's depth-0 polymorphic relationship shape (`value` is
14
- * the bare id, never populated): the HOST resolves this to a URL itself (e.g. by looking up
15
- * the referenced document's own route), since the plugin has no notion of the host's
16
- * routing, matching the model of Payload's native `plugin-form-builder` redirect relationship.
13
+ * internal document. At `depth: 0` this is Payload's raw `{ relationTo, value }` id pair; at
14
+ * `depth > 0` (or via the generated `Form` type) `value` is a populated document, which is why
15
+ * it widens to `object`. Resolve it to a URL host-side (write `redirect.url`) or by passing
16
+ * `resolveRedirect` to `toFormDocument`, since the routing is the host's knowledge.
17
17
  */
18
18
  reference?: {
19
19
  relationTo: string;
20
- value: number | string;
20
+ value: number | string | object;
21
21
  } | null;
22
22
  } | null;
23
23
  };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { AnyPollOptionSource, PollOption, PollOptionResolveArgs, PollOptionSource, definePollOptionSource } from "./poll/definePollOptionSource.js";
2
2
  import { FileFieldConfig, FileRef, FileRefError } from "./uploads/types.js";
3
3
  import { AnyFormFieldDefinition, FormFieldDefinition, FormFieldFormat, FormFieldValidate, FormFieldValueKind, OmittableSharedField, ResolveFieldOptionsArgs } from "./fields/types.js";
4
- import { TranslationsOption } from "./translations/index.js";
5
4
  import { BodyConverter, BodyConverterArgs, BodyRender, defaultBodyConverters, sanitizeUrl } from "./actions/body/converters.js";
6
5
  import { BodyContext, RichTextBodyOption, SerializeBodyArgs, serializeBody } from "./actions/body/serializeBody.js";
7
6
  import { textOfBody } from "./actions/body/textOfBody.js";
@@ -17,11 +16,10 @@ import { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSetting
17
16
  import { ToFormDocumentOptions, toFormDocument } from "./form/toFormDocument.js";
18
17
  import { FieldTypeOption, FieldTypeRegistry, FieldTypesConfig } from "./fields/registry.js";
19
18
  import { PrefillOptions, valuesFromSearchParams } from "./prefill/valuesFromSearchParams.js";
20
- import { FormEventSink } from "./events/types.js";
21
19
  import { AnyValidationRuleDefinition, ValidationRuleDefinition, ValidationRuleResult } from "./validation/types.js";
22
20
  import { PresentationDensity, PresentationDescriptor, PresentationSurface } from "./presentations/types.js";
23
- import { interpolate } from "./recall/interpolate.js";
24
21
  import { RecallResolver, buildRecallResolver, optionLabelsFor } from "./recall/resolver.js";
22
+ import { interpolate } from "./recall/interpolate.js";
25
23
  import { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD } from "./spam/constants.js";
26
24
  import { formatBytes } from "./uploads/formatBytes.js";
27
25
  import { isPollClosed } from "./form/pollState.js";
@@ -38,20 +36,20 @@ import { AggregateFieldResponsesArgs, AggregateFormResponsesArgs, aggregateField
38
36
  import { FormResultsAccess, FormResultsAccessArgs, ResolveResultsRequestArgs, ResolveResultsRequestResult, resolveFormResultsRequest } from "./aggregation/resolveResultsRequest.js";
39
37
  import { ButtonFieldsOverride, ButtonsOption, DefaultButtonFields, buildDefaultButtonFields, buildNextLabelField, buildPrevLabelField, buildSubmitLabelField } from "./collections/buttonFields.js";
40
38
  import { FieldCondition } from "./conditions/types.js";
41
- import { ConsentProof, captureConsent } from "./consent/captureConsent.js";
39
+ import { ConsentProof, ConsentSnapshotMode, captureConsent } from "./consent/captureConsent.js";
42
40
  import { ConsentSourcesFieldOptions, consentSourcesField } from "./consent/consentSourcesField.js";
43
41
  import { ResolveConsentEntriesArgs, resolveConsentEntries } from "./consent/resolveConsentEntries.js";
44
42
  import { ConsentSourceOption, ResolveConsentSourcesRequestArgs, ResolveConsentSourcesRequestResult, resolveConsentSourcesRequest } from "./consent/resolveConsentSourcesRequest.js";
45
43
  import { ResolvePublishedVersionRefArgs, resolvePublishedVersionRef } from "./consent/resolvePublishedVersionRef.js";
46
- import { RedirectFieldsOverride, RedirectOption, ResponseOption } from "./collections/redirectFields.js";
47
44
  import { DepartmentEmailsResolver, DepartmentOption, ResolveDepartmentOptionsArgs, ResolveDepartmentsRequestArgs, ResolveDepartmentsRequestResult, resolveDepartmentOptions } from "./email/departments.js";
48
- import { CollectionOverrides } from "./plugin/collectionOverrides.js";
45
+ import { RedirectFieldsOverride, RedirectOption, ResponseOption } from "./collections/redirectFields.js";
49
46
  import { CaptchaProvider, CaptchaVerifyArgs, IdentifyFn, RateLimitCheckArgs, RateLimitConfig, RateLimitResult, RateLimiter, SpamConfig, SpamMetadataConfig, SpamOption } from "./spam/types.js";
50
47
  import { UploadsOption } from "./plugin/uploadsCollection.js";
51
48
  import { DefaultOutcomeFields, OutcomeFieldsOverride, buildDefaultOutcomeFields, buildResolvedAtField, buildWinningValuesField } from "./poll/outcomeFields.js";
52
49
  import { PollOutcomeStrategy, PollOutcomeStrategyArgs, definePollType } from "./poll/definePollType.js";
53
50
  import { PollTypeRegistry, PollTypesConfig, manualStrategy, pollTypesOf, resolvePollTypes, sourceStrategy, stashPollTypes } from "./poll/pollTypeRegistry.js";
54
51
  import { ValidationRuleOption, ValidationRuleRegistry, ValidationRulesConfig } from "./validation/registry.js";
52
+ import { FormBuilderPluginOptions } from "./options.js";
55
53
  import { escapeHtml } from "./actions/body/escapeHtml.js";
56
54
  import { renderAllValues, renderAllValuesTable } from "./actions/body/wildcards.js";
57
55
  import { buildDefaultActionDefinitions, defaultActionDefinitions } from "./actions/builtin/index.js";
@@ -82,195 +80,9 @@ import { captureFileRef } from "./uploads/captureFileRef.js";
82
80
  import { defaultValidationRules, defaultValidationRulesByType } from "./validation/builtin/index.js";
83
81
  import { defineValidationRule } from "./validation/defineValidationRule.js";
84
82
  import { FieldTargetParamOptions, fieldTargetParam } from "./validation/fieldTargetParam.js";
85
- import { CollectionSlug } from "payload";
86
83
 
87
84
  //#region src/index.d.ts
88
- type FormBuilderPluginOptions = {
89
- disabled?: boolean;
90
- /**
91
- * Per-locale overrides for this plugin's UI strings, keyed by the typed
92
- * translation keys exported from `@10x-media/form-builder/i18n`. Values win
93
- * over the built-in locales key-by-key; locales the plugin does not ship are
94
- * added whole. App-level `i18n.translations` still wins over both.
95
- */
96
- translations?: TranslationsOption; /** Pluggable sink for form lifecycle events. Defaults to a no-op; analytics adapters or a future analytics plugin subscribe here. */
97
- events?: FormEventSink;
98
- /**
99
- * Content-bearing author fields (labels, placeholders, option labels, action subjects and
100
- * bodies) are localized by default. Payload strips the `localized` flag on hosts without
101
- * `localization` configured, so the default is safe everywhere. Set `false` to keep form
102
- * content single-locale even on localized hosts. Spread-overrides of the prebuilt default
103
- * exports (`defaultFieldDefinitionsByType`, `defaultActionDefinitions`) carry `localized`
104
- * flags from the default-true set; when opting out, derive overrides from
105
- * `buildDefaultFieldDefinitions(false)` / `buildDefaultActionDefinitions(false)` instead.
106
- * Consent statements are unaffected either way: they live on the host's own
107
- * `consentSourcesField()`, which carries its own `localized` option.
108
- */
109
- localizeContent?: boolean; /** Add, override, or remove field types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */
110
- fields?: FieldTypesConfig; /** Add, override, or remove validation rule types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */
111
- rules?: ValidationRulesConfig; /** Add, override, or remove post-submit action types. `false` removes a built-in, `true` keeps it, an object adds or replaces one. */
112
- actions?: ActionsConfig;
113
- /**
114
- * Customize how the plugin's rich text is authored and rendered. `editor` is the default
115
- * Lexical/richText editor for every plugin richText field (message content, consent
116
- * statement, response message, action bodies); `bodyEditor` overrides the action body
117
- * fields specifically, falling back to `editor`. `converters` spread over the default
118
- * Lexical node converters; `serialize` replaces the whole action-body pipeline (e.g. to
119
- * target chat or plain-text channels instead of email HTML). A custom `serialize` receives
120
- * the submitted `form` (id/title) and `req`, enabling per-tenant lookups or handing the raw
121
- * body off to a renderer like react-email.
122
- */
123
- richText?: RichTextBodyOption;
124
- /**
125
- * Email routing for the `emailTeam` and `confirmation` actions. Both sub-options are opt-in by the
126
- * presence of a resolver (not a `false`/`true`/object flag) and share a shape: a `req`-scoped
127
- * resolver, evaluated per request via an endpoint, whose choice is validated at save time only and
128
- * never re-checked when the action sends (the config is admin-authored, not visitor-controlled).
129
- *
130
- * `fromAddresses` gives both actions a `from` select whose options come from the resolver; absent,
131
- * neither action has a `from` field and every send uses the email adapter's default sender. The
132
- * intended use is multi-tenant hosts where each tenant may only send from particular addresses
133
- * (derive the tenant from `req`, return its allowed senders). Values are the literal string
134
- * `payload.sendEmail` accepts as `from` (e.g. `'Name <addr@x.com>'` or a plain address).
135
- *
136
- * `departments` turns the `emailTeam` `to` into a select whose options come from the resolver
137
- * (a `/:id/departments` endpoint); the intended use is to place `departmentsField()` on a document
138
- * you own and read it back with `resolveDepartmentOptions`, which resolves each department's address
139
- * for the requesting locale. Because `to` is localized, each admin locale stores its own resolved
140
- * address and a submission's locale selects the address it routes to (Payload-native, no per-send
141
- * lookup). Storing the resolved address, rather than a department id resolved live at send (consent's
142
- * model), is deliberate: the routing target a form was saved with stays audit-stable even if the
143
- * resolver's data later changes, so do not "fix" it into a live lookup. Multi-tenant hosts scope
144
- * which document they read by the tenant derived from `req`; absent, `to` stays a plain localized
145
- * text field.
146
- */
147
- email?: {
148
- fromAddresses?: FromAddressesResolver;
149
- departments?: DepartmentEmailsResolver;
150
- };
151
- /**
152
- * Where the consent statements a form can reference come from. Absent (the default): no sources,
153
- * so the built-in `consent` field type is not registered at all and authors cannot add a consent
154
- * field with nothing to reference (a developer-registered custom `consent` type via `fields`
155
- * still wins).
156
- *
157
- * `sources` is an async `req`-scoped resolver returning the sources available to this request.
158
- * The intended shape: place `consentSourcesField()` on a collection or global you own and read it
159
- * back here. Multi-tenant hosts scope that read by the tenant derived from `req`, so a tenant's
160
- * authors only ever see, and their visitors only ever agree to, their own statements. A form
161
- * stores only a source's row `id`; the statement is resolved live per request (see
162
- * `resolveConsentStatements`) and the proof is rebuilt from the source at submit, so neither is
163
- * ever a copy the client could stale or forge.
164
- */
165
- consent?: {
166
- sources: ConsentSourcesResolver;
167
- };
168
- /**
169
- * Form-level button labels: `submitLabel` at the bottom of the Fields tab, `prevLabel` and
170
- * `nextLabel` in a row at the bottom of the Flow tab (shown once the flow has a step). The
171
- * rendered chrome resolves each label as `<Form>` prop, then the stored value, then the
172
- * translated default. `fields` composes them: it receives the three default fields as a
173
- * `{ submit, prev, next }` map with the content localization flag already applied and returns
174
- * the map, so wrapping a default in a row with a host field (e.g. an icon select) or replacing
175
- * one is explicit. `FormDocument.buttons` reassembles only those three known labels, so a
176
- * host-added sibling field is still stored but is read off the raw document, not `doc.buttons`.
177
- */
178
- buttons?: ButtonsOption;
179
- /**
180
- * The success-response group. `redirect.fields` composes the fields inside the `response.redirect`
181
- * group, mirroring `buttons.fields`: it receives the default fields (the `url` text field, plus the
182
- * polymorphic `reference` relationship when `redirectRelationships` is set) and returns the group's
183
- * final field array, so a host can prepend a custom link field (the pattern most projects already
184
- * have), swap `url` for their own picker, reorder, or filter. Omit it and the group stays the
185
- * built-in `url` (+ optional `reference`), unchanged.
186
- *
187
- * The plugin's built-in redirect handling reads `redirect.url` (and `redirect.reference` when
188
- * configured); `toFormDocument` passes the whole `response` group through, so a host that replaces
189
- * `url` with their own link field owns resolving it to a destination in their frontend, the same as
190
- * the internal-reference case.
191
- */
192
- response?: ResponseOption;
193
- /**
194
- * Collections whose documents `response.redirect` can reference (`response.redirect.reference`),
195
- * letting an author redirect a visitor to an internal document instead of a URL after a
196
- * successful submit. Absent (the default): no `reference` field exists at all, matching today's
197
- * URL-only redirect. Always polymorphic, even for a single slug (a `CollectionSlug[]` array, not
198
- * a bare `CollectionSlug`), so a host adding a second collection later never changes the stored
199
- * shape, the same precedent as `consentSourcesField()`'s `page` picker. The plugin never resolves
200
- * the reference to a URL itself (it has no notion of the host's routing); `toFormDocument` passes
201
- * the raw `{ relationTo, value }` pair through on `FormDocument.response.redirect.reference` for
202
- * the host to resolve. Mirrors the `redirectRelationships` option of Payload's native
203
- * `plugin-form-builder`.
204
- */
205
- redirectRelationships?: CollectionSlug[];
206
- /**
207
- * File uploads are bring-your-own. Default `false`: no upload collection is involved and the
208
- * built-in `file` field type is removed from the registry, so form authors cannot add a field
209
- * with nowhere to land (a developer-registered custom `file` type via `fields` still wins).
210
- * `{ collection: 'slug' }` points at a host-owned upload collection (created by the app with
211
- * its storage adapter); the plugin validates it at boot, appends its hidden `owner` field when
212
- * absent, and prepends the spam upload hooks.
213
- */
214
- uploads?: UploadsOption; /** Honeypot + rate-limiting (on by default) + a captcha adapter seam + upload-ownership scoping. `false` disables the whole subsystem. */
215
- spam?: SpamOption;
216
- /**
217
- * Aggregate-results endpoint options. `access` gates anonymous reads after the form is loaded
218
- * and before anything is served; absent keeps the plugin-default gating (poll opt-in +
219
- * visibility + enumerable-field guard). Multi-tenant hosts should compare `form.tenant` against
220
- * the tenant derived from `req` so one tenant's poll counts are never readable under another
221
- * tenant's id. Authenticated callers bypass this seam.
222
- */
223
- results?: {
224
- access?: FormResultsAccess;
225
- };
226
- /**
227
- * Poll behavior. `votedCookie: true` sets an httpOnly `fb-voted-{formId}=1` cookie on each
228
- * successful submission to a poll-enabled form, letting SSR hosts read the voted state via
229
- * `hasVotedCookie` and pass it to `<Poll hasVoted>`. Default `false`.
230
- * `sources` registers poll option sources (`definePollOptionSource`), letting authors populate
231
- * a poll's choices from host domain data with stable values; there are no built-ins. With at
232
- * least one source registered, the forms poll group gains an `optionSource` select plus its
233
- * per-source `sourceConfig`, and submissions to a sourced poll only accept resolved values.
234
- * `outcomeFields` composes the poll `outcome` group: it receives the two default fields
235
- * (`winningValues`, `resolvedAt`) and returns the group's final field array verbatim, so a host
236
- * can swap `winningValues` for its own component (e.g. a relationship picker over the voteable
237
- * records). Membership validation runs server-side regardless, so a swap cannot bypass it.
238
- * `types` registers outcome strategies (`definePollType`) shown in the poll `type` select. The
239
- * built-ins `manual` (default, hand-picked), `mostVoted` (auto-resolves to the top choice(s) on
240
- * close), and `source` (delegates to the option source) are always registered; a host entry keyed
241
- * by (or carrying) a built-in slug replaces it. A closed poll whose strategy is not `manual`
242
- * auto-resolves via a scheduled job when a runner is present, or on the next results read otherwise.
243
- */
244
- poll?: {
245
- votedCookie?: boolean;
246
- sources?: PollOptionSourcesConfig;
247
- types?: PollTypesConfig;
248
- outcomeFields?: OutcomeFieldsOverride;
249
- };
250
- /**
251
- * When `true`, the raw `values`, `descriptors`, and `consent` JSON fields are visible in the
252
- * submission admin view. Default `false`, because the `SubmissionAnswers` UI component already
253
- * represents those fields fully and they are noisy shown alongside it.
254
- */
255
- showSubmissionRawFields?: boolean;
256
- /**
257
- * Override individual plugin-managed collections using explicit spreads. Each key accepts a
258
- * `CollectionOverrides` object: top-level keys are spread with the plugin's defaults (spread
259
- * order per key determines who wins), hooks are appended after the plugin's own hooks, and
260
- * `fields` is a function that receives the default fields and returns the final array so
261
- * additions/removals are always intentional.
262
- */
263
- overrides?: {
264
- forms?: CollectionOverrides;
265
- formSubmissions?: CollectionOverrides;
266
- };
267
- };
268
- declare module 'payload' {
269
- interface RegisteredPlugins {
270
- '@10x-media/form-builder': FormBuilderPluginOptions;
271
- }
272
- }
273
85
  declare const formBuilder: (options: FormBuilderPluginOptions) => import("payload").Plugin;
274
86
  //#endregion
275
- export { type ActionDefinition, type ActionOption, type ActionRegistry, type ActionResult, type ActionRunArgs, type ActionsConfig, type AggregateFieldResponsesArgs, type AggregateFormResponsesArgs, type AggregationBucket, type AggregationRow, type AnyActionDefinition, type AnyFormFieldDefinition, type AnyPollOptionSource, type AnyValidationRuleDefinition, type BodyContext, type BodyConverter, type BodyConverterArgs, type BodyRender, type ButtonFieldsOverride, type ButtonsOption, CAPTCHA_TOKEN_KEY, type CalcExpression, type CaptchaProvider, type CaptchaVerifyArgs, type ConsentProof, type ConsentSourceEntry, type ConsentSourceOption, type ConsentSourcePage, type ConsentSourcesFieldOptions, type ConsentSourcesResolver, type ConsentStatement, type ConsentStatements, type CreateSubmissionArgs, type CreatedSubmission, DEFAULT_HONEYPOT_FIELD, DEFAULT_PRESENTATION_NAME, type DefaultButtonFields, type DefaultOutcomeFields, type DepartmentEmailsResolver, type DepartmentOption, type DepartmentsFieldOptions, type FieldAggregation, type FieldCondition, type FieldMeta, type FieldTargetParamOptions, type FieldTypeOption, type FieldTypeRegistry, type FieldTypesConfig, type FileFieldConfig, type FileRef, type FileRefError, FormBuilderPluginOptions, type FormBuilderPluginOptions as PluginOptions, type FormButtonSettings, type FormDocument, type FormFieldDefinition, type FormFieldFormat, type FormFieldValidate, type FormFieldValueKind, type FormPollSettings, type FormResponseSettings, type FormResultsAccess, type FormResultsAccessArgs, type FromAddressOption, type FromAddressesResolver, type HcaptchaProviderOptions, type IdentifyFn, type OmittableSharedField, type OutcomeFieldsOverride, POLL_CLOSE_TASK_SLUG, type PollCloseTaskInput, type PollOption, type PollOptionResolveArgs, type PollOptionSource, type PollOptionSourceOption, type PollOptionSourceRegistry, type PollOptionSourcesConfig, type PollOutcomeStrategy, type PollOutcomeStrategyArgs, type PollTypeRegistry, type PollTypesConfig, type PrefillOptions, type PresentationDensity, type PresentationDescriptor, type PresentationSurface, type RateLimitCheckArgs, type RateLimitConfig, type RateLimitResult, type RateLimiter, type RecallResolver, type RecaptchaProviderOptions, type RedirectFieldsOverride, type RedirectOption, type ResolveConsentEntriesArgs, type ResolveConsentSourcesRequestArgs, type ResolveConsentSourcesRequestResult, type ResolveConsentStatementsArgs, type ResolveDepartmentOptionsArgs, type ResolveDepartmentsRequestArgs, type ResolveDepartmentsRequestResult, type ResolveEffectivePollOptionsArgs, type ResolveFieldOptionsArgs, type ResolvePollOptionsArgs, type ResolvePollOutcomeArgs, type ResolvePublishedVersionRefArgs, type ResolveResultsRequestArgs, type ResolveResultsRequestResult, type ResponseOption, type RichTextBodyOption, SIGNATURE_HEADER, type SerializeBodyArgs, type SpamConfig, type SpamMetadataConfig, type SpamOption, type SubmissionStatusFilter, type ToFormDocumentOptions, type TurnstileProviderOptions, type UploadsOption, type ValidationRuleDefinition, type ValidationRuleOption, type ValidationRuleRegistry, type ValidationRuleResult, type ValidationRulesConfig, aggregateFieldResponses, aggregateFormResponses, aggregateRowForField, aggregateRowsForFields, applyConsentStatements, buildDefaultActionDefinitions, buildDefaultButtonFields, buildDefaultFieldDefinitions, buildDefaultOutcomeFields, buildNextLabelField, buildPollCloseTask, buildPrevLabelField, buildRecallResolver, buildResolvedAtField, buildSubmitLabelField, buildWinningValuesField, calcExpressionOf, captureConsent, captureFileRef, computeCalcFields, consentSourcesField, countryField, createKvRateLimiter, createSubmission, defaultActionDefinitions, defaultBodyConverters, defaultFieldDefinitions, defaultFieldDefinitionsByType, defaultIdentify, defaultPresentationDescriptors, defaultValidationRules, defaultValidationRulesByType, defineAction, defineCaptchaProvider, defineFormField, definePollOptionSource, definePollType, defineValidationRule, departmentsField, enqueuePollClose, escapeHtml, evaluateCalc, evaluateCondition, fieldHasOptions, fieldKey, fieldTargetParam, fileMimeTypeOptions, formBuilder, formatBytes, hasVotedCookie, hcaptchaProvider, interpolate, isPollClosed, localizedIf, manualStrategy, mostVotedStrategy, normalizeCalc, optionLabelsFor, pollTypesOf, recaptchaProvider, registerPollCloseTask, renderAllValues, renderAllValuesTable, resolveActions, resolveConsentEntries, resolveConsentSourcesRequest, resolveConsentStatements, resolveDepartmentOptions, resolveEffectivePollOptions, resolveFileRef, resolveFormResultsRequest, resolvePollOptionSources, resolvePollOptions, resolvePollOutcome, resolvePollTypes, resolvePublishedVersionRef, resolveSpamConfig, runPollClose, sanitizeUrl, serializeBody, shouldAutoResolvePoll, signPayload, sourceStrategy, stashPollTypes, stateField, textOfBody, toFormDocument, topBucketValues, turnstileProvider, valuesFromSearchParams, votedCookieName };
87
+ export { type ActionDefinition, type ActionOption, type ActionRegistry, type ActionResult, type ActionRunArgs, type ActionsConfig, type AggregateFieldResponsesArgs, type AggregateFormResponsesArgs, type AggregationBucket, type AggregationRow, type AnyActionDefinition, type AnyFormFieldDefinition, type AnyPollOptionSource, type AnyValidationRuleDefinition, type BodyContext, type BodyConverter, type BodyConverterArgs, type BodyRender, type ButtonFieldsOverride, type ButtonsOption, CAPTCHA_TOKEN_KEY, type CalcExpression, type CaptchaProvider, type CaptchaVerifyArgs, type ConsentProof, type ConsentSnapshotMode, type ConsentSourceEntry, type ConsentSourceOption, type ConsentSourcePage, type ConsentSourcesFieldOptions, type ConsentSourcesResolver, type ConsentStatement, type ConsentStatements, type CreateSubmissionArgs, type CreatedSubmission, DEFAULT_HONEYPOT_FIELD, DEFAULT_PRESENTATION_NAME, type DefaultButtonFields, type DefaultOutcomeFields, type DepartmentEmailsResolver, type DepartmentOption, type DepartmentsFieldOptions, type FieldAggregation, type FieldCondition, type FieldMeta, type FieldTargetParamOptions, type FieldTypeOption, type FieldTypeRegistry, type FieldTypesConfig, type FileFieldConfig, type FileRef, type FileRefError, type FormBuilderPluginOptions, type FormButtonSettings, type FormDocument, type FormFieldDefinition, type FormFieldFormat, type FormFieldValidate, type FormFieldValueKind, type FormPollSettings, type FormResponseSettings, type FormResultsAccess, type FormResultsAccessArgs, type FromAddressOption, type FromAddressesResolver, type HcaptchaProviderOptions, type IdentifyFn, type OmittableSharedField, type OutcomeFieldsOverride, POLL_CLOSE_TASK_SLUG, type FormBuilderPluginOptions as PluginOptions, type PollCloseTaskInput, type PollOption, type PollOptionResolveArgs, type PollOptionSource, type PollOptionSourceOption, type PollOptionSourceRegistry, type PollOptionSourcesConfig, type PollOutcomeStrategy, type PollOutcomeStrategyArgs, type PollTypeRegistry, type PollTypesConfig, type PrefillOptions, type PresentationDensity, type PresentationDescriptor, type PresentationSurface, type RateLimitCheckArgs, type RateLimitConfig, type RateLimitResult, type RateLimiter, type RecallResolver, type RecaptchaProviderOptions, type RedirectFieldsOverride, type RedirectOption, type ResolveConsentEntriesArgs, type ResolveConsentSourcesRequestArgs, type ResolveConsentSourcesRequestResult, type ResolveConsentStatementsArgs, type ResolveDepartmentOptionsArgs, type ResolveDepartmentsRequestArgs, type ResolveDepartmentsRequestResult, type ResolveEffectivePollOptionsArgs, type ResolveFieldOptionsArgs, type ResolvePollOptionsArgs, type ResolvePollOutcomeArgs, type ResolvePublishedVersionRefArgs, type ResolveResultsRequestArgs, type ResolveResultsRequestResult, type ResponseOption, type RichTextBodyOption, SIGNATURE_HEADER, type SerializeBodyArgs, type SpamConfig, type SpamMetadataConfig, type SpamOption, type SubmissionStatusFilter, type ToFormDocumentOptions, type TurnstileProviderOptions, type UploadsOption, type ValidationRuleDefinition, type ValidationRuleOption, type ValidationRuleRegistry, type ValidationRuleResult, type ValidationRulesConfig, aggregateFieldResponses, aggregateFormResponses, aggregateRowForField, aggregateRowsForFields, applyConsentStatements, buildDefaultActionDefinitions, buildDefaultButtonFields, buildDefaultFieldDefinitions, buildDefaultOutcomeFields, buildNextLabelField, buildPollCloseTask, buildPrevLabelField, buildRecallResolver, buildResolvedAtField, buildSubmitLabelField, buildWinningValuesField, calcExpressionOf, captureConsent, captureFileRef, computeCalcFields, consentSourcesField, countryField, createKvRateLimiter, createSubmission, defaultActionDefinitions, defaultBodyConverters, defaultFieldDefinitions, defaultFieldDefinitionsByType, defaultIdentify, defaultPresentationDescriptors, defaultValidationRules, defaultValidationRulesByType, defineAction, defineCaptchaProvider, defineFormField, definePollOptionSource, definePollType, defineValidationRule, departmentsField, enqueuePollClose, escapeHtml, evaluateCalc, evaluateCondition, fieldHasOptions, fieldKey, fieldTargetParam, fileMimeTypeOptions, formBuilder, formatBytes, hasVotedCookie, hcaptchaProvider, interpolate, isPollClosed, localizedIf, manualStrategy, mostVotedStrategy, normalizeCalc, optionLabelsFor, pollTypesOf, recaptchaProvider, registerPollCloseTask, renderAllValues, renderAllValuesTable, resolveActions, resolveConsentEntries, resolveConsentSourcesRequest, resolveConsentStatements, resolveDepartmentOptions, resolveEffectivePollOptions, resolveFileRef, resolveFormResultsRequest, resolvePollOptionSources, resolvePollOptions, resolvePollOutcome, resolvePollTypes, resolvePublishedVersionRef, resolveSpamConfig, runPollClose, sanitizeUrl, serializeBody, shouldAutoResolvePoll, signPayload, sourceStrategy, stashPollTypes, stateField, textOfBody, toFormDocument, topBucketValues, turnstileProvider, valuesFromSearchParams, votedCookieName };
276
88
  //# sourceMappingURL=index.d.ts.map