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

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @10x-media/form-builder
2
2
 
3
+ ## 0.1.0-beta.8
4
+
5
+ ### Minor Changes
6
+
7
+ - Second feedback round: submit control, step field order, response editor, and host-rendered success.
8
+
9
+ - **Implicit Enter-submit is suppressed on multi-step forms.** A form-level key handler prevents a lone text input from submitting the whole form on Enter; only the explicit Submit control submits. Textareas (newline), selects (confirm), and buttons are exempt, and single-step forms keep native Enter-to-submit. The `<form>` is the plugin's even in `children` mode, so this is the only place a host could apply the guard.
10
+ - **Step fields render in `form.fields` order.** A step's `fields` list is treated as membership only; the render order follows the form's field order rather than the flow-builder entry order.
11
+ - **`richText.responseEditor`.** The success response message field's editor can now be set independently via `richText.responseEditor` (falling back to `richText.editor`), mirroring `bodyEditor` for action bodies. The message block stays on the plugin-wide `editor`.
12
+ - **Host-rendered success.** `onSuccess` gains a second argument, `{ response }`, carrying the recall-resolved success response (a redirect url, or the message serialized with the form's converters), so a host can toast or render it; `<Poll>` forwards the same payload to its host. A new `converters` prop on `<Form>` threads custom block converters (e.g. host `icon`/`badge` blocks) into the client serializer, so those blocks survive in the success message, the resolved response, and inline `message` blocks. `successBehavior: 'reset'` clears the form in place after a successful submit instead of swapping in the success screen (returning a multi-step form to its first step), so a host can keep the form usable and give feedback via `onSuccess`. Defaults (`onSuccess`'s first argument, `successBehavior: 'replace'`) are unchanged. New exported types: `FormSuccessResponse`, `FormSuccessResult`.
13
+
3
14
  ## 0.1.0-beta.7
4
15
 
5
16
  ### Minor Changes
@@ -33,13 +33,15 @@ type SerializeBodyArgs = {
33
33
  * off to a renderer like react-email). `editor` is the default Lexical/richText editor for every
34
34
  * plugin-authored richText field: message content, consent statement, the response message, and
35
35
  * the action body fields. `bodyEditor` overrides the action body fields specifically (emailTeam
36
- * and confirmation), falling back to `editor` when absent.
36
+ * and confirmation), and `responseEditor` overrides the success `response` message field; both fall
37
+ * back to `editor` when absent.
37
38
  */
38
39
  type RichTextBodyOption = {
39
40
  converters?: Record<string, BodyConverter>;
40
41
  serialize?: (args: SerializeBodyArgs) => Promise<string> | string;
41
42
  editor?: RichTextField['editor'];
42
43
  bodyEditor?: RichTextField['editor'];
44
+ responseEditor?: RichTextField['editor'];
43
45
  };
44
46
  /**
45
47
  * Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is
@@ -1 +1 @@
1
- {"version":3,"file":"serializeBody.js","names":[],"sources":["../../../src/actions/body/serializeBody.ts"],"sourcesContent":["import type { PayloadRequest, RichTextField } from 'payload'\nimport { interpolate } from '../../recall/interpolate'\nimport type { SubmissionDescriptor, SubmissionValue } from '../../submissions/types'\nimport type { BodyConverter, BodyRender } from './converters'\nimport { defaultBodyConverters } from './converters'\nimport { escapeHtml } from './escapeHtml'\nimport { serializeSlate } from './serializeSlate'\nimport { renderAllValues, renderAllValuesTable } from './wildcards'\n\n/** Minimal form identity threaded alongside a rendered body (e.g. per-tenant template lookups). */\ntype SerializeBodyForm = { id: number | string; title?: string }\n\n/** Submission data plus optional converter overrides available while serializing a body. */\nexport type BodyContext = {\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tconverters?: Record<string, BodyConverter>\n}\n\n/**\n * Args a custom `richText.serialize` replacement receives per rendered body. Always populated by\n * `makeRenderBody` (the action-body pipeline): `form` and `req` enable per-tenant template\n * lookups or handing the body off to a renderer like react-email.\n */\nexport type SerializeBodyArgs = {\n\tbody: unknown\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tform: SerializeBodyForm\n\treq?: PayloadRequest\n}\n\n/**\n * Customizes how the plugin's rich text is authored and rendered. `converters` spread over the\n * default Lexical node converters; `serialize` replaces the whole action-body pipeline (for\n * non-HTML channels like chat or plain text, or to hand the body plus the submitted `form`/`req`\n * off to a renderer like react-email). `editor` is the default Lexical/richText editor for every\n * plugin-authored richText field: message content, consent statement, the response message, and\n * the action body fields. `bodyEditor` overrides the action body fields specifically (emailTeam\n * and confirmation), falling back to `editor` when absent.\n */\nexport type RichTextBodyOption = {\n\tconverters?: Record<string, BodyConverter>\n\tserialize?: (args: SerializeBodyArgs) => Promise<string> | string\n\teditor?: RichTextField['editor']\n\tbodyEditor?: RichTextField['editor']\n}\n\n/** Recall resolver over submission values: field name to stringified value, `''` when absent. */\nexport const resolverFor =\n\t(values: SubmissionValue[]) =>\n\t(name: string): string => {\n\t\tconst entry = values.find((value) => value.field === name)\n\t\treturn entry == null ? '' : String(entry.value ?? '')\n\t}\n\nconst renderFor = (ctx: BodyContext): BodyRender => {\n\tconst resolve = resolverFor(ctx.values)\n\tconst htmlResolve = (name: string): string => {\n\t\tif (name === '*') {\n\t\t\treturn renderAllValues(ctx.values, ctx.descriptors)\n\t\t}\n\t\tif (name === '*:table') {\n\t\t\treturn renderAllValuesTable(ctx.values, ctx.descriptors)\n\t\t}\n\t\treturn escapeHtml(resolve(name))\n\t}\n\treturn {\n\t\ttext: (raw) => interpolate(escapeHtml(raw), htmlResolve),\n\t\tinterpolate: (raw) => interpolate(raw, resolve),\n\t}\n}\n\nconst serializeNodes = (\n\tnodes: unknown[],\n\tconverters: Record<string, BodyConverter>,\n\trender: BodyRender\n): string =>\n\tnodes\n\t\t.map((node) => {\n\t\t\tif (node == null || typeof node !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst lexicalNode = node as Record<string, unknown>\n\t\t\tconst children = Array.isArray(lexicalNode.children)\n\t\t\t\t? serializeNodes(lexicalNode.children, converters, render)\n\t\t\t\t: ''\n\t\t\tconst converter =\n\t\t\t\ttypeof lexicalNode.type === 'string' ? converters[lexicalNode.type] : undefined\n\t\t\treturn converter ? converter({ node: lexicalNode, children, render }) : children\n\t\t})\n\t\t.join('')\n\nconst lexicalRootOf = (body: unknown): Record<string, unknown> | null => {\n\tif (body == null || typeof body !== 'object' || Array.isArray(body)) {\n\t\treturn null\n\t}\n\tconst root = (body as { root?: unknown }).root\n\treturn root != null && typeof root === 'object' ? (root as Record<string, unknown>) : null\n}\n\n/**\n * Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is\n * (pre-richText behavior, no escaping); a Lexical state walks the converter registry; a Slate\n * array uses the minimal legacy serializer; anything else yields `''`. Rendered text is\n * HTML-escaped and supports `{{ name|fallback }}`, `{{*}}`, and `{{*:table}}` tokens.\n */\nexport const serializeBody = (body: unknown, ctx: BodyContext): string => {\n\tif (typeof body === 'string') {\n\t\treturn interpolate(body, resolverFor(ctx.values))\n\t}\n\tconst render = renderFor(ctx)\n\tif (Array.isArray(body)) {\n\t\treturn serializeSlate(body, render)\n\t}\n\tconst root = lexicalRootOf(body)\n\tif (root) {\n\t\tconst converters = { ...defaultBodyConverters, ...(ctx.converters ?? {}) }\n\t\treturn serializeNodes(Array.isArray(root.children) ? root.children : [], converters, render)\n\t}\n\treturn ''\n}\n\n/** Build the `renderBody` passed to actions, honoring a plugin-level `richText` customization. */\nexport const makeRenderBody =\n\t(args: {\n\t\tvalues: SubmissionValue[]\n\t\tdescriptors: SubmissionDescriptor[]\n\t\tform: SerializeBodyForm\n\t\treq?: PayloadRequest\n\t\trichText?: RichTextBodyOption\n\t}) =>\n\tasync (body: unknown): Promise<string> => {\n\t\tif (args.richText?.serialize) {\n\t\t\treturn await args.richText.serialize({\n\t\t\t\tbody,\n\t\t\t\tvalues: args.values,\n\t\t\t\tdescriptors: args.descriptors,\n\t\t\t\tform: args.form,\n\t\t\t\treq: args.req,\n\t\t\t})\n\t\t}\n\t\treturn serializeBody(body, {\n\t\t\tvalues: args.values,\n\t\t\tdescriptors: args.descriptors,\n\t\t\tconverters: args.richText?.converters,\n\t\t})\n\t}\n"],"mappings":";;;;;;;AAiDA,MAAa,eACX,YACA,SAAyB;CACzB,MAAM,QAAQ,OAAO,MAAM,UAAU,MAAM,UAAU,IAAI;CACzD,OAAO,SAAS,OAAO,KAAK,OAAO,MAAM,SAAS,EAAE;AACrD;AAED,MAAM,aAAa,QAAiC;CACnD,MAAM,UAAU,YAAY,IAAI,MAAM;CACtC,MAAM,eAAe,SAAyB;EAC7C,IAAI,SAAS,KACZ,OAAO,gBAAgB,IAAI,QAAQ,IAAI,WAAW;EAEnD,IAAI,SAAS,WACZ,OAAO,qBAAqB,IAAI,QAAQ,IAAI,WAAW;EAExD,OAAO,WAAW,QAAQ,IAAI,CAAC;CAChC;CACA,OAAO;EACN,OAAO,QAAQ,YAAY,WAAW,GAAG,GAAG,WAAW;EACvD,cAAc,QAAQ,YAAY,KAAK,OAAO;CAC/C;AACD;AAEA,MAAM,kBACL,OACA,YACA,WAEA,MACE,KAAK,SAAS;CACd,IAAI,QAAQ,QAAQ,OAAO,SAAS,UACnC,OAAO;CAER,MAAM,cAAc;CACpB,MAAM,WAAW,MAAM,QAAQ,YAAY,QAAQ,IAChD,eAAe,YAAY,UAAU,YAAY,MAAM,IACvD;CACH,MAAM,YACL,OAAO,YAAY,SAAS,WAAW,WAAW,YAAY,QAAQ,KAAA;CACvE,OAAO,YAAY,UAAU;EAAE,MAAM;EAAa;EAAU;CAAO,CAAC,IAAI;AACzE,CAAC,EACA,KAAK,EAAE;AAEV,MAAM,iBAAiB,SAAkD;CACxE,IAAI,QAAQ,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GACjE,OAAO;CAER,MAAM,OAAQ,KAA4B;CAC1C,OAAO,QAAQ,QAAQ,OAAO,SAAS,WAAY,OAAmC;AACvF;;;;;;;AAQA,MAAa,iBAAiB,MAAe,QAA6B;CACzE,IAAI,OAAO,SAAS,UACnB,OAAO,YAAY,MAAM,YAAY,IAAI,MAAM,CAAC;CAEjD,MAAM,SAAS,UAAU,GAAG;CAC5B,IAAI,MAAM,QAAQ,IAAI,GACrB,OAAO,eAAe,MAAM,MAAM;CAEnC,MAAM,OAAO,cAAc,IAAI;CAC/B,IAAI,MAAM;EACT,MAAM,aAAa;GAAE,GAAG;GAAuB,GAAI,IAAI,cAAc,CAAC;EAAG;EACzE,OAAO,eAAe,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC,GAAG,YAAY,MAAM;CAC5F;CACA,OAAO;AACR;;AAGA,MAAa,kBACX,SAOD,OAAO,SAAmC;CACzC,IAAI,KAAK,UAAU,WAClB,OAAO,MAAM,KAAK,SAAS,UAAU;EACpC;EACA,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,MAAM,KAAK;EACX,KAAK,KAAK;CACX,CAAC;CAEF,OAAO,cAAc,MAAM;EAC1B,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,YAAY,KAAK,UAAU;CAC5B,CAAC;AACF"}
1
+ {"version":3,"file":"serializeBody.js","names":[],"sources":["../../../src/actions/body/serializeBody.ts"],"sourcesContent":["import type { PayloadRequest, RichTextField } from 'payload'\nimport { interpolate } from '../../recall/interpolate'\nimport type { SubmissionDescriptor, SubmissionValue } from '../../submissions/types'\nimport type { BodyConverter, BodyRender } from './converters'\nimport { defaultBodyConverters } from './converters'\nimport { escapeHtml } from './escapeHtml'\nimport { serializeSlate } from './serializeSlate'\nimport { renderAllValues, renderAllValuesTable } from './wildcards'\n\n/** Minimal form identity threaded alongside a rendered body (e.g. per-tenant template lookups). */\ntype SerializeBodyForm = { id: number | string; title?: string }\n\n/** Submission data plus optional converter overrides available while serializing a body. */\nexport type BodyContext = {\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tconverters?: Record<string, BodyConverter>\n}\n\n/**\n * Args a custom `richText.serialize` replacement receives per rendered body. Always populated by\n * `makeRenderBody` (the action-body pipeline): `form` and `req` enable per-tenant template\n * lookups or handing the body off to a renderer like react-email.\n */\nexport type SerializeBodyArgs = {\n\tbody: unknown\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tform: SerializeBodyForm\n\treq?: PayloadRequest\n}\n\n/**\n * Customizes how the plugin's rich text is authored and rendered. `converters` spread over the\n * default Lexical node converters; `serialize` replaces the whole action-body pipeline (for\n * non-HTML channels like chat or plain text, or to hand the body plus the submitted `form`/`req`\n * off to a renderer like react-email). `editor` is the default Lexical/richText editor for every\n * plugin-authored richText field: message content, consent statement, the response message, and\n * the action body fields. `bodyEditor` overrides the action body fields specifically (emailTeam\n * and confirmation), and `responseEditor` overrides the success `response` message field; both fall\n * back to `editor` when absent.\n */\nexport type RichTextBodyOption = {\n\tconverters?: Record<string, BodyConverter>\n\tserialize?: (args: SerializeBodyArgs) => Promise<string> | string\n\teditor?: RichTextField['editor']\n\tbodyEditor?: RichTextField['editor']\n\tresponseEditor?: RichTextField['editor']\n}\n\n/** Recall resolver over submission values: field name to stringified value, `''` when absent. */\nexport const resolverFor =\n\t(values: SubmissionValue[]) =>\n\t(name: string): string => {\n\t\tconst entry = values.find((value) => value.field === name)\n\t\treturn entry == null ? '' : String(entry.value ?? '')\n\t}\n\nconst renderFor = (ctx: BodyContext): BodyRender => {\n\tconst resolve = resolverFor(ctx.values)\n\tconst htmlResolve = (name: string): string => {\n\t\tif (name === '*') {\n\t\t\treturn renderAllValues(ctx.values, ctx.descriptors)\n\t\t}\n\t\tif (name === '*:table') {\n\t\t\treturn renderAllValuesTable(ctx.values, ctx.descriptors)\n\t\t}\n\t\treturn escapeHtml(resolve(name))\n\t}\n\treturn {\n\t\ttext: (raw) => interpolate(escapeHtml(raw), htmlResolve),\n\t\tinterpolate: (raw) => interpolate(raw, resolve),\n\t}\n}\n\nconst serializeNodes = (\n\tnodes: unknown[],\n\tconverters: Record<string, BodyConverter>,\n\trender: BodyRender\n): string =>\n\tnodes\n\t\t.map((node) => {\n\t\t\tif (node == null || typeof node !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst lexicalNode = node as Record<string, unknown>\n\t\t\tconst children = Array.isArray(lexicalNode.children)\n\t\t\t\t? serializeNodes(lexicalNode.children, converters, render)\n\t\t\t\t: ''\n\t\t\tconst converter =\n\t\t\t\ttypeof lexicalNode.type === 'string' ? converters[lexicalNode.type] : undefined\n\t\t\treturn converter ? converter({ node: lexicalNode, children, render }) : children\n\t\t})\n\t\t.join('')\n\nconst lexicalRootOf = (body: unknown): Record<string, unknown> | null => {\n\tif (body == null || typeof body !== 'object' || Array.isArray(body)) {\n\t\treturn null\n\t}\n\tconst root = (body as { root?: unknown }).root\n\treturn root != null && typeof root === 'object' ? (root as Record<string, unknown>) : null\n}\n\n/**\n * Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is\n * (pre-richText behavior, no escaping); a Lexical state walks the converter registry; a Slate\n * array uses the minimal legacy serializer; anything else yields `''`. Rendered text is\n * HTML-escaped and supports `{{ name|fallback }}`, `{{*}}`, and `{{*:table}}` tokens.\n */\nexport const serializeBody = (body: unknown, ctx: BodyContext): string => {\n\tif (typeof body === 'string') {\n\t\treturn interpolate(body, resolverFor(ctx.values))\n\t}\n\tconst render = renderFor(ctx)\n\tif (Array.isArray(body)) {\n\t\treturn serializeSlate(body, render)\n\t}\n\tconst root = lexicalRootOf(body)\n\tif (root) {\n\t\tconst converters = { ...defaultBodyConverters, ...(ctx.converters ?? {}) }\n\t\treturn serializeNodes(Array.isArray(root.children) ? root.children : [], converters, render)\n\t}\n\treturn ''\n}\n\n/** Build the `renderBody` passed to actions, honoring a plugin-level `richText` customization. */\nexport const makeRenderBody =\n\t(args: {\n\t\tvalues: SubmissionValue[]\n\t\tdescriptors: SubmissionDescriptor[]\n\t\tform: SerializeBodyForm\n\t\treq?: PayloadRequest\n\t\trichText?: RichTextBodyOption\n\t}) =>\n\tasync (body: unknown): Promise<string> => {\n\t\tif (args.richText?.serialize) {\n\t\t\treturn await args.richText.serialize({\n\t\t\t\tbody,\n\t\t\t\tvalues: args.values,\n\t\t\t\tdescriptors: args.descriptors,\n\t\t\t\tform: args.form,\n\t\t\t\treq: args.req,\n\t\t\t})\n\t\t}\n\t\treturn serializeBody(body, {\n\t\t\tvalues: args.values,\n\t\t\tdescriptors: args.descriptors,\n\t\t\tconverters: args.richText?.converters,\n\t\t})\n\t}\n"],"mappings":";;;;;;;AAmDA,MAAa,eACX,YACA,SAAyB;CACzB,MAAM,QAAQ,OAAO,MAAM,UAAU,MAAM,UAAU,IAAI;CACzD,OAAO,SAAS,OAAO,KAAK,OAAO,MAAM,SAAS,EAAE;AACrD;AAED,MAAM,aAAa,QAAiC;CACnD,MAAM,UAAU,YAAY,IAAI,MAAM;CACtC,MAAM,eAAe,SAAyB;EAC7C,IAAI,SAAS,KACZ,OAAO,gBAAgB,IAAI,QAAQ,IAAI,WAAW;EAEnD,IAAI,SAAS,WACZ,OAAO,qBAAqB,IAAI,QAAQ,IAAI,WAAW;EAExD,OAAO,WAAW,QAAQ,IAAI,CAAC;CAChC;CACA,OAAO;EACN,OAAO,QAAQ,YAAY,WAAW,GAAG,GAAG,WAAW;EACvD,cAAc,QAAQ,YAAY,KAAK,OAAO;CAC/C;AACD;AAEA,MAAM,kBACL,OACA,YACA,WAEA,MACE,KAAK,SAAS;CACd,IAAI,QAAQ,QAAQ,OAAO,SAAS,UACnC,OAAO;CAER,MAAM,cAAc;CACpB,MAAM,WAAW,MAAM,QAAQ,YAAY,QAAQ,IAChD,eAAe,YAAY,UAAU,YAAY,MAAM,IACvD;CACH,MAAM,YACL,OAAO,YAAY,SAAS,WAAW,WAAW,YAAY,QAAQ,KAAA;CACvE,OAAO,YAAY,UAAU;EAAE,MAAM;EAAa;EAAU;CAAO,CAAC,IAAI;AACzE,CAAC,EACA,KAAK,EAAE;AAEV,MAAM,iBAAiB,SAAkD;CACxE,IAAI,QAAQ,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GACjE,OAAO;CAER,MAAM,OAAQ,KAA4B;CAC1C,OAAO,QAAQ,QAAQ,OAAO,SAAS,WAAY,OAAmC;AACvF;;;;;;;AAQA,MAAa,iBAAiB,MAAe,QAA6B;CACzE,IAAI,OAAO,SAAS,UACnB,OAAO,YAAY,MAAM,YAAY,IAAI,MAAM,CAAC;CAEjD,MAAM,SAAS,UAAU,GAAG;CAC5B,IAAI,MAAM,QAAQ,IAAI,GACrB,OAAO,eAAe,MAAM,MAAM;CAEnC,MAAM,OAAO,cAAc,IAAI;CAC/B,IAAI,MAAM;EACT,MAAM,aAAa;GAAE,GAAG;GAAuB,GAAI,IAAI,cAAc,CAAC;EAAG;EACzE,OAAO,eAAe,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC,GAAG,YAAY,MAAM;CAC5F;CACA,OAAO;AACR;;AAGA,MAAa,kBACX,SAOD,OAAO,SAAmC;CACzC,IAAI,KAAK,UAAU,WAClB,OAAO,MAAM,KAAK,SAAS,UAAU;EACpC;EACA,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,MAAM,KAAK;EACX,KAAK,KAAK;CACX,CAAC;CAEF,OAAO,cAAc,MAAM;EAC1B,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,YAAY,KAAK,UAAU;CAC5B,CAAC;AACF"}
@@ -90,6 +90,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
90
90
  const pollResultsTypes = pollEligibleTypes(registry);
91
91
  const bareTypes = new Set([...registry.values()].filter((d) => d.bare === true).map((d) => d.type));
92
92
  const bareTypeLabels = Object.fromEntries([...registry.values()].filter((d) => d.bare === true).map((d) => [d.type, d.label]));
93
+ const responseEditor = richText?.responseEditor ?? richText?.editor;
93
94
  const FLOW_BUILDER_REF = "@10x-media/form-builder/client#FlowBuilder";
94
95
  const FIELD_NAME_SELECT_REF = "@10x-media/form-builder/client#FieldNameSelect";
95
96
  const FLOW_STEPS_CELL_REF = "@10x-media/form-builder/client#FlowStepsCell";
@@ -256,7 +257,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
256
257
  type: "richText",
257
258
  label: labelForKey(keys.responseMessage),
258
259
  admin: { condition: (_data, siblingData) => siblingData?.type !== "redirect" },
259
- ...richText?.editor ? { editor: richText.editor } : {},
260
+ ...responseEditor ? { editor: responseEditor } : {},
260
261
  ...localizedIf(localizeContent)
261
262
  },
