@nextlyhq/plugin-form-builder 0.0.2-alpha.74 → 0.0.2-alpha.76

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission-title-CVixn15Z.js","names":[],"sources":["../src/form-pages.ts","../src/utils/redirect-reference.ts","../src/utils/schema-only-read.ts","../src/dashboard/new-submissions.ts","../src/utils/evaluate-conditions.ts","../src/utils/condition-visibility.ts","../src/utils/asked-fields.ts","../src/utils/consent.ts","../src/utils/export-formats.ts","../src/emails/starter-messages.ts","../src/config/removed-options.ts","../src/config/defaults.ts","../src/utils/form-settings.ts","../src/utils/send-status.ts","../src/types.ts","../src/utils/enforced-validation.ts","../src/utils/generate-schema.ts","../src/utils/one-response-field.ts","../src/utils/sent-from.ts","../src/admin/components/submissions/submission-list-scope.ts","../src/utils/notification-model.ts","../src/utils/field-references.ts","../src/utils/submission-title.ts"],"sourcesContent":["/**\n * The form editor's pages, read by the plugin's declaration (which gives each\n * page its address) and by the editor (which draws a tab for each). One list,\n * so a tab cannot lead to an address nobody declared, nor an address go\n * without its tab.\n *\n * No React and no admin imports here: the server reads this module too.\n *\n * @module form-pages\n */\n\n/**\n * The form editor, the forms collection's own edit view. The admin draws it at\n * the form's edit address and at each page's address below, so it stays\n * mounted, unsaved work and all, while a person moves between the tabs.\n */\nexport const FORM_EDITOR_VIEW =\n \"@nextlyhq/plugin-form-builder/admin#FormBuilderView\";\n\n/** A page of the form editor. */\ninterface FormPage {\n /** What the editor calls it, and the tab's label. */\n readonly label: string;\n /**\n * The address segment after the form's own, or `undefined` for Builder,\n * which is the form's edit address itself.\n */\n readonly path: string | undefined;\n}\n\n/** One page's id: its address segment, or `\"builder\"` for the edit address. */\nexport type FormPageId =\n | \"builder\"\n | \"preview\"\n | \"settings\"\n | \"notifications\"\n | \"submissions\";\n\n/** A page as the list spells it: its id's segment, or none for Builder. */\ntype DeclaredFormPage = FormPage & {\n readonly path: Exclude<FormPageId, \"builder\"> | undefined;\n};\n\n/** Every page, in the order the tabs show them. */\nexport const FORM_PAGES: readonly DeclaredFormPage[] = [\n { label: \"Builder\", path: undefined },\n { label: \"Preview\", path: \"preview\" },\n { label: \"Settings\", path: \"settings\" },\n { label: \"Notifications\", path: \"notifications\" },\n { label: \"Submissions\", path: \"submissions\" },\n];\n\n/** A page's id. */\nexport function formPageId(page: DeclaredFormPage): FormPageId {\n return page.path ?? \"builder\";\n}\n\n/**\n * The page an address segment names: Builder for none, or for a segment that\n * is not one of the editor's pages.\n */\nexport function formPageOf(view: string | undefined): FormPageId {\n const page = FORM_PAGES.find(\n each => each.path !== undefined && each.path === view\n );\n return page === undefined ? \"builder\" : formPageId(page);\n}\n\n/** The pages with an address of their own, for the plugin's declaration. */\nexport function addressedFormPages(): { path: string; label: string }[] {\n return FORM_PAGES.flatMap(page =>\n page.path === undefined ? [] : [{ path: page.path, label: page.label }]\n );\n}\n","/**\n * Reading the document a form's redirect setting points at.\n *\n * Separate from `redirect-target` because this half has no dependencies and\n * the other half imports `nextly/config`: the admin picker needs to read a\n * stored reference to show what is selected, and pulling the config surface\n * into the browser bundle to do it would be a real cost for no gain.\n */\n\n/** A document being turned into a URL. `id` is always present; the rest is the row. */\nexport interface RedirectTargetDocument {\n id: string;\n [field: string]: unknown;\n}\n\n/** A stored relationship value reduced to the collection and id it names. */\nexport interface RedirectReference {\n collection: string;\n id: string;\n}\n\n/**\n * The reference a `redirectPage` value holds, in any shape a read produces.\n *\n * Three arrive in practice and they are not interchangeable: a polymorphic\n * field stores `{ relationTo, value }`; a read at depth replaces `value` with\n * the populated row; and a single-target field stores the bare id. The last\n * names no collection, so it resolves only when exactly one is configured —\n * guessing which of several a bare id belongs to would send visitors to a\n * URL built from the wrong collection's pattern.\n */\nexport function parseRedirectReference(\n value: unknown,\n collections: readonly string[]\n): RedirectReference | null {\n if (value == null) return null;\n\n if (typeof value === \"string\") {\n // Trimmed, as the object branch trims its collection name. A cleared or\n // whitespace-only `redirectPage` reads as a reference otherwise, which\n // validation accepts as a usable target while the collection hook refuses\n // it as missing and the submit path resolves to nothing.\n const id = value.trim();\n return id && collections.length === 1\n ? { collection: collections[0], id }\n : null;\n }\n\n if (typeof value !== \"object\" || Array.isArray(value)) return null;\n\n const record = value as Record<string, unknown>;\n const relationTo = record.relationTo;\n const inner = record.value;\n\n if (typeof relationTo === \"string\" && relationTo.trim()) {\n // Trimmed for the TEST and in the VALUE returned. A blank or whitespace\n // name is not a collection, and a padded one is not the same string as the\n // configured key — accepting `\" pages \"` while returning it unchanged\n // makes this read valid to the public validator and unusable to every\n // consumer that looks the name up.\n const id = referenceId(inner);\n return id ? { collection: relationTo.trim(), id } : null;\n }\n\n // A populated row with no `relationTo` beside it: same ambiguity as a bare\n // id, and the same answer.\n const id = referenceId(record);\n if (!id) return null;\n return collections.length === 1 ? { collection: collections[0], id } : null;\n}\n\nfunction referenceId(value: unknown): string | null {\n if (typeof value === \"string\") return value.trim() || null;\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const { id } = value as Record<string, unknown>;\n if (typeof id === \"string\" && id) return id;\n }\n return null;\n}\n\n/**\n * The field the publish lifecycle puts on every row it manages.\n *\n * A collection without the lifecycle may legally define an ordinary field\n * named `status` — \"active\", \"archived\", anything — so the NAME `status` does\n * not identify a lifecycle and reading it as one marks live documents as\n * drafts. This field does identify it: it is written by the framework, not by\n * a schema author.\n *\n * Measured on 2026-08-25, creating one document in each kind of collection:\n * a `status: true` collection returns\n * `[createdAt, firstPublishedAt, id, slug, status, title, updatedAt]`, and a\n * plain collection carrying its own `status` text field returns the same list\n * WITHOUT `firstPublishedAt`. It is present on a never-published draft too, so\n * presence marks the lifecycle rather than the act of publishing.\n */\nconst PUBLISH_LIFECYCLE_FIELD = \"firstPublishedAt\";\n\n/**\n * Whether this document comes from a collection with the publish lifecycle.\n *\n * Callers that project their reads must request\n * {@link PUBLISH_LIFECYCLE_FIELD}; a projection that omits it makes every\n * document look unmanaged, which reads as \"always reachable\".\n */\nexport function hasPublishLifecycle(document: RedirectTargetDocument): boolean {\n return PUBLISH_LIFECYCLE_FIELD in document;\n}\n\n/**\n * How reachable a document is, as one of three answers.\n *\n * Three rather than a boolean because one of the cases is genuinely\n * undecidable from a redirect target's row, and collapsing it into either\n * answer is wrong in a different direction.\n */\nexport type Reachability = \"reachable\" | \"unreachable\" | \"unknown\";\n\n/**\n * Whether a visitor can reach this document.\n *\n * `collectionIsLocalized` is the caller's knowledge of the target collection's\n * `localized` setting, or `undefined` where the caller has no way to know.\n *\n * A localized collection publishes PER LOCALE, on a companion row that no read\n * available here returns. Measured on 2026-08-25 against a collection with\n * `localized: true` whose Spanish translation was published while the default\n * locale stayed a draft: `findByID` answers `status: \"draft\"` at the default\n * locale AND at `es`, and a `find` filtered to `status: \"published\"` returns\n * the document at no locale at all — no locale, `en` or `es`. So the main\n * row's `status` does not answer for a localized document, and reading it as\n * an answer marks a page visitors can reach as a draft, refuses a correct save\n * and drops a working redirect.\n *\n * `firstPublishedAt` is the one fact that survives that: it records whether the\n * document has ever been public in ANY language, so its absence is decisive\n * whether or not the collection is localized.\n */\nexport function documentReachability(\n document: RedirectTargetDocument,\n collectionIsLocalized: boolean | undefined\n): Reachability {\n // No lifecycle at all: there is no unpublished state to be in.\n if (!hasPublishLifecycle(document)) return \"reachable\";\n if (document.status === \"published\") return \"reachable\";\n\n // Only a caller that knows the collection is NOT localized can read the main\n // row's status as the document's own. There is no fact on the row that\n // substitutes for that knowledge, and each candidate is wrong in a real\n // configuration:\n //\n // `status` — a collection without the lifecycle may define its\n // own field of that name, so the NAME answers\n // nothing (`hasPublishLifecycle` above).\n // the row's status — a localized document publishes on a locale\n // companion, and the main row reads `draft` while a\n // translation is public.\n // `firstPublishedAt` — absent on a row published before that column\n // existed, so it records \"never public\" and \"public,\n // never recorded\" identically on an upgraded\n // database.\n //\n // So the undecided case is reported rather than guessed at.\n if (collectionIsLocalized === false) return \"unreachable\";\n\n return \"unknown\";\n}\n/** The settings keys that can carry the document a form redirects to. */\nexport type PickedDocumentField = \"redirectPage\" | \"redirectRelation\";\n\n/**\n * Which settings key names the document this form redirects to, if any.\n *\n * TWO options carry one. \"Redirect to a page\" stores `redirectPage`, and the\n * URL option falls back to `redirectRelation` when no URL is typed — the shape\n * code-first and legacy forms use, and one the submission path has always\n * resolved. One definition of the question, so the rule that guards a target\n * cannot recognise fewer shapes than the resolver that acts on it: a form the\n * rule skips is a form the resolver still redirects.\n *\n * The two are not equally strict about being EMPTY, deliberately. Naming\n * nothing under \"Redirect to a page\" contradicts the option itself, so it is\n * reported. Under the URL option an absent relation only means no destination\n * is configured yet — `validateFormConfig` already governs that, and refusing\n * it here would reject saves this rule never governed.\n */\nexport function pickedDocumentField(\n settings: Record<string, unknown>\n): PickedDocumentField | null {\n if (settings.confirmationType === \"relationship\") return \"redirectPage\";\n if (\n settings.confirmationType === \"redirect\" &&\n !settings.redirectUrl &&\n settings.redirectRelation != null\n ) {\n return \"redirectRelation\";\n }\n return null;\n}\n","/**\n * The hook context that tells the forms collection's `afterRead` hook a read\n * wants the forms themselves and nothing only a display needs, so it skips\n * the submission count it would otherwise run per form: a write checking a\n * submission against its form, and the dashboard card scanning which forms a\n * reader may read, need the count no more than they need a second copy of\n * the form.\n *\n * @module utils/schema-only-read\n */\n\n/** The context key the forms `afterRead` hook reads; `true` skips the count. */\nexport const SCHEMA_ONLY_READ = \"formBuilder.schemaOnlyRead\";\n\n/** The context a schema-only read hands the forms collection's hooks. */\nexport const SCHEMA_ONLY_CONTEXT: Readonly<Record<string, unknown>> = {\n [SCHEMA_ONLY_READ]: true,\n};\n","/**\n * What the New form submissions card and its header part read: the\n * submissions whose status is \"new\", on the forms the reader may read, newest\n * first, each made into the row the card draws.\n *\n * Every read goes through the services a widget's `load`, `shows` and\n * `summary.count` are handed, which core binds to the reader, so nothing here\n * reads as the system. The header's count is the card's own read asked for no\n * rows (`readNewSubmissions` with a `limit` of 0), so the two cannot disagree.\n *\n * Reads the collections by their declared slugs: core renames a plugin's own\n * collections on the way in, for a site that renamed them.\n *\n * @module dashboard/new-submissions\n */\n\nimport type { WidgetReadServices, WidgetScope } from \"@nextlyhq/plugin-sdk\";\n\nimport { SCHEMA_ONLY_CONTEXT } from \"../utils/schema-only-read\";\n\n/** The widget's id, the one core's default names. */\nexport const NEW_SUBMISSIONS_WIDGET_ID = \"form-builder/new-submissions\";\n\n/** The collections the card reads, as the plugin declares and installs them. */\nexport interface SubmissionsCollections {\n /** The forms collection's slug. */\n forms: string;\n /** The submissions collection's slug. */\n submissions: string;\n}\n\n/** One new submission, as the card draws it. */\nexport interface NewSubmissionRow {\n id: string;\n /** The submitter's name or email, or \"Submission\" where the form has neither answered. */\n title: string;\n /** The form's name, where the reader may read it. */\n form?: string;\n /** The first line of the first text answer not already the title. */\n excerpt?: string;\n /** When it was submitted, as an ISO string. */\n at?: string;\n /** The submission, in the admin. */\n href: string;\n}\n\n/** The card's data: its \"Number of entries\" of rows, how many new submissions there are, and their list. */\nexport interface NewSubmissionsData {\n items: NewSubmissionRow[];\n /** How many new submissions the reader may read, in all. */\n total: number;\n /** `total` is a floor: there are at least this many. */\n atLeast?: true;\n /** Every submission, in the admin. */\n allHref: string;\n /**\n * The submissions collection's slug as the site installed it, the one a\n * row is marked read in when it is opened.\n */\n collection: string;\n}\n\n/** What a read is asked with beyond its collections. */\nexport interface NewSubmissionsRead {\n /** The most rows the card draws. */\n limit: number;\n signal?: AbortSignal;\n /** The submissions slug the admin knows them by: the site's, where it renamed it. */\n submissionsPath: string;\n /**\n * The work this reader's request shares (a `load`'s or a summary's\n * `scope`): the forms they may read are scanned once in it, however many\n * of the card's reads ask.\n */\n scope?: WidgetScope;\n}\n\n/** The status a submission arrives with, and keeps until someone opens it. */\nexport const NEW_STATUS = \"new\";\n\n/** The admin's root, which a row's link and the list's link start from. */\nconst ADMIN_ROOT = \"/admin\";\n\n/** The most forms one read answers, the widget reads' own cap. */\nconst FORM_PAGE = 50;\n\n/**\n * The most pages of forms read for the filter: 1,000 forms. Past that, the\n * later forms' submissions are left out of the card and the count, never\n * shown: what is missed is a reader's own, not someone else's. The count then\n * says it is a floor (`atLeast`), so nobody reads it as every one.\n */\nconst MAX_FORM_PAGES = 20;\n\n/** The submission fields a row is made from; no other is asked for. */\nconst SUBMISSION_FIELDS = [\n \"id\",\n \"form\",\n \"data\",\n \"submittedAt\",\n \"createdAt\",\n] as const;\n\n/** The form fields a row is made from. */\nconst FORM_FIELDS = [\"id\", \"name\", \"fields\"] as const;\n\n/**\n * Field names and labels that mark a form's name field, compared as letters\n * only (`lettersOf`), so case, spaces, underscores, hyphens and camelCase do\n * not matter: `firstName`, `first_name` and \"First name\" are all \"firstname\".\n */\nconst NAME_KEYS: ReadonlySet<string> = new Set([\n \"name\",\n \"fullname\",\n \"firstname\",\n \"yourname\",\n \"yourfullname\",\n \"yourfirstname\",\n]);\n\n/**\n * The most characters of an answer or a name a row carries. A form may leave\n * a field's length open, so a visitor's answer can be any length; the card\n * draws one line, and the rest would only weigh down its data and the page.\n */\nconst MAX_ROW_TEXT = 140;\n\n/** The answer types whose first line a row may show under its title. */\nconst TEXT_TYPES: ReadonlySet<string> = new Set([\"text\", \"textarea\"]);\n\n/** One row a read service answered. */\ntype Row = { readonly [field: string]: unknown };\n\n/** One field of a form, as far as a row reads it. */\ninterface FormFieldShape {\n type: string;\n name: string;\n label?: string;\n}\n\n/** The forms the reader may read, by id, and whether that is every one of them. */\nexport interface ReadableForms {\n ids: string[];\n /** `false` when the scan stopped at its budget with forms still unread. */\n complete: boolean;\n}\n\n/** Up to `limit` ids of forms the reader may read, by id, after `after`. */\nasync function formIdPage(\n services: Pick<WidgetReadServices, \"find\">,\n forms: string,\n page: { after: string | undefined; limit: number; signal?: AbortSignal }\n): Promise<string[]> {\n const { items } = await services.find(forms, {\n ...(page.after === undefined\n ? {}\n : { where: { id: { greater_than: page.after } } }),\n sort: \"id\",\n limit: page.limit,\n select: [\"id\"],\n status: \"all\",\n // Ids are all this needs: the forms' `afterRead` count of each form's\n // submissions would be a query per form, up to a thousand a scan.\n context: SCHEMA_ONLY_CONTEXT,\n ...(page.signal === undefined ? {} : { signal: page.signal }),\n });\n return items.flatMap(item => (typeof item.id === \"string\" ? [item.id] : []));\n}\n\n/**\n * The ids of the forms the reader may read, in pages of `FORM_PAGE`, by id,\n * at most `MAX_FORM_PAGES` of them. A form a read rule keeps from them is\n * never among them. Where the budget is spent, one more id says whether any\n * form was left out.\n */\nexport async function readableFormIds(\n services: Pick<WidgetReadServices, \"find\">,\n forms: string,\n signal?: AbortSignal\n): Promise<ReadableForms> {\n const ids: string[] = [];\n let after: string | undefined;\n for (let page = 0; page < MAX_FORM_PAGES; page += 1) {\n const pageIds = await formIdPage(services, forms, {\n after,\n limit: FORM_PAGE,\n signal,\n });\n ids.push(...pageIds);\n after = pageIds.at(-1);\n if (pageIds.length < FORM_PAGE) return { ids, complete: true };\n }\n const beyond = await formIdPage(services, forms, { after, limit: 1, signal });\n return { ids, complete: beyond.length === 0 };\n}\n\n/**\n * The forms the reader may read, scanned once in `scope` where one is given:\n * the card's data and the header's count, answered in one request, share one\n * scan. Keyed by the forms collection, so a site that renamed it scans its own.\n */\nfunction readableFormsOnce(\n services: Pick<WidgetReadServices, \"find\">,\n forms: string,\n read: Pick<NewSubmissionsRead, \"scope\" | \"signal\">\n): Promise<ReadableForms> {\n const scan = () => readableFormIds(services, forms, read.signal);\n return read.scope === undefined\n ? scan()\n : read.scope.getOrCompute(`readable-forms:${forms}`, scan);\n}\n\n/** The new submissions of the forms named: what the card lists and the header counts. */\nfunction newSubmissionsWhere(formIds: readonly string[]): {\n status: { equals: string };\n form: { in: string[] };\n} {\n return { status: { equals: NEW_STATUS }, form: { in: [...formIds] } };\n}\n\n/**\n * The card's data: the newest new submissions the card holds, and how many\n * there are. Asked with a `limit` of 0 it reads no rows and still counts\n * them, which is the header's count.\n */\nexport async function readNewSubmissions(\n services: Pick<WidgetReadServices, \"find\">,\n collections: SubmissionsCollections,\n read: NewSubmissionsRead\n): Promise<NewSubmissionsData> {\n const signal = read.signal === undefined ? {} : { signal: read.signal };\n const allHref = `${ADMIN_ROOT}/collections/${read.submissionsPath}`;\n const collection = read.submissionsPath;\n const readable = await readableFormsOnce(services, collections.forms, read);\n if (readable.ids.length === 0) {\n return { items: [], total: 0, allHref, collection };\n }\n const found = await services.find(collections.submissions, {\n where: newSubmissionsWhere(readable.ids),\n sort: \"-submittedAt\",\n limit: read.limit,\n select: SUBMISSION_FIELDS,\n status: \"all\",\n total: true,\n // Each row's form as the id it stores: the forms drawn are read once,\n // below, so expanding them here would read them twice.\n depth: 0,\n ...signal,\n });\n const forms = await formsOf(services, collections.forms, found.items, signal);\n return {\n items: found.items.map(item =>\n submissionRow(item, forms.get(formIdOf(item.form) ?? \"\"), allHref)\n ),\n total: found.total ?? found.items.length,\n // A floor where the read counted only so far, or where forms past the\n // scan's budget were left out of it.\n ...(found.atLeast === true || !readable.complete\n ? { atLeast: true as const }\n : {}),\n allHref,\n collection,\n };\n}\n\n/** The forms of the submissions drawn, by id: one read, of those forms only. */\nasync function formsOf(\n services: Pick<WidgetReadServices, \"find\">,\n forms: string,\n submissions: readonly Row[],\n signal: { signal?: AbortSignal }\n): Promise<ReadonlyMap<string, Row>> {\n const ids = [\n ...new Set(submissions.flatMap(row => formIdOf(row.form) ?? [])),\n ];\n if (ids.length === 0) return new Map();\n const { items } = await services.find(forms, {\n where: { id: { in: ids } },\n limit: ids.length,\n select: FORM_FIELDS,\n status: \"all\",\n // The rows draw a form's name and fields, never its submission count.\n context: SCHEMA_ONLY_CONTEXT,\n ...signal,\n });\n return new Map(\n items.flatMap((item): [string, Row][] =>\n typeof item.id === \"string\" ? [[item.id, item]] : []\n )\n );\n}\n\n/**\n * One submission as its row: who sent it, which form and what it opens with,\n * and when. `form` is absent where the reader may not read it, and every value\n * comes from a field the read answered, so a field kept from the reader is a\n * part of the row left out, never guessed.\n */\nexport function submissionRow(\n submission: Row,\n form: Row | undefined,\n allHref: string\n): NewSubmissionRow {\n const id = typeof submission.id === \"string\" ? submission.id : \"\";\n const answers = recordOf(submission.data);\n const fields = fieldsOf(form?.fields);\n const nameField = fields.find(isNameField);\n const emailField = fields.find(field => field.type === \"email\");\n const titleField = [nameField, emailField].find(\n field => field !== undefined && firstLine(answers[field.name]) !== undefined\n );\n const title =\n titleField === undefined ? undefined : firstLine(answers[titleField.name]);\n const excerpt = fields\n .filter(field => TEXT_TYPES.has(field.type) && field !== titleField)\n .map(field => firstLine(answers[field.name]))\n .find(line => line !== undefined);\n const formName = firstLine(form?.name);\n const at =\n instantOf(submission.submittedAt) ?? instantOf(submission.createdAt);\n return {\n id,\n title: title ?? \"Submission\",\n ...(formName === undefined ? {} : { form: formName }),\n ...(excerpt === undefined ? {} : { excerpt }),\n ...(at === undefined ? {} : { at }),\n href: `${allHref}/${encodeURIComponent(id)}`,\n };\n}\n\n/** Whether a form field asks for the submitter's name, by its name or its label. */\nfunction isNameField(field: FormFieldShape): boolean {\n return (\n field.type === \"text\" &&\n (NAME_KEYS.has(lettersOf(field.name)) ||\n NAME_KEYS.has(lettersOf(field.label ?? \"\")))\n );\n}\n\n/** A name or label as its letters alone, lower case: \"Full name\" is \"fullname\". */\nfunction lettersOf(text: string): string {\n return text.toLowerCase().replace(/[^a-z]/g, \"\");\n}\n\n/**\n * The first line of a text answer, trimmed and held to `MAX_ROW_TEXT`\n * characters, or `undefined` for none.\n */\nexport function firstLine(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const line = value\n .split(/\\r?\\n/)\n .map(part => part.trim())\n .find(part => part !== \"\");\n return line === undefined ? undefined : clipped(line);\n}\n\n/**\n * `text` cut to its first `MAX_ROW_TEXT` characters with an ellipsis, where it\n * is longer. Counted in whole characters, so an emoji is never split in two.\n */\nfunction clipped(text: string): string {\n const characters = Array.from(text);\n if (characters.length <= MAX_ROW_TEXT) return text;\n return `${characters.slice(0, MAX_ROW_TEXT).join(\"\").trimEnd()}…`;\n}\n\n/** A relationship's id, whether the read answered the id or the related row. */\nfunction formIdOf(value: unknown): string | undefined {\n if (typeof value === \"string\") return value;\n if (typeof value === \"object\" && value !== null && \"id\" in value) {\n const { id } = value;\n return typeof id === \"string\" ? id : undefined;\n }\n return undefined;\n}\n\n/** A JSON column's value, which some dialects answer as its text. */\nfunction parsedJson(value: unknown): unknown {\n if (typeof value !== \"string\") return value;\n try {\n return JSON.parse(value) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/** A submission's answers, by field name. */\nfunction recordOf(value: unknown): Readonly<Record<string, unknown>> {\n const parsed = parsedJson(value);\n return typeof parsed === \"object\" && parsed !== null && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n}\n\n/** A form's fields, as far as a row reads them; any other entry is skipped. */\nfunction fieldsOf(value: unknown): FormFieldShape[] {\n const parsed = parsedJson(value);\n if (!Array.isArray(parsed)) return [];\n return parsed.flatMap((entry: unknown): FormFieldShape[] => {\n if (typeof entry !== \"object\" || entry === null) return [];\n const { type, name, label } = entry as Record<string, unknown>;\n if (typeof type !== \"string\" || typeof name !== \"string\") return [];\n return [{ type, name, ...(typeof label === \"string\" ? { label } : {}) }];\n });\n}\n\n/** A date as an ISO string, or `undefined` when it is not one. */\nfunction instantOf(value: unknown): string | undefined {\n if (typeof value !== \"string\" && typeof value !== \"number\") return undefined;\n const at = new Date(value);\n return Number.isNaN(at.getTime()) ? undefined : at.toISOString();\n}\n","/**\n * Conditional Logic Evaluator\n *\n * Evaluates conditional logic rules to determine field visibility in forms.\n * Used by the FormRenderer component to show/hide fields based on user input.\n *\n * @module utils/evaluate-conditions\n * @since 1.0.0\n *\n * @example\n * ```typescript\n * import { evaluateConditions } from '@nextly/plugin-form-builder';\n *\n * const conditionalLogic = {\n * enabled: true,\n * action: 'show',\n * operator: 'AND',\n * conditions: [\n * { field: 'country', comparison: 'equals', value: 'US' },\n * { field: 'age', comparison: 'greaterThan', value: 18 },\n * ],\n * };\n *\n * const formData = { country: 'US', age: 25 };\n * const isVisible = evaluateConditions(conditionalLogic, formData);\n * // isVisible = true (all conditions met, action is 'show')\n * ```\n */\n\nimport type { ConditionalLogic, ConditionalLogicCondition } from \"../types\";\n\n// ============================================================\n// Types\n// ============================================================\n\n/**\n * Comparison operators supported by the conditional logic system.\n */\nexport type ComparisonOperator =\n | \"equals\"\n | \"notEquals\"\n | \"contains\"\n | \"isEmpty\"\n | \"isNotEmpty\"\n | \"greaterThan\"\n | \"lessThan\";\n\n// ============================================================\n// Internal Helper Functions\n// ============================================================\n\n/**\n * Checks if a value is considered \"empty\".\n * Empty values: undefined, null, empty string, empty array.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value === undefined || value === null) {\n return true;\n }\n if (typeof value === \"string\" && value.trim() === \"\") {\n return true;\n }\n if (Array.isArray(value) && value.length === 0) {\n return true;\n }\n return false;\n}\n\n/**\n * Converts a value to a number for numeric comparisons.\n * Returns NaN if conversion is not possible.\n */\nfunction toNumber(value: unknown): number {\n if (typeof value === \"number\") {\n return value;\n }\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed === \"\") {\n return NaN;\n }\n return Number(trimmed);\n }\n return NaN;\n}\n\n/**\n * Converts a value to a string for string comparisons.\n */\nfunction toString(value: unknown): string {\n if (value === undefined || value === null) {\n return \"\";\n }\n if (typeof value === \"string\") {\n return value;\n }\n if (Array.isArray(value)) {\n return value.join(\", \");\n }\n if (typeof value === \"object\") {\n return JSON.stringify(value);\n }\n // Primitive (number, boolean, bigint, symbol) — safe to coerce.\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- value narrowed to primitive above; rule doesn't follow control flow on unknown\n return String(value);\n}\n\n/**\n * Compares two values using the specified operator.\n *\n * @param operator - The comparison operator to use\n * @param fieldValue - The actual value from the form data\n * @param conditionValue - The expected value from the condition\n * @returns Whether the comparison is true\n */\nfunction compareValues(\n operator: ComparisonOperator,\n fieldValue: unknown,\n conditionValue: unknown\n): boolean {\n switch (operator) {\n case \"equals\": {\n // Strict equality for most cases\n // Handle array values (e.g., checkbox groups, multi-selects)\n if (Array.isArray(fieldValue)) {\n // If conditionValue is also an array, check for deep equality\n if (Array.isArray(conditionValue)) {\n if (fieldValue.length !== conditionValue.length) {\n return false;\n }\n return fieldValue.every((v, i) => v === conditionValue[i]);\n }\n // Check if the array contains the condition value\n return fieldValue.includes(conditionValue);\n }\n // Standard equality - string comparison for flexibility\n return toString(fieldValue) === toString(conditionValue);\n }\n\n case \"notEquals\": {\n // Inverse of equals\n if (Array.isArray(fieldValue)) {\n if (Array.isArray(conditionValue)) {\n if (fieldValue.length !== conditionValue.length) {\n return true;\n }\n return !fieldValue.every((v, i) => v === conditionValue[i]);\n }\n return !fieldValue.includes(conditionValue);\n }\n return toString(fieldValue) !== toString(conditionValue);\n }\n\n case \"contains\": {\n // Case-insensitive substring check\n const fieldStr = toString(fieldValue).toLowerCase();\n const conditionStr = toString(conditionValue).toLowerCase();\n\n // Handle array values - check if any element contains the substring\n if (Array.isArray(fieldValue)) {\n return fieldValue.some(v =>\n toString(v).toLowerCase().includes(conditionStr)\n );\n }\n\n return fieldStr.includes(conditionStr);\n }\n\n case \"isEmpty\": {\n // Check if the field value is empty\n // conditionValue is ignored for this operator\n return isEmptyValue(fieldValue);\n }\n\n case \"isNotEmpty\": {\n // Check if the field value is not empty\n // conditionValue is ignored for this operator\n return !isEmptyValue(fieldValue);\n }\n\n case \"greaterThan\": {\n // Numeric comparison with loose coercion\n const fieldNum = toNumber(fieldValue);\n const conditionNum = toNumber(conditionValue);\n\n // If both can be converted to numbers, use numeric comparison\n if (!Number.isNaN(fieldNum) && !Number.isNaN(conditionNum)) {\n return fieldNum > conditionNum;\n }\n\n // Fall back to string comparison for non-numeric values\n return toString(fieldValue) > toString(conditionValue);\n }\n\n case \"lessThan\": {\n // Numeric comparison with loose coercion\n const fieldNum = toNumber(fieldValue);\n const conditionNum = toNumber(conditionValue);\n\n // If both can be converted to numbers, use numeric comparison\n if (!Number.isNaN(fieldNum) && !Number.isNaN(conditionNum)) {\n return fieldNum < conditionNum;\n }\n\n // Fall back to string comparison for non-numeric values\n return toString(fieldValue) < toString(conditionValue);\n }\n\n default: {\n // Unknown operator - return false to be safe\n // `operator` is `never` here because the switch is exhaustive over ComparisonOperator;\n // cast to string so the diagnostic survives if a new operator is added without a case.\n console.warn(\n `[evaluateConditions] Unknown comparison operator: ${String(operator)}`\n );\n return false;\n }\n }\n}\n\n/**\n * Evaluates a single condition against the form data.\n *\n * Exported for consumers that gate on one condition rather than a full\n * conditional-logic block — notification send-conditions use exactly this.\n *\n * @param condition - The condition to evaluate\n * @param formData - The current form data\n * @returns Whether the condition is met\n */\nexport function evaluateSingleCondition(\n condition: ConditionalLogicCondition,\n formData: Record<string, unknown>\n): boolean {\n const { field, comparison, value: conditionValue } = condition;\n\n // Get the field value from form data\n // Support nested field access with dot notation (e.g., \"address.city\")\n const fieldValue = getNestedValue(formData, field);\n\n // Evaluate the comparison\n return compareValues(comparison, fieldValue, conditionValue);\n}\n\n/**\n * Gets a nested value from an object using dot notation.\n *\n * @param obj - The object to get the value from\n * @param path - The path to the value (e.g., \"address.city\")\n * @returns The value at the path, or undefined if not found\n */\nfunction getNestedValue(obj: Record<string, unknown>, path: string): unknown {\n if (!path) {\n return undefined;\n }\n\n // Simple case - no nesting\n if (!path.includes(\".\")) {\n return obj[path];\n }\n\n // Nested case - traverse the path\n const parts = path.split(\".\");\n let current: unknown = obj;\n\n for (const part of parts) {\n if (current === undefined || current === null) {\n return undefined;\n }\n if (typeof current !== \"object\") {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n return current;\n}\n\n// ============================================================\n// Main Evaluation Function\n// ============================================================\n\n/**\n * Evaluates conditional logic to determine field visibility.\n *\n * This is the main entry point for conditional logic evaluation.\n * It evaluates all conditions according to the logical operator (AND/OR)\n * and returns whether the field should be visible based on the action\n * (show/hide).\n *\n * @param logic - The conditional logic configuration\n * @param formData - The current form data (field values)\n * @returns Whether the field should be visible\n *\n * @example\n * ```typescript\n * // Show field when country is US AND age is over 18\n * const logic: ConditionalLogic = {\n * enabled: true,\n * action: 'show',\n * operator: 'AND',\n * conditions: [\n * { field: 'country', comparison: 'equals', value: 'US' },\n * { field: 'age', comparison: 'greaterThan', value: 18 },\n * ],\n * };\n *\n * evaluateConditions(logic, { country: 'US', age: 25 }); // true\n * evaluateConditions(logic, { country: 'US', age: 15 }); // false\n * evaluateConditions(logic, { country: 'CA', age: 25 }); // false\n *\n * // Hide field when status is \"completed\"\n * const hideLogic: ConditionalLogic = {\n * enabled: true,\n * action: 'hide',\n * operator: 'AND',\n * conditions: [\n * { field: 'status', comparison: 'equals', value: 'completed' },\n * ],\n * };\n *\n * evaluateConditions(hideLogic, { status: 'completed' }); // false (hidden)\n * evaluateConditions(hideLogic, { status: 'pending' }); // true (visible)\n * ```\n *\n * @example\n * ```typescript\n * // Show field when either condition is met (OR)\n * const orLogic: ConditionalLogic = {\n * enabled: true,\n * action: 'show',\n * operator: 'OR',\n * conditions: [\n * { field: 'role', comparison: 'equals', value: 'admin' },\n * { field: 'role', comparison: 'equals', value: 'editor' },\n * ],\n * };\n *\n * evaluateConditions(orLogic, { role: 'admin' }); // true\n * evaluateConditions(orLogic, { role: 'editor' }); // true\n * evaluateConditions(orLogic, { role: 'viewer' }); // false\n * ```\n */\nexport function evaluateConditions(\n logic: ConditionalLogic | undefined,\n formData: Record<string, unknown>\n): boolean {\n // If no logic provided or logic is disabled, field is visible by default\n if (!logic || !logic.enabled) {\n return true;\n }\n\n const { action, operator, conditions } = logic;\n\n // If no conditions, field is visible by default\n if (!conditions || conditions.length === 0) {\n return true;\n }\n\n // Evaluate all conditions\n const results = conditions.map(condition =>\n evaluateSingleCondition(condition, formData)\n );\n\n // Combine results based on logical operator\n let conditionsMet: boolean;\n\n if (operator === \"OR\") {\n // OR: At least one condition must be true\n conditionsMet = results.some(result => result);\n } else {\n // AND (default): All conditions must be true\n conditionsMet = results.every(result => result);\n }\n\n // Apply the action\n // - \"show\": field is visible when conditions are met\n // - \"hide\": field is visible when conditions are NOT met\n if (action === \"hide\") {\n return !conditionsMet;\n }\n\n // Default action is \"show\"\n return conditionsMet;\n}\n\n/**\n * Checks if a comparison operator is valid.\n *\n * @param operator - The operator to check\n * @returns Whether the operator is valid\n */\nexport function isValidComparisonOperator(\n operator: string\n): operator is ComparisonOperator {\n const validOperators: ComparisonOperator[] = [\n \"equals\",\n \"notEquals\",\n \"contains\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"greaterThan\",\n \"lessThan\",\n ];\n return validOperators.includes(operator as ComparisonOperator);\n}\n\n/**\n * Gets the list of supported comparison operators.\n *\n * @returns Array of supported operator names\n */\nexport function getSupportedComparisonOperators(): ComparisonOperator[] {\n return [\n \"equals\",\n \"notEquals\",\n \"contains\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"greaterThan\",\n \"lessThan\",\n ];\n}\n","/**\n * Which of a form's fields its conditions hide, for a given set of answers.\n *\n * One answer for every place that needs it: the submission's checks, which\n * must not ask for a field the visitor was never shown, and the admin's\n * preview, which must refuse exactly what the site refuses.\n *\n * @module utils/condition-visibility\n */\nimport type { AnyFormField } from \"../types\";\n\nimport { evaluateConditions } from \"./evaluate-conditions\";\n\n/** The names a field's enabled condition reads; none when it has none. */\nfunction namesRead(field: AnyFormField): string[] {\n const logic = field.conditionalLogic;\n return logic?.enabled === true\n ? logic.conditions.map(condition => condition.field)\n : [];\n}\n\n/**\n * Which of a form's fields its conditions hide, for a given set of answers.\n *\n * A condition reads the answers of fields that are themselves shown, so a\n * hidden field's leftover answer never decides another field, and it may read\n * a field before or after its own. Fields are decided in the order their\n * conditions depend on each other, whatever their order on the form.\n *\n * Conditions can read each other in a loop (A shown when B is answered, B\n * shown when A is), and no order settles a loop. The rule for one: fields in\n * the same loop read each other's answers as given, and only the loop's\n * fields decide it, so an unrelated field elsewhere on the form never changes\n * the outcome.\n *\n * A field of type `hidden` is not one of them: it is sent with every\n * submission, unseen.\n */\nexport function fieldsHiddenByCondition(\n fields: readonly AnyFormField[],\n answers: Record<string, unknown>\n): Set<string> {\n const byName = new Map(fields.map(field => [field.name, field]));\n const reach = new Map<string, Set<string>>();\n /** Every field `name`'s condition depends on, directly or through others. */\n const dependsOn = (name: string): Set<string> => {\n const known = reach.get(name);\n if (known) return known;\n const found = new Set<string>();\n reach.set(name, found);\n const pending = [name];\n for (let next = pending.pop(); next !== undefined; next = pending.pop()) {\n const field = byName.get(next);\n for (const read of field ? namesRead(field) : []) {\n if (!found.has(read)) {\n found.add(read);\n pending.push(read);\n }\n }\n }\n return found;\n };\n\n const shown = new Map<string, boolean>();\n /** Whether field `name` is on the page, deciding what it reads first. */\n const isShown = (name: string): boolean => {\n const decided = shown.get(name);\n if (decided !== undefined) return decided;\n const field = byName.get(name);\n const logic = field?.conditionalLogic;\n if (!field || logic?.enabled !== true) return true;\n // A field read from inside this one's loop counts as given; any other is\n // decided first, which ends: it cannot lead back here.\n const read = Object.fromEntries(\n Object.entries(answers).filter(\n ([other]) =>\n !byName.has(other) ||\n !namesRead(field).includes(other) ||\n dependsOn(other).has(name) ||\n isShown(other)\n )\n );\n const result = evaluateConditions(logic, read);\n shown.set(name, result);\n return result;\n };\n\n return new Set(\n fields.filter(field => !isShown(field.name)).map(field => field.name)\n );\n}\n","/**\n * What a submission was asked: each field's label, type and option labels,\n * and which fields its conditions hid, kept with the submission so its page\n * reads as it was answered after the form changes. A field renamed, an\n * option relabelled or a condition changed later leaves the submission as it\n * was. A submission stored before this kept nothing, and reads with the form\n * as it is now.\n *\n * @module utils/asked-fields\n */\n\nimport type { AnyFormField } from \"../types\";\n\nimport { fieldsHiddenByCondition } from \"./condition-visibility\";\n\n/** One field as the submission was asked it. */\nexport interface AskedField {\n name: string;\n label: string;\n type: string;\n /** The choices of a select or radio, value and label as shown. */\n options?: Array<{ value: string; label: string }>;\n}\n\n/** What a submission was asked, as stored with it. */\nexport interface AskedSnapshot {\n fields: AskedField[];\n /** The fields its conditions hid from the visitor, so never asked. */\n hidden: string[];\n}\n\n/** A stored option value or label as text: a string or a number. */\nfunction textOf(value: unknown): string {\n if (typeof value === \"string\") return value;\n return typeof value === \"number\" ? String(value) : \"\";\n}\n\n/** One form field as a submission keeps it. */\nexport function askedField(field: AnyFormField): AskedField {\n const options = (field as { options?: unknown }).options;\n return {\n name: field.name,\n label: field.label || field.name,\n type: field.type,\n ...(Array.isArray(options)\n ? {\n options: options.map(option => {\n const { value, label } = option as {\n value?: unknown;\n label?: unknown;\n };\n const text = textOf(value);\n return { value: text, label: textOf(label) || text };\n }),\n }\n : {}),\n };\n}\n\n/** What a new submission with `data` was asked, from its form's fields. */\nexport function askedSnapshotOf(\n fields: readonly AnyFormField[],\n data: Record<string, unknown>\n): AskedSnapshot {\n const hidden = fieldsHiddenByCondition([...fields], data);\n return {\n fields: fields.map(askedField),\n hidden: fields.map(field => field.name).filter(name => hidden.has(name)),\n };\n}\n\n/** A stored snapshot, read: as written, or a JSON string of it. */\nexport function readAskedSnapshot(value: unknown): AskedSnapshot | undefined {\n let parsed = value;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed) as unknown;\n } catch {\n return undefined;\n }\n }\n if (!parsed || typeof parsed !== \"object\") return undefined;\n const { fields, hidden } = parsed as Record<string, unknown>;\n if (!Array.isArray(fields)) return undefined;\n return {\n fields: fields.filter(\n (field): field is AskedField =>\n !!field &&\n typeof (field as AskedField).name === \"string\" &&\n typeof (field as AskedField).label === \"string\" &&\n typeof (field as AskedField).type === \"string\"\n ),\n hidden: Array.isArray(hidden)\n ? hidden.filter((name): name is string => typeof name === \"string\")\n : [],\n };\n}\n\n/**\n * The snapshot after staff edit answers. An answer that changed is read with\n * the field as it was when edited to, so an edit to an option added later\n * keeps that option's label; every other field stays as the visitor was\n * asked it. A submission with no snapshot keeps none.\n */\nexport function askedAfterEdit(\n stored: AskedSnapshot | undefined,\n fields: readonly AnyFormField[],\n before: Record<string, unknown>,\n after: Record<string, unknown>\n): AskedSnapshot | undefined {\n if (stored === undefined) return undefined;\n const changed = new Set(\n fields\n .map(field => field.name)\n .filter(\n name => JSON.stringify(before[name]) !== JSON.stringify(after[name])\n )\n );\n if (changed.size === 0) return stored;\n const current = new Map(fields.map(field => [field.name, field]));\n const kept = stored.fields.map(field =>\n changed.has(field.name) && current.has(field.name)\n ? askedField(current.get(field.name)!)\n : field\n );\n // A field answered by the edit that the visitor was never asked.\n for (const name of changed) {\n if (!kept.some(field => field.name === name) && current.has(name)) {\n kept.push(askedField(current.get(name)!));\n }\n }\n return {\n fields: kept,\n hidden: stored.hidden.filter(name => !changed.has(name)),\n };\n}\n\n/**\n * The fields a submission's page reads its answers with: as it was asked, or\n * the form as it is now for one stored before submissions kept that.\n */\nexport function askedFieldsOf(\n stored: unknown,\n formFields: readonly AnyFormField[],\n data: Record<string, unknown>\n): AskedSnapshot {\n return readAskedSnapshot(stored) ?? askedSnapshotOf(formFields, data);\n}\n","/**\n * A consent field's answer: whether the visitor agreed, kept with the text the\n * form showed them. The text comes from the form at the moment of submission,\n * never from the visitor, and a submitted consent is never edited, so the\n * stored answer says exactly what this visitor agreed to.\n *\n * @module utils/consent\n */\n\nimport type { AnyFormField, ConsentAnswer, ConsentFormField } from \"../types\";\n\n/** Said when an edit of a submission would change a consent. */\nexport const CONSENT_EDIT_REFUSED =\n \"A consent cannot be edited: it keeps what the visitor agreed to\";\n\n/** Whether a value is a stored consent answer. */\nexport function isConsentAnswer(value: unknown): value is ConsentAnswer {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n const answer = value as Record<string, unknown>;\n return typeof answer.agreed === \"boolean\" && typeof answer.text === \"string\";\n}\n\n/**\n * Whether the visitor agreed. Only an explicit yes counts: a box sends `on`,\n * a script `true`; anything else, a stray string included, is not consent.\n */\nfunction agreedIn(value: unknown): boolean {\n if (value === true) return true;\n if (typeof value === \"string\") {\n return value === \"true\" || value === \"1\" || value === \"on\";\n }\n if (isConsentAnswer(value)) return value.agreed;\n return false;\n}\n\n/**\n * The answer to store for what the visitor sent. The text is the form's, so a\n * visitor who posts an answer object with a text of their own still stores\n * the text the form shows.\n */\nexport function consentAnswerOf(\n value: unknown,\n field: ConsentFormField\n): ConsentAnswer {\n return { agreed: agreedIn(value), text: field.agreementText };\n}\n\nfunction sameConsent(before: unknown, after: unknown): boolean {\n if (before === undefined) return after === undefined;\n if (isConsentAnswer(before)) {\n return (\n isConsentAnswer(after) &&\n before.agreed === after.agreed &&\n before.text === after.text\n );\n }\n // An answer stored before its field became a consent (a checkbox's yes or\n // no, say): kept as it was, and compared as stored.\n return JSON.stringify(before) === JSON.stringify(after);\n}\n\n/**\n * The answers of a stored submission that are consents: each consent field\n * of the form, and any answer stored as a consent, whatever field now has\n * its name. Read from the submission as well as the form, since a form can\n * lose its consent field, or give its name to another field, while its\n * submissions keep what was agreed to.\n */\nexport function storedConsentNames(\n fields: readonly AnyFormField[],\n stored: Record<string, unknown>\n): string[] {\n const names = new Set(\n fields.filter(field => field.type === \"consent\").map(field => field.name)\n );\n for (const [name, value] of Object.entries(stored)) {\n if (isConsentAnswer(value)) names.add(name);\n }\n return [...names];\n}\n\n/**\n * The consents whose stored answer an edit would change: the edit's answers\n * against the stored ones, for each consent the submission holds. One left\n * out of the edit is a change too, since it would drop the answer.\n */\nexport function editedConsents(\n fields: readonly AnyFormField[],\n stored: Record<string, unknown>,\n edited: Record<string, unknown>\n): string[] {\n return storedConsentNames(fields, stored).filter(\n name => !sameConsent(stored[name], edited[name])\n );\n}\n\n/** Said when the builder is asked to make a consent optional. */\nexport const CONSENT_REQUIRED_REASON =\n \"A consent is always required: an answer nobody has to give records nothing\";\n\n/**\n * A field as the builder keeps it after an edit, or why the edit is refused.\n * A consent stays required, so turning Required off is refused and said; a\n * field just made a consent becomes required, with a text to agree to.\n */\nexport function consentFieldEdit(\n before: AnyFormField,\n after: AnyFormField\n): { field: AnyFormField } | { refused: string } {\n if (after.type !== \"consent\") return { field: after };\n if (before.type === \"consent\" && after.required !== true) {\n return { refused: CONSENT_REQUIRED_REASON };\n }\n const text = (after as { agreementText?: unknown }).agreementText;\n return {\n field: {\n ...after,\n required: true,\n agreementText:\n typeof text === \"string\" ? text : \"I agree to the privacy policy\",\n } as ConsentFormField,\n };\n}\n\n/** Said when a consent has no text to agree to. */\nexport const CONSENT_TEXT_REQUIRED =\n \"A consent field needs the text the visitor agrees to.\";\n\n/**\n * The consent fields with no text to agree to. A consent with a blank text\n * would record agreement to nothing, so a form holding one is not saved, by\n * the builder or any other door.\n */\nexport function blankConsentFields(\n fields: readonly AnyFormField[]\n): ConsentFormField[] {\n return fields.filter(\n (field): field is ConsentFormField =>\n field.type === \"consent\" &&\n (typeof (field as { agreementText?: unknown }).agreementText !==\n \"string\" ||\n (field as ConsentFormField).agreementText.trim() === \"\")\n );\n}\n\n/** Said when a visitor agreed to a text the form no longer shows. */\nexport const CONSENT_TEXT_CHANGED =\n \"The text to agree to has changed since the form was loaded. Review it and agree again.\";\n\n/**\n * The consent fields a new submission agreed to under a text the form no\n * longer shows. A visitor who sends the text they were shown (an answer of\n * `{ agreed, text }`) is held to it: if the author changed it while the form\n * was open, the submission is refused rather than recorded as agreement to\n * terms the visitor never saw. A bare yes carries no text, so it is taken\n * as agreement to the text the form shows now.\n */\nexport function staleConsentNames(\n fields: readonly AnyFormField[],\n data: Record<string, unknown>\n): string[] {\n return fields\n .filter((field): field is ConsentFormField => field.type === \"consent\")\n .filter(field => {\n const sent = data[field.name];\n if (!sent || typeof sent !== \"object\" || Array.isArray(sent)) {\n return false;\n }\n const text = (sent as { text?: unknown }).text;\n return typeof text === \"string\" && text !== field.agreementText;\n })\n .map(field => field.name);\n}\n","/**\n * Export Formats Utilities\n *\n * Pure utility functions for exporting form submissions to CSV and JSON formats.\n * These functions generate formatted strings that can be used for file downloads\n * or API responses.\n *\n * @module utils/export-formats\n * @since 0.1.0\n */\n\nimport type { SubmissionDocument, FormDocument, AnyFormField } from \"../types\";\n\nimport { askedField, askedFieldsOf, type AskedField } from \"./asked-fields\";\nimport { isConsentAnswer } from \"./consent\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Options for CSV export.\n */\nexport interface CSVExportOptions {\n /**\n * Whether to include metadata columns (_id, _status, _submittedAt, etc.).\n * @default true\n */\n includeMetadata?: boolean;\n\n /**\n * Delimiter character for CSV.\n * @default \",\"\n */\n delimiter?: string;\n\n /**\n * Whether to include UTF-8 BOM for Excel compatibility.\n * @default true\n */\n includeBOM?: boolean;\n\n /**\n * Date format for timestamps.\n * @default \"iso\" (ISO 8601)\n */\n dateFormat?: \"iso\" | \"locale\";\n}\n\n/**\n * Options for JSON export.\n */\nexport interface JSONExportOptions {\n /**\n * Whether to include metadata in each submission.\n * @default true\n */\n includeMetadata?: boolean;\n\n /**\n * Whether to include form definition in the export.\n * @default true\n */\n includeFormDefinition?: boolean;\n\n /**\n * Number of spaces for indentation (0 for minified).\n * @default 2\n */\n indent?: number;\n}\n\n/**\n * Exported JSON structure.\n */\nexport interface ExportedJSON {\n /** Export metadata */\n exportedAt: string;\n\n /** Form information (if includeFormDefinition is true) */\n form?: {\n id: string;\n name: string;\n slug: string;\n fields: Array<{\n name: string;\n label: string;\n type: string;\n }>;\n };\n\n /** Total number of submissions in export */\n totalSubmissions: number;\n\n /** Array of submission data */\n submissions: Array<Record<string, unknown>>;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** UTF-8 BOM character sequence */\nconst UTF8_BOM = \"\\uFEFF\";\n\n// ============================================================================\n// Value Formatting Helpers\n// ============================================================================\n\n/**\n * Stringify a value safely, falling back to JSON for plain objects so we\n * never emit the default `[object Object]` representation.\n */\nfunction safeString(value: unknown): string {\n if (typeof value === \"object\" && value !== null) {\n return JSON.stringify(value);\n }\n return String(value);\n}\n\n/**\n * Format a field value for export based on field type.\n *\n * @param value - The raw value from submission data\n * @param field - The field definition (optional, for type-specific formatting)\n * @returns Formatted string representation of the value\n *\n * @example\n * ```typescript\n * formatExportValue(true, { type: 'checkbox' }); // \"Yes\"\n * formatExportValue(['a', 'b'], { type: 'select' }); // \"a, b\"\n * formatExportValue(new Date(), { type: 'date' }); // \"2024-01-15\"\n * ```\n */\nexport function formatExportValue(\n value: unknown,\n // Only the field's type decides how a value reads, so a field as a\n // submission was asked it reads the same as the form's own.\n field?: { readonly type: string }\n): string {\n // Handle null/undefined\n if (value === null || value === undefined) {\n return \"\";\n }\n\n // Type-specific formatting when field definition is available\n if (field) {\n switch (field.type) {\n case \"checkbox\":\n return value ? \"Yes\" : \"No\";\n\n case \"select\":\n if (Array.isArray(value)) {\n return value.join(\", \");\n }\n return safeString(value);\n\n case \"file\":\n if (Array.isArray(value)) {\n // File URLs or IDs\n return value.map(v => safeString(v)).join(\", \");\n }\n return safeString(value);\n\n case \"date\":\n if (value instanceof Date) {\n return value.toISOString().split(\"T\")[0];\n }\n if (typeof value === \"string\") {\n // Already a date string, return as-is or normalize\n const parsed = new Date(value);\n if (!isNaN(parsed.getTime())) {\n return parsed.toISOString().split(\"T\")[0];\n }\n }\n return safeString(value);\n\n case \"time\":\n return safeString(value);\n\n case \"hidden\":\n return safeString(value);\n\n case \"consent\":\n // The choice with the text the visitor was shown, which is what the\n // answer records; the form's text today may differ.\n if (isConsentAnswer(value)) {\n return `${value.agreed ? \"Agreed\" : \"Not agreed\"}: \"${value.text}\"`;\n }\n return safeString(value);\n\n default:\n // text, email, phone, url, textarea, number, radio\n break;\n }\n }\n\n // Generic formatting for values without field definition\n if (Array.isArray(value)) {\n return value.join(\", \");\n }\n\n if (typeof value === \"boolean\") {\n return value ? \"Yes\" : \"No\";\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return safeString(value);\n}\n\n/**\n * A choice answer by its options' labels as asked, several joined. A value\n * no option has (an option removed since) reads as itself.\n */\nexport function optionLabels(\n field: Pick<AskedField, \"options\">,\n value: unknown\n): string {\n const values = Array.isArray(value) ? value : [value];\n return values\n .map(each => {\n const text = typeof each === \"string\" ? each : formatExportValue(each);\n return (\n field.options?.find(option => option.value === text)?.label ?? text\n );\n })\n .join(\", \");\n}\n\n/** The kinds of field answered by picking among options. */\nconst CHOICE_TYPES: ReadonlySet<string> = new Set([\"select\", \"radio\"]);\n\n/**\n * An answer as one line of text, read with the field as it was asked: a\n * choice by its options' labels, so a list and an export read \"Backend\n * engineer\" rather than the stored `backend-engineer`; anything else as\n * `formatExportValue` reads it.\n */\nfunction answerText(value: unknown, field: AskedField): string {\n if (value === null || value === undefined) return \"\";\n return CHOICE_TYPES.has(field.type)\n ? optionLabels(field, value)\n : formatExportValue(value, field);\n}\n\n/**\n * A submission's answers as text, each read with its field as the submission\n * was asked it, or as the form has it now for a field it was not asked (one\n * added since) or a submission stored before submissions kept that. The\n * submissions list and the CSV export both read answers through this, so the\n * two cannot show one answer differently.\n */\nexport function answerReader(\n submission: { data?: Record<string, unknown> | null; asked?: unknown },\n formFields: readonly AnyFormField[]\n): (field: AnyFormField) => string {\n const data = submission.data ?? {};\n const asked = new Map(\n askedFieldsOf(submission.asked, formFields, data).fields.map(each => [\n each.name,\n each,\n ])\n );\n return field =>\n answerText(data[field.name], asked.get(field.name) ?? askedField(field));\n}\n\n/**\n * Format a date for export.\n *\n * @param date - Date value (Date object or string)\n * @param format - Output format (\"iso\" or \"locale\")\n * @returns Formatted date string\n */\nfunction formatDate(\n date: Date | string | undefined,\n format: \"iso\" | \"locale\" = \"iso\"\n): string {\n if (!date) return \"\";\n\n const d = typeof date === \"string\" ? new Date(date) : date;\n\n if (isNaN(d.getTime())) {\n return \"\";\n }\n\n return format === \"iso\" ? d.toISOString() : d.toLocaleString();\n}\n\n// ============================================================================\n// CSV Export\n// ============================================================================\n\n/**\n * Escape a value for CSV format (RFC 4180 compliant).\n *\n * Rules:\n * - Fields containing comma, double-quote, or newline must be quoted\n * - Double-quotes within fields must be escaped with another double-quote\n *\n * @param value - The value to escape\n * @param delimiter - The delimiter being used\n * @returns Properly escaped CSV value\n */\nfunction escapeCSVValue(value: string, delimiter: string = \",\"): string {\n // CSV injection: spreadsheets execute cells starting with a formula\n // trigger character, and syntax quoting does NOT stop that. A leading\n // apostrophe forces text interpretation (the OWASP mitigation). Plain\n // negative numbers are exempt — they are data, not formulas.\n const isFormulaLike =\n /^[=+\\-@\\t\\r]/.test(value) && !/^-?\\d+(\\.\\d+)?$/.test(value);\n const escapedFormula = isFormulaLike ? `'${value}` : value;\n\n // Check if quoting is needed\n const needsQuoting =\n escapedFormula.includes(delimiter) ||\n escapedFormula.includes('\"') ||\n escapedFormula.includes(\"\\n\") ||\n escapedFormula.includes(\"\\r\");\n\n if (!needsQuoting) {\n return escapedFormula;\n }\n\n // Escape double quotes by doubling them\n const escaped = escapedFormula.replace(/\"/g, '\"\"');\n return `\"${escaped}\"`;\n}\n\n/**\n * Export submissions to CSV format.\n *\n * Generates an RFC 4180 compliant CSV string with:\n * - Form field values as columns (in form definition order), headed by the\n * fields' labels, a choice answer by its option's label\n * - Optional metadata columns at the end (_id, _status, _submittedAt, etc.)\n * - Proper escaping of special characters\n * - Optional UTF-8 BOM for Excel compatibility\n *\n * @param submissions - Array of submission documents to export\n * @param form - Form document containing field definitions\n * @param options - Export options\n * @returns CSV formatted string\n *\n * @example\n * ```typescript\n * const csv = exportToCSV(submissions, form, {\n * includeMetadata: true,\n * includeBOM: true,\n * });\n *\n * // Download in browser\n * downloadFile(csv, 'submissions.csv', 'text/csv;charset=utf-8');\n * ```\n */\nexport function exportToCSV(\n submissions: SubmissionDocument[],\n form: FormDocument,\n options: CSVExportOptions = {}\n): string {\n const {\n includeMetadata = true,\n delimiter = \",\",\n includeBOM = true,\n dateFormat = \"iso\",\n } = options;\n\n const exportableFields = form.fields;\n\n // Build header row\n const headers: string[] = [];\n\n // Form field headers (using labels)\n for (const field of exportableFields) {\n headers.push(field.label);\n }\n\n // Metadata headers\n if (includeMetadata) {\n headers.push(\"ID\", \"Status\", \"Submitted At\", \"IP Address\", \"User Agent\");\n }\n\n // Build data rows\n const rows: string[][] = [];\n\n for (const submission of submissions) {\n const row: string[] = [];\n // Each answer as the list shows it: a choice by its option's label.\n const answer = answerReader(submission, exportableFields);\n\n // Form field values\n for (const field of exportableFields) {\n row.push(escapeCSVValue(answer(field), delimiter));\n }\n\n // Metadata values\n if (includeMetadata) {\n row.push(escapeCSVValue(submission.id, delimiter));\n row.push(escapeCSVValue(submission.status, delimiter));\n row.push(\n escapeCSVValue(\n formatDate(submission.submittedAt, dateFormat),\n delimiter\n )\n );\n row.push(escapeCSVValue(submission.ipAddress || \"\", delimiter));\n row.push(escapeCSVValue(submission.userAgent || \"\", delimiter));\n }\n\n rows.push(row);\n }\n\n // Escape headers\n const escapedHeaders = headers.map(h => escapeCSVValue(h, delimiter));\n\n // Build CSV string\n const lines = [\n escapedHeaders.join(delimiter),\n ...rows.map(row => row.join(delimiter)),\n ];\n\n // Use CRLF line endings for maximum compatibility\n const csvContent = lines.join(\"\\r\\n\");\n\n // Prepend BOM if requested\n return includeBOM ? UTF8_BOM + csvContent : csvContent;\n}\n\n// ============================================================================\n// JSON Export\n// ============================================================================\n\n/**\n * Export submissions to JSON format.\n *\n * Generates a structured JSON export with:\n * - Export metadata (timestamp)\n * - Optional form definition\n * - Array of submissions with field values\n * - Optional submission metadata (_id, _status, etc.)\n *\n * @param submissions - Array of submission documents to export\n * @param form - Form document containing field definitions\n * @param options - Export options\n * @returns JSON formatted string\n *\n * @example\n * ```typescript\n * const json = exportToJSON(submissions, form, {\n * includeMetadata: true,\n * includeFormDefinition: true,\n * indent: 2,\n * });\n *\n * // Download in browser\n * downloadFile(json, 'submissions.json', 'application/json');\n * ```\n */\nexport function exportToJSON(\n submissions: SubmissionDocument[],\n form: FormDocument,\n options: JSONExportOptions = {}\n): string {\n const {\n includeMetadata = true,\n includeFormDefinition = true,\n indent = 2,\n } = options;\n\n const exportableFields = form.fields;\n\n // Build form definition\n const formDefinition = includeFormDefinition\n ? {\n id: form.id,\n name: form.name,\n slug: form.slug,\n fields: exportableFields.map(f => ({\n name: f.name,\n label: f.label,\n type: f.type,\n })),\n }\n : undefined;\n\n // Build submission data\n const exportedSubmissions = submissions.map(submission => {\n const data = submission.data;\n\n // Build field data object with labels as keys for readability\n const fieldData: Record<string, unknown> = {};\n\n for (const field of exportableFields) {\n const value = data[field.name];\n // Use field name as key (consistent with submission data)\n fieldData[field.name] = value;\n }\n\n // Add metadata if requested\n if (includeMetadata) {\n return {\n ...fieldData,\n _metadata: {\n id: submission.id,\n status: submission.status,\n submittedAt: submission.submittedAt,\n ipAddress: submission.ipAddress || null,\n userAgent: submission.userAgent || null,\n createdAt: submission.createdAt,\n updatedAt: submission.updatedAt,\n },\n };\n }\n\n return fieldData;\n });\n\n // Build export object\n const exportData: ExportedJSON = {\n exportedAt: new Date().toISOString(),\n totalSubmissions: submissions.length,\n submissions: exportedSubmissions,\n };\n\n // Add form definition if requested\n if (formDefinition) {\n exportData.form = formDefinition;\n }\n\n return JSON.stringify(exportData, null, indent);\n}\n\n// ============================================================================\n// Browser Download Helper\n// ============================================================================\n\n/**\n * Trigger a file download in the browser.\n *\n * Creates a Blob from the content and triggers a download using a temporary\n * anchor element. This function is browser-only and will throw in Node.js.\n *\n * @param content - File content as string\n * @param filename - Name for the downloaded file\n * @param mimeType - MIME type of the file\n *\n * @example\n * ```typescript\n * const csv = exportToCSV(submissions, form);\n * downloadFile(csv, 'contact-form-submissions.csv', 'text/csv;charset=utf-8');\n *\n * const json = exportToJSON(submissions, form);\n * downloadFile(json, 'submissions.json', 'application/json');\n * ```\n */\nexport function downloadFile(\n content: string,\n filename: string,\n mimeType: string\n): void {\n // Check for browser environment\n if (typeof window === \"undefined\" || typeof document === \"undefined\") {\n throw new Error(\"downloadFile is only available in browser environments\");\n }\n\n // Create blob\n const blob = new Blob([content], { type: mimeType });\n\n // Create download URL\n const url = URL.createObjectURL(blob);\n\n // Create temporary anchor element\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = filename;\n\n // Trigger download\n document.body.appendChild(link);\n link.click();\n\n // Cleanup\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n}\n\n/**\n * Generate a filename for export.\n *\n * Creates a filename with form slug and current timestamp.\n *\n * @param formSlug - The form's slug\n * @param format - Export format ('csv' or 'json')\n * @returns Generated filename\n *\n * @example\n * ```typescript\n * generateExportFilename('contact-form', 'csv');\n * // Returns: \"contact-form-submissions-2024-01-15.csv\"\n * ```\n */\nexport function generateExportFilename(\n formSlug: string,\n format: \"csv\" | \"json\"\n): string {\n // The name ends up inside a Content-Disposition header — restrict it to\n // characters that can never break the header value or the filesystem.\n const safeSlug =\n formSlug.toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\") || \"form\";\n const date = new Date().toISOString().split(\"T\")[0];\n return `${safeSlug}-submissions-${date}.${format}`;\n}\n\n// ============================================================================\n// Convenience Export Functions\n// ============================================================================\n\n/**\n * Export submissions and trigger browser download.\n *\n * Convenience function that combines export and download in one call.\n * Browser-only.\n *\n * @param submissions - Array of submission documents\n * @param form - Form document\n * @param format - Export format ('csv' or 'json')\n * @param options - Format-specific options\n *\n * @example\n * ```typescript\n * // In a React component\n * const handleExport = (format: 'csv' | 'json') => {\n * exportAndDownload(submissions, form, format);\n * };\n * ```\n */\nexport function exportAndDownload(\n submissions: SubmissionDocument[],\n form: FormDocument,\n format: \"csv\" | \"json\",\n options?: CSVExportOptions | JSONExportOptions\n): void {\n const filename = generateExportFilename(form.slug, format);\n\n if (format === \"csv\") {\n const content = exportToCSV(submissions, form, options);\n downloadFile(content, filename, \"text/csv;charset=utf-8\");\n } else {\n const content = exportToJSON(submissions, form, options);\n downloadFile(content, filename, \"application/json\");\n }\n}\n","/**\n * The two emails a form sends out of the box: one to the team, listing every\n * answer, and one confirming to the visitor that their submission arrived.\n *\n * A notification sends only with a message from Settings › Email, so without\n * these every new notification started unsendable and every new form sent\n * nothing until someone wrote an email for it. Registering them as the plugin's\n * own messages means a new notification has one to use from the start, while\n * a site still edits their wording and design in the Email hub like any other\n * message, where every email it sends lives.\n *\n * The bodies are Liquid: they loop over `_submission.answers`, the\n * submission's answers by label, built by `buildNotificationEmails`. Every value is escaped by the\n * engine, so an answer can never be read as markup.\n */\nimport type { PluginEmailTemplate } from \"@nextlyhq/plugin-sdk\";\n\nimport type { FormNotification } from \"../types\";\n\n/** The team's email: every answer, so the inbox has the whole submission. */\nexport const FORM_SUBMISSION_TEMPLATE = \"forms-submission\";\n\n/** The visitor's email: confirms the submission arrived, with a copy. */\nexport const FORM_CONFIRMATION_TEMPLATE = \"forms-confirmation\";\n\n/** The answers as a two-column table, label then value, one row each. */\nconst ANSWERS_TABLE =\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" style=\"width: 100%; border-collapse: collapse;\">` +\n `{% for answer in _submission.answers %}<tr>` +\n `<td style=\"width: 40%; padding: 8px 16px 8px 0; vertical-align: top; font-weight: 600; overflow-wrap: anywhere;\">{{ answer.label }}</td>` +\n `<td style=\"padding: 8px 0; vertical-align: top; overflow-wrap: anywhere;\">{{ answer.value }}</td>` +\n `</tr>{% endfor %}</table>`;\n\n/**\n * Shown where the table would be when no answer list was sent: the Email hub's\n * preview and Send test, whose sample data has a placeholder string for each\n * variable. A real send always carries a list, even an empty one, which\n * Liquid treats as present, so a sent email never shows this line.\n */\nconst PREVIEW_NOTE = `{% unless _submission.answers %}<p><em>Each answer appears here, its question then what was answered.</em></p>{% endunless %}`;\n\n/** The answers as plain text, one per line, for mail clients that show text. */\nconst ANSWERS_TEXT = `{% for answer in _submission.answers %}{{ answer.label }}: {{ answer.value }}\\n{% endfor %}`;\n\n/** The variables both messages are sent with, as the Email hub lists them. */\nconst VARIABLES = [\n { name: \"formName\", description: \"The form's name\", required: true },\n {\n name: \"_submission\",\n description:\n \"The submission: `_submission.answers` holds each answer as `label` and `value`, in the form's order, leaving out questions left blank or hidden by a condition; `_submission.id` is its id. It starts with an underscore, which no form field's name may, so it never meets a submitted field of the same name\",\n required: true,\n },\n];\n\n/** The messages the plugin contributes to Settings › Email. */\nexport function formsEmailTemplates(): PluginEmailTemplate[] {\n return [\n {\n slug: FORM_SUBMISSION_TEMPLATE,\n name: \"Form submission\",\n description:\n \"Sent to your team when someone submits a form, with every answer.\",\n subject: \"New submission: {{ formName }}\",\n htmlContent: `<p>Someone submitted <strong>{{ formName }}</strong>.</p>${ANSWERS_TABLE}${PREVIEW_NOTE}`,\n plainTextContent: `Someone submitted {{ formName }}.\\n\\n${ANSWERS_TEXT}`,\n variables: VARIABLES,\n engine: \"liquid\",\n },\n {\n slug: FORM_CONFIRMATION_TEMPLATE,\n name: \"Form confirmation\",\n description:\n \"Sent to the person who submitted a form, confirming it arrived, with a copy of their answers.\",\n subject: \"We received your submission\",\n htmlContent:\n `<p>Thank you. We received your answers to <strong>{{ formName }}</strong> and will be in touch if we need anything else.</p>` +\n `<p>Here is a copy of what you sent:</p>${ANSWERS_TABLE}${PREVIEW_NOTE}`,\n plainTextContent:\n `Thank you. We received your answers to {{ formName }} and will be in touch if we need anything else.\\n\\n` +\n `Here is a copy of what you sent:\\n\\n${ANSWERS_TEXT}`,\n variables: VARIABLES,\n engine: \"liquid\",\n },\n ];\n}\n\n/**\n * The starter message for a notification's kind: the confirmation when it goes\n * to the visitor (the address in one of the form's fields), the team's email\n * otherwise.\n */\nexport function starterTemplateFor(\n recipientType: FormNotification[\"recipientType\"]\n): string {\n return recipientType === \"field\"\n ? FORM_CONFIRMATION_TEMPLATE\n : FORM_SUBMISSION_TEMPLATE;\n}\n","/**\n * Options the plugin has removed, and the one place that says so when a form\n * config still sets one.\n *\n * Both public ways into a code-defined form, `applyFormDefaults` (through\n * `createFormConfig`) and `validateFormConfig`, call this, so a config is\n * told whichever it goes through. A JavaScript config, or one written before\n * the removal, gets no type error, and without this would lose what it set\n * without a word.\n *\n * @module config/removed-options\n */\n\nimport type { FormConfig } from \"../types\";\n\n/** Forms already told, so a config passing through both entry points hears it once. */\nconst warned = new Set<string>();\n\n/**\n * Say so when a form config still sets `notifications.webhooks`.\n *\n * The plugin sends no webhooks of its own: a submission reaches other systems\n * through Nextly's `form.submission.created` webhook, so the option sends\n * nothing and the config is told what to use instead.\n */\nexport function warnIfWebhooksConfigured(config: FormConfig): void {\n const notifications: unknown = config.notifications;\n if (typeof notifications !== \"object\" || notifications === null) return;\n if (!(\"webhooks\" in notifications) || warned.has(config.slug)) return;\n warned.add(config.slug);\n console.warn(\n `[form-builder] Form \"${config.slug}\" sets notifications.webhooks, which the Form Builder no longer reads, so those webhooks are not sent. Subscribe an endpoint to Nextly's form.submission.created webhook instead: https://nextlyhq.com/docs/plugins/form-builder#webhooks`\n );\n}\n","/**\n * Form Configuration Defaults\n *\n * Applies default values to form configurations, ensuring all required\n * properties have sensible defaults while preserving user-specified values.\n *\n * @module config/defaults\n * @since 1.0.0\n *\n * @example\n * ```typescript\n * import { applyFormDefaults } from '@nextly/plugin-form-builder';\n *\n * const config = {\n * slug: 'contact-form',\n * fields: [\n * { type: 'text', name: 'name', label: 'Name' },\n * { type: 'email', name: 'email', label: 'Email' },\n * ],\n * };\n *\n * const configWithDefaults = applyFormDefaults(config);\n * // configWithDefaults now has labels, settings, etc. with defaults applied\n * ```\n */\n\nimport type { FormConfig, FormSettings, FormNotifications } from \"../types\";\n\nimport { warnIfWebhooksConfigured } from \"./removed-options\";\n\n// ============================================================\n// Default Values\n// ============================================================\n\n/**\n * Default form settings.\n */\nexport const DEFAULT_FORM_SETTINGS: Required<\n Pick<\n FormSettings,\n | \"submitButtonText\"\n | \"confirmationType\"\n | \"successMessage\"\n | \"allowMultipleSubmissions\"\n | \"oneResponse\"\n | \"oneResponseVersion\"\n | \"botCheck\"\n | \"storeIpAndBrowser\"\n >\n> = {\n submitButtonText: \"Submit\",\n confirmationType: \"message\",\n successMessage: \"Thank you for your submission!\",\n // Kept for readers of this constant; forms read `oneResponse`.\n allowMultipleSubmissions: true,\n // Today's behaviour for a form that has never set them: several responses\n // allowed, the plugin's bot check, and the visitor's IP and browser kept.\n oneResponse: \"off\",\n oneResponseVersion: 0,\n botCheck: \"default\",\n storeIpAndBrowser: true,\n};\n\n/**\n * Default notification settings.\n */\nexport const DEFAULT_NOTIFICATION_SETTINGS: Pick<\n FormNotifications,\n \"enabled\" | \"replyTo\"\n> = {\n enabled: true,\n replyTo: true,\n};\n\n// ============================================================\n// Helper Functions\n// ============================================================\n\n/**\n * Converts a slug to title case.\n *\n * @param str - The slug string to convert\n * @returns Title case string\n *\n * @example\n * ```typescript\n * toTitleCase('contact-form'); // 'Contact Form'\n * toTitleCase('newsletter_signup'); // 'Newsletter Signup'\n * ```\n */\nexport function toTitleCase(str: string): string {\n return str.replace(/[-_]/g, \" \").replace(/\\b\\w/g, char => char.toUpperCase());\n}\n\n/**\n * Generates a plural form from a singular label.\n * Handles common English pluralization rules.\n *\n * @param singular - The singular form\n * @returns The plural form\n *\n * @example\n * ```typescript\n * pluralize('Form'); // 'Forms'\n * pluralize('Category'); // 'Categories'\n * pluralize('Quiz'); // 'Quizzes'\n * ```\n */\nexport function pluralize(singular: string): string {\n if (singular.endsWith(\"y\") && !/[aeiou]y$/i.test(singular)) {\n return singular.slice(0, -1) + \"ies\";\n }\n if (\n singular.endsWith(\"s\") ||\n singular.endsWith(\"x\") ||\n singular.endsWith(\"z\") ||\n singular.endsWith(\"ch\") ||\n singular.endsWith(\"sh\")\n ) {\n return singular + \"es\";\n }\n return singular + \"s\";\n}\n\n// ============================================================\n// Main Default Application Function\n// ============================================================\n\n/**\n * Apply default values to form configuration.\n *\n * This function takes a partial form configuration and returns a complete\n * configuration with all default values applied. User-specified values\n * are preserved and take precedence over defaults.\n *\n * @param config - Form configuration (may be partial)\n * @returns Configuration with defaults applied\n *\n * @example\n * ```typescript\n * import { applyFormDefaults } from '@nextly/plugin-form-builder';\n *\n * const config = {\n * slug: 'contact-form',\n * fields: [\n * { type: 'text', name: 'name', label: 'Name' },\n * { type: 'email', name: 'email', label: 'Email' },\n * ],\n * };\n *\n * const result = applyFormDefaults(config);\n *\n * // Result:\n * // {\n * // slug: 'contact-form',\n * // labels: { singular: 'Contact Form', plural: 'Contact Forms' },\n * // fields: [...],\n * // settings: {\n * // submitButtonText: 'Submit',\n * // confirmationType: 'message',\n * // successMessage: 'Thank you for your submission!',\n * // allowMultipleSubmissions: true,\n * // },\n * // access: {\n * // submit: () => true,\n * // read: ({ req }) => !!req.user,\n * // },\n * // }\n * ```\n */\nexport function applyFormDefaults(config: FormConfig): FormConfig {\n warnIfWebhooksConfigured(config);\n\n // Generate singular label from slug if not provided\n const singularLabel = config.labels?.singular || toTitleCase(config.slug);\n\n return {\n ...config,\n\n // Generate labels from slug if not provided\n labels: {\n singular: singularLabel,\n plural: config.labels?.plural || pluralize(singularLabel),\n },\n\n // Every setting the caller gave is kept, so one this helper does not name\n // is not dropped on the way through; only these have defaults to fill.\n settings: {\n ...config.settings,\n submitButtonText:\n config.settings?.submitButtonText ||\n DEFAULT_FORM_SETTINGS.submitButtonText,\n confirmationType:\n config.settings?.confirmationType ||\n DEFAULT_FORM_SETTINGS.confirmationType,\n successMessage:\n config.settings?.successMessage || DEFAULT_FORM_SETTINGS.successMessage,\n // Still filled, as callers have always read it here. Where `oneResponse`\n // is given too, the reader takes `oneResponse`.\n allowMultipleSubmissions:\n config.settings?.allowMultipleSubmissions ??\n DEFAULT_FORM_SETTINGS.allowMultipleSubmissions,\n },\n\n // Apply notification defaults if notifications are configured\n notifications: config.notifications\n ? {\n enabled:\n config.notifications.enabled ??\n DEFAULT_NOTIFICATION_SETTINGS.enabled,\n recipients: config.notifications.recipients || [],\n subject:\n config.notifications.subject ||\n `New submission for ${singularLabel}`,\n replyTo:\n config.notifications.replyTo ??\n DEFAULT_NOTIFICATION_SETTINGS.replyTo,\n template: config.notifications.template,\n }\n : undefined,\n\n // Apply access defaults\n access: {\n // Default: anyone can submit the form\n submit: config.access?.submit || (() => true),\n // Default: only authenticated users can view submissions\n read: config.access?.read || (({ req }) => !!req.user),\n },\n };\n}\n\n/**\n * Apply default values to individual form field.\n *\n * This is a utility function for applying defaults to a single field,\n * useful when processing fields individually (e.g., in the Schema Builder).\n *\n * @param field - The field to apply defaults to\n * @returns Field with defaults applied\n *\n * @example\n * ```typescript\n * import { applyFieldDefaults } from '@nextly/plugin-form-builder';\n *\n * const field = { type: 'text', name: 'firstName' };\n * const fieldWithDefaults = applyFieldDefaults(field);\n * // { type: 'text', name: 'firstName', label: 'First Name', required: false }\n * ```\n */\nexport function applyFieldDefaults<\n T extends { type: string; name?: string; label?: string; required?: boolean },\n>(field: T): T {\n // Generate label from name if not provided\n const label =\n field.label ||\n (field.name\n ? toTitleCase(field.name.replace(/([A-Z])/g, \" $1\").trim())\n : \"\");\n\n return {\n ...field,\n label,\n required: field.required ?? false,\n };\n}\n\n/**\n * Creates a complete form config with minimal input.\n *\n * This is a convenience function for quickly creating form configs\n * with sensible defaults. Only the slug and fields are required.\n *\n * @param slug - The form slug\n * @param fields - Array of form fields\n * @param options - Optional additional configuration\n * @returns Complete form configuration\n *\n * @example\n * ```typescript\n * import { createFormConfig } from '@nextly/plugin-form-builder';\n *\n * const config = createFormConfig('contact', [\n * { type: 'text', name: 'name', label: 'Name', required: true },\n * { type: 'email', name: 'email', label: 'Email', required: true },\n * { type: 'textarea', name: 'message', label: 'Message' },\n * ]);\n * ```\n */\nexport function createFormConfig(\n slug: string,\n fields: FormConfig[\"fields\"],\n options?: Partial<Omit<FormConfig, \"slug\" | \"fields\">>\n): FormConfig {\n return applyFormDefaults({\n slug,\n fields,\n ...options,\n });\n}\n","/**\n * The ONE reader for stored form settings. Settings were historically\n * written in two shapes (the builder's old context shape and the collection\n * group's declared shape), so every consumer — the builder UI and the\n * submit handler alike — normalizes through here instead of trusting the\n * stored keys.\n */\n\nimport { DEFAULT_FORM_SETTINGS } from \"../config/defaults\";\nimport type { BotCheckMode, FormSettings, OneResponseMode } from \"../types\";\n\nexport { DEFAULT_FORM_SETTINGS };\n\nfunction asOptionalBoolean(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined;\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\n/** A whole number of zero or more, or `undefined`. */\nfunction asWholeNumber(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0\n ? value\n : undefined;\n}\n\n/** A whole number above zero, or `undefined`. */\nfunction asPositiveWholeNumber(value: unknown): number | undefined {\n const whole = asWholeNumber(value);\n return whole === undefined || whole === 0 ? undefined : whole;\n}\n\nconst ONE_RESPONSE_MODES: readonly OneResponseMode[] = [\n \"off\",\n \"email\",\n \"browser\",\n \"ip\",\n];\n\nconst BOT_CHECK_MODES: readonly BotCheckMode[] = [\n \"default\",\n \"off\",\n \"recaptcha\",\n \"turnstile\",\n];\n\n/**\n * The form's one-response mode: as stored, or read from the setting it\n * replaced, so no form's behaviour changes. Several responses allowed reads\n * as `\"off\"`; one per network address as `\"ip\"`, the only check it had.\n */\nfunction oneResponseOf(source: Record<string, unknown>): OneResponseMode {\n const stored = ONE_RESPONSE_MODES.find(mode => mode === source.oneResponse);\n if (stored !== undefined) return stored;\n const allowMultiple = asOptionalBoolean(source.allowMultipleSubmissions);\n if (allowMultiple === false) return \"ip\";\n return DEFAULT_FORM_SETTINGS.oneResponse;\n}\n\n/**\n * The form's bot check: as stored, or read from the per-form reCAPTCHA\n * switch it replaced: unset follows the plugin, on is reCAPTCHA, off is off,\n * so no form's protection changes.\n */\nfunction botCheckOf(\n source: Record<string, unknown>,\n captchaEnabled: boolean | undefined\n): BotCheckMode {\n const stored = BOT_CHECK_MODES.find(mode => mode === source.botCheck);\n if (stored !== undefined) return stored;\n if (captchaEnabled === true) return \"recaptcha\";\n if (captchaEnabled === false) return \"off\";\n return DEFAULT_FORM_SETTINGS.botCheck;\n}\n\n/**\n * Normalize a stored settings value into the canonical {@link FormSettings}.\n *\n * Legacy keys are migrated on read (no data is lost, nothing is rewritten\n * until the form is next saved):\n * - `confirmationMessage` → `successMessage` + `confirmationType: \"message\"`\n * - nested `captcha: { enabled, siteKey }` → flat `captchaSiteKey`, and its\n * `enabled` read as `botCheck`\n * - `allowMultipleSubmissions` → `oneResponse`; `captchaEnabled` → `botCheck`\n *\n * Keys with no consumer (`showResetButton`, `resetButtonText`,\n * `storeSubmissions`) are dropped: a setting that does nothing must not\n * survive into the UI.\n */\nexport function normalizeFormSettings(raw: unknown): FormSettings {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n return { ...DEFAULT_FORM_SETTINGS };\n }\n const source = raw as Record<string, unknown>;\n\n const legacyMessage = asOptionalString(source.confirmationMessage);\n const legacyCaptcha =\n source.captcha && typeof source.captcha === \"object\"\n ? (source.captcha as { enabled?: unknown; siteKey?: unknown })\n : undefined;\n\n // \"relationship\" is a real stored value (redirect to a linked document) —\n // coercing it to \"message\" would disable the form's redirect on the next\n // save. Only genuinely unknown values fall back to \"message\".\n const confirmationType =\n source.confirmationType === \"redirect\" ||\n source.confirmationType === \"relationship\"\n ? source.confirmationType\n : \"message\";\n\n return {\n submitButtonText:\n asOptionalString(source.submitButtonText) ??\n DEFAULT_FORM_SETTINGS.submitButtonText,\n confirmationType,\n successMessage:\n asOptionalString(source.successMessage) ??\n legacyMessage ??\n DEFAULT_FORM_SETTINGS.successMessage,\n redirectUrl: asOptionalString(source.redirectUrl),\n redirectPage: source.redirectPage ?? undefined,\n redirectRelation:\n source.redirectRelation && typeof source.redirectRelation === \"object\"\n ? (source.redirectRelation as FormSettings[\"redirectRelation\"])\n : undefined,\n oneResponse: oneResponseOf(source),\n oneResponseField: asOptionalString(source.oneResponseField),\n oneResponseVersion:\n asWholeNumber(source.oneResponseVersion) ??\n DEFAULT_FORM_SETTINGS.oneResponseVersion,\n oneResponseKeyedVersion: asWholeNumber(source.oneResponseKeyedVersion),\n honeypotEnabled: asOptionalBoolean(source.honeypotEnabled),\n botCheck: botCheckOf(\n source,\n asOptionalBoolean(source.captchaEnabled) ??\n asOptionalBoolean(legacyCaptcha?.enabled)\n ),\n closesAt: asOptionalString(source.closesAt),\n submissionLimit: asPositiveWholeNumber(source.submissionLimit),\n storeIpAndBrowser:\n asOptionalBoolean(source.storeIpAndBrowser) ??\n DEFAULT_FORM_SETTINGS.storeIpAndBrowser,\n keepDays: asPositiveWholeNumber(source.keepDays),\n internalNote: asOptionalString(source.internalNote),\n captchaSiteKey:\n asOptionalString(source.captchaSiteKey) ??\n asOptionalString(legacyCaptcha?.siteKey),\n };\n}\n","/**\n * What became of a submission's notifications, as its page and a\n * notification's recent sends say it: one reading of a send's stored\n * outcome, shared by the server that answers the routes and the screens that\n * draw them, so the two cannot come to disagree about what \"Partly sent\" or\n * \"Sent after 2 retries\" means.\n *\n * A send is stored as `form-submission-sends` keeps it (`StoredSend`): where\n * it stands, its attempts, and each recipient with what became of it. Each\n * recipient reads as one of Waiting, Retrying, Sending, Sent (after N\n * retries), Failed or Cancelled, and a notification as the one its\n * recipients share, or Partly sent when they differ.\n *\n * @module utils/send-status\n */\n\n/** Where a send stands, as stored. */\nexport type SendState =\n | \"waiting\"\n | \"retrying\"\n | \"sending\"\n | \"sent\"\n | \"failed\"\n | \"cancelled\";\n\n/** The states in which another attempt may still come. */\nconst IN_PROGRESS: ReadonlySet<SendState> = new Set([\n \"waiting\",\n \"retrying\",\n \"sending\",\n]);\n\n/**\n * Why a send was cancelled: a person (in Settings › Email), the submission\n * marked Spam, an erasure, or a cancel the plugin made for another reason.\n */\nexport type CancelCause =\n | { by: \"person\"; userId: string | null }\n | { by: \"spam\" }\n | { by: \"erasure\" }\n | { by: \"sender\" }\n | { by: \"request\" };\n\n/** Where a recipient came from: the notification's own lines, or a filter. */\nexport type RecipientSource = \"team\" | \"visitor\" | \"cc\" | \"bcc\" | \"added\";\n\n/** One recipient of a send, as stored: a hint and a digest, never an address. */\nexport interface StoredRecipient {\n digest: string;\n hint: string;\n kind: \"to\" | \"cc\" | \"bcc\";\n source: RecipientSource;\n /** What became of it, once a provider was asked. */\n outcome?: \"accepted\" | \"refused\" | \"unknown\" | \"skipped\";\n /** The retries recorded before it was accepted. */\n retries?: number;\n}\n\n/** A send as `form-submission-sends` keeps it. */\nexport interface StoredSend {\n queuedId: string;\n notification?: string | null;\n notificationName?: string | null;\n state?: SendState | null;\n attempts?: number | null;\n /** The attempts recorded as failing in a way a retry could help. */\n retries?: number | null;\n changedAt?: string | null;\n createdAt?: string | null;\n cancel?: CancelCause | null;\n /** A cancel asked while an attempt was with the provider. */\n cancelRequested?: CancelCause | null;\n retiredBy?: string | null;\n configured?: StoredRecipient[] | null;\n recipients?: StoredRecipient[] | null;\n requestedBy?: string | null;\n}\n\n/** One recipient's latest result, as a page shows it. */\nexport interface RecipientStatus {\n digest: string;\n hint: string;\n kind: StoredRecipient[\"kind\"];\n source: RecipientSource;\n state: SendState;\n /** Attempts before the one that sent it; 0 for the first. */\n retries: number;\n at: string | null;\n cancel?: CancelCause;\n}\n\n/** Where a send stands, an unrecorded one read as waiting. */\nfunction stateOf(send: StoredSend): SendState {\n return send.state ?? \"waiting\";\n}\n\n/** Whether another attempt at `send` may still come. */\nexport function sendInProgress(send: StoredSend): boolean {\n return IN_PROGRESS.has(stateOf(send));\n}\n\n/** The recipients a send lists: those it went to, else those it was for. */\nfunction listedOf(send: StoredSend): StoredRecipient[] {\n const sent = send.recipients ?? [];\n return sent.length > 0 ? sent : (send.configured ?? []);\n}\n\n/** A recipient's state in one send, from its outcome and the send's state. */\nfunction recipientState(\n send: StoredSend,\n recipient: StoredRecipient\n): SendState {\n if (recipient.outcome === \"accepted\") return \"sent\";\n const state = stateOf(send);\n // The send went out, and this recipient was refused or not answered for.\n if (state === \"sent\") return \"failed\";\n return state;\n}\n\n/** One recipient's result in one send. */\nfunction statusIn(\n send: StoredSend,\n recipient: StoredRecipient\n): RecipientStatus {\n const state = recipientState(send, recipient);\n return {\n digest: recipient.digest,\n hint: recipient.hint,\n kind: recipient.kind,\n source: recipient.source,\n state,\n retries: state === \"sent\" ? (recipient.retries ?? 0) : 0,\n at: send.changedAt ?? send.createdAt ?? null,\n ...(state === \"cancelled\" && send.cancel ? { cancel: send.cancel } : {}),\n };\n}\n\n/**\n * Every recipient a notification's sends had for a submission, each with its\n * latest result, oldest first. A recipient a send left out (one Resend found\n * already reached) keeps the result it had.\n */\nexport function latestByRecipient(\n sends: readonly StoredSend[]\n): RecipientStatus[] {\n const latest = new Map<string, RecipientStatus>();\n for (const send of sends) {\n for (const recipient of listedOf(send)) {\n if (recipient.outcome === \"skipped\") continue;\n latest.set(recipient.digest, statusIn(send, recipient));\n }\n }\n return [...latest.values()];\n}\n\n/** A recipient Resend can send a notification to now, and whether it has it. */\nexport interface AddressableRecipient {\n digest: string;\n hint: string;\n source: RecipientSource;\n reached: boolean;\n}\n\n/**\n * Who Resend can send a notification to now: each recipient the notification\n * addresses as it is now (`configured`), then each recipient a send filter\n * added that its sends have not reached, since the filters add it again to\n * the next send. Whether each has received it is read from `history`, the\n * latest result per recipient.\n */\nexport function addressableNow(\n configured: readonly Pick<StoredRecipient, \"digest\" | \"hint\" | \"source\">[],\n history: readonly RecipientStatus[]\n): AddressableRecipient[] {\n const latest = new Map(history.map(status => [status.digest, status]));\n const now = configured.map(recipient => ({\n digest: recipient.digest,\n hint: recipient.hint,\n source: recipient.source,\n reached: latest.get(recipient.digest)?.state === \"sent\",\n }));\n const listed = new Set(now.map(recipient => recipient.digest));\n const missedAdded = history\n .filter(\n status =>\n status.source === \"added\" &&\n status.state !== \"sent\" &&\n !listed.has(status.digest)\n )\n .map(status => ({\n digest: status.digest,\n hint: status.hint,\n source: status.source,\n reached: false,\n }));\n return [...now, ...missedAdded];\n}\n\n/**\n * The recipients a resend leaves out: each one `history` shows has received\n * it, except those ticked to send it to again.\n */\nexport function reachedExcept(\n history: readonly RecipientStatus[],\n ticked: readonly string[]\n): string[] {\n const again = new Set(ticked);\n return history\n .filter(status => status.state === \"sent\" && !again.has(status.digest))\n .map(status => status.digest);\n}\n\n/** How a status is drawn: its words and the tone of its dot. */\nexport interface StatusLook {\n label: string;\n tone: \"ok\" | \"warn\" | \"bad\" | \"quiet\";\n}\n\n/** A recipient's status in words. */\nexport function recipientLook(status: RecipientStatus): StatusLook {\n switch (status.state) {\n case \"sent\":\n return status.retries === 0\n ? { label: \"Sent\", tone: \"ok\" }\n : {\n label: `Sent after ${status.retries} ${status.retries === 1 ? \"retry\" : \"retries\"}`,\n tone: \"warn\",\n };\n case \"failed\":\n return { label: \"Failed\", tone: \"bad\" };\n case \"cancelled\":\n return { label: \"Cancelled\", tone: \"quiet\" };\n case \"sending\":\n return { label: \"Sending\", tone: \"quiet\" };\n case \"retrying\":\n return { label: \"Retrying\", tone: \"warn\" };\n default:\n return { label: \"Waiting\", tone: \"quiet\" };\n }\n}\n\n/**\n * A notification's status in words: the one its recipients share, or Partly\n * sent when they differ. Recipients all sent read as the one that took the\n * most retries.\n */\nexport function notificationLook(\n statuses: readonly RecipientStatus[]\n): StatusLook {\n const [first] = statuses;\n if (!first) return { label: \"Waiting\", tone: \"quiet\" };\n if (!statuses.every(status => status.state === first.state)) {\n return { label: \"Partly sent\", tone: \"warn\" };\n }\n const most = statuses.reduce(\n (latest, status) => (status.retries > latest.retries ? status : latest),\n first\n );\n return recipientLook(most);\n}\n\n/** Why a send was cancelled, in words. */\nexport function cancelText(cancel: CancelCause | undefined): string {\n switch (cancel?.by) {\n case \"person\":\n return \"Cancelled in Settings › Email\";\n case \"spam\":\n return \"Cancelled when the submission was marked Spam\";\n case \"erasure\":\n return \"Cancelled when the address was erased\";\n default:\n return \"Cancelled\";\n }\n}\n","/**\n * Form Builder Plugin Types\n *\n * Defines configuration options and types for the form builder plugin.\n *\n * @packageDocumentation\n */\n\nimport type { CollectionConfig, FieldDefinition, RequestContext } from \"nextly\";\nimport type { ComponentType } from \"react\";\n\nimport type { RedirectRelationships } from \"./utils/redirect-target\";\n\n// ============================================================\n// Supporting Types\n// ============================================================\n\n/**\n * Configuration for customizing field block behavior.\n */\nexport interface FieldBlockConfig {\n /** Default label for this field type */\n label?: string;\n /** Default validation settings */\n validation?: Record<string, unknown>;\n /** Custom component to use for this field type */\n component?: ComponentType<unknown>;\n}\n\n/**\n * Email configuration for notifications.\n */\nexport interface EmailConfig {\n to: string | string[];\n from?: string;\n subject: string;\n html?: string;\n text?: string;\n replyTo?: string;\n cc?: string | string[];\n bcc?: string | string[];\n}\n\n/**\n * @experimental A resolved outgoing form notification (template-based) as built\n * by the submission handler, before it is sent. This is the value transformed by\n * the `form-builder.beforeEmail` D63 filter seam.\n */\nexport interface FormEmailNotification {\n /**\n * Resolved recipient address, or the addresses of a team notification\n * that has several. A notification with one recipient keeps a string.\n */\n to: string | string[];\n /** Email template slug to render. */\n templateSlug: string;\n /** Template variables (submitted data + form metadata). */\n variables: Record<string, unknown>;\n /** Email provider id; undefined = system default. */\n providerId?: string;\n /**\n * Resolved sender address (rule's senderEmail, else the plugin's\n * defaultFrom); undefined = template/provider default.\n */\n from?: string;\n /** Resolved Reply-To address, if the rule configured one. */\n replyTo?: string;\n /** CC addresses, if any. */\n cc?: string[];\n /** BCC addresses, if any. */\n bcc?: string[];\n /** Source notification id; undefined for filter-injected emails. */\n notificationId?: string;\n /**\n * The files that go with this email, by media id: the notification's own\n * choice of file fields. Absent on a filter-injected email, which takes the\n * files of the fields marked to attach to notification emails.\n */\n attachments?: Array<{ mediaId: string }>;\n}\n\n/** @experimental Context passed to the `form-builder.beforeEmail` filter seam. */\nexport interface BeforeEmailFilterContext {\n form: Record<string, unknown>;\n submission: Record<string, unknown>;\n}\n\n/**\n * A single email notification rule stored on a form.\n * Matches the shape saved by the form builder admin UI. This is the ONE\n * notification rule type — the admin context re-exports it rather than\n * keeping a parallel copy.\n */\nexport interface FormNotification {\n /** Unique ID for this notification */\n id: string;\n /** Display name for this notification rule */\n name: string;\n /** Whether this notification is active */\n enabled: boolean;\n /** ID of the email provider to use; undefined = system default */\n providerId?: string;\n /**\n * Sender email override. Resolution order at send time:\n * this value, else the plugin's `notifications.defaultFrom` option, else\n * the template/provider default.\n */\n senderEmail?: string;\n /**\n * Which preset the notification started from: one to the team, or a\n * confirmation to the person who submitted. Absent on rules saved before\n * presets, which read by their recipient.\n */\n kind?: \"team\" | \"visitor\";\n /** How the recipient address is determined */\n recipientType: \"static\" | \"field\";\n /**\n * Recipient: static email address or a {{fieldName}} reference. A team\n * notification with several addresses keeps them in `recipients`; this\n * then holds the first, for readers that know only one.\n */\n to: string;\n /**\n * A team notification's addresses. Absent on rules saved before several\n * were allowed, whose single `to` reads as a list of one.\n */\n recipients?: string[];\n /** CC email addresses */\n cc: string[];\n /** BCC email addresses */\n bcc: string[];\n /**\n * Reply-To address: a `{{fieldName}}` reference to one of the form's\n * email fields (resolved against the submission) or a literal address.\n */\n replyTo?: string;\n /**\n * Optional send condition evaluated against the submitted data. When set\n * and unmet, the rule is skipped for that submission. A single condition\n * covers the common case; the JSON storage leaves room to grow into a\n * multi-condition tree without a migration.\n */\n condition?: ConditionalLogicCondition;\n /**\n * Several send conditions: all must hold, or any one. Takes the place of\n * `condition`, which reads as a list of one under `all` when this is absent.\n */\n conditions?: {\n match: \"all\" | \"any\";\n list: ConditionalLogicCondition[];\n };\n /**\n * The names of the form's file fields whose files go with this email.\n * Absent on rules saved before it, which attach exactly the file fields\n * marked to attach to notification emails.\n */\n attachFields?: string[];\n /** Slug of the email template to use */\n templateSlug?: string;\n}\n\n/**\n * @deprecated Use {@link FormNotification}. Alias kept so existing imports\n * keep compiling during the alpha; will be removed before a stable release.\n */\nexport type FormNotificationItem = FormNotification;\n\n/**\n * Form document structure (stored in database).\n */\nexport interface FormDocument {\n id: string;\n slug: string;\n name: string;\n fields: AnyFormField[];\n settings: FormSettings;\n notifications: FormNotification[];\n status: \"draft\" | \"published\" | \"closed\";\n /**\n * What a closed form tells a visitor who reaches it.\n *\n * Optional because the field is only written once an author has a reason to\n * give, and a row created before it existed has none. A closed form with\n * nothing here falls back to the generic refusal, which is also what a draft\n * gets — a draft has never been public, so there is no author intent to\n * relay.\n */\n closedMessage?: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\n/**\n * Submission document structure (stored in database).\n */\nexport interface SubmissionDocument {\n id: string;\n form: string | FormDocument;\n data: Record<string, unknown>;\n /**\n * Where the submission sits in review.\n *\n * `spam` is one of them, not a separate flag: the stored field offers it, the\n * admin has a Spam tab and filters the other views with `not_equals: \"spam\"`,\n * the notification hook skips it, and marking something \"Not spam\" moves it\n * back to `new`. `spamReason` records WHICH check flagged it.\n */\n status: \"new\" | \"read\" | \"archived\" | \"spam\";\n ipAddress?: string;\n userAgent?: string;\n /** Which spam check flagged this, when one did. */\n spamReason?: string | null;\n /**\n * What it was asked, as the write seam stored it (`utils/asked-fields`):\n * its fields' labels and option labels at the time. Absent on a\n * submission stored before submissions kept it.\n */\n asked?: unknown;\n submittedAt: Date;\n createdAt: Date;\n updatedAt: Date;\n}\n\n// ============================================================\n// Plugin Configuration\n// ============================================================\n\n/**\n * Configuration options for the form builder plugin.\n */\n/**\n * Which field types the builder offers, and how each one is customised.\n *\n * Declared ONCE and viewed twice: the options surface takes\n * `Partial<FormFieldToggles>` and the resolved config takes it whole, because\n * \"resolved\" means exactly \"every toggle now has a value\". Written out twice\n * the two lists agree until someone adds a field type to one of them.\n */\nexport interface FormFieldToggles {\n text: boolean | Partial<FieldBlockConfig>;\n email: boolean | Partial<FieldBlockConfig>;\n number: boolean | Partial<FieldBlockConfig>;\n phone: boolean | Partial<FieldBlockConfig>;\n url: boolean | Partial<FieldBlockConfig>;\n textarea: boolean | Partial<FieldBlockConfig>;\n select: boolean | Partial<FieldBlockConfig>;\n checkbox: boolean | Partial<FieldBlockConfig>;\n radio: boolean | Partial<FieldBlockConfig>;\n file: boolean | Partial<FieldBlockConfig>;\n date: boolean | Partial<FieldBlockConfig>;\n time: boolean | Partial<FieldBlockConfig>;\n hidden: boolean | Partial<FieldBlockConfig>;\n consent: boolean | Partial<FieldBlockConfig>;\n}\n\n/** What a host may override on a collection this plugin contributes. */\nexport type CollectionOverrides = Partial<CollectionConfig> & {\n slug?: string;\n labels?: { singular?: string; plural?: string };\n /** Fields can be an array or a function receiving defaultFields */\n fields?:\n | FieldDefinition[]\n | ((args: { defaultFields: FieldDefinition[] }) => FieldDefinition[]);\n};\n\n/** The same, once the plugin's defaults have filled in what a host omitted. */\nexport type ResolvedCollectionOverrides = {\n slug: string;\n labels: { singular: string; plural: string };\n} & Partial<CollectionConfig>;\n\nexport interface FormBuilderPluginOptions {\n /**\n * Override the default Forms collection configuration.\n *\n * Allows customizing the slug, labels, fields, access control, and other\n * collection options.\n *\n * The `fields` option can be:\n * - An array of fields (merged with defaults)\n * - A function receiving `{ defaultFields }` for full control\n *\n * @example\n * ```typescript\n * // Option 1: Array (fields merged with defaults)\n * formOverrides: {\n * slug: 'my-forms',\n * labels: { singular: 'My Form', plural: 'My Forms' },\n * access: {\n * read: () => true,\n * // Narrow the permission, never replace it: the role screen still\n * // decides who holds `create-my-forms`.\n * create: ({ permissions }) =>\n * permissions.includes('create-my-forms') && process.env.CONTENT_FREEZE !== 'on',\n * },\n * fields: [\n * text({ name: 'internalNotes', label: 'Internal Notes' }),\n * ],\n * }\n *\n * // Option 2: Function (full control over fields)\n * formOverrides: {\n * slug: 'contact-forms',\n * access: {\n * read: ({ permissions }) => permissions.includes('read-contact-forms'),\n * update: () => false,\n * },\n * fields: ({ defaultFields }) => {\n * return [\n * ...defaultFields,\n * { name: 'custom', type: 'text' },\n * ];\n * },\n * }\n * ```\n */\n formOverrides?: CollectionOverrides;\n\n /**\n * Override the default Form Submissions collection configuration.\n *\n * Allows customizing the slug, labels, fields, access control, and other\n * collection options.\n *\n * The `fields` option can be:\n * - An array of fields (merged with defaults)\n * - A function receiving `{ defaultFields }` for full control\n *\n * @example\n * ```typescript\n * // Option 1: Array (fields merged with defaults)\n * formSubmissionOverrides: {\n * slug: 'my-submissions',\n * labels: { singular: 'Response', plural: 'Responses' },\n * access: {\n * read: ({ permissions }) => permissions.includes('read-my-submissions'),\n * },\n * fields: [\n * select({\n * name: 'priority',\n * label: 'Priority',\n * options: [\n * { label: 'Low', value: 'low' },\n * { label: 'High', value: 'high' },\n * ],\n * }),\n * ],\n * }\n *\n * // Option 2: Function (full control over fields)\n * formSubmissionOverrides: {\n * fields: ({ defaultFields }) => {\n * // Add field after 'form' field\n * const formIndex = defaultFields.findIndex(f => f.name === 'form');\n * return [\n * ...defaultFields.slice(0, formIndex + 1),\n * { name: 'source', type: 'text' },\n * ...defaultFields.slice(formIndex + 1),\n * ];\n * },\n * }\n * ```\n */\n formSubmissionOverrides?: CollectionOverrides;\n\n /**\n * Configure which field types are available in the form builder.\n *\n * Set to `false` to disable a field type, or provide partial configuration\n * to customize default behavior.\n *\n * @example\n * ```typescript\n * fields: {\n * // Disable payment field\n * payment: false,\n *\n * // Customize text field defaults\n * text: {\n * maxLength: 500,\n * },\n *\n * // Disable file uploads\n * file: false,\n * }\n * ```\n */\n fields?: Partial<FormFieldToggles>;\n\n /**\n * Collections whose documents a form may redirect to after submission, and\n * how each one's documents become URLs.\n *\n * The mapping is configuration because it cannot be derived: a site's URL\n * structure belongs to the site. `{field}` placeholders are filled from the\n * document; a function covers a path this package cannot express.\n *\n * The array shorthand names the collections and takes `/{slug}` for all of\n * them, which is the routing the page builder ships.\n *\n * @example\n * ```typescript\n * redirectRelationships: ['pages']\n *\n * redirectRelationships: {\n * pages: '/{slug}',\n * posts: '/blog/{slug}',\n * docs: page => `/docs/${String(page.section)}/${String(page.slug)}`,\n * }\n * ```\n */\n redirectRelationships?: string[] | RedirectRelationships;\n\n /**\n * Hook called before sending email notifications.\n *\n * Runs as the `form-builder.beforeEmail` D63 filter: the resolved outgoing\n * notifications (template + recipient + variables) are threaded through this\n * callback before they are sent. Allows modifying, adding, or filtering the\n * outgoing notifications. Return the modified array.\n *\n * @example\n * ```typescript\n * beforeEmail: async ({ emails, form, submission }) => {\n * // Add BCC to all outgoing notifications\n * return emails.map((e) => ({ ...e, bcc: ['archive@example.com'] }));\n * }\n * ```\n */\n beforeEmail?: (args: {\n emails: FormEmailNotification[];\n form: FormDocument;\n submission: SubmissionDocument;\n }) => Promise<FormEmailNotification[]> | FormEmailNotification[];\n\n /**\n * Email notification settings.\n */\n notifications?: {\n /**\n * Default \"from\" email address.\n */\n defaultFrom?: string;\n\n /**\n * Default \"to\" email address for all form submissions.\n *\n * If specified, this email will receive all form submissions\n * regardless of form-specific notification settings.\n * @example\n * ```typescript\n * notifications: {\n * defaultToEmail: 'submissions@example.com',\n * }\n * ```\n */\n defaultToEmail?: string;\n\n /**\n * Enable/disable email notifications globally.\n * @default true\n */\n enabled?: boolean;\n };\n\n /**\n * Spam protection configuration.\n */\n /**\n * One response per person.\n */\n oneResponse?: {\n /**\n * How many earlier submissions a form may have for a change of its\n * one-response setting to give them their keys inside the save itself,\n * so the save answers with the form as it then stands. A form with more\n * is brought up to date by the plugin's background sweep instead, and\n * until it is, each new submission is compared with the earlier answers.\n * Lower it to keep saves short on a slow database.\n * @default 200\n */\n keysInSave?: number;\n };\n\n spamProtection?: {\n /**\n * Enable honeypot field for spam detection.\n * @default true\n */\n honeypot?: boolean;\n\n /**\n * Google reCAPTCHA v3: a score for each visitor, in the background.\n * `enabled` makes it the site default for forms whose bot check is \"Site\n * default\". The token is verified on the server with `secretKey`; the\n * visitor's form renders the widget with `siteKey`.\n * @default { enabled: false }\n */\n recaptcha?: {\n enabled: boolean;\n siteKey?: string;\n secretKey?: string;\n /** Below this score (0.0 to 1.0) a visitor is a bot. @default 0.5 */\n scoreThreshold?: number;\n };\n\n /**\n * Cloudflare Turnstile: a check in the background that rarely shows a\n * puzzle. `enabled` makes it the site default when reCAPTCHA's is not.\n * @default { enabled: false }\n */\n turnstile?: {\n enabled: boolean;\n siteKey?: string;\n secretKey?: string;\n };\n\n /**\n * Rate limiting configuration.\n */\n rateLimit?: {\n /**\n * Maximum submissions per window.\n * @default 10\n */\n maxSubmissions?: number;\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n };\n };\n\n /**\n * File upload settings.\n */\n uploads?: {\n /**\n * Maximum file size in bytes.\n * @default 10485760 (10MB)\n */\n maxFileSize?: number;\n\n /**\n * Allowed file types (MIME types).\n * @default ['image/*', 'application/pdf', 'text/*']\n */\n allowedMimeTypes?: string[];\n\n /**\n * Collection to store uploaded files.\n * @default 'media'\n */\n uploadCollection?: string;\n };\n\n /**\n * Enable/disable features.\n */\n features?: {\n /**\n * Enable visual form builder UI.\n * @default true\n */\n builder?: boolean;\n\n /**\n * Enable conditional logic.\n * @default true\n */\n conditionalLogic?: boolean;\n\n /**\n * Enable file uploads.\n * @default true\n */\n fileUploads?: boolean;\n };\n}\n\n// ============================================================\n// Form Configuration Types\n// ============================================================\n\n/**\n * Form configuration interface.\n * Similar to CollectionConfig but tailored for forms.\n */\nexport interface FormConfig {\n /**\n * Unique identifier for the form.\n */\n slug: string;\n\n /**\n * Display labels for the form.\n */\n labels?: {\n singular?: string;\n plural?: string;\n };\n\n /**\n * Form fields definition.\n */\n fields: AnyFormField[];\n\n /**\n * Form settings and behavior.\n */\n settings?: FormSettings;\n\n /**\n * Email notification configuration.\n */\n notifications?: FormNotifications;\n\n /**\n * Access control for form submission and viewing.\n */\n access?: FormAccess;\n\n /**\n * Lifecycle hooks for form submissions.\n */\n hooks?: FormHooks;\n\n /**\n * Custom metadata.\n */\n custom?: Record<string, unknown>;\n}\n\n// ============================================================\n// Form Field Types (Discriminated Union)\n// ============================================================\n\n/**\n * Form field type (discriminated union).\n * Uses the `type` property as the discriminant.\n */\nexport type FormField =\n | TextFormField\n | EmailFormField\n | NumberFormField\n | PhoneFormField\n | UrlFormField\n | TextareaFormField\n | SelectFormField\n | CheckboxFormField\n | RadioFormField\n | FileFormField\n | DateFormField\n | TimeFormField\n | HiddenFormField\n | ConsentFormField;\n\n/**\n * A form field of a built-in type OR a plugin-contributed type. Stored form\n * data and the builder work with this; `isKnownFormField` narrows it back to\n * the closed `FormField` union so `switch (field.type)` keeps discriminating\n * the built-ins (a `CustomFormField` in the union itself would collapse that).\n */\nexport type AnyFormField = FormField | CustomFormField;\n\n/**\n * All available built-in form field type identifiers.\n */\nexport type FormFieldType =\n | \"text\"\n | \"email\"\n | \"number\"\n | \"phone\"\n | \"url\"\n | \"textarea\"\n | \"select\"\n | \"checkbox\"\n | \"radio\"\n | \"file\"\n | \"date\"\n | \"time\"\n | \"hidden\"\n | \"consent\";\n\n/**\n * A form field's type on the wire: a built-in or a plugin-contributed type id\n * that opted into the forms surface. The `(string & {})` arm admits the plugin\n * id while keeping built-in autocomplete. Built-in `case` narrowing still works\n * because every field-type switch keeps a `default` for the custom arm.\n */\nexport type FormFieldTypeId = FormFieldType | (string & {});\n\n/** Every built-in form field type, as a value list (the single source shared\n * by the field-type guard and the config validator). */\nexport const BUILT_IN_FORM_FIELD_TYPES: readonly FormFieldType[] = [\n \"text\",\n \"email\",\n \"number\",\n \"phone\",\n \"url\",\n \"textarea\",\n \"select\",\n \"checkbox\",\n \"radio\",\n \"file\",\n \"date\",\n \"time\",\n \"hidden\",\n \"consent\",\n];\n\nconst BUILT_IN_FORM_FIELD_TYPES_SET = new Set<string>(\n BUILT_IN_FORM_FIELD_TYPES\n);\n\n/**\n * Narrow an `AnyFormField` to the closed built-in `FormField` union. Returns\n * `false` for a plugin-contributed field, so callers handle it generically\n * before switching on the built-in discriminant.\n */\nexport function isKnownFormField(field: AnyFormField): field is FormField {\n return BUILT_IN_FORM_FIELD_TYPES_SET.has(field.type);\n}\n\n/**\n * Base form field properties.\n * All form field types extend this interface.\n */\nexport interface BaseFormField {\n /** Field type identifier (discriminant) */\n type: FormFieldType;\n\n /** Field name (used as key in submission data) */\n name: string;\n\n /** Display label */\n label: string;\n\n /** Placeholder text */\n placeholder?: string;\n\n /** Help text shown below field */\n helpText?: string;\n\n /** Default value */\n defaultValue?: unknown;\n\n /** Whether field is required */\n required?: boolean;\n\n /** Validation rules */\n validation?: ValidationRules;\n\n /** Conditional logic configuration */\n conditionalLogic?: ConditionalLogic;\n\n /** Admin UI configuration */\n admin?: {\n /** CSS classes */\n className?: string;\n\n /** Field width */\n width?: \"25%\" | \"33%\" | \"50%\" | \"66%\" | \"75%\" | \"100%\";\n };\n}\n\n/**\n * A plugin-contributed form field type (outside the built-in union). Carries\n * only the base properties — the plugin owns its editor component and value\n * semantics — and its `type` is the plugin's registered field-type id. Kept a\n * distinct union member so built-in `case` narrowing is unaffected while a\n * plugin field still type-checks as a `FormField`.\n */\nexport interface CustomFormField extends Omit<BaseFormField, \"type\"> {\n type: string & {};\n}\n\n/**\n * Text input field.\n */\nexport interface TextFormField extends Omit<BaseFormField, \"type\"> {\n type: \"text\";\n defaultValue?: string;\n validation?: ValidationRules & {\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n };\n}\n\n/**\n * Email input field.\n */\nexport interface EmailFormField extends Omit<BaseFormField, \"type\"> {\n type: \"email\";\n defaultValue?: string;\n validation?: ValidationRules & {\n pattern?: string;\n };\n}\n\n/**\n * Number input field.\n */\nexport interface NumberFormField extends Omit<BaseFormField, \"type\"> {\n type: \"number\";\n defaultValue?: number;\n validation?: ValidationRules & {\n min?: number;\n max?: number;\n step?: number;\n };\n}\n\n/**\n * Phone input field.\n */\nexport interface PhoneFormField extends Omit<BaseFormField, \"type\"> {\n type: \"phone\";\n defaultValue?: string;\n validation?: ValidationRules & {\n pattern?: string;\n };\n}\n\n/**\n * URL input field.\n */\nexport interface UrlFormField extends Omit<BaseFormField, \"type\"> {\n type: \"url\";\n defaultValue?: string;\n validation?: ValidationRules & {\n pattern?: string;\n };\n}\n\n/**\n * Textarea field.\n */\nexport interface TextareaFormField extends Omit<BaseFormField, \"type\"> {\n type: \"textarea\";\n defaultValue?: string;\n rows?: number;\n validation?: ValidationRules & {\n minLength?: number;\n maxLength?: number;\n };\n}\n\n/**\n * Select dropdown field.\n */\nexport interface SelectFormField extends Omit<BaseFormField, \"type\"> {\n type: \"select\";\n options: Array<{ label: string; value: string }>;\n defaultValue?: string | string[];\n allowMultiple?: boolean;\n}\n\n/**\n * Single checkbox field.\n */\nexport interface CheckboxFormField extends Omit<BaseFormField, \"type\"> {\n type: \"checkbox\";\n defaultValue?: boolean;\n}\n\n/**\n * Radio group field.\n */\nexport interface RadioFormField extends Omit<BaseFormField, \"type\"> {\n type: \"radio\";\n options: Array<{ label: string; value: string }>;\n defaultValue?: string;\n}\n\n/**\n * File upload field.\n */\nexport interface FileFormField extends Omit<BaseFormField, \"type\"> {\n type: \"file\";\n accept?: string; // MIME types\n multiple?: boolean;\n maxFileSize?: number; // bytes\n /**\n * When true, files uploaded through this field are attached to all\n * notification emails for this form. Default false.\n */\n attachToEmail?: boolean;\n}\n\n/**\n * Date picker field.\n */\nexport interface DateFormField extends Omit<BaseFormField, \"type\"> {\n type: \"date\";\n defaultValue?: string; // ISO date string\n min?: string; // ISO date\n max?: string; // ISO date\n}\n\n/**\n * Time picker field.\n */\nexport interface TimeFormField extends Omit<BaseFormField, \"type\"> {\n type: \"time\";\n defaultValue?: string; // HH:mm format\n}\n\n/**\n * Hidden field.\n */\nexport interface HiddenFormField extends Omit<BaseFormField, \"type\"> {\n type: \"hidden\";\n defaultValue?: string;\n}\n\n/**\n * A box the visitor ticks to agree to a text, such as a privacy policy. It is\n * always required, and its answer keeps the text the visitor was shown, so a\n * later change to the text does not change what an earlier visitor agreed to.\n */\nexport interface ConsentFormField extends Omit<BaseFormField, \"type\"> {\n type: \"consent\";\n /** What the visitor agrees to, shown beside the box and stored with it. */\n agreementText: string;\n}\n\n/**\n * A consent field's stored answer: whether the visitor agreed, and the text\n * the form showed them. Made by the server from the form, never taken from\n * the visitor, and never changed by an edit of the submission.\n */\nexport interface ConsentAnswer {\n agreed: boolean;\n text: string;\n}\n\n// ============================================================\n// Validation Types\n// ============================================================\n\n/**\n * Validation rules for form fields.\n */\n/**\n * Turn a union of object types into their intersection.\n *\n * The standard conditional-type idiom: distributing the union into contravariant\n * positions makes the compiler infer the intersection that satisfies all of them.\n */\ntype UnionToIntersection<U> = (\n U extends unknown ? (k: U) => void : never\n) extends (k: infer I) => void\n ? I\n : never;\n\n/**\n * Every validation key any form field type may carry, each optional.\n *\n * For surfaces that read validation ACROSS field types rather than inside one —\n * a shared rules editor cannot narrow to `TextFormField` without becoming the\n * per-type branching it exists to remove.\n *\n * DERIVED from the field union rather than restated. A field type that declares\n * a new rule becomes readable here with nobody remembering to add it, and a rule\n * removed there stops compiling here. A hand-kept list would agree on the day it\n * was written and drift after, silently, because both would look correct.\n */\nexport type AnyFieldValidation = Partial<\n UnionToIntersection<NonNullable<FormField[\"validation\"]>>\n>;\n\nexport interface ValidationRules {\n /** Custom error message */\n errorMessage?: string;\n\n /** Custom validation function */\n validate?: (\n value: unknown,\n formData: Record<string, unknown>\n ) => boolean | string | Promise<boolean | string>;\n}\n\n// ============================================================\n// Conditional Logic Types\n// ============================================================\n\n/**\n * Conditional logic configuration.\n */\nexport interface ConditionalLogic {\n /** Whether conditional logic is enabled */\n enabled: boolean;\n\n /** Action to perform when conditions are met */\n action: \"show\" | \"hide\";\n\n /** Logical operator for multiple conditions */\n operator: \"AND\" | \"OR\";\n\n /** Conditions to evaluate */\n conditions: ConditionalLogicCondition[];\n}\n\n/**\n * A single conditional logic condition.\n */\nexport interface ConditionalLogicCondition {\n /** Field name to check */\n field: string;\n\n /** Comparison operator */\n comparison:\n | \"equals\"\n | \"notEquals\"\n | \"contains\"\n | \"isEmpty\"\n | \"isNotEmpty\"\n | \"greaterThan\"\n | \"lessThan\";\n\n /** Value to compare against (not used for isEmpty/isNotEmpty) */\n value?: unknown;\n}\n\n// ============================================================\n// Form Settings Types\n// ============================================================\n\n/** How a form tells one person's response from another's. */\nexport type OneResponseMode = \"off\" | \"email\" | \"browser\" | \"ip\";\n\n/** Which bot check a form asks for. */\nexport type BotCheckMode = \"default\" | \"off\" | \"recaptcha\" | \"turnstile\";\n\n/**\n * Form settings and behavior.\n */\nexport interface FormSettings {\n /** Submit button text */\n submitButtonText?: string;\n\n /**\n * Confirmation type after successful submission.\n * - \"message\": Display a success message\n * - \"redirect\": Redirect to a URL\n * - \"relationship\": Redirect to a linked document (requires\n * `redirectRelationships` in the plugin options)\n *\n * @default \"message\"\n */\n confirmationType?: \"message\" | \"redirect\" | \"relationship\";\n\n /**\n * Linked document for relationship-based redirects (the collection's\n * `redirectPage` relationship value). Preserved verbatim — resolution\n * happens where the relationship is rendered.\n */\n redirectPage?: unknown;\n\n /** Success message (supports HTML) - used when confirmationType is \"message\" */\n successMessage?: string;\n\n /** Redirect URL after successful submission - used when confirmationType is \"redirect\" */\n redirectUrl?: string;\n\n /**\n * Relationship to redirect document - used when confirmationType is \"redirect\"\n * and redirectRelationships are configured in plugin options.\n */\n redirectRelation?: {\n relationTo: string;\n value: string; // Document ID\n };\n\n /**\n * Allow multiple submissions from same IP (default true).\n * @deprecated Read as `oneResponse` (true is `\"off\"`, false is `\"ip\"`);\n * no longer written.\n */\n allowMultipleSubmissions?: boolean;\n\n /**\n * One response per person, and how a person is told apart: `\"off\"` allows\n * several; `\"email\"` by the answer to `oneResponseField`, `\"browser\"` by a\n * token the browser keeps, `\"ip\"` by network address.\n *\n * @default \"off\"\n */\n oneResponse?: OneResponseMode;\n\n /** The email field `oneResponse: \"email\"` reads, by name. */\n oneResponseField?: string;\n\n /**\n * Which run of one-response checks the form is on: a whole number from 0,\n * moved on when the mode changes so earlier responses count afresh.\n *\n * @default 0\n */\n oneResponseVersion?: number;\n\n /**\n * The one-response version whose earlier submissions all carry their keys.\n * Behind `oneResponseVersion` while the plugin is still making them, after\n * a change of mode or Email field; unset reads as up to date. Written by\n * the plugin only.\n */\n oneResponseKeyedVersion?: number;\n\n /**\n * The bot check: `\"default\"` follows the plugin's option, `\"off\"` none, or\n * a named provider.\n *\n * @default \"default\"\n */\n botCheck?: BotCheckMode;\n\n /** When the form stops taking submissions, as an ISO date and time. */\n closesAt?: string;\n\n /** How many submissions the form takes before it closes; none for no limit. */\n submissionLimit?: number;\n\n /**\n * Whether a submission keeps the visitor's IP address and browser.\n *\n * @default true\n */\n storeIpAndBrowser?: boolean;\n\n /** How many days submissions are kept; none to keep them. */\n keepDays?: number;\n\n /** A note about the form, for people in the admin only. */\n internalNote?: string;\n\n /**\n * Per-form honeypot override. Unset inherits the plugin's\n * `spamProtection.honeypot`; the form wins where set.\n */\n honeypotEnabled?: boolean;\n\n /**\n * Per-form reCAPTCHA override. Unset inherits the plugin's\n * `spamProtection.recaptcha.enabled`; the form wins where set.\n * @deprecated Read as `botCheck` (unset is `\"default\"`, true\n * `\"recaptcha\"`, false `\"off\"`); no longer written.\n */\n captchaEnabled?: boolean;\n\n /** reCAPTCHA site key (client-facing) when captcha is enabled per-form */\n captchaSiteKey?: string;\n}\n\n// ============================================================\n// Notification Types\n// ============================================================\n\n/**\n * Email notification configuration for a form.\n */\nexport interface FormNotifications {\n /** Enable email notifications */\n enabled: boolean;\n\n /** Recipient email addresses */\n recipients: Array<{\n email: string;\n name?: string;\n }>;\n\n /** Email subject line */\n subject?: string;\n\n /** Use submitter's email as reply-to */\n replyTo?: boolean;\n\n /** Custom email template */\n template?: string; // HTML template\n}\n\n// ============================================================\n// Access Control Types\n// ============================================================\n\n/**\n * Form access control.\n */\nexport interface FormAccess {\n /** Who can submit the form */\n submit?: (context: { req: RequestContext }) => boolean | Promise<boolean>;\n\n /** Who can view submissions */\n read?: (context: { req: RequestContext }) => boolean | Promise<boolean>;\n}\n\n// ============================================================\n// Hook Types\n// ============================================================\n\n/**\n * Form lifecycle hooks.\n */\nexport interface FormHooks {\n /** Before submission validation */\n beforeValidate?: Array<(context: FormHookContext) => void | Promise<void>>;\n\n /** Before submission is saved */\n beforeSubmit?: Array<(context: FormHookContext) => void | Promise<void>>;\n\n /** After submission is saved */\n afterSubmit?: Array<(context: FormHookContext) => void | Promise<void>>;\n}\n\n/**\n * Hook context for form hooks.\n */\nexport interface FormHookContext {\n /** Form configuration */\n form: FormConfig;\n\n /** Submission data */\n data: Record<string, unknown>;\n\n /** Request context */\n req: RequestContext;\n\n /** Submission metadata */\n metadata?: {\n ipAddress?: string;\n userAgent?: string;\n };\n}\n\n// ============================================================\n// Submission Types\n// ============================================================\n\n/**\n * Form submission data structure.\n */\nexport interface FormSubmission {\n /** Submission ID */\n id: string;\n\n /** Form slug */\n formSlug: string;\n\n /** Submission data (JSON) */\n data: Record<string, unknown>;\n\n /**\n * Submission status.\n *\n * `spam` is one of them, not a separate flag: the stored field offers it, the\n * admin has a Spam tab and filters its other views with `not_equals: \"spam\"`,\n * and marking something \"Not spam\" moves it back to `new`.\n */\n status: \"new\" | \"read\" | \"archived\" | \"spam\";\n\n /** Which spam check flagged this, when one did. */\n spamReason?: string | null;\n\n /** Internal notes (admin only) */\n notes?: string;\n\n /** Metadata */\n ipAddress?: string;\n userAgent?: string;\n submittedAt: Date;\n\n /** Created/updated timestamps */\n createdAt: Date;\n updatedAt: Date;\n}\n\n// ============================================================\n// Resolved Configuration Types (Internal)\n// ============================================================\n\n/**\n * Resolved form builder configuration after applying defaults.\n * Used internally by the plugin.\n */\nexport interface ResolvedFormBuilderConfig {\n formOverrides: ResolvedCollectionOverrides;\n\n formSubmissionOverrides: ResolvedCollectionOverrides;\n\n fields: FormFieldToggles;\n\n /**\n * The collection-to-URL map, with the array shorthand already expanded.\n *\n * ONE shape, so nothing downstream branches on which spelling was written.\n * The list of redirect collections is `Object.keys` of this rather than a\n * second field beside it, which would agree with it today and drift later.\n */\n redirectRelationships: RedirectRelationships;\n\n /**\n * Whether each redirect target collection is localized, keyed by slug.\n *\n * Resolved at `init` from the collection's own declaration rather than\n * asked for in options, because the collection already declares it and a\n * second declaration drifts: the configuration's for a collection code\n * declares, the registry row's for one made in the Schema Builder.\n *\n * It decides whether a target's main-row `status` answers for the document:\n * a localized collection publishes on a locale companion, so the row reads\n * `draft` while a translation is public. A slug missing here is a collection\n * whose metadata could not be read — undefined, not false, because reading\n * an unknown as \"not localized\" is what marks a reachable translation as a\n * draft.\n */\n redirectTargetLocalization: Record<string, boolean>;\n\n beforeEmail?: FormBuilderPluginOptions[\"beforeEmail\"];\n\n notifications: {\n defaultFrom?: string;\n defaultToEmail?: string;\n enabled: boolean;\n };\n\n /** `FormBuilderPluginOptions[\"oneResponse\"]`, every option filled. */\n oneResponse: Required<NonNullable<FormBuilderPluginOptions[\"oneResponse\"]>>;\n\n spamProtection: {\n honeypot: boolean;\n recaptcha?: {\n enabled: boolean;\n siteKey?: string;\n secretKey?: string;\n scoreThreshold?: number;\n };\n turnstile?: {\n enabled: boolean;\n siteKey?: string;\n secretKey?: string;\n };\n rateLimit?: {\n maxSubmissions: number;\n windowMs: number;\n };\n };\n\n uploads: {\n maxFileSize: number;\n allowedMimeTypes: string[];\n uploadCollection: string;\n };\n\n features: {\n builder: boolean;\n conditionalLogic: boolean;\n fileUploads: boolean;\n };\n}\n\n// ============================================================\n// Type Guards\n// ============================================================\n\n/**\n * Check if a value is a FormField.\n */\nexport function isFormField(value: unknown): value is FormField {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"type\" in value &&\n \"name\" in value &&\n \"label\" in value\n );\n}\n\n/**\n * Check if a FormField is a specific type.\n */\nexport function isFormFieldType<T extends FormField>(\n field: FormField,\n type: T[\"type\"]\n): field is T {\n return field.type === type;\n}\n\n/**\n * Type guard for text field.\n */\nexport function isTextFormField(field: FormField): field is TextFormField {\n return field.type === \"text\";\n}\n\n/**\n * Type guard for email field.\n */\nexport function isEmailFormField(field: FormField): field is EmailFormField {\n return field.type === \"email\";\n}\n\n/**\n * Type guard for select field.\n */\nexport function isSelectFormField(field: FormField): field is SelectFormField {\n return field.type === \"select\";\n}\n\n/**\n * Type guard for checkbox field.\n */\nexport function isCheckboxFormField(\n field: FormField\n): field is CheckboxFormField {\n return field.type === \"checkbox\";\n}\n\n/**\n * Type guard for file field.\n */\nexport function isFileFormField(field: FormField): field is FileFormField {\n return field.type === \"file\";\n}\n","/**\n * Which validation rules this runtime actually enforces, per field type.\n *\n * Core's `validationRulesForFieldType` answers what a type MEANS — what a field\n * of that type could sensibly be bounded by. This answers a narrower and quite\n * different question: which of those bounds the form's schema generator reads\n * back and applies. A rule a surface offers but the runtime ignores is worse\n * than no control at all, because the author configures a restriction, believes\n * the form is guarded, and submissions violating it still pass.\n *\n * **The answer is per type, and cannot be collapsed into one list.** A single\n * set of \"rules this runtime enforces\" only says a rule is honoured for SOME\n * type. Intersected with a type's vocabulary it then offers, for example, `min`\n * and `max` on a date — because the number schema honours them — while the date\n * schema reads its bounds from `field.min`/`field.max` and never looks at\n * `validation`. The control stores a bound nothing reads. The same collapse\n * offers length limits on email, a pattern on textarea, and all three on time\n * and hidden.\n *\n * Each row below is what its generator in `generate-schema.ts` reads, and the\n * test beside this file asserts every entry CHANGES what the generated schema\n * accepts for that specific type — so a row that stops being true fails rather\n * than quietly becoming an inert control.\n *\n * Kept free of `zod` and of the generator itself on purpose. The field editor\n * needs this metadata to decide what to draw, and importing it from the module\n * that builds schemas would ship the whole schema implementation and the\n * validation library to every admin client, before anyone opens a field.\n *\n * @module utils/enforced-validation\n */\n\nimport type { FieldValidationRule } from \"nextly/field-catalog\";\n\nimport type { FormFieldType } from \"../types\";\n\n/**\n * `message` appears on every row because it is honoured everywhere: each\n * generator passes `validation.errorMessage` to `applyRequired`, so even a type\n * with no bounds of its own uses it for the required failure.\n *\n * Typed as a total `Record` rather than a partial one, so adding a field type\n * without deciding what it enforces is a compile error instead of a type that\n * silently offers nothing.\n */\nexport const ENFORCED_VALIDATION_RULES: Record<\n FormFieldType,\n readonly FieldValidationRule[]\n> = {\n text: [\"minLength\", \"maxLength\", \"pattern\", \"message\"],\n // Length bounds are deliberately absent: the email schema applies its own\n // format check and a pattern, and reads neither `minLength` nor `maxLength`.\n email: [\"pattern\", \"message\"],\n number: [\"min\", \"max\", \"message\"],\n phone: [\"pattern\", \"message\"],\n url: [\"pattern\", \"message\"],\n // No pattern: the textarea schema reads only the two length bounds.\n textarea: [\"minLength\", \"maxLength\", \"message\"],\n select: [\"message\"],\n checkbox: [\"message\"],\n radio: [\"message\"],\n // A file's size limit is stored on the field itself, not under `validation`.\n file: [\"message\"],\n // The date schema reads `field.min`/`field.max` — top level, not `validation`\n // — so a `validation.min` written here would never be consulted.\n date: [\"message\"],\n time: [\"message\"],\n hidden: [\"message\"],\n // A consent is always required; only the refusal's wording is the author's.\n consent: [\"message\"],\n};\n\n/**\n * The rules a field of this type both accepts and has enforced.\n *\n * A plugin-contributed type has no row, and correctly gets nothing: this\n * runtime's generator has no clause for it, so every rule would be inert.\n */\nexport function enforcedValidationRules(\n type: string\n): readonly FieldValidationRule[] {\n return ENFORCED_VALIDATION_RULES[type as FormFieldType] ?? [];\n}\n","/**\n * Dynamic Zod Schema Generator\n *\n * Converts FormField[] configuration to Zod validation schemas.\n * Supports all form field types with proper validation rules.\n *\n * @module utils/generate-schema\n * @since 0.1.0\n */\n\nimport type { FieldValidationRule } from \"nextly/field-catalog\";\nimport { z } from \"zod\";\n\nimport { isKnownFormField } from \"../types\";\nimport type {\n AnyFieldValidation,\n AnyFormField,\n FormField,\n TextFormField,\n EmailFormField,\n NumberFormField,\n PhoneFormField,\n UrlFormField,\n TextareaFormField,\n SelectFormField,\n CheckboxFormField,\n RadioFormField,\n FileFormField,\n DateFormField,\n TimeFormField,\n HiddenFormField,\n ConsentFormField,\n} from \"../types\";\n\nimport { consentAnswerOf } from \"./consent\";\nimport { ENFORCED_VALIDATION_RULES } from \"./enforced-validation\";\n\n// ============================================================\n// Main Schema Generator\n// ============================================================\n\n/**\n * Generate a Zod validation schema from form field configuration.\n *\n * Creates a dynamic Zod schema based on the form fields array,\n * applying appropriate validation rules for each field type.\n *\n * @param fields - Array of form field configurations\n * @returns Zod object schema for validating submission data\n *\n * @example\n * ```typescript\n * const fields: FormField[] = [\n * { type: 'text', name: 'firstName', label: 'First Name', required: true },\n * { type: 'email', name: 'email', label: 'Email', required: true },\n * { type: 'number', name: 'age', label: 'Age', validation: { min: 18, max: 120 } },\n * ];\n *\n * const schema = generateZodSchema(fields);\n * const result = schema.safeParse({ firstName: 'John', email: 'john@example.com', age: 25 });\n * ```\n */\nexport function generateZodSchema(\n fields: AnyFormField[]\n): z.ZodObject<Record<string, z.ZodTypeAny>> {\n const schemaShape: Record<string, z.ZodTypeAny> = {};\n\n for (const field of fields) {\n // A plugin-contributed field's value shape is owned by the plugin: accept\n // any value, but keep the base required check so a required plugin field\n // cannot be omitted (z.unknown() alone treats a missing key as valid).\n if (!isKnownFormField(field)) {\n schemaShape[field.name] = field.required\n ? z\n .unknown()\n .refine(\n value => value !== undefined && value !== null && value !== \"\",\n { message: `${field.label || field.name} is required` }\n )\n : z.unknown();\n continue;\n }\n\n // Skip hidden fields without required flag\n if (field.type === \"hidden\" && !field.required) {\n // Still add to schema but make optional\n schemaShape[field.name] = z.string().optional();\n continue;\n }\n\n const fieldSchema = generateFieldSchema(field);\n if (fieldSchema) {\n schemaShape[field.name] = field.required\n ? answered(fieldSchema, requiredMessage(field))\n : fieldSchema;\n }\n }\n\n return z.object(schemaShape);\n}\n\n/** What a required field missing from a submission says. */\nfunction requiredMessage(field: FormField): string {\n return customMessage(field) || `${field.label || field.name} is required`;\n}\n\n/**\n * `schema`, after a check that an answer was given at all. A required field\n * left out of a submission would otherwise fail on its type (\"expected\n * string, received undefined\"), which tells the visitor nothing; it is\n * refused as required instead, and the field's own checks run on an answer\n * that is there.\n */\nfunction answered(schema: z.ZodTypeAny, message: string): z.ZodTypeAny {\n return z\n .custom(value => value !== undefined && value !== null, { message })\n .pipe(schema);\n}\n\n// ============================================================\n// Field Schema Generators\n// ============================================================\n\n/**\n * Generate a Zod schema for a single field.\n */\nfunction generateFieldSchema(field: FormField): z.ZodTypeAny | null {\n switch (field.type) {\n case \"text\":\n return generateTextSchema(field);\n case \"email\":\n return generateEmailSchema(field);\n case \"number\":\n return generateNumberSchema(field);\n case \"phone\":\n return generatePhoneSchema(field);\n case \"url\":\n return generateUrlSchema(field);\n case \"textarea\":\n return generateTextareaSchema(field);\n case \"select\":\n return generateSelectSchema(field);\n case \"checkbox\":\n return generateCheckboxSchema(field);\n case \"radio\":\n return generateRadioSchema(field);\n case \"file\":\n return generateFileSchema(field);\n case \"date\":\n return generateDateSchema(field);\n case \"time\":\n return generateTimeSchema(field);\n case \"hidden\":\n return generateHiddenSchema(field);\n case \"consent\":\n return generateConsentSchema(field);\n default:\n // Unreachable for a known FormField; plugin types are handled in\n // generateZodSchema before this switch is reached.\n return z.unknown();\n }\n}\n\n/**\n * Generate schema for text field.\n */\n/**\n * The author's own wording for a failure, or `undefined` when this field's type\n * does not enforce `message`.\n *\n * Asked of the row rather than read off `validation` directly, for the reason\n * the table exists at all: the editor decides whether to OFFER the control from\n * that row, so a generator honouring a stored message regardless would enforce\n * a value the author was never shown a box for — and, worse, the table would\n * describe a rule it did not govern.\n *\n * `message` was the one rule that stayed outside this. It agreed with the table\n * only because every row happens to list it, which is agreement by coincidence:\n * drop `message` from a row and the control disappears while the wording went\n * on being applied.\n */\nfunction customMessage(field: FormField): string | undefined {\n const enforced = ENFORCED_VALIDATION_RULES[field.type] ?? [];\n if (!enforced.includes(\"message\")) return undefined;\n return field.validation?.errorMessage;\n}\n\n/**\n * A field's stored bounds, with the custom message already resolved by the row.\n *\n * Handed to the appliers so they cannot read an ungated message: they take a\n * validation object, and giving them one whose `errorMessage` has been through\n * {@link customMessage} makes the rule hold for every applier without each of\n * them having to remember it.\n */\nfunction enforcedValidation(field: FormField): AnyFieldValidation {\n const stored = (field.validation ?? {}) as AnyFieldValidation;\n return { ...stored, errorMessage: customMessage(field) };\n}\n\n/**\n * Which rules a type's schema applies, and how each one constrains a string.\n *\n * The WHICH comes from {@link ENFORCED_VALIDATION_RULES}, which the field\n * editor also reads to decide what to offer. One definition answers both\n * questions, so a rule cannot be offered by the editor and ignored here, or\n * honoured here and hidden there — the two used to state the same thing\n * separately and could only agree by being edited together.\n *\n * The HOW stays here, because it is about zod and the table is deliberately\n * free of it: the editor imports the table, and pulling the schema builder in\n * behind it would ship this file and its validation library to every admin\n * client before anyone opened a field.\n */\nconst STRING_RULE_APPLIERS: Partial<\n Record<\n FieldValidationRule,\n (\n schema: z.ZodString,\n validation: AnyFieldValidation,\n fallback: string\n ) => z.ZodString\n >\n> = {\n minLength: (schema, validation) =>\n validation.minLength === undefined\n ? schema\n : schema.min(\n validation.minLength,\n validation.errorMessage ||\n `Minimum ${validation.minLength} characters required`\n ),\n maxLength: (schema, validation) =>\n validation.maxLength === undefined\n ? schema\n : schema.max(\n validation.maxLength,\n validation.errorMessage ||\n `Maximum ${validation.maxLength} characters allowed`\n ),\n pattern: (schema, validation, fallback) =>\n validation.pattern\n ? schema.regex(\n new RegExp(validation.pattern),\n validation.errorMessage || fallback\n )\n : schema,\n};\n\n/** The same, for the bounds a number carries. */\nconst NUMBER_RULE_APPLIERS: Partial<\n Record<\n FieldValidationRule,\n (schema: z.ZodNumber, validation: AnyFieldValidation) => z.ZodNumber\n >\n> = {\n min: (schema, validation) =>\n validation.min === undefined\n ? schema\n : schema.min(\n validation.min,\n validation.errorMessage || `Minimum value is ${validation.min}`\n ),\n max: (schema, validation) =>\n validation.max === undefined\n ? schema\n : schema.max(\n validation.max,\n validation.errorMessage || `Maximum value is ${validation.max}`\n ),\n};\n\n/**\n * Whether an author-supplied pattern actually replaces a type's intrinsic one.\n *\n * Two conditions, and both are load-bearing: a pattern must be stored, AND the\n * type's row must enforce `pattern`. A type that keeps its own shape check —\n * a phone, a URL — stands it down only for an override that will really be\n * applied, because standing down for one that will not leaves nothing behind.\n */\nfunction overridesPattern(field: FormField): boolean {\n const enforced = ENFORCED_VALIDATION_RULES[field.type] ?? [];\n if (!enforced.includes(\"pattern\")) return false;\n const validation = (field.validation ?? {}) as AnyFieldValidation;\n return Boolean(validation.pattern);\n}\n\n/**\n * Constrain a string schema by the rules this field's TYPE enforces.\n *\n * A rule the type does not list is not applied even when the field carries a\n * value for it, which is the point: a `pattern` stored on a textarea is a bound\n * the editor never offered, and honouring it here would enforce a restriction\n * its author could not see.\n *\n * `patternMessage` is the type's own wording for a failed pattern — \"Invalid\n * URL format\" reads differently from \"Invalid format\" — so the rule set decides\n * WHICH constraints apply while each type keeps its own voice.\n */\nfunction applyStringRules(\n schema: z.ZodString,\n field: FormField,\n patternMessage: string\n): z.ZodString {\n const validation = enforcedValidation(field);\n let constrained = schema;\n for (const rule of ENFORCED_VALIDATION_RULES[field.type] ?? []) {\n const apply = STRING_RULE_APPLIERS[rule];\n if (apply) constrained = apply(constrained, validation, patternMessage);\n }\n return constrained;\n}\n\nfunction generateTextSchema(field: TextFormField): z.ZodTypeAny {\n const schema = applyStringRules(z.string(), field, \"Invalid format\");\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for email field.\n */\nfunction generateEmailSchema(field: EmailFormField): z.ZodTypeAny {\n // The format check is the type's own, not a validation rule: an email field\n // is an email field whether or not its author bounded it further.\n const schema = applyStringRules(\n z.string().email(customMessage(field) || \"Invalid email address\"),\n field,\n \"Invalid email format\"\n );\n\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for number field.\n */\nfunction generateNumberSchema(field: NumberFormField): z.ZodTypeAny {\n // Zod 4 unified message customization under a single `error` callback;\n // an undefined input is the \"missing required\" case, anything else is a\n // type mismatch.\n let schema = z.number({\n error: issue =>\n issue.input === undefined\n ? customMessage(field) || \"This field is required\"\n : \"Must be a number\",\n });\n\n // The same rule set the editor reads decides which bounds apply here.\n const validation = enforcedValidation(field);\n for (const rule of ENFORCED_VALIDATION_RULES[field.type] ?? []) {\n const apply = NUMBER_RULE_APPLIERS[rule];\n if (apply) schema = apply(schema, validation);\n }\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional().nullable();\n}\n\n/**\n * Generate schema for phone field.\n */\nfunction generatePhoneSchema(field: PhoneFormField): z.ZodTypeAny {\n // The default pattern is part of what a phone field IS, so it belongs to the\n // base rather than to the rule set: it applies when the author supplied none,\n // and the author's own pattern replaces it through the rule below.\n //\n // Whether the author CAN replace it is the row's decision, not the stored\n // value's. Standing the base down because a pattern happens to be stored,\n // while `applyStringRules` then declines to apply it because the row does not\n // list `pattern`, would leave the field with no check at all — every string\n // accepted, on the one type whose whole purpose is a shape.\n const base = overridesPattern(field)\n ? z.string()\n : z\n .string()\n .regex(\n /^[\\d\\s\\-+()]+$/,\n customMessage(field) || \"Invalid phone number\"\n );\n\n const schema = applyStringRules(base, field, \"Invalid phone number format\");\n\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for URL field.\n */\nfunction generateUrlSchema(field: UrlFormField): z.ZodTypeAny {\n const schema = applyStringRules(\n z.string().url(customMessage(field) || \"Invalid URL\"),\n field,\n \"Invalid URL format\"\n );\n\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for textarea field.\n */\nfunction generateTextareaSchema(field: TextareaFormField): z.ZodTypeAny {\n const schema = applyStringRules(z.string(), field, \"Invalid format\");\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for select field.\n */\nfunction generateSelectSchema(field: SelectFormField): z.ZodTypeAny {\n const validValues = field.options.map(opt => opt.value);\n\n if (field.allowMultiple) {\n // Multi-select: array of valid values\n const schema = z.array(z.enum(validValues as [string, ...string[]]));\n\n if (field.required) {\n return schema.min(\n 1,\n customMessage(field) || \"Please select at least one option\"\n );\n }\n\n return schema.optional();\n }\n\n // Single select\n const schema = z.enum(validValues as [string, ...string[]], {\n error: () => customMessage(field) || \"Please select a valid option\",\n });\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for checkbox field.\n */\nfunction generateCheckboxSchema(field: CheckboxFormField): z.ZodTypeAny {\n const schema = z.boolean();\n\n if (field.required) {\n // For required checkbox, value must be true\n return schema.refine(val => val === true, {\n message: customMessage(field) || \"This field is required\",\n });\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for radio field.\n */\nfunction generateRadioSchema(field: RadioFormField): z.ZodTypeAny {\n const validValues = field.options.map(opt => opt.value);\n\n const schema = z.enum(validValues as [string, ...string[]], {\n error: () => customMessage(field) || \"Please select an option\",\n });\n\n if (field.required) {\n return schema;\n }\n\n return schema.optional();\n}\n\n/**\n * Generate schema for file field.\n *\n * Note: File validation is typically handled server-side.\n * This schema validates file references (IDs or URLs).\n */\nfunction generateFileSchema(field: FileFormField): z.ZodTypeAny {\n if (field.multiple) {\n const schema = z.array(z.string());\n\n if (field.required) {\n return schema.min(\n 1,\n customMessage(field) || \"Please upload at least one file\"\n );\n }\n\n return schema.optional();\n }\n\n // Single file\n const schema = z.string();\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for date field.\n */\nfunction generateDateSchema(field: DateFormField): z.ZodTypeAny {\n // Start with base string schema\n let schema: z.ZodTypeAny = z.string();\n\n // Add date format validation\n schema = (schema as z.ZodString).refine(\n val => {\n if (!val) return true; // Allow empty for optional fields\n const date = new Date(val);\n return !isNaN(date.getTime());\n },\n { message: customMessage(field) || \"Invalid date\" }\n );\n\n // Min/max date validation\n if (field.min || field.max) {\n schema = schema.refine(\n (val: unknown) => {\n if (!val) return true;\n const date = new Date(val as string);\n if (field.min && date < new Date(field.min)) return false;\n if (field.max && date > new Date(field.max)) return false;\n return true;\n },\n {\n message:\n customMessage(field) ||\n `Date must be between ${field.min || \"any\"} and ${field.max || \"any\"}`,\n }\n );\n }\n\n // Handle required vs optional\n if (field.required) {\n // Add non-empty check for required fields\n return schema.refine(\n (val: unknown) => val !== undefined && val !== null && val !== \"\",\n { message: customMessage(field) || \"This field is required\" }\n );\n }\n\n return schema.optional().or(z.literal(\"\"));\n}\n\n/**\n * Generate schema for time field.\n */\nfunction generateTimeSchema(field: TimeFormField): z.ZodTypeAny {\n const schema = z\n .string()\n .regex(\n /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/,\n customMessage(field) || \"Invalid time format (HH:mm)\"\n );\n\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n/**\n * Generate schema for a consent field: the stored answer, which must say the\n * visitor agreed. Always required, whatever the field's own flag says, since\n * a consent nobody has to give records nothing.\n */\nfunction generateConsentSchema(field: ConsentFormField): z.ZodTypeAny {\n return z\n .object({ agreed: z.boolean(), text: z.string() })\n .refine(answer => answer.agreed, {\n message: customMessage(field) || \"This field is required\",\n });\n}\n\n/**\n * Generate schema for hidden field.\n */\nfunction generateHiddenSchema(field: HiddenFormField): z.ZodTypeAny {\n const schema = z.string();\n return applyRequired(schema, field.required, customMessage(field));\n}\n\n// ============================================================\n// Helper Functions\n// ============================================================\n\n/**\n * Apply required/optional to a schema.\n */\nfunction applyRequired(\n schema: z.ZodString,\n required?: boolean,\n errorMessage?: string\n): z.ZodTypeAny {\n if (required) {\n return schema.min(1, errorMessage || \"This field is required\");\n }\n\n return schema.optional().or(z.literal(\"\"));\n}\n\n// ============================================================\n// Data Transformation\n// ============================================================\n\n/**\n * Transform form data before validation.\n *\n * Handles type coercion and normalization:\n * - Converts string numbers to actual numbers\n * - Converts string booleans to actual booleans\n * - Trims string values\n * - Handles empty strings\n *\n * @param data - Raw form data\n * @param fields - Form field configurations\n * @returns Transformed data ready for validation\n */\nexport function transformFormData(\n data: Record<string, unknown>,\n fields: AnyFormField[]\n): Record<string, unknown> {\n const transformed: Record<string, unknown> = {};\n\n for (const field of fields) {\n const value = data[field.name];\n\n // Skip undefined values\n if (value === undefined) {\n continue;\n }\n\n // A plugin field's value passes through untransformed — the plugin owns its\n // value shape; only built-in types get type coercion.\n transformed[field.name] = isKnownFormField(field)\n ? transformFieldValue(value, field)\n : value;\n }\n\n return transformed;\n}\n\n/**\n * Transform a single field value based on its type.\n */\nfunction transformFieldValue(value: unknown, field: FormField): unknown {\n // Handle null/undefined\n if (value === null || value === undefined) {\n return value;\n }\n\n switch (field.type) {\n case \"number\":\n // Coerce to number\n if (typeof value === \"string\") {\n const num = parseFloat(value);\n return isNaN(num) ? value : num;\n }\n return value;\n\n case \"checkbox\":\n // Coerce to boolean\n if (typeof value === \"string\") {\n return value === \"true\" || value === \"1\" || value === \"on\";\n }\n return Boolean(value);\n\n case \"text\":\n case \"email\":\n case \"phone\":\n case \"url\":\n case \"textarea\":\n case \"hidden\":\n // Trim strings\n if (typeof value === \"string\") {\n return value.trim();\n }\n return value;\n\n case \"consent\":\n // The answer as stored, with the form's text rather than any the\n // visitor sent.\n return consentAnswerOf(value, field);\n\n case \"select\":\n // Ensure array for multi-select\n if (field.allowMultiple) {\n if (!Array.isArray(value)) {\n return value ? [value] : [];\n }\n }\n return value;\n\n default:\n return value;\n }\n}\n\n// ============================================================\n// Validation Helper\n// ============================================================\n\n/**\n * Validate form data against a form's field configuration.\n *\n * Combines transformation and validation in one step.\n *\n * @param data - Raw form data\n * @param fields - Form field configurations\n * @returns Zod safe parse result\n *\n * @example\n * ```typescript\n * const result = validateFormData({ name: 'John', email: 'john@example.com' }, fields);\n *\n * if (result.success) {\n * console.log('Valid data:', result.data);\n * } else {\n * console.log('Validation errors:', result.error.flatten());\n * }\n * ```\n */\nexport function validateFormData(\n data: Record<string, unknown>,\n fields: AnyFormField[]\n): z.ZodSafeParseResult<Record<string, unknown>> {\n const transformed = transformFormData(data, fields);\n const schema = generateZodSchema(fields);\n return schema.safeParse(transformed);\n}\n\n/**\n * Get validation errors as a flat object.\n *\n * @param result - Zod safe parse result\n * @returns Object mapping field names to error messages\n */\n/** A field that failed, with the machine code a wire error reports. */\nexport interface ValidationIssue {\n path: string;\n /** `REQUIRED` for an answer that is missing or empty, else `INVALID`. */\n code: \"REQUIRED\" | \"INVALID\";\n message: string;\n}\n\n/**\n * Whether the visitor supplied nothing at this field.\n *\n * The same four shapes the endpoint has always counted as blank. A `false`\n * checkbox and a `0` are answers, so neither is absent.\n */\nfunction isBlank(value: unknown): boolean {\n return (\n value === undefined ||\n value === null ||\n value === \"\" ||\n (Array.isArray(value) && value.length === 0)\n );\n}\n\n/**\n * The failures with their codes, first one per field.\n *\n * A code as well as a sentence, because the public submission error has always\n * carried one and a client uses it to tell \"you left this blank\" from \"this is\n * not an email\" without parsing English. Flattening every failure to `INVALID`\n * silently stopped required-field detection working for those clients.\n *\n * The code is decided by looking at the SUBMITTED VALUE, not at the Zod issue.\n * Zod's codes do not divide the same way: `too_small` covers a blank answer and\n * a four-character password under a minimum of eight, and `invalid_type` covers\n * an absent key and a string sent to a number field. Reading the code alone\n * told a visitor to fill in a field they had already filled in.\n */\nexport function getValidationIssues(\n result: z.ZodSafeParseResult<unknown>,\n submitted: Record<string, unknown> = {}\n): ValidationIssue[] {\n if (result.success) return [];\n\n const seen = new Set<string>();\n const issues: ValidationIssue[] = [];\n\n // Zod 4 renamed ZodError.errors to ZodError.issues.\n for (const issue of result.error.issues) {\n const path = issue.path.join(\".\");\n if (seen.has(path)) continue;\n seen.add(path);\n issues.push({\n path,\n // Read at the top level, which is the only depth a form field has: a\n // declared field is one key, so a dotted path can only come from inside a\n // value the visitor did supply.\n code: isBlank(submitted[path]) ? \"REQUIRED\" : \"INVALID\",\n message: issue.message,\n });\n }\n\n return issues;\n}\n\n/**\n * The same failures as a field-to-message map.\n *\n * Derived from {@link getValidationIssues} rather than walking the issues a\n * second time, so the two cannot disagree about which message belongs to a\n * field or which failure won when one field had several.\n */\nexport function getValidationErrors(\n result: z.ZodSafeParseResult<unknown>\n): Record<string, string> {\n return Object.fromEntries(\n getValidationIssues(result).map(issue => [issue.path, issue.message])\n );\n}\n","/**\n * Which Email field one response per email may compare, and why a form's\n * choice is refused.\n *\n * Every accepted submission must carry the address it is compared by, so the\n * field must be an Email field, required, and never hidden by a condition: a\n * hidden or optional field lets a submission through with no address, and so\n * with no key. The builder, Settings and the save all ask this module.\n *\n * @module utils/one-response-field\n */\nimport type { AnyFormField, FormSettings } from \"../types\";\n\n/** Why the chosen field cannot be made optional, conditional, or deleted. */\nexport const ONE_RESPONSE_FIELD_REASON =\n \"This field is used for one response per person, so it stays required and always shown. Choose another Email field in Settings first.\";\n\n/** Whether `field` can be the Email field one response per email compares. */\nexport function comparesByEmail(field: AnyFormField): boolean {\n return (\n field.type === \"email\" &&\n field.required === true &&\n field.conditionalLogic?.enabled !== true\n );\n}\n\n/** The fields one response per email may compare, in the form's order. */\nexport function emailFieldsToCompare(\n fields: readonly AnyFormField[]\n): AnyFormField[] {\n return fields.filter(comparesByEmail);\n}\n\n/**\n * Why a form's one response per email cannot stand as saved, or undefined\n * when it can (or the form does not compare by email).\n */\nexport function oneResponseFieldProblem(\n settings: Pick<FormSettings, \"oneResponse\" | \"oneResponseField\">,\n fields: readonly AnyFormField[]\n): string | undefined {\n if (settings.oneResponse !== \"email\") return undefined;\n const name = settings.oneResponseField;\n if (!name) {\n return \"One response per email address needs an Email field to compare. Choose one in Settings.\";\n }\n const field = fields.find(each => each.name === name);\n if (!field || field.type !== \"email\") {\n return `One response per email address compares \"${name}\", which is not an Email field of this form.`;\n }\n return comparesByEmail(field)\n ? undefined\n : `The Email field one response per person compares, \"${field.label || name}\", must be required and not hidden by a condition.`;\n}\n\n/**\n * Why an edit of `before` into `after` is refused, or undefined: the field\n * one response per email compares may be renamed but not made optional,\n * conditional or another type.\n */\nexport function oneResponseEditProblem(\n before: AnyFormField,\n after: AnyFormField,\n settings: Pick<FormSettings, \"oneResponse\" | \"oneResponseField\">\n): string | undefined {\n if (settings.oneResponse !== \"email\") return undefined;\n if (settings.oneResponseField !== before.name) return undefined;\n return comparesByEmail(after) ? undefined : ONE_RESPONSE_FIELD_REASON;\n}\n","/**\n * The page a submission was sent from, as the submission keeps it: the web\n * address the visitor's browser reported in `Referer`, reduced to its origin\n * and path. Its query and fragment are dropped, since a page's address can\n * carry personal data there (a search, an email in a link, a token), and an\n * address that is not a web page (another scheme, or none) is not kept. The\n * browser reports it, so it is a hint for staff, not evidence.\n *\n * @module utils/sent-from\n */\n\n/** The longest address kept, in characters; a longer one is not kept. */\nconst PAGE_MAX = 2048;\n\n/** The page a `Referer` names, or undefined when it names no web page. */\nexport function pageFromReferer(\n referer: string | null | undefined\n): string | undefined {\n if (!referer) return undefined;\n let url: URL;\n try {\n url = new URL(referer);\n } catch {\n return undefined;\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") return undefined;\n const page = `${url.origin}${url.pathname}`;\n return page.length <= PAGE_MAX ? page : undefined;\n}\n","/**\n * Which submissions a list shows, carried from the list to a submission's\n * page so its previous and next move through the same list, and to the\n * export so it exports what the list shows: the form it is fixed to or\n * filtered by, the status tab, and the dates it was sent between. One place\n * builds the list's query from it, so the page cannot step through a\n * different list than the one it was opened from, and the export cannot\n * read a different one.\n *\n * Pure, with no browser or React import: the export route on the server\n * reads a request's scope with these same functions.\n *\n * @module admin/components/submissions/submission-list-scope\n */\n\n/** A status tab of the submissions list. */\nexport type StatusTab = \"all\" | \"new\" | \"read\" | \"archived\" | \"spam\";\n\nconst STATUS_TABS: readonly StatusTab[] = [\n \"all\",\n \"new\",\n \"read\",\n \"archived\",\n \"spam\",\n];\n\n/** The list a submission was opened from. */\nexport interface SubmissionListScope {\n /** The form the list is fixed to or filtered by; every form when absent. */\n form?: string;\n status: StatusTab;\n /** Sent at or after this moment (an ISO date and time); no bound when absent. */\n from?: string;\n /** Sent before this moment, which it leaves out; no bound when absent. */\n to?: string;\n}\n\n/**\n * The scope's dates as a filter of `submittedAt`, or none for any date. The\n * upper bound is exclusive, so a range ending at the start of a day leaves\n * that day out, whatever the precision a dialect stores the time with.\n */\nfunction sentBetween(\n scope: SubmissionListScope\n): Record<string, string> | undefined {\n if (!scope.from && !scope.to) return undefined;\n return {\n ...(scope.from ? { greater_than_equal: scope.from } : {}),\n ...(scope.to ? { less_than: scope.to } : {}),\n };\n}\n\n/** The list's filter as the entries API takes it. */\nexport function listWhere(scope: SubmissionListScope): Record<string, unknown> {\n const where: Record<string, unknown> = {};\n if (scope.form) where.form = { equals: scope.form };\n // \"All\" means every human status; spam only shows when asked for, as the\n // export does.\n where.status =\n scope.status === \"all\" ? { not_equals: \"spam\" } : { equals: scope.status };\n const sent = sentBetween(scope);\n if (sent) where.submittedAt = sent;\n return where;\n}\n\n/**\n * The list's order: newest first, and among submissions sent at the same\n * moment, by id. The id makes the order total, so a submission's newer and\n * older neighbours are always defined even when timestamps tie (imported\n * rows can share one). It is the tiebreak the entries API adds last anyway,\n * spelled out so the page's neighbour queries mirror it exactly.\n */\nexport const LIST_SORT = \"-submittedAt,id\";\n\n/**\n * The list a submission's page counts it in: the list's filter, and the\n * submission itself. Opened from the New tab, the submission is marked Read\n * and leaves that filter; archiving or recovering it can too. Counted with\n * it, the page's \"3 of 3\" stays true to the list it was opened from rather\n * than reading \"3 of 2\", and its newer neighbours still count its place.\n */\nexport function listWithWhere(\n scope: SubmissionListScope,\n id: string\n): Record<string, unknown> {\n return { or: [listWhere(scope), { id: { equals: id } }] };\n}\n\n/** A query for one end of the list around a submission. */\nexport interface NeighbourQuery {\n where: Record<string, unknown>;\n sort: string;\n}\n\n/**\n * The queries that find the submissions just newer and just older than the\n * one sent at `at` with id `id`, in LIST_SORT's order: each side is a strict\n * comparison on the timestamp, or the same timestamp and the id on that side.\n * The newer side is read in reverse, so its first row is the nearest one, and\n * its total is how many rows come before this submission in the list.\n */\nexport function neighbourQueries(\n scope: SubmissionListScope,\n at: string,\n id: string\n): { newer: NeighbourQuery; older: NeighbourQuery } {\n const base = listWhere(scope);\n const side = (time: string, tie: string) => ({\n ...base,\n or: [\n { submittedAt: { [time]: at } },\n { and: [{ submittedAt: { equals: at } }, { id: { [tie]: id } }] },\n ],\n });\n return {\n newer: {\n where: side(\"greater_than\", \"less_than\"),\n sort: \"submittedAt,-id\",\n },\n older: { where: side(\"less_than\", \"greater_than\"), sort: LIST_SORT },\n };\n}\n\n/** The scope as an address carries it: a submission page's, or the export's. */\nfunction scopeParams(scope: SubmissionListScope): URLSearchParams {\n const params = new URLSearchParams({ status: scope.status });\n if (scope.form) params.set(\"form\", scope.form);\n if (scope.from) params.set(\"from\", scope.from);\n if (scope.to) params.set(\"to\", scope.to);\n return params;\n}\n\n/** A moment an address names, as an ISO date and time; none for anything else. */\nfunction momentOf(text: string | null): string | undefined {\n if (!text) return undefined;\n const at = Date.parse(text);\n return Number.isNaN(at) ? undefined : new Date(at).toISOString();\n}\n\n/**\n * The scope an address carries, read defensively: an unknown status reads as\n * \"all\" and a date that is not one as no bound, so a hand-edited address\n * still lists something.\n */\nexport function scopeFromSearch(search: string): SubmissionListScope {\n const params = new URLSearchParams(search);\n const status = params.get(\"status\");\n const form = params.get(\"form\");\n const from = momentOf(params.get(\"from\"));\n const to = momentOf(params.get(\"to\"));\n return {\n status: STATUS_TABS.includes(status as StatusTab)\n ? (status as StatusTab)\n : \"all\",\n ...(form ? { form } : {}),\n ...(from ? { from } : {}),\n ...(to ? { to } : {}),\n };\n}\n\n/** A submission's page, opened from the list `scope`. */\nexport function submissionPageAddress(\n submissionsSlug: string,\n id: string,\n scope: SubmissionListScope\n): string {\n return `/admin/collections/${encodeURIComponent(submissionsSlug)}/${encodeURIComponent(id)}?${scopeParams(scope).toString()}`;\n}\n\n/** The export route, as the admin's API serves it. */\nconst EXPORT_ROUTE =\n \"/admin/api/plugins/@nextlyhq/plugin-form-builder/submissions/export\";\n\n/**\n * The export of what a list shows, in `format`: every submission its scope\n * matches, or, given `ids`, exactly those (the rows ticked in it).\n */\nexport function exportAddress(\n scope: SubmissionListScope,\n format: \"csv\" | \"json\",\n ids?: readonly string[]\n): string {\n const params = scopeParams(scope);\n params.set(\"format\", format);\n if (ids !== undefined) params.set(\"ids\", ids.join(\",\"));\n return `${EXPORT_ROUTE}?${params.toString()}`;\n}\n","/**\n * How a stored notification reads, whatever shape it was saved in. The admin\n * and the send path both read notifications through this module, so a rule\n * saved before several recipients, several conditions or per-notification\n * attachments existed means the same thing to each.\n *\n * - Recipients: a team notification's `recipients` list, or its single `to`\n * address as one, as notifications were saved before.\n * - Conditions: `conditions` (a match, all or any, and a list), or the single\n * `condition` as a list of one under all.\n * - Attachments: `attachFields`, the names of the form's file fields whose\n * files go with this notification, or, for a rule saved before it, exactly\n * the file fields marked to attach to notification emails, so nothing extra\n * is ever attached on upgrade.\n *\n * @module utils/notification-model\n */\nimport type {\n AnyFormField,\n ConditionalLogicCondition,\n FormNotification,\n} from \"../types\";\n\nimport { evaluateSingleCondition } from \"./evaluate-conditions\";\n\n/** How several conditions combine: all must hold, or any one. */\nexport type ConditionMatch = \"all\" | \"any\";\n\n/** A notification's send conditions, as read. */\nexport interface NotificationConditions {\n match: ConditionMatch;\n list: ConditionalLogicCondition[];\n}\n\n/**\n * Split typed addresses into a list: separated by commas, semicolons,\n * spaces or new lines, each trimmed, blanks and repeats dropped.\n */\nexport function parseAddressList(text: string): string[] {\n const seen = new Set<string>();\n const list: string[] = [];\n for (const part of text.split(/[\\s,;]+/)) {\n const address = part.trim();\n if (!address || seen.has(address.toLowerCase())) continue;\n seen.add(address.toLowerCase());\n list.push(address);\n }\n return list;\n}\n\n/**\n * A team notification's addresses: its list, or the single address it was\n * saved with. Trimmed, blanks dropped.\n */\nexport function teamAddressesOf(notification: FormNotification): string[] {\n const stored = Array.isArray(notification.recipients)\n ? notification.recipients\n : typeof notification.to === \"string\"\n ? [notification.to]\n : [];\n return stored\n .map(address => (typeof address === \"string\" ? address.trim() : \"\"))\n .filter(Boolean);\n}\n\n/** A notification's conditions, as a match and a list. */\nexport function conditionsOf(\n notification: FormNotification\n): NotificationConditions {\n const stored = notification.conditions;\n if (stored && Array.isArray(stored.list)) {\n return {\n match: stored.match === \"any\" ? \"any\" : \"all\",\n list: stored.list.filter(isCondition),\n };\n }\n return {\n match: \"all\",\n list: notification.condition ? [notification.condition] : [],\n };\n}\n\nfunction isCondition(value: unknown): value is ConditionalLogicCondition {\n return (\n !!value &&\n typeof value === \"object\" &&\n typeof (value as { field?: unknown }).field === \"string\" &&\n typeof (value as { comparison?: unknown }).comparison === \"string\"\n );\n}\n\n/**\n * Whether a notification sends for these answers: every condition holds\n * (all) or one does (any). One with no conditions always sends.\n */\nexport function conditionsMet(\n conditions: NotificationConditions,\n data: Record<string, unknown>\n): boolean {\n if (conditions.list.length === 0) return true;\n const holds = (condition: ConditionalLogicCondition) =>\n evaluateSingleCondition(condition, data);\n return conditions.match === \"any\"\n ? conditions.list.some(holds)\n : conditions.list.every(holds);\n}\n\n/**\n * The file fields whose files go with a notification, by name: its own\n * choice, or for a rule saved before it, the fields marked to attach.\n */\nexport function attachFieldsOf(\n notification: FormNotification,\n fields: readonly AnyFormField[]\n): string[] {\n if (Array.isArray(notification.attachFields)) {\n return notification.attachFields.filter(\n (name): name is string => typeof name === \"string\"\n );\n }\n return fieldsMarkedToAttach(fields);\n}\n\n/**\n * The file fields marked to attach to notification emails, by name: what\n * every email carried before each notification chose its own.\n */\nexport function fieldsMarkedToAttach(\n fields: ReadonlyArray<{ type?: unknown; name?: unknown }>\n): string[] {\n return fields\n .filter(\n field =>\n field.type === \"file\" &&\n (field as { attachToEmail?: unknown }).attachToEmail === true &&\n typeof field.name === \"string\"\n )\n .map(field => field.name as string);\n}\n\n/** The media ids a submission holds under the named file fields. */\nexport function attachmentsFrom(\n fieldNames: readonly string[],\n data: Record<string, unknown>\n): Array<{ mediaId: string }> {\n const attachments: Array<{ mediaId: string }> = [];\n for (const name of fieldNames) {\n const value = data[name];\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item === \"string\" && item) {\n attachments.push({ mediaId: item });\n }\n }\n }\n return attachments;\n}\n","/**\n * Find what references a form field by name, so destructive actions can be\n * guarded: a field that another field's conditional logic, a notification's\n * recipient/reply-to interpolation, or the form's one response per email\n * points at must not be silently deletable.\n */\n\nimport type { AnyFormField, FormNotification, FormSettings } from \"../types\";\n\nimport { conditionsOf } from \"./notification-model\";\n\n/**\n * The one spelling of a field reference this plugin recognises: `{{fieldName}}`\n * with no spaces, and `\\w` names.\n *\n * Exact, because the SEND path is exact — a value the admin accepts as a\n * reference and the send path does not is delivered with its braces intact,\n * straight to the mail provider. Both sides read this constant so the question\n * has one answer.\n */\nexport const FIELD_REF_PATTERN: RegExp = /^\\{\\{(\\w+)\\}\\}$/;\n\n/** The field named by a reference, or `null` when the value is not one. */\nexport function parseFieldRef(value: string | undefined): string | null {\n if (!value) return null;\n const match = value.match(FIELD_REF_PATTERN);\n return match ? match[1] : null;\n}\n\nexport interface FieldReference {\n /** What kind of thing holds the reference. */\n kind: \"condition\" | \"notification\" | \"setting\";\n /** Human label of the referrer (field label or notification name). */\n label: string;\n}\n\n/**\n * Match `{{ fieldName }}` interpolations (whitespace-tolerant), the syntax the\n * notification send path resolves recipients and reply-to values with.\n */\nfunction templatePattern(fieldName: string, flags?: string): RegExp {\n return new RegExp(\n `\\\\{\\\\{\\\\s*${fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\s*\\\\}\\\\}`,\n flags\n );\n}\n\nfunction referencesInTemplate(template: string, fieldName: string): boolean {\n return templatePattern(fieldName).test(template);\n}\n\n/** What the form's settings say about one response per person. */\nexport type OneResponseChoice = Pick<\n FormSettings,\n \"oneResponse\" | \"oneResponseField\"\n>;\n\n/** The label a setting that refers to a field is named by. */\nconst ONE_RESPONSE_SETTING = \"One response per person\";\n\n/**\n * The field one response per email compares, when the form takes one\n * response per email address; none otherwise.\n */\nexport function oneResponseEmailField(\n settings: OneResponseChoice | undefined\n): string | undefined {\n return settings?.oneResponse === \"email\"\n ? settings.oneResponseField\n : undefined;\n}\n\n/**\n * All references to `fieldName` across the form's fields, notifications and\n * settings (the Email field one response per person compares).\n */\nexport function findFieldReferences(\n fieldName: string,\n fields: readonly AnyFormField[],\n notifications: readonly FormNotification[],\n settings?: OneResponseChoice\n): FieldReference[] {\n const references: FieldReference[] = [];\n if (oneResponseEmailField(settings) === fieldName) {\n references.push({ kind: \"setting\", label: ONE_RESPONSE_SETTING });\n }\n\n for (const field of fields) {\n if (field.name === fieldName) continue;\n // Disabled conditional logic has no runtime effect, so it must not block\n // deletion; the dormant condition just goes stale.\n if (!field.conditionalLogic?.enabled) continue;\n const conditions = field.conditionalLogic.conditions ?? [];\n if (conditions.some(condition => condition.field === fieldName)) {\n references.push({ kind: \"condition\", label: field.label || field.name });\n }\n }\n\n for (const notification of notifications) {\n // A disabled rule has no runtime effect — like disabled conditional\n // logic above, it must not block deletion; its stale reference just\n // needs re-pointing if the rule is ever re-enabled.\n if (!notification.enabled) continue;\n // Field-sourced recipients and reply-to always carry the {{fieldName}}\n // syntax (the send path's resolveFieldRef only matches that form), so\n // one template check covers to/cc/bcc/replyTo uniformly.\n const templates = [\n notification.to,\n ...(notification.cc ?? []),\n ...(notification.bcc ?? []),\n notification.replyTo,\n ].filter((value): value is string => typeof value === \"string\");\n const referencedByTemplate = templates.some(template =>\n referencesInTemplate(template, fieldName)\n );\n // Send-conditions name the field directly (no interpolation syntax),\n // read through the notification model so one condition and several\n // count alike; so does a file field the notification attaches by name.\n const referencedByCondition = conditionsOf(notification).list.some(\n condition => condition.field === fieldName\n );\n const referencedByAttachment =\n notification.attachFields?.includes(fieldName) ?? false;\n if (\n referencedByTemplate ||\n referencedByCondition ||\n referencedByAttachment\n ) {\n references.push({\n kind: \"notification\",\n label: notification.name || \"Notification\",\n });\n }\n }\n\n return references;\n}\n\n/**\n * Reference lists for every field in one pass, keyed by field name, so a\n * per-card lookup is O(1) instead of re-walking fields and notifications for\n * each card on every render.\n */\nexport function buildFieldReferenceMap(\n fields: readonly AnyFormField[],\n notifications: readonly FormNotification[],\n settings?: OneResponseChoice\n): Map<string, FieldReference[]> {\n const map = new Map<string, FieldReference[]>();\n for (const field of fields) {\n map.set(\n field.name,\n findFieldReferences(field.name, fields, notifications, settings)\n );\n }\n return map;\n}\n\n/**\n * The fields with every condition on `from` naming `to` instead: the\n * conditions `findFieldReferences` reads, disabled ones too, so a rename never\n * leaves one pointing at a field that no longer has that name.\n */\nexport function renameInFieldConditions(\n fields: readonly AnyFormField[],\n from: string,\n to: string\n): AnyFormField[] {\n return fields.map(field => {\n const logic = field.conditionalLogic;\n if (!logic?.conditions?.some(condition => condition.field === from)) {\n return field;\n }\n return {\n ...field,\n conditionalLogic: {\n ...logic,\n conditions: logic.conditions.map(condition =>\n condition.field === from ? { ...condition, field: to } : condition\n ),\n },\n };\n });\n}\n\n/**\n * The notifications with every reference to `from` naming `to` instead: the\n * addresses (`{{ from }}`), the conditions and the attached file fields\n * `findFieldReferences` reads.\n */\nexport function renameInNotifications(\n notifications: readonly FormNotification[],\n from: string,\n to: string\n): FormNotification[] {\n const pattern = templatePattern(from, \"g\");\n const rename = (template: string): string =>\n template.replace(pattern, `{{${to}}}`);\n return notifications.map(notification => {\n // Stored notifications can predate the cc and bcc lists, so each part is\n // renamed only where it is there.\n const { cc, bcc } = notification as Partial<FormNotification>;\n const next: FormNotification = {\n ...notification,\n to: rename(notification.to),\n };\n if (cc) next.cc = cc.map(rename);\n if (bcc) next.bcc = bcc.map(rename);\n if (notification.replyTo !== undefined) {\n next.replyTo = rename(notification.replyTo);\n }\n if (notification.condition?.field === from) {\n next.condition = { ...notification.condition, field: to };\n }\n if (notification.conditions) {\n next.conditions = {\n ...notification.conditions,\n list: notification.conditions.list.map(condition =>\n condition.field === from ? { ...condition, field: to } : condition\n ),\n };\n }\n if (notification.attachFields) {\n next.attachFields = notification.attachFields.map(name =>\n name === from ? to : name\n );\n }\n return next;\n });\n}\n","/**\n * Who sent a submission, as the admin names it: the first answer to a text\n * field named or labelled as a name, else the first email address answered,\n * else none. One rule, read by the write seam (which keeps it as the\n * submission's title, the admin's breadcrumb and list name) and by the\n * submission's page.\n *\n * @module utils/submission-title\n */\n\n/**\n * The longest title kept, in characters. The title is a display copy of an\n * answer, stored as a text column, which MySQL holds to 255 characters; an\n * answer has no such limit, so a longer one is shortened here rather than\n * refusing the submission it names.\n */\nconst TITLE_MAX = 200;\n\n/**\n * The title a submission is stored with: who sent it, shortened to\n * TITLE_MAX characters with an ellipsis, or none.\n */\nexport function submissionTitleOf(\n fields: readonly NamedField[],\n data: Record<string, unknown>\n): string | undefined {\n const sender = senderOf(fields, data);\n if (sender === undefined) return undefined;\n // By code point, so a character outside the basic plane is never split.\n const characters = Array.from(sender);\n return characters.length <= TITLE_MAX\n ? sender\n : `${characters\n .slice(0, TITLE_MAX - 1)\n .join(\"\")\n .trimEnd()}…`;\n}\n\n/** A field as this needs it: its name, label and type. */\ninterface NamedField {\n name: string;\n label?: string;\n type: string;\n}\n\nexport function senderOf(\n fields: readonly NamedField[],\n data: Record<string, unknown>\n): string | undefined {\n const answer = (name: string) => {\n const value = data[name];\n return typeof value === \"string\" ? value.trim() : \"\";\n };\n const named = fields.find(\n field =>\n field.type === \"text\" &&\n /name/i.test(`${field.name} ${field.label ?? \"\"}`) &&\n answer(field.name)\n );\n if (named) return answer(named.name);\n const email = fields.find(\n field => field.type === \"email\" && answer(field.name)\n );\n return email ? answer(email.name) : undefined;\n}\n"],"mappings":"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"}