262
263
  {
@@ -1 +1 @@
1
- {"version":3,"file":"forms.js","names":[],"sources":["../../src/collections/forms.ts"],"sourcesContent":["import {\n\ttype CollectionAfterChangeHook,\n\ttype CollectionAfterReadHook,\n\ttype CollectionBeforeValidateHook,\n\ttype CollectionConfig,\n\ttype CollectionSlug,\n\ttype Field,\n\ttype TextFieldSingleValidation,\n\tValidationError,\n} from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport { buildActionBlocks } from '../actions/buildActionBlocks'\nimport type { FromAddressesResolver } from '../actions/fromAddresses'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { FormResultsAccess } from '../aggregation/resolveResultsRequest'\nimport { normalizeCalc } from '../calc/normalizeCalc'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport {\n\tbuildOperandTypes,\n\ttype FieldRow,\n\tnormalizeFormConditions,\n\tnormalizeWhere,\n} from '../conditions/normalizeConditions'\nimport { resolveConsentStatements } from '../consent/resolveConsentStatements'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport type { DepartmentEmailsResolver } from '../email/departments'\nimport { buildFieldBlocks } from '../fields/buildFieldBlocks'\nimport { fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport { localizedIf } from '../fields/localizedIf'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { normalizeFlow } from '../flow/normalizeFlow'\nimport { END_OF_FORM } from '../flow/types'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport { buildPollOptionSourceFields } from '../poll/buildPollOptionSourceFields'\nimport { enqueuePollClose } from '../poll/closeJob'\nimport { pollOutcomeBeforeChange } from '../poll/outcomeBeforeChange'\nimport { buildDefaultOutcomeFields, type OutcomeFieldsOverride } from '../poll/outcomeFields'\nimport { type PollTypeRegistry, resolvePollTypes } from '../poll/pollTypeRegistry'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { buildValidateResultsField, pollEligibleTypes } from '../poll/resultsField'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelForKey, resolveDefinitionLabel } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { validateUrl } from '../validation/validateUrl'\nimport { type ButtonsOption, buildDefaultButtonFields } from './buttonFields'\nimport { buildFormsEndpoints } from './formsEndpoints'\nimport type { ResponseOption } from './redirectFields'\n\nexport const FORMS_SLUG = 'forms'\n\n/** `req.context` key under which `consentAfterRead` tracks the form ids it is currently resolving, to break re-entrant reads. */\nconst CONSENT_AFTER_READ_GUARD = 'formBuilderConsentAfterReadInFlight'\n\n/**\n * Require the title in the default locale (and on hosts without localization), but let it be empty in\n * other locales so a localized form falls back to the default-locale title rather than forcing a\n * translation for every locale. Payload's field-level `required` cannot express this: it enforces per\n * write-locale, which would make a title mandatory in every locale and break the documented fallback.\n */\nconst validateFormTitle: TextFieldSingleValidation = (value, { req }) => {\n\tconst localization = req.payload.config.localization\n\tconst defaultLocale = localization ? localization.defaultLocale : undefined\n\tconst enforced =\n\t\tdefaultLocale === undefined || req.locale === undefined || req.locale === defaultLocale\n\tif (enforced && (typeof value !== 'string' || value.trim().length === 0)) {\n\t\treturn req.t('validation:required')\n\t}\n\treturn true\n}\n\n/**\n * Field-level validation for the raw flow JSON: step ids must be non-empty, unique, and not the\n * reserved `END_OF_FORM` sentinel, and every explicit step-id reference (a string `next`, a\n * transition `to`) must resolve to a known step. `next: null` (explicit end of form) and an\n * absent `next` (fall through to the next step in array order) are always valid. On full-document\n * saves the beforeValidate pass has already run `normalizeFlow` and laundered dangling\n * references, so the unknown-target checks mainly protect partial API updates that send `flow`\n * without `fields`.\n */\nconst validateFlow = (raw: unknown): string | true => {\n\tif (raw === null || raw === undefined) return true\n\tconst r = raw as Record<string, unknown>\n\tif (!Array.isArray(r.steps)) return true\n\tconst steps = r.steps as Array<Record<string, unknown>>\n\tconst emptyIdStep = steps.find((s) => typeof s?.id !== 'string' || s.id.length === 0)\n\tif (emptyIdStep) return 'Flow: every step must have a non-empty ID'\n\tif (steps.some((s) => s.id === END_OF_FORM)) {\n\t\treturn `Flow: step ID \"${END_OF_FORM}\" is reserved`\n\t}\n\tconst ids = steps.map((s) => s.id as string)\n\tif (new Set(ids).size !== ids.length) {\n\t\treturn 'Flow: duplicate step IDs found'\n\t}\n\tconst idSet = new Set(ids)\n\tfor (const step of steps) {\n\t\tconst id = step.id as string\n\t\tif (typeof step.next === 'string' && step.next.length > 0 && !idSet.has(step.next)) {\n\t\t\treturn `Flow: step \"${id}\" references unknown next step \"${step.next}\"`\n\t\t}\n\t\tif (Array.isArray(step.transitions)) {\n\t\t\tfor (const t of step.transitions as Array<Record<string, unknown>>) {\n\t\t\t\tif (typeof t?.to === 'string' && t.to.length > 0 && !idSet.has(t.to)) {\n\t\t\t\t\treturn `Flow: step \"${id}\" has a transition to unknown step \"${t.to}\"`\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n/** How many steps the caller actually submitted, before normalization strips/collapses the flow. */\nconst providedFlowStepCount = (raw: unknown): number => {\n\tif (raw === null || typeof raw !== 'object') return 0\n\tconst steps = (raw as { steps?: unknown }).steps\n\treturn Array.isArray(steps) ? steps.length : 0\n}\n\n/**\n * Stamp the configured uploads collection slug onto every `file` block (including repeater\n * sub-fields) on each save. The block carries a hidden `uploadsCollection` field so the slug\n * reaches the client renderer through the form document; the server always overwrites it from\n * plugin config, so the stored value is never author- or client-controlled.\n */\nconst stampFileCollections = (rows: FieldRow[], slug: string): void => {\n\tfor (const row of rows) {\n\t\tif (row.blockType === 'file') {\n\t\t\t;(row as { uploadsCollection?: string }).uploadsCollection = slug\n\t\t}\n\t\tconst subFields = (row as { subFields?: unknown }).subFields\n\t\tif (Array.isArray(subFields)) {\n\t\t\tstampFileCollections(subFields as FieldRow[], slug)\n\t\t}\n\t}\n}\n\ntype BuildFormsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/**\n\t * The plugin `consent.sources` option. Present: the `/:id/consent-sources` endpoint backing the\n\t * consent field's source select is registered. Absent: neither it nor the consent field type exists.\n\t */\n\tconsentSources?: ConsentSourcesResolver\n\t/** Plugin `consent.resolveOnRead` (default true); `false` skips the per-read consent afterRead hook. */\n\tconsentResolveOnRead?: boolean\n\tactionRegistry?: ActionRegistry\n\tlocalizeContent?: boolean\n\t/** The plugin `richText` option; `editor` overrides the response message's richText editor. */\n\trichText?: RichTextBodyOption\n\t/** The host-owned uploads collection slug from plugin config; absent when uploads are disabled. */\n\tuploadsCollectionSlug?: string\n\t/** Host seam gating anonymous results reads (plugin option `results.access`). */\n\tresultsAccess?: FormResultsAccess\n\t/** Registered poll option sources (plugin option `poll.sources`); empty registry means no source fields. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Registered poll outcome strategies (`poll.types`); drives the poll `type` select options. Defaults to the built-ins. */\n\tpollTypeRegistry?: PollTypeRegistry\n\t/** The plugin `poll.outcomeFields` seam; composes the outcome group from the two default fields. */\n\toutcomeFields?: OutcomeFieldsOverride\n\t/** The plugin `buttons` option; `fields` composes the `{ submit, prev, next }` labels from the localized defaults. */\n\tbuttons?: ButtonsOption\n\t/** The plugin `response` option; `redirect.fields` composes the `response.redirect` group from its default fields. */\n\tresponse?: ResponseOption\n\t/**\n\t * The plugin `email.fromAddresses` option. Present: both email actions gain a `from` select and\n\t * the `/:id/from-addresses` endpoint is registered. Absent: neither exists.\n\t */\n\tfromAddresses?: FromAddressesResolver\n\t/**\n\t * The plugin `email.departments` option. Present: the `emailTeam` `to` becomes a department select\n\t * and the `/:id/departments` endpoint backing it is registered. Absent: `to` stays a plain field.\n\t */\n\tdepartments?: DepartmentEmailsResolver\n\t/**\n\t * The plugin `redirectRelationships` option. Non-empty: `response.redirect` gains a polymorphic\n\t * `reference` relationship field so an author can redirect to an internal document instead of\n\t * (or alongside) a URL. Absent or empty: no `reference` field exists, matching today's URL-only\n\t * redirect.\n\t */\n\tredirectRelationships?: CollectionSlug[]\n\toverrides?: CollectionOverrides\n}\n\nexport const buildFormsCollection = ({\n\toverrides,\n\tregistry,\n\truleRegistry,\n\tconsentSources,\n\tconsentResolveOnRead,\n\tactionRegistry = new Map(),\n\tlocalizeContent = true,\n\trichText,\n\tuploadsCollectionSlug,\n\tresultsAccess,\n\tpollSourceRegistry,\n\tpollTypeRegistry,\n\toutcomeFields,\n\tbuttons,\n\tresponse,\n\tfromAddresses,\n\tdepartments,\n\tredirectRelationships,\n}: BuildFormsCollectionArgs): CollectionConfig => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst pollTypes = pollTypeRegistry ?? resolvePollTypes()\n\tconst pollResultsTypes = pollEligibleTypes(registry)\n\tconst bareTypes = new Set(\n\t\t[...registry.values()].filter((d) => d.bare === true).map((d) => d.type)\n\t)\n\t// Handed to the FlowBuilder so it can list bare (nameless) blocks in the step picker: slug ->\n\t// label (an i18n key or literal, resolved client-side).\n\tconst bareTypeLabels = Object.fromEntries(\n\t\t[...registry.values()].filter((d) => d.bare === true).map((d) => [d.type, d.label])\n\t)\n\tconst FLOW_BUILDER_REF = '@10x-media/form-builder/client#FlowBuilder'\n\tconst FIELD_NAME_SELECT_REF = '@10x-media/form-builder/client#FieldNameSelect'\n\tconst FLOW_STEPS_CELL_REF = '@10x-media/form-builder/client#FlowStepsCell'\n\tconst FIELD_COUNT_CELL_REF = '@10x-media/form-builder/client#FieldCountCell'\n\tconst CLOSE_POLL_BUTTON_REF = '@10x-media/form-builder/client#ClosePollButton'\n\t// The `source` outcome strategy delegates to the poll's option source, so it is useless without one:\n\t// offer it in the author-facing select only when option sources are registered. The strategy stays in\n\t// the registry regardless, so `resolvePollOutcome` and host code can still address it.\n\tconst hasOptionSources = (pollSourceRegistry?.size ?? 0) > 0\n\t// `mostVoted` leads the author-facing select because it is the default: an enabled poll works\n\t// immediately, with no winner to hand-pick. Only the select order changes; the registry keeps its\n\t// own order (host strategies stay after the built-ins).\n\tconst orderedPollTypes = [\n\t\t...[...pollTypes.values()].filter((strategy) => strategy.type === 'mostVoted'),\n\t\t...[...pollTypes.values()].filter((strategy) => strategy.type !== 'mostVoted'),\n\t]\n\n\tconst beforeValidate: CollectionBeforeValidateHook = ({ data, req }) => {\n\t\tif (data && Array.isArray(data.fields)) {\n\t\t\tconst normalized: FieldRow[] = normalizeFormConditions(\n\t\t\t\tdata.fields as FieldRow[],\n\t\t\t\tconditionTypes\n\t\t\t)\n\t\t\tfor (const field of normalized) {\n\t\t\t\tif ('expression' in field) {\n\t\t\t\t\tfield.expression = normalizeCalc(field.expression)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (uploadsCollectionSlug) {\n\t\t\t\tstampFileCollections(normalized, uploadsCollectionSlug)\n\t\t\t}\n\t\t\tdata.fields = normalized\n\t\t\t// Flow step assignments store field keys: machine names for named fields, block row ids\n\t\t\t// for bare (nameless) blocks. Mirrors `fieldKey` over the raw rows.\n\t\t\tconst fieldKeys = normalized\n\t\t\t\t.map((field: FieldRow) => {\n\t\t\t\t\tif (typeof field.name === 'string' && field.name.length > 0) {\n\t\t\t\t\t\treturn field.name\n\t\t\t\t\t}\n\t\t\t\t\treturn bareTypes.has(field.blockType) && field.id != null ? String(field.id) : undefined\n\t\t\t\t})\n\t\t\t\t.filter((key): key is string => key !== undefined)\n\t\t\t// Launder flow transition `when` clauses with the same operand-type rules as field conditions,\n\t\t\t// so a transition referencing a deleted field is dropped, not left as a route that always\n\t\t\t// matches at navigation time and force-routes the visitor.\n\t\t\tconst operandTypes = buildOperandTypes(normalized, conditionTypes)\n\t\t\tconst normalizedFlow = normalizeFlow(data.flow, fieldKeys, (w) =>\n\t\t\t\tnormalizeWhere(w, operandTypes)\n\t\t\t)\n\t\t\t// A flow the author built but that collapses to fewer than two valid steps would\n\t\t\t// otherwise vanish silently. Surface it instead of discarding their work.\n\t\t\tif (providedFlowStepCount(data.flow) > 0 && normalizedFlow === undefined) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tpath: 'flow',\n\t\t\t\t\t\t\t\tmessage: 'A flow needs at least two steps. Add another step or remove the flow.',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t\tdata.flow = normalizedFlow\n\t\t\t// A poll needs a field whose answers are the votes. With the poll enabled and no vote field\n\t\t\t// chosen (and the outcome not coming from an option source), bind one: auto-fill the sole\n\t\t\t// eligible field for a one-choice form, otherwise surface a clear error rather than storing a\n\t\t\t// poll that can never aggregate. The optionSource/`source` exemption preserves domain-driven\n\t\t\t// polls that have no on-form vote field. `buildValidateResultsField` still checks a set value;\n\t\t\t// this is the enforcer for enabled polls.\n\t\t\tif (data.pollEnabled === true) {\n\t\t\t\tconst poll =\n\t\t\t\t\tdata.poll != null && typeof data.poll === 'object'\n\t\t\t\t\t\t? (data.poll as Record<string, unknown>)\n\t\t\t\t\t\t: {}\n\t\t\t\tconst resultsField = typeof poll.resultsField === 'string' ? poll.resultsField.trim() : ''\n\t\t\t\tconst optionSource = typeof poll.optionSource === 'string' ? poll.optionSource.trim() : ''\n\t\t\t\tconst sourceDriven = optionSource.length > 0 || poll.type === 'source'\n\t\t\t\tif (resultsField.length === 0 && !sourceDriven) {\n\t\t\t\t\tconst eligible = fieldNamesOfType(data.fields, pollResultsTypes)\n\t\t\t\t\tif (eligible.length === 1) {\n\t\t\t\t\t\tdata.poll = { ...poll, resultsField: eligible[0] }\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst messageKey =\n\t\t\t\t\t\t\teligible.length > 1 ? keys.pollVoteFieldChoose : keys.pollVoteFieldMissing\n\t\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\t\t\terrors: [{ path: 'poll.resultsField', message: asTranslate(req.t)(messageKey) }],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treq.t\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn data\n\t}\n\n\t// After every save, (re)schedule the poll's auto-close job when it applies (enabled + closesAt +\n\t// unresolved + non-manual strategy + a job runner present). Best-effort and non-throwing, so it\n\t// never affects the save; without a runner the resolve-on-read fallback in the results endpoint\n\t// heals the outcome instead.\n\tconst pollCloseAfterChange: CollectionAfterChangeHook = async ({ doc, req }) => {\n\t\tawait enqueuePollClose({ payload: req.payload, form: doc, req })\n\t\treturn doc\n\t}\n\n\tconst fieldsField: Field = {\n\t\tname: 'fields',\n\t\ttype: 'blocks',\n\t\tblocks: buildFieldBlocks({ registry, ruleRegistry, localize: localizeContent }),\n\t\tadmin: { components: { Cell: FIELD_COUNT_CELL_REF } },\n\t}\n\n\tconst flowField: Field = {\n\t\tname: 'flow',\n\t\ttype: 'json',\n\t\tvalidate: validateFlow,\n\t\tadmin: {\n\t\t\tcomponents: {\n\t\t\t\tCell: FLOW_STEPS_CELL_REF,\n\t\t\t\tField: { path: FLOW_BUILDER_REF, clientProps: { conditionTypes, bareTypeLabels } },\n\t\t\t},\n\t\t},\n\t\t// Narrows the generated TypeScript type from opaque JSON to FormFlow so callers\n\t\t// don't need a cast. Keep this in sync with src/flow/types.ts.\n\t\ttypescriptSchema: [\n\t\t\t() => ({\n\t\t\t\ttype: 'object' as const,\n\t\t\t\trequired: ['steps'],\n\t\t\t\tadditionalProperties: false,\n\t\t\t\tproperties: {\n\t\t\t\t\tsteps: {\n\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\trequired: ['id'],\n\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\tid: { type: 'string' as const },\n\t\t\t\t\t\t\t\ttitle: { type: 'string' as const },\n\t\t\t\t\t\t\t\tfields: { type: 'array' as const, items: { type: 'string' as const } },\n\t\t\t\t\t\t\t\tnext: { type: ['string', 'null'] as ('string' | 'null')[] },\n\t\t\t\t\t\t\t\ttransitions: {\n\t\t\t\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\t\t\t\trequired: ['to'],\n\t\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\tto: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\t\twhen: { type: 'object' as const, additionalProperties: true },\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t}\n\n\tconst actionsField: Field = {\n\t\tname: 'actions',\n\t\ttype: 'blocks',\n\t\tblocks: buildActionBlocks(actionRegistry),\n\t\tlabel: labelForKey(keys.configActions),\n\t\t// Action config can contain secrets (e.g. signedWebhook.secret). The collection\n\t\t// itself is publicly readable so forms can be rendered by anonymous clients, but\n\t\t// action config must never be exposed to anonymous callers.\n\t\taccess: { read: isLoggedIn },\n\t}\n\n\t// Optional polymorphic reference to an internal document, for `response.redirect` to point at\n\t// instead of (or alongside) a URL. Absent unless `redirectRelationships` is a non-empty array:\n\t// always polymorphic even for one slug (the consent `page` field's precedent), so a host adding\n\t// a second collection later never changes the stored shape. The plugin never resolves this to a\n\t// URL itself; `toFormDocument` passes the raw `{ relationTo, value }` through for the host to\n\t// resolve, since only the host knows its own routing.\n\tconst redirectReferenceField: Field[] =\n\t\tredirectRelationships && redirectRelationships.length > 0\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'reference',\n\t\t\t\t\t\ttype: 'relationship',\n\t\t\t\t\t\trelationTo: redirectRelationships,\n\t\t\t\t\t\tlabel: labelForKey(keys.responseRedirectReference),\n\t\t\t\t\t\tadmin: { description: labelForKey(keys.responseRedirectReferenceDescription) },\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []\n\n\t// The redirect group's fields: the `url` text field plus the optional polymorphic `reference`.\n\t// The `response.redirect.fields` seam composes them (mirroring `buttons.fields`), so a host can\n\t// prepend a custom link field, swap `url` for their own picker, or filter; the default (no\n\t// override) is the same `[url, ...reference]` as before. Built-in redirect handling still reads\n\t// `redirect.url`/`redirect.reference`, so a host replacing `url` owns resolving their field to a\n\t// destination in their frontend.\n\tconst urlField: Field = {\n\t\tname: 'url',\n\t\ttype: 'text',\n\t\tlabel: labelForKey(keys.responseUrl),\n\t\tvalidate: validateUrl,\n\t}\n\tconst defaultRedirectFields: Field[] = [urlField, ...redirectReferenceField]\n\tconst redirectFields = response?.redirect?.fields\n\t\t? response.redirect.fields({ defaultFields: defaultRedirectFields })\n\t\t: defaultRedirectFields\n\n\t// What the visitor sees after a successful submit. Publicly readable (unlike actions): the\n\t// client renderer needs message/redirect. `type`/`url` are behavior, never localized;\n\t// `message` is visitor-facing content and follows `localizeContent`.\n\t// `type` is defaulted and not clearable rather than `required`: a required member would make\n\t// the whole group required in generated types, breaking typed `payload.create` calls that\n\t// omit `response`. Consumers treat a missing type as 'message'.\n\tconst responseField: Field = {\n\t\tname: 'response',\n\t\ttype: 'group',\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'type',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'message',\n\t\t\t\tlabel: labelForKey(keys.responseType),\n\t\t\t\tadmin: { isClearable: false },\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: labelForKey(keys.responseTypeMessage), value: 'message' },\n\t\t\t\t\t{ label: labelForKey(keys.responseTypeRedirect), value: 'redirect' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'message',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelForKey(keys.responseMessage),\n\t\t\t\t// Unset type (docs predating this field) means 'message', matching the client fallback.\n\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.type !== 'redirect' },\n\t\t\t\t...(richText?.editor ? { editor: richText.editor } : {}),\n\t\t\t\t...localizedIf(localizeContent),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'redirect',\n\t\t\t\ttype: 'group',\n\t\t\t\tlabel: labelForKey(keys.responseRedirect),\n\t\t\t\tadmin: {\n\t\t\t\t\tcondition: (_data, siblingData) => siblingData?.type === 'redirect',\n\t\t\t\t\thideGutter: true,\n\t\t\t\t},\n\t\t\t\tfields: redirectFields,\n\t\t\t},\n\t\t],\n\t}\n\n\tconst defaultOutcomeFields = buildDefaultOutcomeFields()\n\tconst defaultButtonFields = buildDefaultButtonFields(localizeContent)\n\t// Half-width copy of a default field for the prev/next row; merges width into whatever admin the\n\t// field already carries (a host's `buttons.fields` override may add its own) rather than replacing\n\t// it. The cast is safe: `width` is a valid admin option on every field variant, but spreading a\n\t// `Field`-typed value's `admin` back into a union-typed object literal loses the discriminant TS\n\t// needs to check it structurally.\n\tconst halfWidth = (field: Field): Field =>\n\t\t({ ...field, admin: { ...field.admin, width: '50%' } }) as Field\n\t// The three button-label fields sit at the document root now (no `buttons` group): `submit` at\n\t// the bottom of the Fields tab, `prev`/`next` in a row on the Flow tab. The `buttons.fields` seam\n\t// composes each slot from the already-localized defaults, so a host can wrap a default in a row\n\t// with its own field (e.g. an icon select) or replace it; `toFormDocument` reassembles the three\n\t// labels into `FormDocument.buttons` for the client.\n\tconst composedButtons = buttons?.fields\n\t\t? buttons.fields({ defaultFields: defaultButtonFields })\n\t\t: defaultButtonFields\n\tconst submitField = composedButtons.submit\n\tconst prevNextRow: Field = {\n\t\ttype: 'row',\n\t\t// The prev/next labels only matter once the flow has a step; the Flow tab itself is gated on\n\t\t// `multistep`. `data` (1st arg) is the whole document, so `flow.steps` reads the stored flow.\n\t\tadmin: {\n\t\t\tcondition: (data) => {\n\t\t\t\tconst steps = (data as { flow?: { steps?: unknown } })?.flow?.steps\n\t\t\t\treturn Array.isArray(steps) && steps.length >= 1\n\t\t\t},\n\t\t},\n\t\tfields: [halfWidth(composedButtons.prev), halfWidth(composedButtons.next)],\n\t}\n\n\t// Poll lifecycle config; identifiers and behavior, never localized. Lives inside the conditional\n\t// Poll tab (gated on the top-level `pollEnabled` flag), so the per-field enabled conditions are\n\t// gone; `label: false` suppresses the group header the tab label already provides.\n\t// `resultsVisibility` is defaulted and not clearable rather than `required` for the same\n\t// generated-types reason as `response.type`.\n\tconst pollGroupField: Field = {\n\t\tname: 'poll',\n\t\ttype: 'group',\n\t\tlabel: false,\n\t\tfields: [\n\t\t\t// Authored by picking from the form's poll-eligible fields; the stored value stays a plain\n\t\t\t// text field name. Select options and server validate share `pollResultsTypes`, so they\n\t\t\t// cannot drift.\n\t\t\t{\n\t\t\t\tname: 'resultsField',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.pollResultsField),\n\t\t\t\tvalidate: buildValidateResultsField(pollResultsTypes),\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: {\n\t\t\t\t\t\tField: {\n\t\t\t\t\t\t\tpath: FIELD_NAME_SELECT_REF,\n\t\t\t\t\t\t\tclientProps: {\n\t\t\t\t\t\t\t\ttypes: pollResultsTypes,\n\t\t\t\t\t\t\t\tdescriptionKey: keys.pollResultsFieldDescription,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// How the winning value(s) get decided. `mostVoted` (default) and `source` auto-resolve on close\n\t\t\t// (via the scheduled job or the results-read fallback); `manual` leaves it to an admin. Options\n\t\t\t// come from the registered `poll.types` strategies, so a host strategy appears here too, with\n\t\t\t// `mostVoted` first so an enabled poll works with no winner to hand-pick.\n\t\t\t{\n\t\t\t\tname: 'type',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'mostVoted',\n\t\t\t\tlabel: labelForKey(keys.pollType),\n\t\t\t\tadmin: { isClearable: false, description: labelForKey(keys.pollTypeDescription) },\n\t\t\t\toptions: orderedPollTypes\n\t\t\t\t\t.filter((strategy) => strategy.type !== 'source' || hasOptionSources)\n\t\t\t\t\t.map((strategy) => ({\n\t\t\t\t\t\tlabel: resolveDefinitionLabel(strategy.label),\n\t\t\t\t\t\tvalue: strategy.type,\n\t\t\t\t\t})),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'resultsVisibility',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'afterVote',\n\t\t\t\tlabel: labelForKey(keys.pollResultsVisibility),\n\t\t\t\tadmin: { isClearable: false },\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: labelForKey(keys.pollVisibilityAfterVote), value: 'afterVote' },\n\t\t\t\t\t{ label: labelForKey(keys.pollVisibilityAfterClose), value: 'afterClose' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'closesAt',\n\t\t\t\ttype: 'date',\n\t\t\t\tlabel: labelForKey(keys.pollClosesAt),\n\t\t\t\tadmin: { date: { pickerAppearance: 'dayAndTime' } },\n\t\t\t},\n\t\t\t// Close / reopen the poll from the admin. The button toggles on the live `closesAt` and saves the\n\t\t\t// whole document (persisting an unsaved winner alongside `closesAt`) rather than calling the close\n\t\t\t// endpoint, so there is no DB-vs-form-state mismatch. Always mounted inside the (pollEnabled-gated)\n\t\t\t// Poll tab now; the button owns both states, so it carries no `admin.condition` of its own.\n\t\t\t{\n\t\t\t\tname: 'closePoll',\n\t\t\t\ttype: 'ui',\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: { Field: CLOSE_POLL_BUTTON_REF },\n\t\t\t\t},\n\t\t\t},\n\t\t\t...buildPollOptionSourceFields(pollSourceRegistry ?? new Map()),\n\t\t\t// `winningValues` is recorded either by an admin picking from the poll's effective options\n\t\t\t// (served by `/:id/poll-options`) or by `resolvePollOutcome` (host domain logic); more than\n\t\t\t// one value records a tie. The `pollOutcomeBeforeChange` hook validates both paths and owns\n\t\t\t// the `resolvedAt` stamp. `resolvedAt` itself stays fully locked: field-level create/update\n\t\t\t// access blocks every non-override caller write (Payload silently drops the denied value\n\t\t\t// rather than erroring) while the hook's stamp, applied after access filtering, still\n\t\t\t// persists. The `poll.outcomeFields` seam receives both defaults and its return becomes the\n\t\t\t// group's fields verbatim, so a host can swap `winningValues` for its own component; the hook\n\t\t\t// still validates every stored value against the effective options, so no swap bypasses it.\n\t\t\t// `hideGutter` stays: outcome is still a group nested inside the poll group.\n\t\t\t{\n\t\t\t\tname: 'outcome',\n\t\t\t\ttype: 'group',\n\t\t\t\tlabel: labelForKey(keys.pollOutcome),\n\t\t\t\tadmin: { hideGutter: true },\n\t\t\t\tfields: outcomeFields\n\t\t\t\t\t? outcomeFields({ defaultFields: defaultOutcomeFields })\n\t\t\t\t\t: [defaultOutcomeFields.winningValues, defaultOutcomeFields.resolvedAt],\n\t\t\t},\n\t\t],\n\t}\n\n\tconst defaultFields: Field[] = [\n\t\t// The document title is the only visitor-facing text the collection renders itself: it is\n\t\t// always required (drives `useAsTitle` in the admin list/relationship views) and is passed\n\t\t// through `toFormDocument` as `FormDocument.title`. Whether and how a host renders it above\n\t\t// the fields is entirely the host's call; the plugin does not gate or duplicate it.\n\t\t{\n\t\t\tname: 'title',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelForKey(keys.fieldTitle),\n\t\t\t// Visitor-facing content, so it follows `localizeContent` like the response message and\n\t\t\t// consent statement: hosts with localization get a per-locale title (and a locale-aware\n\t\t\t// `useAsTitle`), hosts without it are unaffected since Payload strips the flag. `validate`\n\t\t\t// (not `required`) keeps it mandatory only in the default locale so other locales fall back.\n\t\t\t...localizedIf(localizeContent),\n\t\t\tvalidate: validateFormTitle,\n\t\t},\n\t\t// The two form-type flags: behavior, never localized. `multistep` gates the Flow tab and the\n\t\t// client's step navigation; `pollEnabled` gates the Poll tab and marks the form a poll.\n\t\t{\n\t\t\ttype: 'row',\n\t\t\tfields: [\n\t\t\t\t{\n\t\t\t\t\tname: 'multistep',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tdefaultValue: false,\n\t\t\t\t\tlabel: labelForKey(keys.formMultistep),\n\t\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'pollEnabled',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tdefaultValue: false,\n\t\t\t\t\tlabel: labelForKey(keys.formPollEnabled),\n\t\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t// Unnamed tabs are presentational: their fields stay at the document root. An unnamed tab's\n\t\t// `admin.condition` receives the whole document as its 2nd arg, so the Flow and Poll tabs gate\n\t\t// on the root-level flags. The poll group nests its config under `form.poll` as before.\n\t\t{\n\t\t\ttype: 'tabs',\n\t\t\ttabs: [\n\t\t\t\t{ label: labelForKey(keys.tabFields), fields: [fieldsField, submitField] },\n\t\t\t\t{\n\t\t\t\t\tlabel: labelForKey(keys.tabFlow),\n\t\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.multistep === true },\n\t\t\t\t\tfields: [flowField, prevNextRow],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: labelForKey(keys.pollGroup),\n\t\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.pollEnabled === true },\n\t\t\t\t\tfields: [pollGroupField],\n\t\t\t\t},\n\t\t\t\t{ label: labelForKey(keys.tabActions), fields: [actionsField] },\n\t\t\t\t{ label: labelForKey(keys.tabResponse), fields: [responseField] },\n\t\t\t],\n\t\t},\n\t]\n\n\t// When `consent.sources` is set, resolve the visitor-facing consent statements onto every read of\n\t// a form doc (REST, local API, relationship population), so a client/modal fetch renders consent,\n\t// not only the RSC path that calls `resolveConsentStatements` itself. Fails open: a resolver outage\n\t// must never break form/admin/relationship reads, and the renderer already tolerates a missing\n\t// statement. `resolveConsentStatements` returns `{}` without calling the resolver when the form has\n\t// no consent fields, and entries are cached per request+form, so the cost stays bounded.\n\tconst consentAfterRead: CollectionAfterReadHook | undefined =\n\t\tconsentSources && consentResolveOnRead !== false\n\t\t\t? async ({ doc, req }) => {\n\t\t\t\t\tconst id = (doc as { id?: unknown }).id\n\t\t\t\t\tconst key = id == null ? undefined : String(id)\n\t\t\t\t\t// Re-entrance guard: a host resolver that reads this same form back (threading req) would\n\t\t\t\t\t// otherwise re-enter this hook and recurse. Payload runs collection afterRead concurrently\n\t\t\t\t\t// across list docs on one shared req.context, so this must be a per-id Set (a boolean would\n\t\t\t\t\t// make sibling docs skip), mutated in place and never reassigned after fan-out. Mirrors\n\t\t\t\t\t// @payloadcms/plugin-search's syncDocAsSearchIndex guard.\n\t\t\t\t\tif (key !== undefined && req.context) {\n\t\t\t\t\t\tconst inFlight =\n\t\t\t\t\t\t\t(req.context[CONSENT_AFTER_READ_GUARD] as Set<string> | undefined) ??\n\t\t\t\t\t\t\tnew Set<string>()\n\t\t\t\t\t\tif (inFlight.has(key)) {\n\t\t\t\t\t\t\treturn doc\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinFlight.add(key)\n\t\t\t\t\t\treq.context[CONSENT_AFTER_READ_GUARD] = inFlight\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst statements = await resolveConsentStatements({\n\t\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\tform: doc,\n\t\t\t\t\t\t\tsources: consentSources,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif (Object.keys(statements).length > 0) {\n\t\t\t\t\t\t\t;(doc as Record<string, unknown>).consentStatements = statements\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\treq.payload.logger?.warn(`form-builder consent afterRead: ${String(error)}`)\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (key !== undefined && req.context) {\n\t\t\t\t\t\t\t;(req.context[CONSENT_AFTER_READ_GUARD] as Set<string> | undefined)?.delete(key)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn doc\n\t\t\t\t}\n\t\t\t: undefined\n\n\tconst defaultEndpoints = buildFormsEndpoints({\n\t\tresultsAccess,\n\t\tpollResultsTypes,\n\t\tconsentSources,\n\t\tfromAddresses,\n\t\tdepartments,\n\t})\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORMS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionFormSingular),\n\t\t\tplural: labelForKey(keys.collectionFormPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: {\n\t\t\tgroup: 'Forms',\n\t\t\tuseAsTitle: 'title',\n\t\t\tdefaultColumns: ['title', 'fields', 'flow', 'pollEnabled', 'updatedAt'],\n\t\t\t...(overrides?.admin ?? {}),\n\t\t},\n\t\taccess: { read: () => true, ...(overrides?.access ?? {}) },\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// beforeValidate normalizes conditions and flow; consumer hooks run after\n\t\t\tbeforeValidate: [beforeValidate, ...(overrides?.hooks?.beforeValidate ?? [])],\n\t\t\tbeforeChange: [pollOutcomeBeforeChange, ...(overrides?.hooks?.beforeChange ?? [])],\n\t\t\tafterChange: [pollCloseAfterChange, ...(overrides?.hooks?.afterChange ?? [])],\n\t\t\tafterRead: [\n\t\t\t\t...(consentAfterRead ? [consentAfterRead] : []),\n\t\t\t\t...(overrides?.hooks?.afterRead ?? []),\n\t\t\t],\n\t\t},\n\t\tendpoints: [\n\t\t\t...defaultEndpoints,\n\t\t\t...(Array.isArray(overrides?.endpoints) ? overrides.endpoints : []),\n\t\t],\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"forms.js","names":[],"sources":["../../src/collections/forms.ts"],"sourcesContent":["import {\n\ttype CollectionAfterChangeHook,\n\ttype CollectionAfterReadHook,\n\ttype CollectionBeforeValidateHook,\n\ttype CollectionConfig,\n\ttype CollectionSlug,\n\ttype Field,\n\ttype TextFieldSingleValidation,\n\tValidationError,\n} from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport { buildActionBlocks } from '../actions/buildActionBlocks'\nimport type { FromAddressesResolver } from '../actions/fromAddresses'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { FormResultsAccess } from '../aggregation/resolveResultsRequest'\nimport { normalizeCalc } from '../calc/normalizeCalc'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport {\n\tbuildOperandTypes,\n\ttype FieldRow,\n\tnormalizeFormConditions,\n\tnormalizeWhere,\n} from '../conditions/normalizeConditions'\nimport { resolveConsentStatements } from '../consent/resolveConsentStatements'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport type { DepartmentEmailsResolver } from '../email/departments'\nimport { buildFieldBlocks } from '../fields/buildFieldBlocks'\nimport { fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport { localizedIf } from '../fields/localizedIf'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { normalizeFlow } from '../flow/normalizeFlow'\nimport { END_OF_FORM } from '../flow/types'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport { buildPollOptionSourceFields } from '../poll/buildPollOptionSourceFields'\nimport { enqueuePollClose } from '../poll/closeJob'\nimport { pollOutcomeBeforeChange } from '../poll/outcomeBeforeChange'\nimport { buildDefaultOutcomeFields, type OutcomeFieldsOverride } from '../poll/outcomeFields'\nimport { type PollTypeRegistry, resolvePollTypes } from '../poll/pollTypeRegistry'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { buildValidateResultsField, pollEligibleTypes } from '../poll/resultsField'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelForKey, resolveDefinitionLabel } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { validateUrl } from '../validation/validateUrl'\nimport { type ButtonsOption, buildDefaultButtonFields } from './buttonFields'\nimport { buildFormsEndpoints } from './formsEndpoints'\nimport type { ResponseOption } from './redirectFields'\n\nexport const FORMS_SLUG = 'forms'\n\n/** `req.context` key under which `consentAfterRead` tracks the form ids it is currently resolving, to break re-entrant reads. */\nconst CONSENT_AFTER_READ_GUARD = 'formBuilderConsentAfterReadInFlight'\n\n/**\n * Require the title in the default locale (and on hosts without localization), but let it be empty in\n * other locales so a localized form falls back to the default-locale title rather than forcing a\n * translation for every locale. Payload's field-level `required` cannot express this: it enforces per\n * write-locale, which would make a title mandatory in every locale and break the documented fallback.\n */\nconst validateFormTitle: TextFieldSingleValidation = (value, { req }) => {\n\tconst localization = req.payload.config.localization\n\tconst defaultLocale = localization ? localization.defaultLocale : undefined\n\tconst enforced =\n\t\tdefaultLocale === undefined || req.locale === undefined || req.locale === defaultLocale\n\tif (enforced && (typeof value !== 'string' || value.trim().length === 0)) {\n\t\treturn req.t('validation:required')\n\t}\n\treturn true\n}\n\n/**\n * Field-level validation for the raw flow JSON: step ids must be non-empty, unique, and not the\n * reserved `END_OF_FORM` sentinel, and every explicit step-id reference (a string `next`, a\n * transition `to`) must resolve to a known step. `next: null` (explicit end of form) and an\n * absent `next` (fall through to the next step in array order) are always valid. On full-document\n * saves the beforeValidate pass has already run `normalizeFlow` and laundered dangling\n * references, so the unknown-target checks mainly protect partial API updates that send `flow`\n * without `fields`.\n */\nconst validateFlow = (raw: unknown): string | true => {\n\tif (raw === null || raw === undefined) return true\n\tconst r = raw as Record<string, unknown>\n\tif (!Array.isArray(r.steps)) return true\n\tconst steps = r.steps as Array<Record<string, unknown>>\n\tconst emptyIdStep = steps.find((s) => typeof s?.id !== 'string' || s.id.length === 0)\n\tif (emptyIdStep) return 'Flow: every step must have a non-empty ID'\n\tif (steps.some((s) => s.id === END_OF_FORM)) {\n\t\treturn `Flow: step ID \"${END_OF_FORM}\" is reserved`\n\t}\n\tconst ids = steps.map((s) => s.id as string)\n\tif (new Set(ids).size !== ids.length) {\n\t\treturn 'Flow: duplicate step IDs found'\n\t}\n\tconst idSet = new Set(ids)\n\tfor (const step of steps) {\n\t\tconst id = step.id as string\n\t\tif (typeof step.next === 'string' && step.next.length > 0 && !idSet.has(step.next)) {\n\t\t\treturn `Flow: step \"${id}\" references unknown next step \"${step.next}\"`\n\t\t}\n\t\tif (Array.isArray(step.transitions)) {\n\t\t\tfor (const t of step.transitions as Array<Record<string, unknown>>) {\n\t\t\t\tif (typeof t?.to === 'string' && t.to.length > 0 && !idSet.has(t.to)) {\n\t\t\t\t\treturn `Flow: step \"${id}\" has a transition to unknown step \"${t.to}\"`\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n/** How many steps the caller actually submitted, before normalization strips/collapses the flow. */\nconst providedFlowStepCount = (raw: unknown): number => {\n\tif (raw === null || typeof raw !== 'object') return 0\n\tconst steps = (raw as { steps?: unknown }).steps\n\treturn Array.isArray(steps) ? steps.length : 0\n}\n\n/**\n * Stamp the configured uploads collection slug onto every `file` block (including repeater\n * sub-fields) on each save. The block carries a hidden `uploadsCollection` field so the slug\n * reaches the client renderer through the form document; the server always overwrites it from\n * plugin config, so the stored value is never author- or client-controlled.\n */\nconst stampFileCollections = (rows: FieldRow[], slug: string): void => {\n\tfor (const row of rows) {\n\t\tif (row.blockType === 'file') {\n\t\t\t;(row as { uploadsCollection?: string }).uploadsCollection = slug\n\t\t}\n\t\tconst subFields = (row as { subFields?: unknown }).subFields\n\t\tif (Array.isArray(subFields)) {\n\t\t\tstampFileCollections(subFields as FieldRow[], slug)\n\t\t}\n\t}\n}\n\ntype BuildFormsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/**\n\t * The plugin `consent.sources` option. Present: the `/:id/consent-sources` endpoint backing the\n\t * consent field's source select is registered. Absent: neither it nor the consent field type exists.\n\t */\n\tconsentSources?: ConsentSourcesResolver\n\t/** Plugin `consent.resolveOnRead` (default true); `false` skips the per-read consent afterRead hook. */\n\tconsentResolveOnRead?: boolean\n\tactionRegistry?: ActionRegistry\n\tlocalizeContent?: boolean\n\t/** The plugin `richText` option; `responseEditor ?? editor` sets the response message field's editor. */\n\trichText?: RichTextBodyOption\n\t/** The host-owned uploads collection slug from plugin config; absent when uploads are disabled. */\n\tuploadsCollectionSlug?: string\n\t/** Host seam gating anonymous results reads (plugin option `results.access`). */\n\tresultsAccess?: FormResultsAccess\n\t/** Registered poll option sources (plugin option `poll.sources`); empty registry means no source fields. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Registered poll outcome strategies (`poll.types`); drives the poll `type` select options. Defaults to the built-ins. */\n\tpollTypeRegistry?: PollTypeRegistry\n\t/** The plugin `poll.outcomeFields` seam; composes the outcome group from the two default fields. */\n\toutcomeFields?: OutcomeFieldsOverride\n\t/** The plugin `buttons` option; `fields` composes the `{ submit, prev, next }` labels from the localized defaults. */\n\tbuttons?: ButtonsOption\n\t/** The plugin `response` option; `redirect.fields` composes the `response.redirect` group from its default fields. */\n\tresponse?: ResponseOption\n\t/**\n\t * The plugin `email.fromAddresses` option. Present: both email actions gain a `from` select and\n\t * the `/:id/from-addresses` endpoint is registered. Absent: neither exists.\n\t */\n\tfromAddresses?: FromAddressesResolver\n\t/**\n\t * The plugin `email.departments` option. Present: the `emailTeam` `to` becomes a department select\n\t * and the `/:id/departments` endpoint backing it is registered. Absent: `to` stays a plain field.\n\t */\n\tdepartments?: DepartmentEmailsResolver\n\t/**\n\t * The plugin `redirectRelationships` option. Non-empty: `response.redirect` gains a polymorphic\n\t * `reference` relationship field so an author can redirect to an internal document instead of\n\t * (or alongside) a URL. Absent or empty: no `reference` field exists, matching today's URL-only\n\t * redirect.\n\t */\n\tredirectRelationships?: CollectionSlug[]\n\toverrides?: CollectionOverrides\n}\n\nexport const buildFormsCollection = ({\n\toverrides,\n\tregistry,\n\truleRegistry,\n\tconsentSources,\n\tconsentResolveOnRead,\n\tactionRegistry = new Map(),\n\tlocalizeContent = true,\n\trichText,\n\tuploadsCollectionSlug,\n\tresultsAccess,\n\tpollSourceRegistry,\n\tpollTypeRegistry,\n\toutcomeFields,\n\tbuttons,\n\tresponse,\n\tfromAddresses,\n\tdepartments,\n\tredirectRelationships,\n}: BuildFormsCollectionArgs): CollectionConfig => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst pollTypes = pollTypeRegistry ?? resolvePollTypes()\n\tconst pollResultsTypes = pollEligibleTypes(registry)\n\tconst bareTypes = new Set(\n\t\t[...registry.values()].filter((d) => d.bare === true).map((d) => d.type)\n\t)\n\t// Handed to the FlowBuilder so it can list bare (nameless) blocks in the step picker: slug ->\n\t// label (an i18n key or literal, resolved client-side).\n\tconst bareTypeLabels = Object.fromEntries(\n\t\t[...registry.values()].filter((d) => d.bare === true).map((d) => [d.type, d.label])\n\t)\n\t// The success `response` message field's editor: `richText.responseEditor` overrides the plugin-wide\n\t// `richText.editor` for this field alone (mirroring `bodyEditor` for action bodies).\n\tconst responseEditor = richText?.responseEditor ?? richText?.editor\n\tconst FLOW_BUILDER_REF = '@10x-media/form-builder/client#FlowBuilder'\n\tconst FIELD_NAME_SELECT_REF = '@10x-media/form-builder/client#FieldNameSelect'\n\tconst FLOW_STEPS_CELL_REF = '@10x-media/form-builder/client#FlowStepsCell'\n\tconst FIELD_COUNT_CELL_REF = '@10x-media/form-builder/client#FieldCountCell'\n\tconst CLOSE_POLL_BUTTON_REF = '@10x-media/form-builder/client#ClosePollButton'\n\t// The `source` outcome strategy delegates to the poll's option source, so it is useless without one:\n\t// offer it in the author-facing select only when option sources are registered. The strategy stays in\n\t// the registry regardless, so `resolvePollOutcome` and host code can still address it.\n\tconst hasOptionSources = (pollSourceRegistry?.size ?? 0) > 0\n\t// `mostVoted` leads the author-facing select because it is the default: an enabled poll works\n\t// immediately, with no winner to hand-pick. Only the select order changes; the registry keeps its\n\t// own order (host strategies stay after the built-ins).\n\tconst orderedPollTypes = [\n\t\t...[...pollTypes.values()].filter((strategy) => strategy.type === 'mostVoted'),\n\t\t...[...pollTypes.values()].filter((strategy) => strategy.type !== 'mostVoted'),\n\t]\n\n\tconst beforeValidate: CollectionBeforeValidateHook = ({ data, req }) => {\n\t\tif (data && Array.isArray(data.fields)) {\n\t\t\tconst normalized: FieldRow[] = normalizeFormConditions(\n\t\t\t\tdata.fields as FieldRow[],\n\t\t\t\tconditionTypes\n\t\t\t)\n\t\t\tfor (const field of normalized) {\n\t\t\t\tif ('expression' in field) {\n\t\t\t\t\tfield.expression = normalizeCalc(field.expression)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (uploadsCollectionSlug) {\n\t\t\t\tstampFileCollections(normalized, uploadsCollectionSlug)\n\t\t\t}\n\t\t\tdata.fields = normalized\n\t\t\t// Flow step assignments store field keys: machine names for named fields, block row ids\n\t\t\t// for bare (nameless) blocks. Mirrors `fieldKey` over the raw rows.\n\t\t\tconst fieldKeys = normalized\n\t\t\t\t.map((field: FieldRow) => {\n\t\t\t\t\tif (typeof field.name === 'string' && field.name.length > 0) {\n\t\t\t\t\t\treturn field.name\n\t\t\t\t\t}\n\t\t\t\t\treturn bareTypes.has(field.blockType) && field.id != null ? String(field.id) : undefined\n\t\t\t\t})\n\t\t\t\t.filter((key): key is string => key !== undefined)\n\t\t\t// Launder flow transition `when` clauses with the same operand-type rules as field conditions,\n\t\t\t// so a transition referencing a deleted field is dropped, not left as a route that always\n\t\t\t// matches at navigation time and force-routes the visitor.\n\t\t\tconst operandTypes = buildOperandTypes(normalized, conditionTypes)\n\t\t\tconst normalizedFlow = normalizeFlow(data.flow, fieldKeys, (w) =>\n\t\t\t\tnormalizeWhere(w, operandTypes)\n\t\t\t)\n\t\t\t// A flow the author built but that collapses to fewer than two valid steps would\n\t\t\t// otherwise vanish silently. Surface it instead of discarding their work.\n\t\t\tif (providedFlowStepCount(data.flow) > 0 && normalizedFlow === undefined) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tpath: 'flow',\n\t\t\t\t\t\t\t\tmessage: 'A flow needs at least two steps. Add another step or remove the flow.',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t\tdata.flow = normalizedFlow\n\t\t\t// A poll needs a field whose answers are the votes. With the poll enabled and no vote field\n\t\t\t// chosen (and the outcome not coming from an option source), bind one: auto-fill the sole\n\t\t\t// eligible field for a one-choice form, otherwise surface a clear error rather than storing a\n\t\t\t// poll that can never aggregate. The optionSource/`source` exemption preserves domain-driven\n\t\t\t// polls that have no on-form vote field. `buildValidateResultsField` still checks a set value;\n\t\t\t// this is the enforcer for enabled polls.\n\t\t\tif (data.pollEnabled === true) {\n\t\t\t\tconst poll =\n\t\t\t\t\tdata.poll != null && typeof data.poll === 'object'\n\t\t\t\t\t\t? (data.poll as Record<string, unknown>)\n\t\t\t\t\t\t: {}\n\t\t\t\tconst resultsField = typeof poll.resultsField === 'string' ? poll.resultsField.trim() : ''\n\t\t\t\tconst optionSource = typeof poll.optionSource === 'string' ? poll.optionSource.trim() : ''\n\t\t\t\tconst sourceDriven = optionSource.length > 0 || poll.type === 'source'\n\t\t\t\tif (resultsField.length === 0 && !sourceDriven) {\n\t\t\t\t\tconst eligible = fieldNamesOfType(data.fields, pollResultsTypes)\n\t\t\t\t\tif (eligible.length === 1) {\n\t\t\t\t\t\tdata.poll = { ...poll, resultsField: eligible[0] }\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst messageKey =\n\t\t\t\t\t\t\teligible.length > 1 ? keys.pollVoteFieldChoose : keys.pollVoteFieldMissing\n\t\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\t\t\terrors: [{ path: 'poll.resultsField', message: asTranslate(req.t)(messageKey) }],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treq.t\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn data\n\t}\n\n\t// After every save, (re)schedule the poll's auto-close job when it applies (enabled + closesAt +\n\t// unresolved + non-manual strategy + a job runner present). Best-effort and non-throwing, so it\n\t// never affects the save; without a runner the resolve-on-read fallback in the results endpoint\n\t// heals the outcome instead.\n\tconst pollCloseAfterChange: CollectionAfterChangeHook = async ({ doc, req }) => {\n\t\tawait enqueuePollClose({ payload: req.payload, form: doc, req })\n\t\treturn doc\n\t}\n\n\tconst fieldsField: Field = {\n\t\tname: 'fields',\n\t\ttype: 'blocks',\n\t\tblocks: buildFieldBlocks({ registry, ruleRegistry, localize: localizeContent }),\n\t\tadmin: { components: { Cell: FIELD_COUNT_CELL_REF } },\n\t}\n\n\tconst flowField: Field = {\n\t\tname: 'flow',\n\t\ttype: 'json',\n\t\tvalidate: validateFlow,\n\t\tadmin: {\n\t\t\tcomponents: {\n\t\t\t\tCell: FLOW_STEPS_CELL_REF,\n\t\t\t\tField: { path: FLOW_BUILDER_REF, clientProps: { conditionTypes, bareTypeLabels } },\n\t\t\t},\n\t\t},\n\t\t// Narrows the generated TypeScript type from opaque JSON to FormFlow so callers\n\t\t// don't need a cast. Keep this in sync with src/flow/types.ts.\n\t\ttypescriptSchema: [\n\t\t\t() => ({\n\t\t\t\ttype: 'object' as const,\n\t\t\t\trequired: ['steps'],\n\t\t\t\tadditionalProperties: false,\n\t\t\t\tproperties: {\n\t\t\t\t\tsteps: {\n\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\trequired: ['id'],\n\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\tid: { type: 'string' as const },\n\t\t\t\t\t\t\t\ttitle: { type: 'string' as const },\n\t\t\t\t\t\t\t\tfields: { type: 'array' as const, items: { type: 'string' as const } },\n\t\t\t\t\t\t\t\tnext: { type: ['string', 'null'] as ('string' | 'null')[] },\n\t\t\t\t\t\t\t\ttransitions: {\n\t\t\t\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\t\t\t\trequired: ['to'],\n\t\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\tto: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\t\twhen: { type: 'object' as const, additionalProperties: true },\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t}\n\n\tconst actionsField: Field = {\n\t\tname: 'actions',\n\t\ttype: 'blocks',\n\t\tblocks: buildActionBlocks(actionRegistry),\n\t\tlabel: labelForKey(keys.configActions),\n\t\t// Action config can contain secrets (e.g. signedWebhook.secret). The collection\n\t\t// itself is publicly readable so forms can be rendered by anonymous clients, but\n\t\t// action config must never be exposed to anonymous callers.\n\t\taccess: { read: isLoggedIn },\n\t}\n\n\t// Optional polymorphic reference to an internal document, for `response.redirect` to point at\n\t// instead of (or alongside) a URL. Absent unless `redirectRelationships` is a non-empty array:\n\t// always polymorphic even for one slug (the consent `page` field's precedent), so a host adding\n\t// a second collection later never changes the stored shape. The plugin never resolves this to a\n\t// URL itself; `toFormDocument` passes the raw `{ relationTo, value }` through for the host to\n\t// resolve, since only the host knows its own routing.\n\tconst redirectReferenceField: Field[] =\n\t\tredirectRelationships && redirectRelationships.length > 0\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'reference',\n\t\t\t\t\t\ttype: 'relationship',\n\t\t\t\t\t\trelationTo: redirectRelationships,\n\t\t\t\t\t\tlabel: labelForKey(keys.responseRedirectReference),\n\t\t\t\t\t\tadmin: { description: labelForKey(keys.responseRedirectReferenceDescription) },\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []\n\n\t// The redirect group's fields: the `url` text field plus the optional polymorphic `reference`.\n\t// The `response.redirect.fields` seam composes them (mirroring `buttons.fields`), so a host can\n\t// prepend a custom link field, swap `url` for their own picker, or filter; the default (no\n\t// override) is the same `[url, ...reference]` as before. Built-in redirect handling still reads\n\t// `redirect.url`/`redirect.reference`, so a host replacing `url` owns resolving their field to a\n\t// destination in their frontend.\n\tconst urlField: Field = {\n\t\tname: 'url',\n\t\ttype: 'text',\n\t\tlabel: labelForKey(keys.responseUrl),\n\t\tvalidate: validateUrl,\n\t}\n\tconst defaultRedirectFields: Field[] = [urlField, ...redirectReferenceField]\n\tconst redirectFields = response?.redirect?.fields\n\t\t? response.redirect.fields({ defaultFields: defaultRedirectFields })\n\t\t: defaultRedirectFields\n\n\t// What the visitor sees after a successful submit. Publicly readable (unlike actions): the\n\t// client renderer needs message/redirect. `type`/`url` are behavior, never localized;\n\t// `message` is visitor-facing content and follows `localizeContent`.\n\t// `type` is defaulted and not clearable rather than `required`: a required member would make\n\t// the whole group required in generated types, breaking typed `payload.create` calls that\n\t// omit `response`. Consumers treat a missing type as 'message'.\n\tconst responseField: Field = {\n\t\tname: 'response',\n\t\ttype: 'group',\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'type',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'message',\n\t\t\t\tlabel: labelForKey(keys.responseType),\n\t\t\t\tadmin: { isClearable: false },\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: labelForKey(keys.responseTypeMessage), value: 'message' },\n\t\t\t\t\t{ label: labelForKey(keys.responseTypeRedirect), value: 'redirect' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'message',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelForKey(keys.responseMessage),\n\t\t\t\t// Unset type (docs predating this field) means 'message', matching the client fallback.\n\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.type !== 'redirect' },\n\t\t\t\t...(responseEditor ? { editor: responseEditor } : {}),\n\t\t\t\t...localizedIf(localizeContent),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'redirect',\n\t\t\t\ttype: 'group',\n\t\t\t\tlabel: labelForKey(keys.responseRedirect),\n\t\t\t\tadmin: {\n\t\t\t\t\tcondition: (_data, siblingData) => siblingData?.type === 'redirect',\n\t\t\t\t\thideGutter: true,\n\t\t\t\t},\n\t\t\t\tfields: redirectFields,\n\t\t\t},\n\t\t],\n\t}\n\n\tconst defaultOutcomeFields = buildDefaultOutcomeFields()\n\tconst defaultButtonFields = buildDefaultButtonFields(localizeContent)\n\t// Half-width copy of a default field for the prev/next row; merges width into whatever admin the\n\t// field already carries (a host's `buttons.fields` override may add its own) rather than replacing\n\t// it. The cast is safe: `width` is a valid admin option on every field variant, but spreading a\n\t// `Field`-typed value's `admin` back into a union-typed object literal loses the discriminant TS\n\t// needs to check it structurally.\n\tconst halfWidth = (field: Field): Field =>\n\t\t({ ...field, admin: { ...field.admin, width: '50%' } }) as Field\n\t// The three button-label fields sit at the document root now (no `buttons` group): `submit` at\n\t// the bottom of the Fields tab, `prev`/`next` in a row on the Flow tab. The `buttons.fields` seam\n\t// composes each slot from the already-localized defaults, so a host can wrap a default in a row\n\t// with its own field (e.g. an icon select) or replace it; `toFormDocument` reassembles the three\n\t// labels into `FormDocument.buttons` for the client.\n\tconst composedButtons = buttons?.fields\n\t\t? buttons.fields({ defaultFields: defaultButtonFields })\n\t\t: defaultButtonFields\n\tconst submitField = composedButtons.submit\n\tconst prevNextRow: Field = {\n\t\ttype: 'row',\n\t\t// The prev/next labels only matter once the flow has a step; the Flow tab itself is gated on\n\t\t// `multistep`. `data` (1st arg) is the whole document, so `flow.steps` reads the stored flow.\n\t\tadmin: {\n\t\t\tcondition: (data) => {\n\t\t\t\tconst steps = (data as { flow?: { steps?: unknown } })?.flow?.steps\n\t\t\t\treturn Array.isArray(steps) && steps.length >= 1\n\t\t\t},\n\t\t},\n\t\tfields: [halfWidth(composedButtons.prev), halfWidth(composedButtons.next)],\n\t}\n\n\t// Poll lifecycle config; identifiers and behavior, never localized. Lives inside the conditional\n\t// Poll tab (gated on the top-level `pollEnabled` flag), so the per-field enabled conditions are\n\t// gone; `label: false` suppresses the group header the tab label already provides.\n\t// `resultsVisibility` is defaulted and not clearable rather than `required` for the same\n\t// generated-types reason as `response.type`.\n\tconst pollGroupField: Field = {\n\t\tname: 'poll',\n\t\ttype: 'group',\n\t\tlabel: false,\n\t\tfields: [\n\t\t\t// Authored by picking from the form's poll-eligible fields; the stored value stays a plain\n\t\t\t// text field name. Select options and server validate share `pollResultsTypes`, so they\n\t\t\t// cannot drift.\n\t\t\t{\n\t\t\t\tname: 'resultsField',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.pollResultsField),\n\t\t\t\tvalidate: buildValidateResultsField(pollResultsTypes),\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: {\n\t\t\t\t\t\tField: {\n\t\t\t\t\t\t\tpath: FIELD_NAME_SELECT_REF,\n\t\t\t\t\t\t\tclientProps: {\n\t\t\t\t\t\t\t\ttypes: pollResultsTypes,\n\t\t\t\t\t\t\t\tdescriptionKey: keys.pollResultsFieldDescription,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// How the winning value(s) get decided. `mostVoted` (default) and `source` auto-resolve on close\n\t\t\t// (via the scheduled job or the results-read fallback); `manual` leaves it to an admin. Options\n\t\t\t// come from the registered `poll.types` strategies, so a host strategy appears here too, with\n\t\t\t// `mostVoted` first so an enabled poll works with no winner to hand-pick.\n\t\t\t{\n\t\t\t\tname: 'type',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'mostVoted',\n\t\t\t\tlabel: labelForKey(keys.pollType),\n\t\t\t\tadmin: { isClearable: false, description: labelForKey(keys.pollTypeDescription) },\n\t\t\t\toptions: orderedPollTypes\n\t\t\t\t\t.filter((strategy) => strategy.type !== 'source' || hasOptionSources)\n\t\t\t\t\t.map((strategy) => ({\n\t\t\t\t\t\tlabel: resolveDefinitionLabel(strategy.label),\n\t\t\t\t\t\tvalue: strategy.type,\n\t\t\t\t\t})),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'resultsVisibility',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'afterVote',\n\t\t\t\tlabel: labelForKey(keys.pollResultsVisibility),\n\t\t\t\tadmin: { isClearable: false },\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: labelForKey(keys.pollVisibilityAfterVote), value: 'afterVote' },\n\t\t\t\t\t{ label: labelForKey(keys.pollVisibilityAfterClose), value: 'afterClose' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'closesAt',\n\t\t\t\ttype: 'date',\n\t\t\t\tlabel: labelForKey(keys.pollClosesAt),\n\t\t\t\tadmin: { date: { pickerAppearance: 'dayAndTime' } },\n\t\t\t},\n\t\t\t// Close / reopen the poll from the admin. The button toggles on the live `closesAt` and saves the\n\t\t\t// whole document (persisting an unsaved winner alongside `closesAt`) rather than calling the close\n\t\t\t// endpoint, so there is no DB-vs-form-state mismatch. Always mounted inside the (pollEnabled-gated)\n\t\t\t// Poll tab now; the button owns both states, so it carries no `admin.condition` of its own.\n\t\t\t{\n\t\t\t\tname: 'closePoll',\n\t\t\t\ttype: 'ui',\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: { Field: CLOSE_POLL_BUTTON_REF },\n\t\t\t\t},\n\t\t\t},\n\t\t\t...buildPollOptionSourceFields(pollSourceRegistry ?? new Map()),\n\t\t\t// `winningValues` is recorded either by an admin picking from the poll's effective options\n\t\t\t// (served by `/:id/poll-options`) or by `resolvePollOutcome` (host domain logic); more than\n\t\t\t// one value records a tie. The `pollOutcomeBeforeChange` hook validates both paths and owns\n\t\t\t// the `resolvedAt` stamp. `resolvedAt` itself stays fully locked: field-level create/update\n\t\t\t// access blocks every non-override caller write (Payload silently drops the denied value\n\t\t\t// rather than erroring) while the hook's stamp, applied after access filtering, still\n\t\t\t// persists. The `poll.outcomeFields` seam receives both defaults and its return becomes the\n\t\t\t// group's fields verbatim, so a host can swap `winningValues` for its own component; the hook\n\t\t\t// still validates every stored value against the effective options, so no swap bypasses it.\n\t\t\t// `hideGutter` stays: outcome is still a group nested inside the poll group.\n\t\t\t{\n\t\t\t\tname: 'outcome',\n\t\t\t\ttype: 'group',\n\t\t\t\tlabel: labelForKey(keys.pollOutcome),\n\t\t\t\tadmin: { hideGutter: true },\n\t\t\t\tfields: outcomeFields\n\t\t\t\t\t? outcomeFields({ defaultFields: defaultOutcomeFields })\n\t\t\t\t\t: [defaultOutcomeFields.winningValues, defaultOutcomeFields.resolvedAt],\n\t\t\t},\n\t\t],\n\t}\n\n\tconst defaultFields: Field[] = [\n\t\t// The document title is the only visitor-facing text the collection renders itself: it is\n\t\t// always required (drives `useAsTitle` in the admin list/relationship views) and is passed\n\t\t// through `toFormDocument` as `FormDocument.title`. Whether and how a host renders it above\n\t\t// the fields is entirely the host's call; the plugin does not gate or duplicate it.\n\t\t{\n\t\t\tname: 'title',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelForKey(keys.fieldTitle),\n\t\t\t// Visitor-facing content, so it follows `localizeContent` like the response message and\n\t\t\t// consent statement: hosts with localization get a per-locale title (and a locale-aware\n\t\t\t// `useAsTitle`), hosts without it are unaffected since Payload strips the flag. `validate`\n\t\t\t// (not `required`) keeps it mandatory only in the default locale so other locales fall back.\n\t\t\t...localizedIf(localizeContent),\n\t\t\tvalidate: validateFormTitle,\n\t\t},\n\t\t// The two form-type flags: behavior, never localized. `multistep` gates the Flow tab and the\n\t\t// client's step navigation; `pollEnabled` gates the Poll tab and marks the form a poll.\n\t\t{\n\t\t\ttype: 'row',\n\t\t\tfields: [\n\t\t\t\t{\n\t\t\t\t\tname: 'multistep',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tdefaultValue: false,\n\t\t\t\t\tlabel: labelForKey(keys.formMultistep),\n\t\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'pollEnabled',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tdefaultValue: false,\n\t\t\t\t\tlabel: labelForKey(keys.formPollEnabled),\n\t\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t// Unnamed tabs are presentational: their fields stay at the document root. An unnamed tab's\n\t\t// `admin.condition` receives the whole document as its 2nd arg, so the Flow and Poll tabs gate\n\t\t// on the root-level flags. The poll group nests its config under `form.poll` as before.\n\t\t{\n\t\t\ttype: 'tabs',\n\t\t\ttabs: [\n\t\t\t\t{ label: labelForKey(keys.tabFields), fields: [fieldsField, submitField] },\n\t\t\t\t{\n\t\t\t\t\tlabel: labelForKey(keys.tabFlow),\n\t\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.multistep === true },\n\t\t\t\t\tfields: [flowField, prevNextRow],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: labelForKey(keys.pollGroup),\n\t\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.pollEnabled === true },\n\t\t\t\t\tfields: [pollGroupField],\n\t\t\t\t},\n\t\t\t\t{ label: labelForKey(keys.tabActions), fields: [actionsField] },\n\t\t\t\t{ label: labelForKey(keys.tabResponse), fields: [responseField] },\n\t\t\t],\n\t\t},\n\t]\n\n\t// When `consent.sources` is set, resolve the visitor-facing consent statements onto every read of\n\t// a form doc (REST, local API, relationship population), so a client/modal fetch renders consent,\n\t// not only the RSC path that calls `resolveConsentStatements` itself. Fails open: a resolver outage\n\t// must never break form/admin/relationship reads, and the renderer already tolerates a missing\n\t// statement. `resolveConsentStatements` returns `{}` without calling the resolver when the form has\n\t// no consent fields, and entries are cached per request+form, so the cost stays bounded.\n\tconst consentAfterRead: CollectionAfterReadHook | undefined =\n\t\tconsentSources && consentResolveOnRead !== false\n\t\t\t? async ({ doc, req }) => {\n\t\t\t\t\tconst id = (doc as { id?: unknown }).id\n\t\t\t\t\tconst key = id == null ? undefined : String(id)\n\t\t\t\t\t// Re-entrance guard: a host resolver that reads this same form back (threading req) would\n\t\t\t\t\t// otherwise re-enter this hook and recurse. Payload runs collection afterRead concurrently\n\t\t\t\t\t// across list docs on one shared req.context, so this must be a per-id Set (a boolean would\n\t\t\t\t\t// make sibling docs skip), mutated in place and never reassigned after fan-out. Mirrors\n\t\t\t\t\t// @payloadcms/plugin-search's syncDocAsSearchIndex guard.\n\t\t\t\t\tif (key !== undefined && req.context) {\n\t\t\t\t\t\tconst inFlight =\n\t\t\t\t\t\t\t(req.context[CONSENT_AFTER_READ_GUARD] as Set<string> | undefined) ??\n\t\t\t\t\t\t\tnew Set<string>()\n\t\t\t\t\t\tif (inFlight.has(key)) {\n\t\t\t\t\t\t\treturn doc\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinFlight.add(key)\n\t\t\t\t\t\treq.context[CONSENT_AFTER_READ_GUARD] = inFlight\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst statements = await resolveConsentStatements({\n\t\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\tform: doc,\n\t\t\t\t\t\t\tsources: consentSources,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif (Object.keys(statements).length > 0) {\n\t\t\t\t\t\t\t;(doc as Record<string, unknown>).consentStatements = statements\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\treq.payload.logger?.warn(`form-builder consent afterRead: ${String(error)}`)\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (key !== undefined && req.context) {\n\t\t\t\t\t\t\t;(req.context[CONSENT_AFTER_READ_GUARD] as Set<string> | undefined)?.delete(key)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn doc\n\t\t\t\t}\n\t\t\t: undefined\n\n\tconst defaultEndpoints = buildFormsEndpoints({\n\t\tresultsAccess,\n\t\tpollResultsTypes,\n\t\tconsentSources,\n\t\tfromAddresses,\n\t\tdepartments,\n\t})\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORMS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionFormSingular),\n\t\t\tplural: labelForKey(keys.collectionFormPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: {\n\t\t\tgroup: 'Forms',\n\t\t\tuseAsTitle: 'title',\n\t\t\tdefaultColumns: ['title', 'fields', 'flow', 'pollEnabled', 'updatedAt'],\n\t\t\t...(overrides?.admin ?? {}),\n\t\t},\n\t\taccess: { read: () => true, ...(overrides?.access ?? {}) },\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// beforeValidate normalizes conditions and flow; consumer hooks run after\n\t\t\tbeforeValidate: [beforeValidate, ...(overrides?.hooks?.beforeValidate ?? [])],\n\t\t\tbeforeChange: [pollOutcomeBeforeChange, ...(overrides?.hooks?.beforeChange ?? [])],\n\t\t\tafterChange: [pollCloseAfterChange, ...(overrides?.hooks?.afterChange ?? [])],\n\t\t\tafterRead: [\n\t\t\t\t...(consentAfterRead ? [consentAfterRead] : []),\n\t\t\t\t...(overrides?.hooks?.afterRead ?? []),\n\t\t\t],\n\t\t},\n\t\tendpoints: [\n\t\t\t...defaultEndpoints,\n\t\t\t...(Array.isArray(overrides?.endpoints) ? overrides.endpoints : []),\n\t\t],\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { FormFieldInstance, SubmissionValue } from "../submissions/types.js";
2
- import { sanitizeUrl } from "../actions/body/converters.js";
2
+ import { BodyConverter, BodyConverterArgs, BodyRender, sanitizeUrl } from "../actions/body/converters.js";
3
3
  import { BodyContext, serializeBody } from "../actions/body/serializeBody.js";
4
4
  import { textOfBody } from "../actions/body/textOfBody.js";
5
5
  import { AggregationBucket, FieldAggregation } from "../aggregation/types.js";
@@ -25,7 +25,7 @@ import { FormPresentation, PresentationWrapperProps } from "../react/presentatio
25
25
  import { PresentationOption, PresentationRegistry, PresentationsConfig, resolvePresentations } from "../react/presentation/registry.js";
26
26
  import { RendererOption, RendererRegistry, RenderersConfig, resolveRenderers } from "../react/registry.js";
27
27
  import { SubmitFormResult, SubmitHandler, submitForm } from "../react/submitForm.js";
28
- import { Form, FormProps } from "../react/Form.js";
28
+ import { Form, FormProps, FormSuccessResponse, FormSuccessResult } from "../react/Form.js";
29
29
  import { RecallResolver, buildRecallResolver } from "../recall/resolver.js";
30
30
  import { FieldErrors, FormAction, FormState } from "../react/state.js";
31
31
  import { FormContextValue, FormControlLabels, FormStepInfo, useFormContext } from "../react/FormContext.js";
@@ -57,4 +57,4 @@ import { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD } from "../spam/constants.js"
57
57
  import { en } from "../translations/en.js";
58
58
  import { makeTranslate } from "../translations/makeTranslate.js";
59
59
  import { formatBytes } from "../uploads/formatBytes.js";
60
- export { type AggregationBucket, type BackButtonRenderProps, Backdrop, type BackdropProps, type BodyContext, CAPTCHA_TOKEN_KEY, COUNTRIES, type CalcExpression, type CaptchaWidgetHandle, Checkbox, type CheckboxProps, type ConsentStatement, type ConsentStatements, DEFAULT_HONEYPOT_FIELD, DialogSurface, type DialogSurfaceProps, type FetchResultsInput, type FetchResultsResult, type FieldAggregation, type FieldErrors, type FieldRenderer, type FieldRendererProps, FieldShell, type FieldShellProps, type FieldWidth, Form, type FormAction, type FormButtonSettings, type FormContextValue, type FormControlLabels, FormControls, type FormControlsProps, type FormDocument, type FormFieldInstance, FormFields, type FormFieldsProps, FormLayout, type FormLayoutProps, type FormPollSettings, type FormPresentation, type FormProps, type FormResponseSettings, FormResults, type FormResultsProps, type FormState, type FormStepInfo, FormSteps, type FormStepsProps, HCAPTCHA_SCRIPT_URL, HcaptchaCaptcha, type HcaptchaCaptchaProps, Honeypot, type HoneypotProps, Input, type InputProps, type NextButtonRenderProps, Poll, type PollProps, type PrefillOptions, type PresentationOption, type PresentationRegistry, type PresentationWrapperProps, type PresentationsConfig, RECAPTCHA_SCRIPT_URL, type RecallResolver, RecaptchaCaptcha, type RecaptchaCaptchaHandle, type RecaptchaCaptchaProps, type RendererOption, type RendererRegistry, type RendererTranslate, type RenderersConfig, Select, type SelectOption, type SelectProps, type SubmissionValue, type SubmitButtonRenderProps, type SubmitFormResult, type SubmitHandler, TURNSTILE_SCRIPT_URL, Textarea, type TextareaProps, type ToFormDocumentOptions, TurnstileCaptcha, type TurnstileCaptchaProps, US_STATES, type UploadFileInput, type UploadFileResult, type UseDismissOptions, type UseFieldResult, buildRecallResolver, cn, computeCalcFields, defaultPresentations, defaultRenderers, defineFieldRenderer, en, evaluateCalc, evaluateCondition, fetchFormResults, fieldKey, firstStepId, formatBytes, interpolate, isTerminalStepId, makeTranslate, resolveNextStepId, resolvePresentations, resolveRenderers, sanitizeUrl, serializeBody, stepFieldNames, submitForm, textOfBody, toFormDocument, uploadFile, useDismiss, useField, useFocusTrap, useFormContext, useFormState, useFormStep, useScrollLock, valuesFromSearchParams, widthProps };
60
+ export { type AggregationBucket, type BackButtonRenderProps, Backdrop, type BackdropProps, type BodyContext, type BodyConverter, type BodyConverterArgs, type BodyRender, CAPTCHA_TOKEN_KEY, COUNTRIES, type CalcExpression, type CaptchaWidgetHandle, Checkbox, type CheckboxProps, type ConsentStatement, type ConsentStatements, DEFAULT_HONEYPOT_FIELD, DialogSurface, type DialogSurfaceProps, type FetchResultsInput, type FetchResultsResult, type FieldAggregation, type FieldErrors, type FieldRenderer, type FieldRendererProps, FieldShell, type FieldShellProps, type FieldWidth, Form, type FormAction, type FormButtonSettings, type FormContextValue, type FormControlLabels, FormControls, type FormControlsProps, type FormDocument, type FormFieldInstance, FormFields, type FormFieldsProps, FormLayout, type FormLayoutProps, type FormPollSettings, type FormPresentation, type FormProps, type FormResponseSettings, FormResults, type FormResultsProps, type FormState, type FormStepInfo, FormSteps, type FormStepsProps, type FormSuccessResponse, type FormSuccessResult, HCAPTCHA_SCRIPT_URL, HcaptchaCaptcha, type HcaptchaCaptchaProps, Honeypot, type HoneypotProps, Input, type InputProps, type NextButtonRenderProps, Poll, type PollProps, type PrefillOptions, type PresentationOption, type PresentationRegistry, type PresentationWrapperProps, type PresentationsConfig, RECAPTCHA_SCRIPT_URL, type RecallResolver, RecaptchaCaptcha, type RecaptchaCaptchaHandle, type RecaptchaCaptchaProps, type RendererOption, type RendererRegistry, type RendererTranslate, type RenderersConfig, Select, type SelectOption, type SelectProps, type SubmissionValue, type SubmitButtonRenderProps, type SubmitFormResult, type SubmitHandler, TURNSTILE_SCRIPT_URL, Textarea, type TextareaProps, type ToFormDocumentOptions, TurnstileCaptcha, type TurnstileCaptchaProps, US_STATES, type UploadFileInput, type UploadFileResult, type UseDismissOptions, type UseFieldResult, buildRecallResolver, cn, computeCalcFields, defaultPresentations, defaultRenderers, defineFieldRenderer, en, evaluateCalc, evaluateCondition, fetchFormResults, fieldKey, firstStepId, formatBytes, interpolate, isTerminalStepId, makeTranslate, resolveNextStepId, resolvePresentations, resolveRenderers, sanitizeUrl, serializeBody, stepFieldNames, submitForm, textOfBody, toFormDocument, uploadFile, useDismiss, useField, useFocusTrap, useFormContext, useFormState, useFormStep, useScrollLock, valuesFromSearchParams, widthProps };
package/dist/options.d.ts CHANGED
@@ -50,7 +50,8 @@ type FormBuilderPluginOptions = {
50
50
  * Customize how the plugin's rich text is authored and rendered. `editor` is the default
51
51
  * Lexical/richText editor for every plugin richText field (message content, consent
52
52
  * statement, response message, action bodies); `bodyEditor` overrides the action body
53
- * fields specifically, falling back to `editor`. `converters` spread over the default
53
+ * fields and `responseEditor` overrides the success response message field, each falling
54
+ * back to `editor`. `converters` spread over the default
54
55
  * Lexical node converters; `serialize` replaces the whole action-body pipeline (e.g. to
55
56
  * target chat or plain-text channels instead of email HTML). A custom `serialize` receives
56
57
  * the submitted `form` (id/title) and `req`, enabling per-tenant lookups or handing the raw
@@ -1,4 +1,5 @@
1
1
  import { AnyFormFieldDefinition } from "../fields/types.js";
2
+ import { BodyConverter } from "../actions/body/converters.js";
2
3
  import { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings } from "../form/types.js";
3
4
  import { RendererTranslate } from "./contract.js";
4
5
  import { FormEventSink } from "../events/types.js";
@@ -11,6 +12,21 @@ import { SubmitHandler } from "./submitForm.js";
11
12
  import { ReactNode } from "react";
12
13
 
13
14
  //#region src/react/Form.d.ts
15
+ /**
16
+ * The success response passed to `onSuccess`, recall-resolved and (for a message) serialized with the
17
+ * form's active converters, so a host can render or toast the resolved response without re-deriving it.
18
+ */
19
+ type FormSuccessResponse = {
20
+ type: 'message';
21
+ html?: string;
22
+ } | {
23
+ type: 'redirect';
24
+ url?: string;
25
+ };
26
+ /** The second argument to `onSuccess`: the resolved success response (an object, so it can grow). */
27
+ type FormSuccessResult = {
28
+ response?: FormSuccessResponse;
29
+ };
14
30
  type FormProps = {
15
31
  form: FormDocument;
16
32
  fieldTypes?: AnyFormFieldDefinition[];
@@ -18,8 +34,24 @@ type FormProps = {
18
34
  renderers?: RenderersConfig;
19
35
  apiRoute?: string;
20
36
  onSubmit?: SubmitHandler;
21
- onSuccess?: (submissionId?: string) => void;
37
+ /**
38
+ * Called after a successful submission with the submission id and the resolved success response
39
+ * (recall-applied, serialized with `converters`), so a host can toast or render it. Fires in both
40
+ * `successBehavior` modes and on the custom-`children` path.
41
+ */
42
+ onSuccess?: (submissionId?: string, result?: FormSuccessResult) => void;
22
43
  onError?: (message: string) => void;
44
+ /**
45
+ * Custom Lexical block converters (e.g. host `icon`/`badge` blocks) spread over the defaults for the
46
+ * client serializer, so those blocks survive in the success message and in the `onSuccess` response.
47
+ */
48
+ converters?: Record<string, BodyConverter>;
49
+ /**
50
+ * What happens on a successful submit. `'replace'` (default) swaps the form for the success screen;
51
+ * `'reset'` clears the fields in place and shows no success screen, so a host can toast via `onSuccess`
52
+ * and keep the form usable.
53
+ */
54
+ successBehavior?: 'replace' | 'reset';
23
55
  events?: FormEventSink;
24
56
  t?: RendererTranslate;
25
57
  locale?: string;
@@ -63,6 +95,8 @@ declare const Form: ({
63
95
  onSubmit,
64
96
  onSuccess,
65
97
  onError,
98
+ converters,
99
+ successBehavior,
66
100
  events,
67
101
  t,
68
102
  locale,
@@ -90,5 +124,5 @@ declare const Form: ({
90
124
  backButtonClassName
91
125
  }: FormProps) => import("react/jsx-runtime").JSX.Element;
92
126
  //#endregion
93
- export { Form, FormProps };
127
+ export { Form, FormProps, FormSuccessResponse, FormSuccessResult };
94
128
  //# sourceMappingURL=Form.d.ts.map
@@ -33,7 +33,7 @@ const isEmpty = (value) => value == null || value === "" || Array.isArray(value)
33
33
  /** A stored button label counts only when it is a non-empty string; anything else falls through. */
34
34
  const storedLabel = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
35
35
  /** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */
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 }) => {
36
+ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, converters, successBehavior = "replace", 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 }) => {
37
37
  const honeypotName = honeypot === false ? null : honeypot?.name ?? "website";
38
38
  const honeypotRef = useRef(null);
39
39
  const registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes]);
@@ -155,7 +155,31 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
155
155
  field: field.name,
156
156
  value: recall(field.name)
157
157
  }));
158
- const stepVisible = (flow && currentStepId ? stepFieldNames(flow, currentStepId) : []).map((key) => visible.find((field) => fieldKey(field) === key)).filter((field) => Boolean(field));
158
+ /**
159
+ * The recall-resolved success response: a redirect's url, or the response message serialized with
160
+ * the active `converters` (so host blocks survive). Shared by the success screen and the value
161
+ * handed to `onSuccess`, so both stay identical.
162
+ */
163
+ const resolveSuccessResponse = () => {
164
+ const response = form.response;
165
+ if (response?.type === "redirect") return {
166
+ type: "redirect",
167
+ url: response.redirect?.url ?? void 0
168
+ };
169
+ const message = response?.type === "message" || response?.type == null ? response?.message : void 0;
170
+ if (!message) return;
171
+ return {
172
+ type: "message",
173
+ html: serializeBody(message, {
174
+ values: formattedValues(),
175
+ descriptors: descriptorsFor(answerableFields()),
176
+ converters
177
+ })
178
+ };
179
+ };
180
+ const stepKeys = flow && currentStepId ? stepFieldNames(flow, currentStepId) : [];
181
+ const stepKeySet = new Set(stepKeys);
182
+ const stepVisible = visible.filter((field) => stepKeySet.has(fieldKey(field)));
159
183
  const validateRepeaterSubFields = async (repeaters) => {
160
184
  const errors = {};
161
185
  for (const field of repeaters.filter((f) => f.blockType === "repeater" && isNamedField(f))) {
@@ -324,13 +348,28 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
324
348
  submittingRef.current = false;
325
349
  if (result.ok) {
326
350
  submittedRef.current = true;
327
- rawDispatch({ type: "SUBMIT_SUCCESS" });
328
351
  emitFormEvent(sinkRef.current, formIdRef.current, {
329
352
  type: "submission.created",
330
353
  submissionId: result.submissionId
331
354
  });
332
- onSuccess?.(result.submissionId);
333
- if (activePresentation.dismissOnSuccess) handleClose();
355
+ onSuccess?.(result.submissionId, { response: resolveSuccessResponse() });
356
+ if (successBehavior === "reset") {
357
+ rawDispatch({
358
+ type: "RESET",
359
+ values: {
360
+ ...seedFieldValues(form.fields),
361
+ ...initialValues ?? {}
362
+ }
363
+ });
364
+ if (flow) {
365
+ setCurrentStepId(firstStepId(flow));
366
+ setHistory([]);
367
+ }
368
+ startedRef.current = false;
369
+ } else {
370
+ rawDispatch({ type: "SUBMIT_SUCCESS" });
371
+ if (activePresentation.dismissOnSuccess) handleClose();
372
+ }
334
373
  const redirectUrl = form.response?.type === "redirect" ? form.response.redirect?.url : void 0;
335
374
  if (typeof redirectUrl === "string" && redirectUrl.length > 0 && typeof window !== "undefined") window.location.assign(redirectUrl);
336
375
  } else {
@@ -346,6 +385,13 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
346
385
  onError?.(message);
347
386
  }
348
387
  };
388
+ const handleKeyDown = (event) => {
389
+ if (!flow || event.key !== "Enter") return;
390
+ const target = event.target;
391
+ if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target instanceof HTMLButtonElement) return;
392
+ if (target instanceof HTMLInputElement && (target.type === "submit" || target.type === "button")) return;
393
+ event.preventDefault();
394
+ };
349
395
  const contextValue = {
350
396
  form,
351
397
  state,
@@ -376,7 +422,8 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
376
422
  t: translate,
377
423
  effectiveValues,
378
424
  recall,
379
- renderedFields: (flow ? stepVisible : visible).filter((field) => field.hidden !== true && field.calcDisplay !== false)
425
+ renderedFields: (flow ? stepVisible : visible).filter((field) => field.hidden !== true && field.calcDisplay !== false),
426
+ converters
380
427
  };
381
428
  const PresentationWrapper = activePresentation.Wrapper;
382
429
  const wrap = (content) => PresentationWrapper ? /* @__PURE__ */ jsx(PresentationWrapper, {
@@ -393,6 +440,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
393
440
  className: cn("fb-form-root", className),
394
441
  noValidate: true,
395
442
  onSubmit: handleSubmit,
443
+ onKeyDown: handleKeyDown,
396
444
  "data-fb-presentation": activePresentation.name,
397
445
  "data-fb-density": activePresentation.density,
398
446
  children: [honeypotName ? /* @__PURE__ */ jsx(Honeypot, {
@@ -402,11 +450,8 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
402
450
  }))
403
451
  });
404
452
  if (state.submitted) {
405
- const responseMessage = form.response?.type === "message" || form.response?.type == null ? form.response?.message : void 0;
406
- const responseHtml = responseMessage ? serializeBody(responseMessage, {
407
- values: formattedValues(),
408
- descriptors: descriptorsFor(answerableFields())
409
- }) : void 0;
453
+ const successResponse = resolveSuccessResponse();
454
+ const responseHtml = successResponse?.type === "message" ? successResponse.html : void 0;
410
455
  return /* @__PURE__ */ jsx(FormContext.Provider, {
411
456
  value: contextValue,
412
457
  children: wrap(responseHtml ? /* @__PURE__ */ jsx("div", {
@@ -430,6 +475,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
430
475
  className: cn("fb-form-root", className),
431
476
  noValidate: true,
432
477
  onSubmit: handleSubmit,
478
+ onKeyDown: handleKeyDown,
433
479
  "data-fb-presentation": activePresentation.name,
434
480
  "data-fb-density": activePresentation.density,
435
481
  children: [
@@ -1 +1 @@
1
- {"version":3,"file":"Form.js","names":[],"sources":["../../src/react/Form.tsx"],"sourcesContent":["'use client'\n\nimport {\n\ttype FormEvent as ReactFormEvent,\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseReducer,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport { serializeBody } from '../actions/body/serializeBody'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport { noopEventSink } from '../events/noopSink'\nimport type { FormEventSink } from '../events/types'\nimport { fieldKey, isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from '../flow/engine'\nimport type {\n\tFormButtonSettings,\n\tFormDocument,\n\tFormPollSettings,\n\tFormResponseSettings,\n} from '../form/types'\nimport {\n\tDEFAULT_PRESENTATION_NAME,\n\tdefaultPresentationDescriptors,\n} from '../presentations/defaults'\nimport { interpolate } from '../recall/interpolate'\nimport { buildRecallResolver, descriptorsFor } from '../recall/resolver'\nimport { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD, HONEYPOT_VALUE_KEY } from '../spam/constants'\nimport type { FormFieldInstance, SubmissionValue } from '../submissions/types'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport type { AnyValidationRuleDefinition } from '../validation/types'\nimport { cn } from './cn'\nimport type { RendererTranslate } from './contract'\nimport { emitFormEvent } from './events'\nimport { FormContext, type FormContextValue, type FormStepInfo } from './FormContext'\nimport {\n\ttype BackButtonRenderProps,\n\tFormControls,\n\ttype NextButtonRenderProps,\n\ttype SubmitButtonRenderProps,\n} from './FormControls'\nimport { FormFields } from './FormFields'\nimport { Honeypot } from './Honeypot'\nimport { defaultPresentations } from './presentation/presentations'\nimport { type PresentationsConfig, resolvePresentations } from './presentation/registry'\nimport type { FormPresentation } from './presentation/types'\nimport { type RenderersConfig, resolveRenderers } from './registry'\nimport { defaultRenderers } from './renderers'\nimport { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from './resolveForm'\nimport {\n\ttype FieldErrors,\n\ttype FormAction,\n\tformReducer,\n\tinitialFormState,\n\tseedFieldValues,\n} from './state'\nimport { type SubmitFormResult, type SubmitHandler, submitForm } from './submitForm'\nimport { validateFieldValue } from './validateField'\n\nexport type {\n\tBackButtonRenderProps,\n\tNextButtonRenderProps,\n\tSubmitButtonRenderProps,\n} from './FormControls'\n// FormResponseSettings, FormButtonSettings, FormPollSettings, and FormDocument live in\n// `../form/types` (no 'use client') so server code (e.g. `toFormDocument` in a Server Component)\n// can use them without pulling in this client module. Re-exported here so `./react` and existing\n// `from './Form'` imports keep working unchanged.\nexport type { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings }\n\nexport type FormProps = {\n\tform: FormDocument\n\tfieldTypes?: AnyFormFieldDefinition[]\n\trules?: AnyValidationRuleDefinition[]\n\trenderers?: RenderersConfig\n\tapiRoute?: string\n\tonSubmit?: SubmitHandler\n\tonSuccess?: (submissionId?: string) => void\n\tonError?: (message: string) => void\n\tevents?: FormEventSink\n\tt?: RendererTranslate\n\tlocale?: string\n\tlayout?: boolean\n\t/** Submit button label. Precedence: this prop, then the form's `buttons.submitLabel`, then the translated default. */\n\tsubmitLabel?: string\n\t/** \"Next\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.nextLabel`, then the translated default. */\n\tnextLabel?: string\n\t/** \"Back\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.prevLabel`, then the translated default. */\n\tprevLabel?: string\n\t/** Label for the overlay close control (modal/drawer). */\n\tcloseLabel?: string\n\tsuccessMessage?: string\n\t/** Active presentation: a name into the registry or an inline presentation. Defaults to `'page'` when omitted. */\n\tpresentation?: string | FormPresentation\n\t/** Per-render presentation overrides merged onto the defaults (add, replace, or `false` to remove). */\n\tpresentations?: PresentationsConfig\n\t/** Invoked when an overlay presentation dismisses (close button, Escape, outside click, or `dismissOnSuccess`). */\n\tonClose?: () => void\n\t/**\n\t * Accessible name for an overlay surface (modal/drawer). Hosts choosing between a trigger\n\t * label and the form's own admin title should prefer `form.title` when set, falling back to\n\t * their own label otherwise.\n\t */\n\ttitle?: string\n\t/** Seed initial field values (e.g. from `valuesFromSearchParams`). Still validated on submit. */\n\tinitialValues?: Record<string, unknown>\n\t/** Honeypot decoy (on by default). `false` removes it; `{ name }` matches a customized server `spam.honeypot.fieldName`. */\n\thoneypot?: false | { name?: string }\n\t/** A token from your captcha widget; verified server-side when a captcha provider is configured. */\n\tcaptchaToken?: string\n\t/** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */\n\tchildren?: ReactNode\n\t/** Chrome rendered inside the form, above the fields, in default mode (e.g. `<FormSteps />`). */\n\theader?: ReactNode\n\t/** Additional CSS class names applied to the root `<form>` element (and the success node). */\n\tclassName?: string\n\t/** Replace the default submit button entirely. Receives the resolved label and submitting state. */\n\trenderSubmit?: (props: SubmitButtonRenderProps) => ReactNode\n\t/** Replace the default \"Next\" button in multi-step forms. */\n\trenderNext?: (props: NextButtonRenderProps) => ReactNode\n\t/** Replace the default \"Back\" button in multi-step forms. */\n\trenderBack?: (props: BackButtonRenderProps) => ReactNode\n\t/** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */\n\tsubmitButtonClassName?: string\n\t/** CSS class forwarded to the default \"Next\" `<button>`. Ignored when `renderNext` is provided. */\n\tnextButtonClassName?: string\n\t/** CSS class forwarded to the default \"Back\" `<button>`. Ignored when `renderBack` is provided. */\n\tbackButtonClassName?: string\n}\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\n/** A stored button label counts only when it is a non-empty string; anything else falls through. */\nconst storedLabel = (value: unknown): string | undefined =>\n\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\n/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */\nexport const Form = ({\n\tform,\n\tfieldTypes,\n\trules,\n\trenderers,\n\tapiRoute,\n\tonSubmit,\n\tonSuccess,\n\tonError,\n\tevents,\n\tt,\n\tlocale = 'en',\n\tlayout,\n\tsubmitLabel,\n\tnextLabel,\n\tprevLabel,\n\tcloseLabel,\n\tsuccessMessage,\n\tpresentation,\n\tpresentations,\n\tonClose,\n\ttitle,\n\tinitialValues,\n\thoneypot,\n\tcaptchaToken,\n\tchildren,\n\theader,\n\tclassName,\n\trenderSubmit,\n\trenderNext,\n\trenderBack,\n\tsubmitButtonClassName,\n\tnextButtonClassName,\n\tbackButtonClassName,\n}: FormProps) => {\n\tconst honeypotName = honeypot === false ? null : (honeypot?.name ?? DEFAULT_HONEYPOT_FIELD)\n\tconst honeypotRef = useRef<HTMLInputElement>(null)\n\tconst registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes])\n\tconst ruleRegistry = useMemo(() => buildValidationRuleRegistry(rules), [rules])\n\tconst rendererRegistry = useMemo(() => resolveRenderers(defaultRenderers, renderers), [renderers])\n\tconst presentationRegistry = useMemo(\n\t\t() => resolvePresentations(defaultPresentations, presentations),\n\t\t[presentations]\n\t)\n\tconst activePresentation: FormPresentation =\n\t\ttypeof presentation === 'object'\n\t\t\t? presentation\n\t\t\t: (presentationRegistry.get(presentation ?? DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tpresentationRegistry.get(DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tdefaultPresentationDescriptors.page)\n\tconst fieldsByName = useMemo(\n\t\t() => new Map(form.fields.filter(isNamedField).map((field) => [field.name, field])),\n\t\t[form.fields]\n\t)\n\tconst translate = useMemo<RendererTranslate>(() => t ?? makeTranslate(en), [t])\n\tconst resolvedCloseLabel = closeLabel ?? translate(keys.formClose)\n\tconst resolvedSuccessMessage = successMessage ?? translate(keys.formSuccess)\n\tconst docButtons: FormButtonSettings | undefined = form.buttons\n\tconst labels = useMemo(\n\t\t() => ({\n\t\t\tprev: prevLabel ?? storedLabel(docButtons?.prevLabel) ?? translate(keys.formBack),\n\t\t\tnext: nextLabel ?? storedLabel(docButtons?.nextLabel) ?? translate(keys.formNext),\n\t\t\tsubmit: submitLabel ?? storedLabel(docButtons?.submitLabel) ?? translate(keys.formSubmit),\n\t\t}),\n\t\t[prevLabel, nextLabel, submitLabel, docButtons, translate]\n\t)\n\n\t// Latest-value refs so event emission and the mount/unmount effect tolerate an inline `events` prop or a changing form id.\n\tconst sinkRef = useRef<FormEventSink>(noopEventSink)\n\tsinkRef.current = events ?? noopEventSink\n\tconst formIdRef = useRef('')\n\tformIdRef.current = String(form.id)\n\n\tconst [state, rawDispatch] = useReducer(formReducer, form.fields, (fields) =>\n\t\tinitialFormState({\n\t\t\t...seedFieldValues(fields),\n\t\t\t...(initialValues ?? {}),\n\t\t})\n\t)\n\n\t// Authoritative values for derived (calc) fields, recomputed from user answers on every change. The\n\t// server recomputes these too at submit; the client copy drives the live calc renderer, recall, and submit.\n\tconst effectiveValues = useMemo(\n\t\t() => computeCalcFields(form.fields, state.values),\n\t\t[form.fields, state.values]\n\t)\n\n\tconst recall = useMemo(\n\t\t() =>\n\t\t\tbuildRecallResolver({\n\t\t\t\tfields: form.fields,\n\t\t\t\tvalues: effectiveValues,\n\t\t\t\tregistry,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}),\n\t\t[form.fields, effectiveValues, registry, locale, translate]\n\t)\n\n\t// Multi-step is active only when the form is flagged `multistep` and its flow declares two or more\n\t// steps. With the flag off the form renders as a single page even if flow data is still stored.\n\tconst flow =\n\t\tform.multistep === true && form.flow && form.flow.steps.length >= 2 ? form.flow : undefined\n\tconst [currentStepId, setCurrentStepId] = useState<string | undefined>(() =>\n\t\tflow ? firstStepId(flow) : undefined\n\t)\n\tconst [history, setHistory] = useState<string[]>([])\n\n\tconst startedRef = useRef(false)\n\tconst submittedRef = useRef(false)\n\tconst submittingRef = useRef(false)\n\tconst advancingRef = useRef(false)\n\tconst flowRef = useRef(flow)\n\tflowRef.current = flow\n\n\tconst dispatch = useCallback((action: FormAction) => {\n\t\tif (action.type === 'SET_VALUE' && !startedRef.current) {\n\t\t\tstartedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.started' })\n\t\t}\n\t\trawDispatch(action)\n\t}, [])\n\n\tconst validateField = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tconst field = fieldsByName.get(name)\n\t\t\tif (!field) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst answers = { ...effectiveValues, [name]: value }\n\t\t\t// Mirror the server: a field whose `validateWhen` is unmet is not validated; clear any stale error.\n\t\t\tif (!evaluateCondition(field.validateWhen, answers)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors: [] })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvoid validateFieldValue({\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tregistry,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}).then(({ errors }) => {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors })\n\t\t\t\tconst [firstError] = errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[fieldsByName, effectiveValues, registry, ruleRegistry, locale, translate]\n\t)\n\n\tconst visible = visibleFields(form.fields, effectiveValues)\n\n\t/** Visible, answered named fields. Display-only ('none' kind) and nameless (bare) fields never contribute. */\n\tconst answerableFields = (): NamedFormFieldInstance[] =>\n\t\tvisibleFields(form.fields, effectiveValues)\n\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t.filter(isNamedField)\n\t\t\t.filter((field) => !isEmpty(effectiveValues[field.name]))\n\n\t/** Answered visible fields as raw submission values, sent to the server on submit. */\n\tconst answeredValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: effectiveValues[field.name] }))\n\n\t/**\n\t * Same fields, formatted via `recall` (option labels, Yes/No, localized dates): recall-fidelity\n\t * values for client-rendered templates, matching what the server gives email-team's body.\n\t */\n\tconst formattedValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: recall(field.name) }))\n\n\t// Steps address fields by key: machine names for named fields, block row ids for bare blocks.\n\tconst stepKeys = flow && currentStepId ? stepFieldNames(flow, currentStepId) : []\n\tconst stepVisible: FormFieldInstance[] = stepKeys\n\t\t.map((key) => visible.find((field) => fieldKey(field) === key))\n\t\t.filter((field): field is FormFieldInstance => Boolean(field))\n\n\t// Validate the sub-fields of the given repeaters, one pass per visible row, returning composite-key\n\t// errors (`fieldName[rowIndex].subFieldName`). Shared by submit and step navigation so both mirror\n\t// the server's per-row pass and neither lets an invalid required sub-field slip through.\n\tconst validateRepeaterSubFields = async (\n\t\trepeaters: FormFieldInstance[]\n\t): Promise<FieldErrors> => {\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const field of repeaters.filter(\n\t\t\t(f): f is NamedFormFieldInstance => f.blockType === 'repeater' && isNamedField(f)\n\t\t)) {\n\t\t\tconst rows = Array.isArray(effectiveValues[field.name])\n\t\t\t\t? (effectiveValues[field.name] as Array<Record<string, unknown>>)\n\t\t\t\t: []\n\t\t\tconst subFields = (\n\t\t\t\tArray.isArray(field.subFields) ? (field.subFields as FormFieldInstance[]) : []\n\t\t\t).filter(isNamedField)\n\t\t\tfor (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n\t\t\t\tconst row = rows[rowIndex] ?? {}\n\t\t\t\tfor (const subField of subFields) {\n\t\t\t\t\tif (!evaluateCondition(subField.visibleWhen, row)) continue\n\t\t\t\t\tif (!evaluateCondition(subField.validateWhen, row)) continue\n\t\t\t\t\tconst subResult = await validateFieldValue({\n\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: row,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})\n\t\t\t\t\tconst compositeKey = `${field.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\tif (subResult.errors.length > 0) errors[compositeKey] = subResult.errors\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn errors\n\t}\n\n\tconst goNext = async () => {\n\t\t// Re-entrancy guard: a double-click during async validation must not push the same step onto\n\t\t// history twice (which would need two Back presses to undo).\n\t\tif (!flow || !currentStepId || advancingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tadvancingRef.current = true\n\t\ttry {\n\t\t\tconst results = await Promise.all(\n\t\t\t\tstepVisible\n\t\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t\t.filter(isNamedField)\n\t\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t\t.map(async (field) => ({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\t\tregistry,\n\t\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\tt: translate,\n\t\t\t\t\t\t})),\n\t\t\t\t\t}))\n\t\t\t)\n\t\t\tlet hasError = false\n\t\t\tfor (const result of results) {\n\t\t\t\trawDispatch({ type: 'TOUCH', name: result.field.name })\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'SET_FIELD_ISSUES',\n\t\t\t\t\tname: result.field.name,\n\t\t\t\t\terrors: result.errors,\n\t\t\t\t})\n\t\t\t\tif (result.errors.length > 0) {\n\t\t\t\t\thasError = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Mirror submit: validate the step's repeater sub-fields too, so a required sub-field can't be\n\t\t\t// skipped past on a non-terminal step (errors surface inline via the composite key).\n\t\t\tconst repeaterErrors = await validateRepeaterSubFields(stepVisible)\n\t\t\tfor (const [compositeKey, errs] of Object.entries(repeaterErrors)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name: compositeKey, errors: errs })\n\t\t\t\thasError = true\n\t\t\t}\n\t\t\tif (hasError) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = resolveNextStepId(flow, currentStepId, effectiveValues)\n\t\t\tif (!next) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'step.completed',\n\t\t\t\tstepId: currentStepId,\n\t\t\t})\n\t\t\tsetHistory((prev) => [...prev, currentStepId])\n\t\t\tsetCurrentStepId(next)\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: next })\n\t\t} finally {\n\t\t\tadvancingRef.current = false\n\t\t}\n\t}\n\n\tconst goBack = () => {\n\t\tconst prev = history[history.length - 1]\n\t\tif (prev === undefined) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory((entries) => entries.slice(0, -1))\n\t\tsetCurrentStepId(prev)\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: prev })\n\t}\n\n\tuseEffect(() => {\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.viewed' })\n\t\tconst mountFlow = flowRef.current\n\t\tif (mountFlow) {\n\t\t\tconst first = firstStepId(mountFlow)\n\t\t\tif (first) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: first })\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tif (!submittedRef.current && !submittingRef.current) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.abandoned' })\n\t\t\t}\n\t\t}\n\t}, [])\n\n\tconst handleClose = useCallback(() => {\n\t\tonClose?.()\n\t}, [onClose])\n\n\tconst handleSubmit = async (event: ReactFormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault()\n\t\t// Re-entrancy guard: claim the in-flight slot before the async validation window so a fast second\n\t\t// activation (double-click, Enter + click) cannot reach the transport and POST the submission twice.\n\t\tif (submittingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tsubmittingRef.current = true\n\t\tconst visible = visibleFields(form.fields, effectiveValues)\n\t\tconst results = await Promise.all(\n\t\t\t// Calc fields carry no rules and have no input; they are always satisfied, so skip validating them.\n\t\t\t// Display-only ('none' kind, e.g. message) and nameless (bare) fields are skipped too, mirroring the server.\n\t\t\t// A field whose `validateWhen` is unmet is skipped too, mirroring the server (no client/server divergence).\n\t\t\tvisible\n\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t.filter(isNamedField)\n\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t.map(async (field) => ({\n\t\t\t\t\tfield,\n\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})),\n\t\t\t\t}))\n\t\t)\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const result of results) {\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\terrors[result.field.name] = result.errors\n\t\t\t\tconst [firstError] = result.errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: result.field.name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate sub-fields within each visible repeater (composite keys `fieldName[rowIndex].subFieldName`),\n\t\t// mirroring the server's per-row pass, so the repeater renderer surfaces them inline.\n\t\tObject.assign(errors, await validateRepeaterSubFields(visible))\n\n\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors })\n\t\tif (Object.keys(errors).length > 0) {\n\t\t\tsubmittingRef.current = false\n\t\t\treturn\n\t\t}\n\t\trawDispatch({ type: 'SUBMIT_START' })\n\t\tconst values: SubmissionValue[] = answeredValues()\n\t\tif (honeypotName) {\n\t\t\tconst decoy = honeypotRef.current?.value ?? ''\n\t\t\tif (decoy !== '') {\n\t\t\t\t// Submit the decoy under a reserved key, not its cosmetic DOM name, so a real field sharing\n\t\t\t\t// that name is never stripped or mistaken for the honeypot on the server.\n\t\t\t\tvalues.push({ field: HONEYPOT_VALUE_KEY, value: decoy })\n\t\t\t}\n\t\t}\n\t\tif (captchaToken) {\n\t\t\tvalues.push({ field: CAPTCHA_TOKEN_KEY, value: captchaToken })\n\t\t}\n\t\tconst result: SubmitFormResult = onSubmit\n\t\t\t? await onSubmit({ formId: form.id, values })\n\t\t\t: await submitForm({ formId: form.id, values, apiRoute })\n\t\tsubmittingRef.current = false\n\t\tif (result.ok) {\n\t\t\tsubmittedRef.current = true\n\t\t\trawDispatch({ type: 'SUBMIT_SUCCESS' })\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'submission.created',\n\t\t\t\tsubmissionId: result.submissionId,\n\t\t\t})\n\t\t\tonSuccess?.(result.submissionId)\n\t\t\tif (activePresentation.dismissOnSuccess) {\n\t\t\t\thandleClose()\n\t\t\t}\n\t\t\tconst redirectUrl =\n\t\t\t\tform.response?.type === 'redirect' ? form.response.redirect?.url : undefined\n\t\t\t// Part of submit handling, not rendering: fires on the custom-`children` path too.\n\t\t\t// Browser-only: no-op during SSR or in non-DOM test environments.\n\t\t\tif (\n\t\t\t\ttypeof redirectUrl === 'string' &&\n\t\t\t\tredirectUrl.length > 0 &&\n\t\t\t\ttypeof window !== 'undefined'\n\t\t\t) {\n\t\t\t\twindow.location.assign(redirectUrl)\n\t\t\t}\n\t\t} else {\n\t\t\tif (result.fieldErrors) {\n\t\t\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors: result.fieldErrors })\n\t\t\t}\n\t\t\tconst message = result.message ?? translate(keys.formSubmitFailed)\n\t\t\trawDispatch({ type: 'SUBMIT_ERROR', message })\n\t\t\tonError?.(message)\n\t\t}\n\t}\n\n\tconst step: FormStepInfo = flow\n\t\t? {\n\t\t\t\tflow,\n\t\t\t\tcurrentStepId,\n\t\t\t\tstepIndex: flow.steps.findIndex((s) => s.id === currentStepId),\n\t\t\t\tstepCount: flow.steps.length,\n\t\t\t\tisFirst: history.length === 0,\n\t\t\t\tisTerminal: currentStepId ? isTerminalStepId(flow, currentStepId, effectiveValues) : true,\n\t\t\t\tgoNext: () => {\n\t\t\t\t\tvoid goNext()\n\t\t\t\t},\n\t\t\t\tgoBack,\n\t\t\t}\n\t\t: {\n\t\t\t\tstepIndex: 0,\n\t\t\t\tstepCount: 1,\n\t\t\t\tisFirst: true,\n\t\t\t\tisTerminal: true,\n\t\t\t\tgoNext: () => {},\n\t\t\t\tgoBack: () => {},\n\t\t\t}\n\n\tconst renderedFields = (flow ? stepVisible : visible).filter(\n\t\t(field) => field.hidden !== true && field.calcDisplay !== false\n\t)\n\n\tconst contextValue: FormContextValue = {\n\t\tform,\n\t\tstate,\n\t\tdispatch,\n\t\tvalidateField,\n\t\tlocale,\n\t\tstep,\n\t\trendererRegistry,\n\t\tlabels,\n\t\tt: translate,\n\t\teffectiveValues,\n\t\trecall,\n\t\trenderedFields,\n\t}\n\n\tconst PresentationWrapper = activePresentation.Wrapper\n\tconst wrap = (content: ReactNode): ReactNode =>\n\t\tPresentationWrapper ? (\n\t\t\t<PresentationWrapper\n\t\t\t\tpresentation={activePresentation}\n\t\t\t\topen\n\t\t\t\tonClose={handleClose}\n\t\t\t\ttitle={title}\n\t\t\t\tcloseLabel={resolvedCloseLabel}\n\t\t\t>\n\t\t\t\t{content}\n\t\t\t</PresentationWrapper>\n\t\t) : (\n\t\t\tcontent\n\t\t)\n\n\tif (children !== undefined) {\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\t<form\n\t\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\t\tnoValidate\n\t\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t>\n\t\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\tif (state.submitted) {\n\t\tconst responseMessage =\n\t\t\tform.response?.type === 'message' || form.response?.type == null\n\t\t\t\t? form.response?.message\n\t\t\t\t: undefined\n\t\tconst responseHtml = responseMessage\n\t\t\t? serializeBody(responseMessage, {\n\t\t\t\t\tvalues: formattedValues(),\n\t\t\t\t\tdescriptors: descriptorsFor(answerableFields()),\n\t\t\t\t})\n\t\t\t: undefined\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\tresponseHtml ? (\n\t\t\t\t\t\t// Safe to inject: serializeBody HTML-escapes all text (recall values included) and sanitizes link URLs.\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is produced by our escaping serializer, never raw user input\n\t\t\t\t\t\t\tdangerouslySetInnerHTML={{ __html: responseHtml }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{interpolate(resolvedSuccessMessage, recall)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t)\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\treturn (\n\t\t<FormContext.Provider value={contextValue}>\n\t\t\t{wrap(\n\t\t\t\t<form\n\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\tnoValidate\n\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t>\n\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t{header}\n\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t{state.submitError ? (\n\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__submit-error\">\n\t\t\t\t\t\t\t{state.submitError}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t) : null}\n\t\t\t\t\t<FormControls\n\t\t\t\t\t\tbackButtonClassName={backButtonClassName}\n\t\t\t\t\t\tnextButtonClassName={nextButtonClassName}\n\t\t\t\t\t\tsubmitButtonClassName={submitButtonClassName}\n\t\t\t\t\t\trenderBack={renderBack}\n\t\t\t\t\t\trenderNext={renderNext}\n\t\t\t\t\t\trenderSubmit={renderSubmit}\n\t\t\t\t\t/>\n\t\t\t\t</form>\n\t\t\t)}\n\t\t</FormContext.Provider>\n\t)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Form.js","names":[],"sources":["../../src/react/Form.tsx"],"sourcesContent":["'use client'\n\nimport {\n\ttype FormEvent as ReactFormEvent,\n\ttype KeyboardEvent as ReactKeyboardEvent,\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseReducer,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport type { BodyConverter } from '../actions/body/converters'\nimport { serializeBody } from '../actions/body/serializeBody'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport { noopEventSink } from '../events/noopSink'\nimport type { FormEventSink } from '../events/types'\nimport { fieldKey, isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from '../flow/engine'\nimport type {\n\tFormButtonSettings,\n\tFormDocument,\n\tFormPollSettings,\n\tFormResponseSettings,\n} from '../form/types'\nimport {\n\tDEFAULT_PRESENTATION_NAME,\n\tdefaultPresentationDescriptors,\n} from '../presentations/defaults'\nimport { interpolate } from '../recall/interpolate'\nimport { buildRecallResolver, descriptorsFor } from '../recall/resolver'\nimport { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD, HONEYPOT_VALUE_KEY } from '../spam/constants'\nimport type { FormFieldInstance, SubmissionValue } from '../submissions/types'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport type { AnyValidationRuleDefinition } from '../validation/types'\nimport { cn } from './cn'\nimport type { RendererTranslate } from './contract'\nimport { emitFormEvent } from './events'\nimport { FormContext, type FormContextValue, type FormStepInfo } from './FormContext'\nimport {\n\ttype BackButtonRenderProps,\n\tFormControls,\n\ttype NextButtonRenderProps,\n\ttype SubmitButtonRenderProps,\n} from './FormControls'\nimport { FormFields } from './FormFields'\nimport { Honeypot } from './Honeypot'\nimport { defaultPresentations } from './presentation/presentations'\nimport { type PresentationsConfig, resolvePresentations } from './presentation/registry'\nimport type { FormPresentation } from './presentation/types'\nimport { type RenderersConfig, resolveRenderers } from './registry'\nimport { defaultRenderers } from './renderers'\nimport { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from './resolveForm'\nimport {\n\ttype FieldErrors,\n\ttype FormAction,\n\tformReducer,\n\tinitialFormState,\n\tseedFieldValues,\n} from './state'\nimport { type SubmitFormResult, type SubmitHandler, submitForm } from './submitForm'\nimport { validateFieldValue } from './validateField'\n\nexport type {\n\tBackButtonRenderProps,\n\tNextButtonRenderProps,\n\tSubmitButtonRenderProps,\n} from './FormControls'\n// FormResponseSettings, FormButtonSettings, FormPollSettings, and FormDocument live in\n// `../form/types` (no 'use client') so server code (e.g. `toFormDocument` in a Server Component)\n// can use them without pulling in this client module. Re-exported here so `./react` and existing\n// `from './Form'` imports keep working unchanged.\nexport type { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings }\n\n/**\n * The success response passed to `onSuccess`, recall-resolved and (for a message) serialized with the\n * form's active converters, so a host can render or toast the resolved response without re-deriving it.\n */\nexport type FormSuccessResponse =\n\t| { type: 'message'; html?: string }\n\t| { type: 'redirect'; url?: string }\n\n/** The second argument to `onSuccess`: the resolved success response (an object, so it can grow). */\nexport type FormSuccessResult = { response?: FormSuccessResponse }\n\nexport type FormProps = {\n\tform: FormDocument\n\tfieldTypes?: AnyFormFieldDefinition[]\n\trules?: AnyValidationRuleDefinition[]\n\trenderers?: RenderersConfig\n\tapiRoute?: string\n\tonSubmit?: SubmitHandler\n\t/**\n\t * Called after a successful submission with the submission id and the resolved success response\n\t * (recall-applied, serialized with `converters`), so a host can toast or render it. Fires in both\n\t * `successBehavior` modes and on the custom-`children` path.\n\t */\n\tonSuccess?: (submissionId?: string, result?: FormSuccessResult) => void\n\tonError?: (message: string) => void\n\t/**\n\t * Custom Lexical block converters (e.g. host `icon`/`badge` blocks) spread over the defaults for the\n\t * client serializer, so those blocks survive in the success message and in the `onSuccess` response.\n\t */\n\tconverters?: Record<string, BodyConverter>\n\t/**\n\t * What happens on a successful submit. `'replace'` (default) swaps the form for the success screen;\n\t * `'reset'` clears the fields in place and shows no success screen, so a host can toast via `onSuccess`\n\t * and keep the form usable.\n\t */\n\tsuccessBehavior?: 'replace' | 'reset'\n\tevents?: FormEventSink\n\tt?: RendererTranslate\n\tlocale?: string\n\tlayout?: boolean\n\t/** Submit button label. Precedence: this prop, then the form's `buttons.submitLabel`, then the translated default. */\n\tsubmitLabel?: string\n\t/** \"Next\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.nextLabel`, then the translated default. */\n\tnextLabel?: string\n\t/** \"Back\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.prevLabel`, then the translated default. */\n\tprevLabel?: string\n\t/** Label for the overlay close control (modal/drawer). */\n\tcloseLabel?: string\n\tsuccessMessage?: string\n\t/** Active presentation: a name into the registry or an inline presentation. Defaults to `'page'` when omitted. */\n\tpresentation?: string | FormPresentation\n\t/** Per-render presentation overrides merged onto the defaults (add, replace, or `false` to remove). */\n\tpresentations?: PresentationsConfig\n\t/** Invoked when an overlay presentation dismisses (close button, Escape, outside click, or `dismissOnSuccess`). */\n\tonClose?: () => void\n\t/**\n\t * Accessible name for an overlay surface (modal/drawer). Hosts choosing between a trigger\n\t * label and the form's own admin title should prefer `form.title` when set, falling back to\n\t * their own label otherwise.\n\t */\n\ttitle?: string\n\t/** Seed initial field values (e.g. from `valuesFromSearchParams`). Still validated on submit. */\n\tinitialValues?: Record<string, unknown>\n\t/** Honeypot decoy (on by default). `false` removes it; `{ name }` matches a customized server `spam.honeypot.fieldName`. */\n\thoneypot?: false | { name?: string }\n\t/** A token from your captcha widget; verified server-side when a captcha provider is configured. */\n\tcaptchaToken?: string\n\t/** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */\n\tchildren?: ReactNode\n\t/** Chrome rendered inside the form, above the fields, in default mode (e.g. `<FormSteps />`). */\n\theader?: ReactNode\n\t/** Additional CSS class names applied to the root `<form>` element (and the success node). */\n\tclassName?: string\n\t/** Replace the default submit button entirely. Receives the resolved label and submitting state. */\n\trenderSubmit?: (props: SubmitButtonRenderProps) => ReactNode\n\t/** Replace the default \"Next\" button in multi-step forms. */\n\trenderNext?: (props: NextButtonRenderProps) => ReactNode\n\t/** Replace the default \"Back\" button in multi-step forms. */\n\trenderBack?: (props: BackButtonRenderProps) => ReactNode\n\t/** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */\n\tsubmitButtonClassName?: string\n\t/** CSS class forwarded to the default \"Next\" `<button>`. Ignored when `renderNext` is provided. */\n\tnextButtonClassName?: string\n\t/** CSS class forwarded to the default \"Back\" `<button>`. Ignored when `renderBack` is provided. */\n\tbackButtonClassName?: string\n}\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\n/** A stored button label counts only when it is a non-empty string; anything else falls through. */\nconst storedLabel = (value: unknown): string | undefined =>\n\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\n/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */\nexport const Form = ({\n\tform,\n\tfieldTypes,\n\trules,\n\trenderers,\n\tapiRoute,\n\tonSubmit,\n\tonSuccess,\n\tonError,\n\tconverters,\n\tsuccessBehavior = 'replace',\n\tevents,\n\tt,\n\tlocale = 'en',\n\tlayout,\n\tsubmitLabel,\n\tnextLabel,\n\tprevLabel,\n\tcloseLabel,\n\tsuccessMessage,\n\tpresentation,\n\tpresentations,\n\tonClose,\n\ttitle,\n\tinitialValues,\n\thoneypot,\n\tcaptchaToken,\n\tchildren,\n\theader,\n\tclassName,\n\trenderSubmit,\n\trenderNext,\n\trenderBack,\n\tsubmitButtonClassName,\n\tnextButtonClassName,\n\tbackButtonClassName,\n}: FormProps) => {\n\tconst honeypotName = honeypot === false ? null : (honeypot?.name ?? DEFAULT_HONEYPOT_FIELD)\n\tconst honeypotRef = useRef<HTMLInputElement>(null)\n\tconst registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes])\n\tconst ruleRegistry = useMemo(() => buildValidationRuleRegistry(rules), [rules])\n\tconst rendererRegistry = useMemo(() => resolveRenderers(defaultRenderers, renderers), [renderers])\n\tconst presentationRegistry = useMemo(\n\t\t() => resolvePresentations(defaultPresentations, presentations),\n\t\t[presentations]\n\t)\n\tconst activePresentation: FormPresentation =\n\t\ttypeof presentation === 'object'\n\t\t\t? presentation\n\t\t\t: (presentationRegistry.get(presentation ?? DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tpresentationRegistry.get(DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tdefaultPresentationDescriptors.page)\n\tconst fieldsByName = useMemo(\n\t\t() => new Map(form.fields.filter(isNamedField).map((field) => [field.name, field])),\n\t\t[form.fields]\n\t)\n\tconst translate = useMemo<RendererTranslate>(() => t ?? makeTranslate(en), [t])\n\tconst resolvedCloseLabel = closeLabel ?? translate(keys.formClose)\n\tconst resolvedSuccessMessage = successMessage ?? translate(keys.formSuccess)\n\tconst docButtons: FormButtonSettings | undefined = form.buttons\n\tconst labels = useMemo(\n\t\t() => ({\n\t\t\tprev: prevLabel ?? storedLabel(docButtons?.prevLabel) ?? translate(keys.formBack),\n\t\t\tnext: nextLabel ?? storedLabel(docButtons?.nextLabel) ?? translate(keys.formNext),\n\t\t\tsubmit: submitLabel ?? storedLabel(docButtons?.submitLabel) ?? translate(keys.formSubmit),\n\t\t}),\n\t\t[prevLabel, nextLabel, submitLabel, docButtons, translate]\n\t)\n\n\t// Latest-value refs so event emission and the mount/unmount effect tolerate an inline `events` prop or a changing form id.\n\tconst sinkRef = useRef<FormEventSink>(noopEventSink)\n\tsinkRef.current = events ?? noopEventSink\n\tconst formIdRef = useRef('')\n\tformIdRef.current = String(form.id)\n\n\tconst [state, rawDispatch] = useReducer(formReducer, form.fields, (fields) =>\n\t\tinitialFormState({\n\t\t\t...seedFieldValues(fields),\n\t\t\t...(initialValues ?? {}),\n\t\t})\n\t)\n\n\t// Authoritative values for derived (calc) fields, recomputed from user answers on every change. The\n\t// server recomputes these too at submit; the client copy drives the live calc renderer, recall, and submit.\n\tconst effectiveValues = useMemo(\n\t\t() => computeCalcFields(form.fields, state.values),\n\t\t[form.fields, state.values]\n\t)\n\n\tconst recall = useMemo(\n\t\t() =>\n\t\t\tbuildRecallResolver({\n\t\t\t\tfields: form.fields,\n\t\t\t\tvalues: effectiveValues,\n\t\t\t\tregistry,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}),\n\t\t[form.fields, effectiveValues, registry, locale, translate]\n\t)\n\n\t// Multi-step is active only when the form is flagged `multistep` and its flow declares two or more\n\t// steps. With the flag off the form renders as a single page even if flow data is still stored.\n\tconst flow =\n\t\tform.multistep === true && form.flow && form.flow.steps.length >= 2 ? form.flow : undefined\n\tconst [currentStepId, setCurrentStepId] = useState<string | undefined>(() =>\n\t\tflow ? firstStepId(flow) : undefined\n\t)\n\tconst [history, setHistory] = useState<string[]>([])\n\n\tconst startedRef = useRef(false)\n\tconst submittedRef = useRef(false)\n\tconst submittingRef = useRef(false)\n\tconst advancingRef = useRef(false)\n\tconst flowRef = useRef(flow)\n\tflowRef.current = flow\n\n\tconst dispatch = useCallback((action: FormAction) => {\n\t\tif (action.type === 'SET_VALUE' && !startedRef.current) {\n\t\t\tstartedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.started' })\n\t\t}\n\t\trawDispatch(action)\n\t}, [])\n\n\tconst validateField = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tconst field = fieldsByName.get(name)\n\t\t\tif (!field) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst answers = { ...effectiveValues, [name]: value }\n\t\t\t// Mirror the server: a field whose `validateWhen` is unmet is not validated; clear any stale error.\n\t\t\tif (!evaluateCondition(field.validateWhen, answers)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors: [] })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvoid validateFieldValue({\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tregistry,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}).then(({ errors }) => {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors })\n\t\t\t\tconst [firstError] = errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[fieldsByName, effectiveValues, registry, ruleRegistry, locale, translate]\n\t)\n\n\tconst visible = visibleFields(form.fields, effectiveValues)\n\n\t/** Visible, answered named fields. Display-only ('none' kind) and nameless (bare) fields never contribute. */\n\tconst answerableFields = (): NamedFormFieldInstance[] =>\n\t\tvisibleFields(form.fields, effectiveValues)\n\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t.filter(isNamedField)\n\t\t\t.filter((field) => !isEmpty(effectiveValues[field.name]))\n\n\t/** Answered visible fields as raw submission values, sent to the server on submit. */\n\tconst answeredValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: effectiveValues[field.name] }))\n\n\t/**\n\t * Same fields, formatted via `recall` (option labels, Yes/No, localized dates): recall-fidelity\n\t * values for client-rendered templates, matching what the server gives email-team's body.\n\t */\n\tconst formattedValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: recall(field.name) }))\n\n\t/**\n\t * The recall-resolved success response: a redirect's url, or the response message serialized with\n\t * the active `converters` (so host blocks survive). Shared by the success screen and the value\n\t * handed to `onSuccess`, so both stay identical.\n\t */\n\tconst resolveSuccessResponse = (): FormSuccessResponse | undefined => {\n\t\tconst response = form.response\n\t\tif (response?.type === 'redirect') {\n\t\t\treturn { type: 'redirect', url: response.redirect?.url ?? undefined }\n\t\t}\n\t\tconst message =\n\t\t\tresponse?.type === 'message' || response?.type == null ? response?.message : undefined\n\t\tif (!message) {\n\t\t\treturn undefined\n\t\t}\n\t\treturn {\n\t\t\ttype: 'message',\n\t\t\thtml: serializeBody(message, {\n\t\t\t\tvalues: formattedValues(),\n\t\t\t\tdescriptors: descriptorsFor(answerableFields()),\n\t\t\t\tconverters,\n\t\t\t}),\n\t\t}\n\t}\n\n\t// Steps address fields by key: machine names for named fields, block row ids for bare blocks.\n\t// `step.fields` is membership only; render order follows `form.fields` (the order of `visible`),\n\t// so a step shows its fields in the form's field order, not the flow-builder entry order.\n\tconst stepKeys = flow && currentStepId ? stepFieldNames(flow, currentStepId) : []\n\tconst stepKeySet = new Set(stepKeys)\n\tconst stepVisible: FormFieldInstance[] = visible.filter((field) =>\n\t\tstepKeySet.has(fieldKey(field))\n\t)\n\n\t// Validate the sub-fields of the given repeaters, one pass per visible row, returning composite-key\n\t// errors (`fieldName[rowIndex].subFieldName`). Shared by submit and step navigation so both mirror\n\t// the server's per-row pass and neither lets an invalid required sub-field slip through.\n\tconst validateRepeaterSubFields = async (\n\t\trepeaters: FormFieldInstance[]\n\t): Promise<FieldErrors> => {\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const field of repeaters.filter(\n\t\t\t(f): f is NamedFormFieldInstance => f.blockType === 'repeater' && isNamedField(f)\n\t\t)) {\n\t\t\tconst rows = Array.isArray(effectiveValues[field.name])\n\t\t\t\t? (effectiveValues[field.name] as Array<Record<string, unknown>>)\n\t\t\t\t: []\n\t\t\tconst subFields = (\n\t\t\t\tArray.isArray(field.subFields) ? (field.subFields as FormFieldInstance[]) : []\n\t\t\t).filter(isNamedField)\n\t\t\tfor (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n\t\t\t\tconst row = rows[rowIndex] ?? {}\n\t\t\t\tfor (const subField of subFields) {\n\t\t\t\t\tif (!evaluateCondition(subField.visibleWhen, row)) continue\n\t\t\t\t\tif (!evaluateCondition(subField.validateWhen, row)) continue\n\t\t\t\t\tconst subResult = await validateFieldValue({\n\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: row,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})\n\t\t\t\t\tconst compositeKey = `${field.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\tif (subResult.errors.length > 0) errors[compositeKey] = subResult.errors\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn errors\n\t}\n\n\tconst goNext = async () => {\n\t\t// Re-entrancy guard: a double-click during async validation must not push the same step onto\n\t\t// history twice (which would need two Back presses to undo).\n\t\tif (!flow || !currentStepId || advancingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tadvancingRef.current = true\n\t\ttry {\n\t\t\tconst results = await Promise.all(\n\t\t\t\tstepVisible\n\t\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t\t.filter(isNamedField)\n\t\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t\t.map(async (field) => ({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\t\tregistry,\n\t\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\tt: translate,\n\t\t\t\t\t\t})),\n\t\t\t\t\t}))\n\t\t\t)\n\t\t\tlet hasError = false\n\t\t\tfor (const result of results) {\n\t\t\t\trawDispatch({ type: 'TOUCH', name: result.field.name })\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'SET_FIELD_ISSUES',\n\t\t\t\t\tname: result.field.name,\n\t\t\t\t\terrors: result.errors,\n\t\t\t\t})\n\t\t\t\tif (result.errors.length > 0) {\n\t\t\t\t\thasError = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Mirror submit: validate the step's repeater sub-fields too, so a required sub-field can't be\n\t\t\t// skipped past on a non-terminal step (errors surface inline via the composite key).\n\t\t\tconst repeaterErrors = await validateRepeaterSubFields(stepVisible)\n\t\t\tfor (const [compositeKey, errs] of Object.entries(repeaterErrors)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name: compositeKey, errors: errs })\n\t\t\t\thasError = true\n\t\t\t}\n\t\t\tif (hasError) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = resolveNextStepId(flow, currentStepId, effectiveValues)\n\t\t\tif (!next) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'step.completed',\n\t\t\t\tstepId: currentStepId,\n\t\t\t})\n\t\t\tsetHistory((prev) => [...prev, currentStepId])\n\t\t\tsetCurrentStepId(next)\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: next })\n\t\t} finally {\n\t\t\tadvancingRef.current = false\n\t\t}\n\t}\n\n\tconst goBack = () => {\n\t\tconst prev = history[history.length - 1]\n\t\tif (prev === undefined) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory((entries) => entries.slice(0, -1))\n\t\tsetCurrentStepId(prev)\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: prev })\n\t}\n\n\tuseEffect(() => {\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.viewed' })\n\t\tconst mountFlow = flowRef.current\n\t\tif (mountFlow) {\n\t\t\tconst first = firstStepId(mountFlow)\n\t\t\tif (first) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: first })\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tif (!submittedRef.current && !submittingRef.current) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.abandoned' })\n\t\t\t}\n\t\t}\n\t}, [])\n\n\tconst handleClose = useCallback(() => {\n\t\tonClose?.()\n\t}, [onClose])\n\n\tconst handleSubmit = async (event: ReactFormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault()\n\t\t// Re-entrancy guard: claim the in-flight slot before the async validation window so a fast second\n\t\t// activation (double-click, Enter + click) cannot reach the transport and POST the submission twice.\n\t\tif (submittingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tsubmittingRef.current = true\n\t\tconst visible = visibleFields(form.fields, effectiveValues)\n\t\tconst results = await Promise.all(\n\t\t\t// Calc fields carry no rules and have no input; they are always satisfied, so skip validating them.\n\t\t\t// Display-only ('none' kind, e.g. message) and nameless (bare) fields are skipped too, mirroring the server.\n\t\t\t// A field whose `validateWhen` is unmet is skipped too, mirroring the server (no client/server divergence).\n\t\t\tvisible\n\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t.filter(isNamedField)\n\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t.map(async (field) => ({\n\t\t\t\t\tfield,\n\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})),\n\t\t\t\t}))\n\t\t)\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const result of results) {\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\terrors[result.field.name] = result.errors\n\t\t\t\tconst [firstError] = result.errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: result.field.name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate sub-fields within each visible repeater (composite keys `fieldName[rowIndex].subFieldName`),\n\t\t// mirroring the server's per-row pass, so the repeater renderer surfaces them inline.\n\t\tObject.assign(errors, await validateRepeaterSubFields(visible))\n\n\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors })\n\t\tif (Object.keys(errors).length > 0) {\n\t\t\tsubmittingRef.current = false\n\t\t\treturn\n\t\t}\n\t\trawDispatch({ type: 'SUBMIT_START' })\n\t\tconst values: SubmissionValue[] = answeredValues()\n\t\tif (honeypotName) {\n\t\t\tconst decoy = honeypotRef.current?.value ?? ''\n\t\t\tif (decoy !== '') {\n\t\t\t\t// Submit the decoy under a reserved key, not its cosmetic DOM name, so a real field sharing\n\t\t\t\t// that name is never stripped or mistaken for the honeypot on the server.\n\t\t\t\tvalues.push({ field: HONEYPOT_VALUE_KEY, value: decoy })\n\t\t\t}\n\t\t}\n\t\tif (captchaToken) {\n\t\t\tvalues.push({ field: CAPTCHA_TOKEN_KEY, value: captchaToken })\n\t\t}\n\t\tconst result: SubmitFormResult = onSubmit\n\t\t\t? await onSubmit({ formId: form.id, values })\n\t\t\t: await submitForm({ formId: form.id, values, apiRoute })\n\t\tsubmittingRef.current = false\n\t\tif (result.ok) {\n\t\t\t// A submission happened, so the unmount effect must not emit `form.abandoned`, in either mode.\n\t\t\tsubmittedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'submission.created',\n\t\t\t\tsubmissionId: result.submissionId,\n\t\t\t})\n\t\t\t// Resolve the response before a reset clears the answers the recall reads from.\n\t\t\tonSuccess?.(result.submissionId, { response: resolveSuccessResponse() })\n\t\t\tif (successBehavior === 'reset') {\n\t\t\t\t// Reset in place: the host handles feedback (e.g. a toast via onSuccess); no success screen.\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'RESET',\n\t\t\t\t\tvalues: { ...seedFieldValues(form.fields), ...(initialValues ?? {}) },\n\t\t\t\t})\n\t\t\t\t// The reducer holds only values/errors; flow position and the lifecycle `started` ref live\n\t\t\t\t// outside it. Reset them too so a multi-step form returns to its first step (not stranded on\n\t\t\t\t// the terminal one) and a fresh fill re-emits `form.started`. `submittedRef` stays set: a\n\t\t\t\t// submission did happen, so the unmount guard must not report the completed form abandoned.\n\t\t\t\tif (flow) {\n\t\t\t\t\tsetCurrentStepId(firstStepId(flow))\n\t\t\t\t\tsetHistory([])\n\t\t\t\t}\n\t\t\t\tstartedRef.current = false\n\t\t\t} else {\n\t\t\t\trawDispatch({ type: 'SUBMIT_SUCCESS' })\n\t\t\t\tif (activePresentation.dismissOnSuccess) {\n\t\t\t\t\thandleClose()\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst redirectUrl =\n\t\t\t\tform.response?.type === 'redirect' ? form.response.redirect?.url : undefined\n\t\t\t// Part of submit handling, not rendering: fires on the custom-`children` path too.\n\t\t\t// Browser-only: no-op during SSR or in non-DOM test environments.\n\t\t\tif (\n\t\t\t\ttypeof redirectUrl === 'string' &&\n\t\t\t\tredirectUrl.length > 0 &&\n\t\t\t\ttypeof window !== 'undefined'\n\t\t\t) {\n\t\t\t\twindow.location.assign(redirectUrl)\n\t\t\t}\n\t\t} else {\n\t\t\tif (result.fieldErrors) {\n\t\t\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors: result.fieldErrors })\n\t\t\t}\n\t\t\tconst message = result.message ?? translate(keys.formSubmitFailed)\n\t\t\trawDispatch({ type: 'SUBMIT_ERROR', message })\n\t\t\tonError?.(message)\n\t\t}\n\t}\n\n\t// On a multi-step form, suppress implicit Enter-submit so a lone text input on a non-terminal step\n\t// cannot submit the whole form on Enter; only the explicit Submit control does. A textarea (Enter =\n\t// newline) and buttons/submit controls (Enter = activation) are exempt. Single-step forms keep the\n\t// native Enter-to-submit behavior. The `<form>` is the plugin's even in children mode, so this is the\n\t// only place a host could get this guard.\n\tconst handleKeyDown = (event: ReactKeyboardEvent<HTMLFormElement>) => {\n\t\tif (!flow || event.key !== 'Enter') {\n\t\t\treturn\n\t\t}\n\t\tconst target = event.target\n\t\t// Exempt controls whose own Enter handling matters: textarea (newline), select (confirm choice),\n\t\t// and buttons/submit inputs (activation). A single-line text input is what implicitly submits.\n\t\tif (\n\t\t\ttarget instanceof HTMLTextAreaElement ||\n\t\t\ttarget instanceof HTMLSelectElement ||\n\t\t\ttarget instanceof HTMLButtonElement\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\ttarget instanceof HTMLInputElement &&\n\t\t\t(target.type === 'submit' || target.type === 'button')\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tevent.preventDefault()\n\t}\n\n\tconst step: FormStepInfo = flow\n\t\t? {\n\t\t\t\tflow,\n\t\t\t\tcurrentStepId,\n\t\t\t\tstepIndex: flow.steps.findIndex((s) => s.id === currentStepId),\n\t\t\t\tstepCount: flow.steps.length,\n\t\t\t\tisFirst: history.length === 0,\n\t\t\t\tisTerminal: currentStepId ? isTerminalStepId(flow, currentStepId, effectiveValues) : true,\n\t\t\t\tgoNext: () => {\n\t\t\t\t\tvoid goNext()\n\t\t\t\t},\n\t\t\t\tgoBack,\n\t\t\t}\n\t\t: {\n\t\t\t\tstepIndex: 0,\n\t\t\t\tstepCount: 1,\n\t\t\t\tisFirst: true,\n\t\t\t\tisTerminal: true,\n\t\t\t\tgoNext: () => {},\n\t\t\t\tgoBack: () => {},\n\t\t\t}\n\n\tconst renderedFields = (flow ? stepVisible : visible).filter(\n\t\t(field) => field.hidden !== true && field.calcDisplay !== false\n\t)\n\n\tconst contextValue: FormContextValue = {\n\t\tform,\n\t\tstate,\n\t\tdispatch,\n\t\tvalidateField,\n\t\tlocale,\n\t\tstep,\n\t\trendererRegistry,\n\t\tlabels,\n\t\tt: translate,\n\t\teffectiveValues,\n\t\trecall,\n\t\trenderedFields,\n\t\tconverters,\n\t}\n\n\tconst PresentationWrapper = activePresentation.Wrapper\n\tconst wrap = (content: ReactNode): ReactNode =>\n\t\tPresentationWrapper ? (\n\t\t\t<PresentationWrapper\n\t\t\t\tpresentation={activePresentation}\n\t\t\t\topen\n\t\t\t\tonClose={handleClose}\n\t\t\t\ttitle={title}\n\t\t\t\tcloseLabel={resolvedCloseLabel}\n\t\t\t>\n\t\t\t\t{content}\n\t\t\t</PresentationWrapper>\n\t\t) : (\n\t\t\tcontent\n\t\t)\n\n\tif (children !== undefined) {\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\t<form\n\t\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\t\tnoValidate\n\t\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t>\n\t\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\tif (state.submitted) {\n\t\tconst successResponse = resolveSuccessResponse()\n\t\tconst responseHtml = successResponse?.type === 'message' ? successResponse.html : undefined\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\tresponseHtml ? (\n\t\t\t\t\t\t// Safe to inject: serializeBody HTML-escapes all text (recall values included) and sanitizes link URLs.\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is produced by our escaping serializer, never raw user input\n\t\t\t\t\t\t\tdangerouslySetInnerHTML={{ __html: responseHtml }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{interpolate(resolvedSuccessMessage, recall)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t)\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\treturn (\n\t\t<FormContext.Provider value={contextValue}>\n\t\t\t{wrap(\n\t\t\t\t<form\n\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\tnoValidate\n\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t>\n\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t{header}\n\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t{state.submitError ? (\n\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__submit-error\">\n\t\t\t\t\t\t\t{state.submitError}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t) : null}\n\t\t\t\t\t<FormControls\n\t\t\t\t\t\tbackButtonClassName={backButtonClassName}\n\t\t\t\t\t\tnextButtonClassName={nextButtonClassName}\n\t\t\t\t\t\tsubmitButtonClassName={submitButtonClassName}\n\t\t\t\t\t\trenderBack={renderBack}\n\t\t\t\t\t\trenderNext={renderNext}\n\t\t\t\t\t\trenderSubmit={renderSubmit}\n\t\t\t\t\t/>\n\t\t\t\t</form>\n\t\t\t)}\n\t\t</FormContext.Provider>\n\t)\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { FormFieldInstance } from "../submissions/types.js";
2
+ import { BodyConverter } from "../actions/body/converters.js";
2
3
  import { FormFlow } from "../flow/types.js";
3
4
  import { FormDocument } from "../form/types.js";
4
5
  import { RendererTranslate } from "./contract.js";
@@ -47,7 +48,8 @@ type FormContextValue = {
47
48
  t: RendererTranslate; /** Calc-authoritative answers: `state.values` overlaid with derived calc values. Consumers fall back to `state.values` when absent. */
48
49
  effectiveValues?: Record<string, unknown>; /** Recall resolver for token interpolation, exposed so `<FormFields>` and custom layouts can format values. */
49
50
  recall?: RecallResolver; /** The exact visible field list the default loop renders (post hidden/calc filter). Consumed by `<FormFields>`. */
50
- renderedFields?: FormFieldInstance[];
51
+ renderedFields?: FormFieldInstance[]; /** The `<Form>` `converters` prop, so client rich-text serialization (e.g. the `message` renderer) honors host blocks. */
52
+ converters?: Record<string, BodyConverter>;
51
53
  };
52
54
  /** Read the form controller context. Throws if used outside `<Form>`. */
53
55
  declare const useFormContext: () => FormContextValue;
@@ -1 +1 @@
1
- {"version":3,"file":"FormContext.js","names":[],"sources":["../../src/react/FormContext.ts"],"sourcesContent":["'use client'\n\nimport type { Dispatch } from 'react'\nimport { createContext, useContext } from 'react'\nimport type { FormFlow } from '../flow/types'\nimport type { FormDocument } from '../form/types'\nimport type { RecallResolver } from '../recall/resolver'\nimport type { FormFieldInstance } from '../submissions/types'\nimport type { RendererTranslate } from './contract'\nimport type { RendererRegistry } from './registry'\nimport type { FormAction, FormState } from './state'\n\n/** Multi-step navigation state. Defaults to a single terminal step when the form has no flow. */\nexport type FormStepInfo = {\n\tflow?: FormFlow\n\tcurrentStepId?: string\n\tstepIndex: number\n\tstepCount: number\n\tisFirst: boolean\n\tisTerminal: boolean\n\tgoNext: () => void\n\tgoBack: () => void\n}\n\n/** Resolved chrome button labels. Precedence per label: the `<Form>` prop, then the form's `buttons` value, then the translated default. */\nexport type FormControlLabels = {\n\tprev: string\n\tnext: string\n\tsubmit: string\n}\n\n/**\n * The form controller's context, read via `useFormContext` (or the focused `useField`,\n * `useFormState`, and `useFormStep` hooks). Available anywhere under `<Form>`, including\n * custom `children` layouts and custom field renderers.\n */\nexport type FormContextValue = {\n\t/** The document rendered by this `<Form>`. Custom chrome reads host-added `buttons` keys from here. */\n\tform: FormDocument\n\t/** Current form state: values, errors, touched, submitting, submitted, submitError. */\n\tstate: FormState\n\t/**\n\t * Dispatch a `FormAction` (see `./state` for the action union). Custom field layouts\n\t * typically dispatch `TOUCH` or `SET_FIELD_ISSUES`; prefer `useField` for value binding,\n\t * which wires `SET_VALUE` and validation for you.\n\t */\n\tdispatch: Dispatch<FormAction>\n\t/** Validate one field now (client mode) against the supplied value and store its issues. */\n\tvalidateField: (name: string, value: unknown) => void\n\t/** The locale passed to `<Form>` (defaults to `'en'`). */\n\tlocale: string\n\t/** Multi-step navigation state and the `goNext`/`goBack` handlers. */\n\tstep: FormStepInfo\n\t/** The active renderer registry, exposed so nested renderers (e.g. repeater) can look up sub-renderers. */\n\trendererRegistry: RendererRegistry\n\t/** Resolved prev/next/submit labels used by `<FormControls>` and available to custom chrome. */\n\tlabels: FormControlLabels\n\t/** The active translator: the `<Form>` `t` prop, else the bundled English fallback. */\n\tt: RendererTranslate\n\t/** Calc-authoritative answers: `state.values` overlaid with derived calc values. Consumers fall back to `state.values` when absent. */\n\teffectiveValues?: Record<string, unknown>\n\t/** Recall resolver for token interpolation, exposed so `<FormFields>` and custom layouts can format values. */\n\trecall?: RecallResolver\n\t/** The exact visible field list the default loop renders (post hidden/calc filter). Consumed by `<FormFields>`. */\n\trenderedFields?: FormFieldInstance[]\n}\n\nexport const FormContext = createContext<FormContextValue | null>(null)\n\n/** Read the form controller context. Throws if used outside `<Form>`. */\nexport const useFormContext = (): FormContextValue => {\n\tconst context = useContext(FormContext)\n\tif (!context) {\n\t\tthrow new Error('useFormContext must be used within a <Form>')\n\t}\n\treturn context\n}\n"],"mappings":";;;AAmEA,MAAa,cAAc,cAAuC,IAAI;;AAGtE,MAAa,uBAAyC;CACrD,MAAM,UAAU,WAAW,WAAW;CACtC,IAAI,CAAC,SACJ,MAAM,IAAI,MAAM,6CAA6C;CAE9D,OAAO;AACR"}
1
+ {"version":3,"file":"FormContext.js","names":[],"sources":["../../src/react/FormContext.ts"],"sourcesContent":["'use client'\n\nimport type { Dispatch } from 'react'\nimport { createContext, useContext } from 'react'\nimport type { BodyConverter } from '../actions/body/converters'\nimport type { FormFlow } from '../flow/types'\nimport type { FormDocument } from '../form/types'\nimport type { RecallResolver } from '../recall/resolver'\nimport type { FormFieldInstance } from '../submissions/types'\nimport type { RendererTranslate } from './contract'\nimport type { RendererRegistry } from './registry'\nimport type { FormAction, FormState } from './state'\n\n/** Multi-step navigation state. Defaults to a single terminal step when the form has no flow. */\nexport type FormStepInfo = {\n\tflow?: FormFlow\n\tcurrentStepId?: string\n\tstepIndex: number\n\tstepCount: number\n\tisFirst: boolean\n\tisTerminal: boolean\n\tgoNext: () => void\n\tgoBack: () => void\n}\n\n/** Resolved chrome button labels. Precedence per label: the `<Form>` prop, then the form's `buttons` value, then the translated default. */\nexport type FormControlLabels = {\n\tprev: string\n\tnext: string\n\tsubmit: string\n}\n\n/**\n * The form controller's context, read via `useFormContext` (or the focused `useField`,\n * `useFormState`, and `useFormStep` hooks). Available anywhere under `<Form>`, including\n * custom `children` layouts and custom field renderers.\n */\nexport type FormContextValue = {\n\t/** The document rendered by this `<Form>`. Custom chrome reads host-added `buttons` keys from here. */\n\tform: FormDocument\n\t/** Current form state: values, errors, touched, submitting, submitted, submitError. */\n\tstate: FormState\n\t/**\n\t * Dispatch a `FormAction` (see `./state` for the action union). Custom field layouts\n\t * typically dispatch `TOUCH` or `SET_FIELD_ISSUES`; prefer `useField` for value binding,\n\t * which wires `SET_VALUE` and validation for you.\n\t */\n\tdispatch: Dispatch<FormAction>\n\t/** Validate one field now (client mode) against the supplied value and store its issues. */\n\tvalidateField: (name: string, value: unknown) => void\n\t/** The locale passed to `<Form>` (defaults to `'en'`). */\n\tlocale: string\n\t/** Multi-step navigation state and the `goNext`/`goBack` handlers. */\n\tstep: FormStepInfo\n\t/** The active renderer registry, exposed so nested renderers (e.g. repeater) can look up sub-renderers. */\n\trendererRegistry: RendererRegistry\n\t/** Resolved prev/next/submit labels used by `<FormControls>` and available to custom chrome. */\n\tlabels: FormControlLabels\n\t/** The active translator: the `<Form>` `t` prop, else the bundled English fallback. */\n\tt: RendererTranslate\n\t/** Calc-authoritative answers: `state.values` overlaid with derived calc values. Consumers fall back to `state.values` when absent. */\n\teffectiveValues?: Record<string, unknown>\n\t/** Recall resolver for token interpolation, exposed so `<FormFields>` and custom layouts can format values. */\n\trecall?: RecallResolver\n\t/** The exact visible field list the default loop renders (post hidden/calc filter). Consumed by `<FormFields>`. */\n\trenderedFields?: FormFieldInstance[]\n\t/** The `<Form>` `converters` prop, so client rich-text serialization (e.g. the `message` renderer) honors host blocks. */\n\tconverters?: Record<string, BodyConverter>\n}\n\nexport const FormContext = createContext<FormContextValue | null>(null)\n\n/** Read the form controller context. Throws if used outside `<Form>`. */\nexport const useFormContext = (): FormContextValue => {\n\tconst context = useContext(FormContext)\n\tif (!context) {\n\t\tthrow new Error('useFormContext must be used within a <Form>')\n\t}\n\treturn context\n}\n"],"mappings":";;;AAsEA,MAAa,cAAc,cAAuC,IAAI;;AAGtE,MAAa,uBAAyC;CACrD,MAAM,UAAU,WAAW,WAAW;CACtC,IAAI,CAAC,SACJ,MAAM,IAAI,MAAM,6CAA6C;CAE9D,OAAO;AACR"}
@@ -77,11 +77,11 @@ const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchForm
77
77
  resultsAwaitClose,
78
78
  finalized
79
79
  ]);
80
- const handleSuccess = useCallback((submissionId) => {
80
+ const handleSuccess = useCallback((submissionId, result) => {
81
81
  writeVoted(key);
82
82
  setVoted(true);
83
83
  if (!resultsAwaitClose) loadResults();
84
- onSuccess?.(submissionId);
84
+ onSuccess?.(submissionId, result);
85
85
  }, [
86
86
  key,
87
87
  loadResults,
@@ -1 +1 @@
1
- {"version":3,"file":"Poll.js","names":[],"sources":["../../src/react/Poll.tsx"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { isPollClosed } from '../form/pollState'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport { Form, type FormProps } from './Form'\nimport { FormResults } from './FormResults'\nimport { type FetchResultsResult, fetchFormResults } from './fetchResults'\n\nexport type PollProps = FormProps & {\n\t/** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */\n\tresultsField: string\n\t/** localStorage key for the per-browser voted guard. Default `fb-poll-{form.id}`. */\n\tstorageKey?: string\n\t/**\n\t * Server-known voted state, ORed with the localStorage guard: `true` marks the visitor as voted;\n\t * `false`/omitted falls back to localStorage. SSR hosts using the plugin's `poll.votedCookie`\n\t * option pass `hasVotedCookie(cookieHeader, form.id)` here.\n\t */\n\thasVoted?: boolean\n\t/**\n\t * Injectable results fetch (testing); defaults to `fetchFormResults`. Pass a stable reference (module\n\t * scope, `useCallback`, or `useMemo`): an inline function re-runs the load effect and double-fetches.\n\t */\n\tfetchResultsImpl?: typeof fetchFormResults\n}\n\nconst readVoted = (key: string): boolean => {\n\ttry {\n\t\treturn window.localStorage.getItem(key) != null\n\t} catch {\n\t\treturn false\n\t}\n}\n\nconst writeVoted = (key: string): void => {\n\ttry {\n\t\twindow.localStorage.setItem(key, '1')\n\t} catch {\n\t\t// Private mode / storage disabled: the guard is best-effort UX, never integrity.\n\t}\n}\n\n/**\n * A poll: renders `<Form>` while open and not yet voted, then fetches the aggregate results and shows\n * `<FormResults>`. Lifecycle comes from `form.poll`: past `closesAt` the poll is closed (a translated\n * notice plus results, which the endpoint serves for any visibility once closed); a voted-but-open\n * `afterClose` poll shows a translated wait notice instead of fetching (the endpoint would refuse). A\n * recorded `outcome.winningValues` (via `resolvePollOutcome`) supersedes everything: a translated final\n * notice plus results with every winning bucket highlighted (a tie highlights more than one). A\n * per-browser localStorage flag (`storageKey`) skips straight to results on revisit; `hasVoted: true`\n * (e.g. from the server-set voted cookie) marks voted regardless of localStorage. The guard is UX, not\n * integrity (bypassable): server-enforced\n * one-per-identity dedup composes via `req.user` (authed forms) or a `notAlreadySubmitted` rule.\n */\nexport const Poll = ({\n\tresultsField,\n\tstorageKey,\n\thasVoted,\n\tfetchResultsImpl = fetchFormResults,\n\tapiRoute,\n\tonSuccess,\n\t...formProps\n}: PollProps) => {\n\tconst key = storageKey ?? `fb-poll-${formProps.form.id}`\n\tconst poll = formProps.form.poll\n\tconst closed = isPollClosed(poll)\n\tconst winningValues = poll?.outcome?.winningValues\n\tconst finalized = Array.isArray(winningValues) && winningValues.length > 0\n\tconst resultsAwaitClose = !closed && !finalized && poll?.resultsVisibility === 'afterClose'\n\tconst [voted, setVoted] = useState(false)\n\tconst [results, setResults] = useState<FieldAggregation[] | null>(null)\n\tconst [loadFailed, setLoadFailed] = useState(false)\n\tconst translate = useMemo(() => formProps.t ?? makeTranslate(en), [formProps.t])\n\n\tconst loadResults = useCallback(async () => {\n\t\tconst result: FetchResultsResult = await fetchResultsImpl({\n\t\t\tformId: formProps.form.id,\n\t\t\tfield: resultsField,\n\t\t\tapiRoute,\n\t\t})\n\t\t// A failed load surfaces as an error, not an empty result set: `[]` would read as \"no votes yet\".\n\t\tif (result.ok) {\n\t\t\tsetResults(result.results)\n\t\t\tsetLoadFailed(false)\n\t\t} else {\n\t\t\tsetLoadFailed(true)\n\t\t}\n\t}, [fetchResultsImpl, formProps.form.id, resultsField, apiRoute])\n\n\tconst resultsError = (\n\t\t<p className=\"fb-poll__error\" role=\"alert\">\n\t\t\t{translate(keys.pollResultsError)}\n\t\t</p>\n\t)\n\n\tuseEffect(() => {\n\t\tconst already = hasVoted === true || readVoted(key)\n\t\tif (already) {\n\t\t\tsetVoted(true)\n\t\t}\n\t\tif ((already && !resultsAwaitClose) || closed || finalized) {\n\t\t\tvoid loadResults()\n\t\t}\n\t}, [hasVoted, key, loadResults, closed, resultsAwaitClose, finalized])\n\n\tconst handleSuccess = useCallback(\n\t\t(submissionId?: string) => {\n\t\t\twriteVoted(key)\n\t\t\tsetVoted(true)\n\t\t\tif (!resultsAwaitClose) {\n\t\t\t\tvoid loadResults()\n\t\t\t}\n\t\t\tonSuccess?.(submissionId)\n\t\t},\n\t\t[key, loadResults, onSuccess, resultsAwaitClose]\n\t)\n\n\tif (finalized) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--final\">\n\t\t\t\t<p className=\"fb-poll__final\">{translate(keys.pollFinalResult)}</p>\n\t\t\t\t{loadFailed ? (\n\t\t\t\t\tresultsError\n\t\t\t\t) : results ? (\n\t\t\t\t\t<FormResults\n\t\t\t\t\t\tresults={results}\n\t\t\t\t\t\twinningValues={winningValues}\n\t\t\t\t\t\tt={formProps.t}\n\t\t\t\t\t\tlocale={formProps.locale}\n\t\t\t\t\t/>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (closed) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--closed\">\n\t\t\t\t<p className=\"fb-poll__closed\">{translate(keys.pollClosed)}</p>\n\t\t\t\t{loadFailed ? (\n\t\t\t\t\tresultsError\n\t\t\t\t) : results ? (\n\t\t\t\t\t<FormResults results={results} t={formProps.t} locale={formProps.locale} />\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (voted) {\n\t\tif (resultsAwaitClose) {\n\t\t\treturn <p className=\"fb-poll__await-close\">{translate(keys.pollResultsAfterClose)}</p>\n\t\t}\n\t\tif (loadFailed) {\n\t\t\treturn resultsError\n\t\t}\n\t\treturn results ? (\n\t\t\t<FormResults results={results} t={formProps.t} locale={formProps.locale} />\n\t\t) : null\n\t}\n\n\treturn <Form {...formProps} apiRoute={apiRoute} onSuccess={handleSuccess} />\n}\n"],"mappings":";;;;;;;;;;;AA8BA,MAAM,aAAa,QAAyB;CAC3C,IAAI;EACH,OAAO,OAAO,aAAa,QAAQ,GAAG,KAAK;CAC5C,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAM,cAAc,QAAsB;CACzC,IAAI;EACH,OAAO,aAAa,QAAQ,KAAK,GAAG;CACrC,QAAQ,CAER;AACD;;;;;;;;;;;;;AAcA,MAAa,QAAQ,EACpB,cACA,YACA,UACA,mBAAmB,kBACnB,UACA,WACA,GAAG,gBACa;CAChB,MAAM,MAAM,cAAc,WAAW,UAAU,KAAK;CACpD,MAAM,OAAO,UAAU,KAAK;CAC5B,MAAM,SAAS,aAAa,IAAI;CAChC,MAAM,gBAAgB,MAAM,SAAS;CACrC,MAAM,YAAY,MAAM,QAAQ,aAAa,KAAK,cAAc,SAAS;CACzE,MAAM,oBAAoB,CAAC,UAAU,CAAC,aAAa,MAAM,sBAAsB;CAC/E,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,CAAC,SAAS,cAAc,SAAoC,IAAI;CACtE,MAAM,CAAC,YAAY,iBAAiB,SAAS,KAAK;CAClD,MAAM,YAAY,cAAc,UAAU,KAAK,cAAc,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;CAE/E,MAAM,cAAc,YAAY,YAAY;EAC3C,MAAM,SAA6B,MAAM,iBAAiB;GACzD,QAAQ,UAAU,KAAK;GACvB,OAAO;GACP;EACD,CAAC;EAED,IAAI,OAAO,IAAI;GACd,WAAW,OAAO,OAAO;GACzB,cAAc,KAAK;EACpB,OACC,cAAc,IAAI;CAEpB,GAAG;EAAC;EAAkB,UAAU,KAAK;EAAI;EAAc;CAAQ,CAAC;CAEhE,MAAM,eACL,oBAAC,KAAD;EAAG,WAAU;EAAiB,MAAK;YACjC,UAAU,KAAK,gBAAgB;CAC9B,CAAA;CAGJ,gBAAgB;EACf,MAAM,UAAU,aAAa,QAAQ,UAAU,GAAG;EAClD,IAAI,SACH,SAAS,IAAI;EAEd,IAAK,WAAW,CAAC,qBAAsB,UAAU,WAChD,YAAiB;CAEnB,GAAG;EAAC;EAAU;EAAK;EAAa;EAAQ;EAAmB;CAAS,CAAC;CAErE,MAAM,gBAAgB,aACpB,iBAA0B;EAC1B,WAAW,GAAG;EACd,SAAS,IAAI;EACb,IAAI,CAAC,mBACJ,YAAiB;EAElB,YAAY,YAAY;CACzB,GACA;EAAC;EAAK;EAAa;EAAW;CAAiB,CAChD;CAEA,IAAI,WACH,OACC,qBAAC,OAAD;EAAK,WAAU;YAAf,CACC,oBAAC,KAAD;GAAG,WAAU;aAAkB,UAAU,KAAK,eAAe;EAAK,CAAA,GACjE,aACA,eACG,UACH,oBAAC,aAAD;GACU;GACM;GACf,GAAG,UAAU;GACb,QAAQ,UAAU;EAClB,CAAA,IACE,IACA;;CAIP,IAAI,QACH,OACC,qBAAC,OAAD;EAAK,WAAU;YAAf,CACC,oBAAC,KAAD;GAAG,WAAU;aAAmB,UAAU,KAAK,UAAU;EAAK,CAAA,GAC7D,aACA,eACG,UACH,oBAAC,aAAD;GAAsB;GAAS,GAAG,UAAU;GAAG,QAAQ,UAAU;EAAS,CAAA,IACvE,IACA;;CAIP,IAAI,OAAO;EACV,IAAI,mBACH,OAAO,oBAAC,KAAD;GAAG,WAAU;aAAwB,UAAU,KAAK,qBAAqB;EAAK,CAAA;EAEtF,IAAI,YACH,OAAO;EAER,OAAO,UACN,oBAAC,aAAD;GAAsB;GAAS,GAAG,UAAU;GAAG,QAAQ,UAAU;EAAS,CAAA,IACvE;CACL;CAEA,OAAO,oBAAC,MAAD;EAAM,GAAI;EAAqB;EAAU,WAAW;CAAgB,CAAA;AAC5E"}
1
+ {"version":3,"file":"Poll.js","names":[],"sources":["../../src/react/Poll.tsx"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { isPollClosed } from '../form/pollState'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport { Form, type FormProps } from './Form'\nimport { FormResults } from './FormResults'\nimport { type FetchResultsResult, fetchFormResults } from './fetchResults'\n\nexport type PollProps = FormProps & {\n\t/** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */\n\tresultsField: string\n\t/** localStorage key for the per-browser voted guard. Default `fb-poll-{form.id}`. */\n\tstorageKey?: string\n\t/**\n\t * Server-known voted state, ORed with the localStorage guard: `true` marks the visitor as voted;\n\t * `false`/omitted falls back to localStorage. SSR hosts using the plugin's `poll.votedCookie`\n\t * option pass `hasVotedCookie(cookieHeader, form.id)` here.\n\t */\n\thasVoted?: boolean\n\t/**\n\t * Injectable results fetch (testing); defaults to `fetchFormResults`. Pass a stable reference (module\n\t * scope, `useCallback`, or `useMemo`): an inline function re-runs the load effect and double-fetches.\n\t */\n\tfetchResultsImpl?: typeof fetchFormResults\n}\n\nconst readVoted = (key: string): boolean => {\n\ttry {\n\t\treturn window.localStorage.getItem(key) != null\n\t} catch {\n\t\treturn false\n\t}\n}\n\nconst writeVoted = (key: string): void => {\n\ttry {\n\t\twindow.localStorage.setItem(key, '1')\n\t} catch {\n\t\t// Private mode / storage disabled: the guard is best-effort UX, never integrity.\n\t}\n}\n\n/**\n * A poll: renders `<Form>` while open and not yet voted, then fetches the aggregate results and shows\n * `<FormResults>`. Lifecycle comes from `form.poll`: past `closesAt` the poll is closed (a translated\n * notice plus results, which the endpoint serves for any visibility once closed); a voted-but-open\n * `afterClose` poll shows a translated wait notice instead of fetching (the endpoint would refuse). A\n * recorded `outcome.winningValues` (via `resolvePollOutcome`) supersedes everything: a translated final\n * notice plus results with every winning bucket highlighted (a tie highlights more than one). A\n * per-browser localStorage flag (`storageKey`) skips straight to results on revisit; `hasVoted: true`\n * (e.g. from the server-set voted cookie) marks voted regardless of localStorage. The guard is UX, not\n * integrity (bypassable): server-enforced\n * one-per-identity dedup composes via `req.user` (authed forms) or a `notAlreadySubmitted` rule.\n */\nexport const Poll = ({\n\tresultsField,\n\tstorageKey,\n\thasVoted,\n\tfetchResultsImpl = fetchFormResults,\n\tapiRoute,\n\tonSuccess,\n\t...formProps\n}: PollProps) => {\n\tconst key = storageKey ?? `fb-poll-${formProps.form.id}`\n\tconst poll = formProps.form.poll\n\tconst closed = isPollClosed(poll)\n\tconst winningValues = poll?.outcome?.winningValues\n\tconst finalized = Array.isArray(winningValues) && winningValues.length > 0\n\tconst resultsAwaitClose = !closed && !finalized && poll?.resultsVisibility === 'afterClose'\n\tconst [voted, setVoted] = useState(false)\n\tconst [results, setResults] = useState<FieldAggregation[] | null>(null)\n\tconst [loadFailed, setLoadFailed] = useState(false)\n\tconst translate = useMemo(() => formProps.t ?? makeTranslate(en), [formProps.t])\n\n\tconst loadResults = useCallback(async () => {\n\t\tconst result: FetchResultsResult = await fetchResultsImpl({\n\t\t\tformId: formProps.form.id,\n\t\t\tfield: resultsField,\n\t\t\tapiRoute,\n\t\t})\n\t\t// A failed load surfaces as an error, not an empty result set: `[]` would read as \"no votes yet\".\n\t\tif (result.ok) {\n\t\t\tsetResults(result.results)\n\t\t\tsetLoadFailed(false)\n\t\t} else {\n\t\t\tsetLoadFailed(true)\n\t\t}\n\t}, [fetchResultsImpl, formProps.form.id, resultsField, apiRoute])\n\n\tconst resultsError = (\n\t\t<p className=\"fb-poll__error\" role=\"alert\">\n\t\t\t{translate(keys.pollResultsError)}\n\t\t</p>\n\t)\n\n\tuseEffect(() => {\n\t\tconst already = hasVoted === true || readVoted(key)\n\t\tif (already) {\n\t\t\tsetVoted(true)\n\t\t}\n\t\tif ((already && !resultsAwaitClose) || closed || finalized) {\n\t\t\tvoid loadResults()\n\t\t}\n\t}, [hasVoted, key, loadResults, closed, resultsAwaitClose, finalized])\n\n\tconst handleSuccess = useCallback<NonNullable<FormProps['onSuccess']>>(\n\t\t(submissionId, result) => {\n\t\t\twriteVoted(key)\n\t\t\tsetVoted(true)\n\t\t\tif (!resultsAwaitClose) {\n\t\t\t\tvoid loadResults()\n\t\t\t}\n\t\t\t// Forward the resolved success response so a Poll host gets the same onSuccess payload as a Form host.\n\t\t\tonSuccess?.(submissionId, result)\n\t\t},\n\t\t[key, loadResults, onSuccess, resultsAwaitClose]\n\t)\n\n\tif (finalized) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--final\">\n\t\t\t\t<p className=\"fb-poll__final\">{translate(keys.pollFinalResult)}</p>\n\t\t\t\t{loadFailed ? (\n\t\t\t\t\tresultsError\n\t\t\t\t) : results ? (\n\t\t\t\t\t<FormResults\n\t\t\t\t\t\tresults={results}\n\t\t\t\t\t\twinningValues={winningValues}\n\t\t\t\t\t\tt={formProps.t}\n\t\t\t\t\t\tlocale={formProps.locale}\n\t\t\t\t\t/>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (closed) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--closed\">\n\t\t\t\t<p className=\"fb-poll__closed\">{translate(keys.pollClosed)}</p>\n\t\t\t\t{loadFailed ? (\n\t\t\t\t\tresultsError\n\t\t\t\t) : results ? (\n\t\t\t\t\t<FormResults results={results} t={formProps.t} locale={formProps.locale} />\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (voted) {\n\t\tif (resultsAwaitClose) {\n\t\t\treturn <p className=\"fb-poll__await-close\">{translate(keys.pollResultsAfterClose)}</p>\n\t\t}\n\t\tif (loadFailed) {\n\t\t\treturn resultsError\n\t\t}\n\t\treturn results ? (\n\t\t\t<FormResults results={results} t={formProps.t} locale={formProps.locale} />\n\t\t) : null\n\t}\n\n\treturn <Form {...formProps} apiRoute={apiRoute} onSuccess={handleSuccess} />\n}\n"],"mappings":";;;;;;;;;;;AA8BA,MAAM,aAAa,QAAyB;CAC3C,IAAI;EACH,OAAO,OAAO,aAAa,QAAQ,GAAG,KAAK;CAC5C,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAM,cAAc,QAAsB;CACzC,IAAI;EACH,OAAO,aAAa,QAAQ,KAAK,GAAG;CACrC,QAAQ,CAER;AACD;;;;;;;;;;;;;AAcA,MAAa,QAAQ,EACpB,cACA,YACA,UACA,mBAAmB,kBACnB,UACA,WACA,GAAG,gBACa;CAChB,MAAM,MAAM,cAAc,WAAW,UAAU,KAAK;CACpD,MAAM,OAAO,UAAU,KAAK;CAC5B,MAAM,SAAS,aAAa,IAAI;CAChC,MAAM,gBAAgB,MAAM,SAAS;CACrC,MAAM,YAAY,MAAM,QAAQ,aAAa,KAAK,cAAc,SAAS;CACzE,MAAM,oBAAoB,CAAC,UAAU,CAAC,aAAa,MAAM,sBAAsB;CAC/E,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,CAAC,SAAS,cAAc,SAAoC,IAAI;CACtE,MAAM,CAAC,YAAY,iBAAiB,SAAS,KAAK;CAClD,MAAM,YAAY,cAAc,UAAU,KAAK,cAAc,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;CAE/E,MAAM,cAAc,YAAY,YAAY;EAC3C,MAAM,SAA6B,MAAM,iBAAiB;GACzD,QAAQ,UAAU,KAAK;GACvB,OAAO;GACP;EACD,CAAC;EAED,IAAI,OAAO,IAAI;GACd,WAAW,OAAO,OAAO;GACzB,cAAc,KAAK;EACpB,OACC,cAAc,IAAI;CAEpB,GAAG;EAAC;EAAkB,UAAU,KAAK;EAAI;EAAc;CAAQ,CAAC;CAEhE,MAAM,eACL,oBAAC,KAAD;EAAG,WAAU;EAAiB,MAAK;YACjC,UAAU,KAAK,gBAAgB;CAC9B,CAAA;CAGJ,gBAAgB;EACf,MAAM,UAAU,aAAa,QAAQ,UAAU,GAAG;EAClD,IAAI,SACH,SAAS,IAAI;EAEd,IAAK,WAAW,CAAC,qBAAsB,UAAU,WAChD,YAAiB;CAEnB,GAAG;EAAC;EAAU;EAAK;EAAa;EAAQ;EAAmB;CAAS,CAAC;CAErE,MAAM,gBAAgB,aACpB,cAAc,WAAW;EACzB,WAAW,GAAG;EACd,SAAS,IAAI;EACb,IAAI,CAAC,mBACJ,YAAiB;EAGlB,YAAY,cAAc,MAAM;CACjC,GACA;EAAC;EAAK;EAAa;EAAW;CAAiB,CAChD;CAEA,IAAI,WACH,OACC,qBAAC,OAAD;EAAK,WAAU;YAAf,CACC,oBAAC,KAAD;GAAG,WAAU;aAAkB,UAAU,KAAK,eAAe;EAAK,CAAA,GACjE,aACA,eACG,UACH,oBAAC,aAAD;GACU;GACM;GACf,GAAG,UAAU;GACb,QAAQ,UAAU;EAClB,CAAA,IACE,IACA;;CAIP,IAAI,QACH,OACC,qBAAC,OAAD;EAAK,WAAU;YAAf,CACC,oBAAC,KAAD;GAAG,WAAU;aAAmB,UAAU,KAAK,UAAU;EAAK,CAAA,GAC7D,aACA,eACG,UACH,oBAAC,aAAD;GAAsB;GAAS,GAAG,UAAU;GAAG,QAAQ,UAAU;EAAS,CAAA,IACvE,IACA;;CAIP,IAAI,OAAO;EACV,IAAI,mBACH,OAAO,oBAAC,KAAD;GAAG,WAAU;aAAwB,UAAU,KAAK,qBAAqB;EAAK,CAAA;EAEtF,IAAI,YACH,OAAO;EAER,OAAO,UACN,oBAAC,aAAD;GAAsB;GAAS,GAAG,UAAU;GAAG,QAAQ,UAAU;EAAS,CAAA,IACvE;CACL;CAEA,OAAO,oBAAC,MAAD;EAAM,GAAI;EAAqB;EAAU,WAAW;CAAgB,CAAA;AAC5E"}
@@ -13,6 +13,7 @@ import { useContext, useMemo } from "react";
13
13
  const messageRenderer = defineFieldRenderer(({ field }) => {
14
14
  const context = useContext(FormContext);
15
15
  const values = context?.effectiveValues ?? context?.state.values;
16
+ const converters = context?.converters;
16
17
  const html = useMemo(() => {
17
18
  const answered = Object.entries(values ?? {}).filter(([, value]) => value != null && value !== "").map(([name, value]) => ({
18
19
  field: name,
@@ -20,9 +21,14 @@ const messageRenderer = defineFieldRenderer(({ field }) => {
20
21
  }));
21
22
  return serializeBody(field.content, {
22
23
  values: answered,
23
- descriptors: []
24
+ descriptors: [],
25
+ converters
24
26
  });
25
- }, [field.content, values]);
27
+ }, [
28
+ field.content,
29
+ values,
30
+ converters
31
+ ]);
26
32
  if (html === "") return null;
27
33
  return /* @__PURE__ */ jsx("div", {
28
34
  className: "fb-field__message",
@@ -1 +1 @@
1
- {"version":3,"file":"message.js","names":[],"sources":["../../../src/react/renderers/message.tsx"],"sourcesContent":["'use client'\n\nimport { useContext, useMemo } from 'react'\nimport { serializeBody } from '../../actions/body/serializeBody'\nimport type { SubmissionValue } from '../../submissions/types'\nimport { defineFieldRenderer } from '../contract'\nimport { FormContext } from '../FormContext'\n\n/**\n * Display-only renderer for a `message` field: serializes the authored `content` rich text to HTML\n * inline between fields. Recall tokens (`{{name}}`) resolve against the current answers (calc\n * values included). Never binds form state; the field writes nothing to submissions.\n */\nexport const messageRenderer = defineFieldRenderer(({ field }) => {\n\tconst context = useContext(FormContext)\n\tconst values = context?.effectiveValues ?? context?.state.values\n\tconst html = useMemo(() => {\n\t\tconst answered: SubmissionValue[] = Object.entries(values ?? {})\n\t\t\t.filter(([, value]) => value != null && value !== '')\n\t\t\t.map(([name, value]) => ({ field: name, value }))\n\t\treturn serializeBody(field.content, { values: answered, descriptors: [] })\n\t}, [field.content, values])\n\tif (html === '') {\n\t\treturn null\n\t}\n\t// Safe to inject: serializeBody HTML-escapes all text (recall values included) and sanitizes link URLs.\n\t// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is produced by our escaping serializer, never raw user input\n\treturn <div className=\"fb-field__message\" dangerouslySetInnerHTML={{ __html: html }} />\n})\n"],"mappings":";;;;;;;;;;;;AAaA,MAAa,kBAAkB,qBAAqB,EAAE,YAAY;CACjE,MAAM,UAAU,WAAW,WAAW;CACtC,MAAM,SAAS,SAAS,mBAAmB,SAAS,MAAM;CAC1D,MAAM,OAAO,cAAc;EAC1B,MAAM,WAA8B,OAAO,QAAQ,UAAU,CAAC,CAAC,EAC7D,QAAQ,GAAG,WAAW,SAAS,QAAQ,UAAU,EAAE,EACnD,KAAK,CAAC,MAAM,YAAY;GAAE,OAAO;GAAM;EAAM,EAAE;EACjD,OAAO,cAAc,MAAM,SAAS;GAAE,QAAQ;GAAU,aAAa,CAAC;EAAE,CAAC;CAC1E,GAAG,CAAC,MAAM,SAAS,MAAM,CAAC;CAC1B,IAAI,SAAS,IACZ,OAAO;CAIR,OAAO,oBAAC,OAAD;EAAK,WAAU;EAAoB,yBAAyB,EAAE,QAAQ,KAAK;CAAI,CAAA;AACvF,CAAC"}
1
+ {"version":3,"file":"message.js","names":[],"sources":["../../../src/react/renderers/message.tsx"],"sourcesContent":["'use client'\n\nimport { useContext, useMemo } from 'react'\nimport { serializeBody } from '../../actions/body/serializeBody'\nimport type { SubmissionValue } from '../../submissions/types'\nimport { defineFieldRenderer } from '../contract'\nimport { FormContext } from '../FormContext'\n\n/**\n * Display-only renderer for a `message` field: serializes the authored `content` rich text to HTML\n * inline between fields. Recall tokens (`{{name}}`) resolve against the current answers (calc\n * values included). Never binds form state; the field writes nothing to submissions.\n */\nexport const messageRenderer = defineFieldRenderer(({ field }) => {\n\tconst context = useContext(FormContext)\n\tconst values = context?.effectiveValues ?? context?.state.values\n\tconst converters = context?.converters\n\tconst html = useMemo(() => {\n\t\tconst answered: SubmissionValue[] = Object.entries(values ?? {})\n\t\t\t.filter(([, value]) => value != null && value !== '')\n\t\t\t.map(([name, value]) => ({ field: name, value }))\n\t\treturn serializeBody(field.content, { values: answered, descriptors: [], converters })\n\t}, [field.content, values, converters])\n\tif (html === '') {\n\t\treturn null\n\t}\n\t// Safe to inject: serializeBody HTML-escapes all text (recall values included) and sanitizes link URLs.\n\t// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is produced by our escaping serializer, never raw user input\n\treturn <div className=\"fb-field__message\" dangerouslySetInnerHTML={{ __html: html }} />\n})\n"],"mappings":";;;;;;;;;;;;AAaA,MAAa,kBAAkB,qBAAqB,EAAE,YAAY;CACjE,MAAM,UAAU,WAAW,WAAW;CACtC,MAAM,SAAS,SAAS,mBAAmB,SAAS,MAAM;CAC1D,MAAM,aAAa,SAAS;CAC5B,MAAM,OAAO,cAAc;EAC1B,MAAM,WAA8B,OAAO,QAAQ,UAAU,CAAC,CAAC,EAC7D,QAAQ,GAAG,WAAW,SAAS,QAAQ,UAAU,EAAE,EACnD,KAAK,CAAC,MAAM,YAAY;GAAE,OAAO;GAAM;EAAM,EAAE;EACjD,OAAO,cAAc,MAAM,SAAS;GAAE,QAAQ;GAAU,aAAa,CAAC;GAAG;EAAW,CAAC;CACtF,GAAG;EAAC,MAAM;EAAS;EAAQ;CAAU,CAAC;CACtC,IAAI,SAAS,IACZ,OAAO;CAIR,OAAO,oBAAC,OAAD;EAAK,WAAU;EAAoB,yBAAyB,EAAE,QAAQ,KAAK;CAAI,CAAA;AACvF,CAAC"}
@@ -30,6 +30,9 @@ type FormAction = {
30
30
  } | {
31
31
  type: 'SUBMIT_ERROR';
32
32
  message: string;
33
+ } | {
34
+ type: 'RESET';
35
+ values: Record<string, unknown>;
33
36
  };
34
37
  //#endregion
35
38
  export { FieldErrors, FormAction, FormState };
@@ -70,6 +70,7 @@ const formReducer = (state, action) => {
70
70
  submitting: false,
71
71
  submitError: action.message
72
72
  };
73
+ case "RESET": return initialFormState(action.values);
73
74
  default: return state;
74
75
  }
75
76
  };
@@ -1 +1 @@
1
- {"version":3,"file":"state.js","names":[],"sources":["../../src/react/state.ts"],"sourcesContent":["import { isNamedField } from '../fields/fieldKey'\nimport type { FormFieldInstance } from '../submissions/types'\n\nexport type FieldErrors = Record<string, string[]>\n\nexport type FormState = {\n\tvalues: Record<string, unknown>\n\terrors: FieldErrors\n\ttouched: Record<string, boolean>\n\tsubmitting: boolean\n\tsubmitted: boolean\n\tsubmitAttempted: boolean\n\tsubmitError?: string\n}\n\nexport type FormAction =\n\t| { type: 'SET_VALUE'; name: string; value: unknown }\n\t| { type: 'TOUCH'; name: string }\n\t| { type: 'SET_FIELD_ISSUES'; name: string; errors: string[] }\n\t| { type: 'SET_ALL_ISSUES'; errors: FieldErrors }\n\t| { type: 'SUBMIT_START' }\n\t| { type: 'SUBMIT_SUCCESS' }\n\t| { type: 'SUBMIT_ERROR'; message: string }\n\n/**\n * Per-field defaults for the reducer's initial state. Nameless (bare) blocks carry no value and\n * are skipped. A repeater with a positive `minRows` starts pre-seeded with that many empty rows,\n * matching the schema's own floor. Computed once, ahead of the reducer, so seeding is never an\n * action: it can't touch a field, trigger validation, or (via `Form`'s dispatch wrapper) be\n * mistaken for the user's first edit and fire `form.started`.\n */\nexport const seedFieldValues = (fields: FormFieldInstance[]): Record<string, unknown> =>\n\tObject.fromEntries(\n\t\tfields.filter(isNamedField).map((field) => {\n\t\t\tif (field.blockType === 'repeater') {\n\t\t\t\tconst minRows = typeof field.minRows === 'number' ? field.minRows : 0\n\t\t\t\tif (minRows > 0) {\n\t\t\t\t\treturn [field.name, Array.from({ length: minRows }, () => ({}))]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn [field.name, undefined]\n\t\t})\n\t)\n\nexport const initialFormState = (values: Record<string, unknown>): FormState => ({\n\tvalues,\n\terrors: {},\n\ttouched: {},\n\tsubmitting: false,\n\tsubmitted: false,\n\tsubmitAttempted: false,\n})\n\n/** Changing a value clears that field's prior errors (re-validated by the caller). */\nexport const formReducer = (state: FormState, action: FormAction): FormState => {\n\tswitch (action.type) {\n\t\tcase 'SET_VALUE': {\n\t\t\tconst { [action.name]: _removed, ...restErrors } = state.errors\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tvalues: { ...state.values, [action.name]: action.value },\n\t\t\t\terrors: restErrors,\n\t\t\t}\n\t\t}\n\t\tcase 'TOUCH':\n\t\t\treturn state.touched[action.name]\n\t\t\t\t? state\n\t\t\t\t: { ...state, touched: { ...state.touched, [action.name]: true } }\n\t\tcase 'SET_FIELD_ISSUES':\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\terrors: { ...state.errors, [action.name]: action.errors },\n\t\t\t}\n\t\tcase 'SET_ALL_ISSUES':\n\t\t\treturn { ...state, errors: action.errors, submitAttempted: true }\n\t\tcase 'SUBMIT_START':\n\t\t\treturn { ...state, submitting: true, submitError: undefined }\n\t\tcase 'SUBMIT_SUCCESS':\n\t\t\treturn { ...state, submitting: false, submitted: true }\n\t\tcase 'SUBMIT_ERROR':\n\t\t\treturn { ...state, submitting: false, submitError: action.message }\n\t\tdefault:\n\t\t\treturn state\n\t}\n}\n"],"mappings":";;;;;;;;;AA+BA,MAAa,mBAAmB,WAC/B,OAAO,YACN,OAAO,OAAO,YAAY,EAAE,KAAK,UAAU;CAC1C,IAAI,MAAM,cAAc,YAAY;EACnC,MAAM,UAAU,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;EACpE,IAAI,UAAU,GACb,OAAO,CAAC,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,QAAQ,UAAU,CAAC,EAAE,CAAC;CAEjE;CACA,OAAO,CAAC,MAAM,MAAM,KAAA,CAAS;AAC9B,CAAC,CACF;AAED,MAAa,oBAAoB,YAAgD;CAChF;CACA,QAAQ,CAAC;CACT,SAAS,CAAC;CACV,YAAY;CACZ,WAAW;CACX,iBAAiB;AAClB;;AAGA,MAAa,eAAe,OAAkB,WAAkC;CAC/E,QAAQ,OAAO,MAAf;EACC,KAAK,aAAa;GACjB,MAAM,GAAG,OAAO,OAAO,UAAU,GAAG,eAAe,MAAM;GACzD,OAAO;IACN,GAAG;IACH,QAAQ;KAAE,GAAG,MAAM;MAAS,OAAO,OAAO,OAAO;IAAM;IACvD,QAAQ;GACT;EACD;EACA,KAAK,SACJ,OAAO,MAAM,QAAQ,OAAO,QACzB,QACA;GAAE,GAAG;GAAO,SAAS;IAAE,GAAG,MAAM;KAAU,OAAO,OAAO;GAAK;EAAE;EACnE,KAAK,oBACJ,OAAO;GACN,GAAG;GACH,QAAQ;IAAE,GAAG,MAAM;KAAS,OAAO,OAAO,OAAO;GAAO;EACzD;EACD,KAAK,kBACJ,OAAO;GAAE,GAAG;GAAO,QAAQ,OAAO;GAAQ,iBAAiB;EAAK;EACjE,KAAK,gBACJ,OAAO;GAAE,GAAG;GAAO,YAAY;GAAM,aAAa,KAAA;EAAU;EAC7D,KAAK,kBACJ,OAAO;GAAE,GAAG;GAAO,YAAY;GAAO,WAAW;EAAK;EACvD,KAAK,gBACJ,OAAO;GAAE,GAAG;GAAO,YAAY;GAAO,aAAa,OAAO;EAAQ;EACnE,SACC,OAAO;CACT;AACD"}
1
+ {"version":3,"file":"state.js","names":[],"sources":["../../src/react/state.ts"],"sourcesContent":["import { isNamedField } from '../fields/fieldKey'\nimport type { FormFieldInstance } from '../submissions/types'\n\nexport type FieldErrors = Record<string, string[]>\n\nexport type FormState = {\n\tvalues: Record<string, unknown>\n\terrors: FieldErrors\n\ttouched: Record<string, boolean>\n\tsubmitting: boolean\n\tsubmitted: boolean\n\tsubmitAttempted: boolean\n\tsubmitError?: string\n}\n\nexport type FormAction =\n\t| { type: 'SET_VALUE'; name: string; value: unknown }\n\t| { type: 'TOUCH'; name: string }\n\t| { type: 'SET_FIELD_ISSUES'; name: string; errors: string[] }\n\t| { type: 'SET_ALL_ISSUES'; errors: FieldErrors }\n\t| { type: 'SUBMIT_START' }\n\t| { type: 'SUBMIT_SUCCESS' }\n\t| { type: 'SUBMIT_ERROR'; message: string }\n\t| { type: 'RESET'; values: Record<string, unknown> }\n\n/**\n * Per-field defaults for the reducer's initial state. Nameless (bare) blocks carry no value and\n * are skipped. A repeater with a positive `minRows` starts pre-seeded with that many empty rows,\n * matching the schema's own floor. Computed once, ahead of the reducer, so seeding is never an\n * action: it can't touch a field, trigger validation, or (via `Form`'s dispatch wrapper) be\n * mistaken for the user's first edit and fire `form.started`.\n */\nexport const seedFieldValues = (fields: FormFieldInstance[]): Record<string, unknown> =>\n\tObject.fromEntries(\n\t\tfields.filter(isNamedField).map((field) => {\n\t\t\tif (field.blockType === 'repeater') {\n\t\t\t\tconst minRows = typeof field.minRows === 'number' ? field.minRows : 0\n\t\t\t\tif (minRows > 0) {\n\t\t\t\t\treturn [field.name, Array.from({ length: minRows }, () => ({}))]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn [field.name, undefined]\n\t\t})\n\t)\n\nexport const initialFormState = (values: Record<string, unknown>): FormState => ({\n\tvalues,\n\terrors: {},\n\ttouched: {},\n\tsubmitting: false,\n\tsubmitted: false,\n\tsubmitAttempted: false,\n})\n\n/** Changing a value clears that field's prior errors (re-validated by the caller). */\nexport const formReducer = (state: FormState, action: FormAction): FormState => {\n\tswitch (action.type) {\n\t\tcase 'SET_VALUE': {\n\t\t\tconst { [action.name]: _removed, ...restErrors } = state.errors\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tvalues: { ...state.values, [action.name]: action.value },\n\t\t\t\terrors: restErrors,\n\t\t\t}\n\t\t}\n\t\tcase 'TOUCH':\n\t\t\treturn state.touched[action.name]\n\t\t\t\t? state\n\t\t\t\t: { ...state, touched: { ...state.touched, [action.name]: true } }\n\t\tcase 'SET_FIELD_ISSUES':\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\terrors: { ...state.errors, [action.name]: action.errors },\n\t\t\t}\n\t\tcase 'SET_ALL_ISSUES':\n\t\t\treturn { ...state, errors: action.errors, submitAttempted: true }\n\t\tcase 'SUBMIT_START':\n\t\t\treturn { ...state, submitting: true, submitError: undefined }\n\t\tcase 'SUBMIT_SUCCESS':\n\t\t\treturn { ...state, submitting: false, submitted: true }\n\t\tcase 'SUBMIT_ERROR':\n\t\t\treturn { ...state, submitting: false, submitError: action.message }\n\t\tcase 'RESET':\n\t\t\treturn initialFormState(action.values)\n\t\tdefault:\n\t\t\treturn state\n\t}\n}\n"],"mappings":";;;;;;;;;AAgCA,MAAa,mBAAmB,WAC/B,OAAO,YACN,OAAO,OAAO,YAAY,EAAE,KAAK,UAAU;CAC1C,IAAI,MAAM,cAAc,YAAY;EACnC,MAAM,UAAU,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;EACpE,IAAI,UAAU,GACb,OAAO,CAAC,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,QAAQ,UAAU,CAAC,EAAE,CAAC;CAEjE;CACA,OAAO,CAAC,MAAM,MAAM,KAAA,CAAS;AAC9B,CAAC,CACF;AAED,MAAa,oBAAoB,YAAgD;CAChF;CACA,QAAQ,CAAC;CACT,SAAS,CAAC;CACV,YAAY;CACZ,WAAW;CACX,iBAAiB;AAClB;;AAGA,MAAa,eAAe,OAAkB,WAAkC;CAC/E,QAAQ,OAAO,MAAf;EACC,KAAK,aAAa;GACjB,MAAM,GAAG,OAAO,OAAO,UAAU,GAAG,eAAe,MAAM;GACzD,OAAO;IACN,GAAG;IACH,QAAQ;KAAE,GAAG,MAAM;MAAS,OAAO,OAAO,OAAO;IAAM;IACvD,QAAQ;GACT;EACD;EACA,KAAK,SACJ,OAAO,MAAM,QAAQ,OAAO,QACzB,QACA;GAAE,GAAG;GAAO,SAAS;IAAE,GAAG,MAAM;KAAU,OAAO,OAAO;GAAK;EAAE;EACnE,KAAK,oBACJ,OAAO;GACN,GAAG;GACH,QAAQ;IAAE,GAAG,MAAM;KAAS,OAAO,OAAO,OAAO;GAAO;EACzD;EACD,KAAK,kBACJ,OAAO;GAAE,GAAG;GAAO,QAAQ,OAAO;GAAQ,iBAAiB;EAAK;EACjE,KAAK,gBACJ,OAAO;GAAE,GAAG;GAAO,YAAY;GAAM,aAAa,KAAA;EAAU;EAC7D,KAAK,kBACJ,OAAO;GAAE,GAAG;GAAO,YAAY;GAAO,WAAW;EAAK;EACvD,KAAK,gBACJ,OAAO;GAAE,GAAG;GAAO,YAAY;GAAO,aAAa,OAAO;EAAQ;EACnE,KAAK,SACJ,OAAO,iBAAiB,OAAO,MAAM;EACtC,SACC,OAAO;CACT;AACD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@10x-media/form-builder",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.8",
4
4
  "description": "End-to-end forms platform for Payload: author, validate, render, collect, aggregate, and act.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -94,8 +94,8 @@
94
94
  "typescript": "5.9.3",
95
95
  "vitest": "4.1.7",
96
96
  "@10x-media/payload-test-harness": "0.0.0",
97
- "@10x-media/tsdown-config": "0.0.0",
98
97
  "@10x-media/tsconfig": "0.0.0",
98
+ "@10x-media/tsdown-config": "0.0.0",
99
99
  "@10x-media/vitest-config": "0.0.0"
100
100
  },
101
101
  "publishConfig": {