@10x-media/form-builder 0.1.0-beta.25 → 0.1.0-beta.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -21
  3. package/dist/actions/body/serializeBody.d.ts +10 -14
  4. package/dist/actions/body/serializeBody.js +5 -10
  5. package/dist/actions/body/serializeBody.js.map +1 -1
  6. package/dist/actions/builtin/emailAction.d.ts +7 -0
  7. package/dist/actions/builtin/emailAction.js +14 -7
  8. package/dist/actions/builtin/emailAction.js.map +1 -1
  9. package/dist/actions/defineAction.d.ts +2 -4
  10. package/dist/actions/defineAction.js.map +1 -1
  11. package/dist/actions/dispatch.js +1 -1
  12. package/dist/actions/emailRender.d.ts +5 -5
  13. package/dist/actions/fromAddresses.d.ts +3 -1
  14. package/dist/actions/fromAddresses.js.map +1 -1
  15. package/dist/actions/recipientSources.d.ts +6 -17
  16. package/dist/actions/recipientSources.js.map +1 -1
  17. package/dist/actions/submissionContext.d.ts +34 -0
  18. package/dist/actions/task.js +12 -15
  19. package/dist/actions/task.js.map +1 -1
  20. package/dist/aggregation/aggregateResponses.js +3 -4
  21. package/dist/aggregation/aggregateResponses.js.map +1 -1
  22. package/dist/aggregation/resolveResultsRequest.js +38 -24
  23. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  24. package/dist/client/EndpointOptionsSelect.d.ts +1 -0
  25. package/dist/client/EndpointOptionsSelect.js +1 -0
  26. package/dist/client/EndpointOptionsSelect.js.map +1 -1
  27. package/dist/client/FieldNameSelect.d.ts +1 -0
  28. package/dist/client/FieldNameSelect.js +1 -0
  29. package/dist/client/FieldNameSelect.js.map +1 -1
  30. package/dist/client/RecipientsSelect.d.ts +1 -0
  31. package/dist/client/RecipientsSelect.js +1 -0
  32. package/dist/client/RecipientsSelect.js.map +1 -1
  33. package/dist/collections/formSubmissions.js +9 -10
  34. package/dist/collections/formSubmissions.js.map +1 -1
  35. package/dist/collections/forms.js +10 -10
  36. package/dist/collections/forms.js.map +1 -1
  37. package/dist/consent/resolveConsentSourcesRequest.js +2 -2
  38. package/dist/consent/resolveConsentSourcesRequest.js.map +1 -1
  39. package/dist/exports/react.d.ts +2 -2
  40. package/dist/exports/react.js +2 -2
  41. package/dist/exports/rsc.d.ts +3 -2
  42. package/dist/exports/rsc.js +5 -4
  43. package/dist/exports/types.d.ts +2 -1
  44. package/dist/form/findFormAtLocale.d.ts +30 -0
  45. package/dist/form/findFormAtLocale.js +88 -0
  46. package/dist/form/findFormAtLocale.js.map +1 -0
  47. package/dist/index.d.ts +5 -2
  48. package/dist/index.js +19 -11
  49. package/dist/index.js.map +1 -1
  50. package/dist/options.d.ts +39 -2
  51. package/dist/plugin/collectionSlugs.d.ts +28 -0
  52. package/dist/plugin/collectionSlugs.js +65 -0
  53. package/dist/plugin/collectionSlugs.js.map +1 -0
  54. package/dist/plugin/customState.js +1 -1
  55. package/dist/plugin/customState.js.map +1 -1
  56. package/dist/plugin/registerCollections.js +9 -4
  57. package/dist/plugin/registerCollections.js.map +1 -1
  58. package/dist/poll/closeJob.js +2 -2
  59. package/dist/poll/closeJob.js.map +1 -1
  60. package/dist/poll/outcomeBeforeChange.js +2 -2
  61. package/dist/poll/outcomeBeforeChange.js.map +1 -1
  62. package/dist/poll/resolvePollCloseRequest.js +3 -3
  63. package/dist/poll/resolvePollCloseRequest.js.map +1 -1
  64. package/dist/poll/resolvePollOptionsRequest.js +2 -2
  65. package/dist/poll/resolvePollOptionsRequest.js.map +1 -1
  66. package/dist/poll/resolvePollOutcome.js +4 -4
  67. package/dist/poll/resolvePollOutcome.js.map +1 -1
  68. package/dist/poll/votes/aggregateFromVotes.js +3 -2
  69. package/dist/poll/votes/aggregateFromVotes.js.map +1 -1
  70. package/dist/poll/votes/bumpPollVote.js +9 -6
  71. package/dist/poll/votes/bumpPollVote.js.map +1 -1
  72. package/dist/poll/votes/recountPollVotes.js +5 -5
  73. package/dist/poll/votes/recountPollVotes.js.map +1 -1
  74. package/dist/poll/votes/voteTallyHook.js +3 -3
  75. package/dist/poll/votes/voteTallyHook.js.map +1 -1
  76. package/dist/poll/votes/votesCollection.d.ts +5 -1
  77. package/dist/poll/votes/votesCollection.js +7 -2
  78. package/dist/poll/votes/votesCollection.js.map +1 -1
  79. package/dist/react/Form.d.ts +20 -7
  80. package/dist/react/Form.js +19 -13
  81. package/dist/react/Form.js.map +1 -1
  82. package/dist/react/Poll.d.ts +1 -1
  83. package/dist/react/Poll.js +7 -2
  84. package/dist/react/Poll.js.map +1 -1
  85. package/dist/react/fetchResults.d.ts +6 -3
  86. package/dist/react/fetchResults.js +10 -5
  87. package/dist/react/fetchResults.js.map +1 -1
  88. package/dist/react/submitForm.d.ts +12 -7
  89. package/dist/react/submitForm.js +5 -3
  90. package/dist/react/submitForm.js.map +1 -1
  91. package/dist/submissions/createSubmission.d.ts +7 -1
  92. package/dist/submissions/createSubmission.js +10 -5
  93. package/dist/submissions/createSubmission.js.map +1 -1
  94. package/dist/submissions/resolveVotedSubmission.js +5 -5
  95. package/dist/submissions/resolveVotedSubmission.js.map +1 -1
  96. package/dist/submissions/runSubmission.js +1 -1
  97. package/dist/submissions/submissionLocale.js +27 -6
  98. package/dist/submissions/submissionLocale.js.map +1 -1
  99. package/dist/submissions/validateSubmission.js +110 -103
  100. package/dist/submissions/validateSubmission.js.map +1 -1
  101. package/dist/submissions/voteChangeEndpoint.js +10 -9
  102. package/dist/submissions/voteChangeEndpoint.js.map +1 -1
  103. package/dist/submissions/votedCookie.d.ts +8 -4
  104. package/dist/submissions/votedCookie.js +18 -15
  105. package/dist/submissions/votedCookie.js.map +1 -1
  106. package/dist/validation/builtin/notAlreadySubmitted.js +2 -2
  107. package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
  108. package/package.json +7 -5
@@ -1 +1 @@
1
- {"version":3,"file":"bumpPollVote.js","names":[],"sources":["../../../src/poll/votes/bumpPollVote.ts"],"sourcesContent":["import type { Payload } from 'payload'\nimport { POLL_VOTES_SLUG, VOTE_SHARDS } from './votesCollection'\n\nconst PG_TABLE_KEY = 'form_poll_votes'\n\ntype PostgresSqlModule = { sql: typeof import('@payloadcms/db-postgres')['sql'] }\n\n/**\n * Loads the optional Postgres peer without a literal specifier: bundlers (Turbopack, webpack,\n * Vite) resolve literal dynamic imports at build time, which fails a Mongo host's build even\n * though this branch is unreachable there. The function-built specifier is opaque to their\n * analyzers, the ignore comments cover bundlers that would still warn, and the type-only\n * reference above erases at compile time, so the package is touched only when the Postgres\n * branch actually runs.\n */\nconst importPostgresSql = (): Promise<PostgresSqlModule> => {\n\tconst specifier = ['@payloadcms', 'db-postgres'].join('/')\n\treturn import(\n\t\t/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ specifier\n\t) as Promise<PostgresSqlModule>\n}\n\n// payload.db raw-access shapes are intentionally loose; these narrow casts reach the\n// Mongoose driver collection / Drizzle instance for atomic upserts (no public typed API).\ntype MongoDb = {\n\tname: 'mongoose'\n\tcollections: Record<\n\t\tstring,\n\t\t{ collection: { updateOne: (f: object, u: object, o: object) => Promise<unknown> } }\n\t>\n\tsessions?: Record<number | string, unknown>\n}\ntype PgInsert = {\n\tinsert: (t: unknown) => {\n\t\tvalues: (v: unknown) => { onConflictDoUpdate: (c: unknown) => Promise<unknown> }\n\t}\n}\ntype PgDb = {\n\tname: 'postgres'\n\tdrizzle: PgInsert\n\tsessions?: Record<number | string, { db: PgInsert }>\n\ttables: Record<string, Record<string, unknown>>\n\ttableNameMap: Map<string, string>\n}\n\n/**\n * Atomically bumps one (form, field, value) tally by `by` in a single upsert-increment\n * statement (Mongo `$inc` + upsert, Postgres `INSERT ... ON CONFLICT DO UPDATE`); the unique\n * compound index makes concurrent bumps for the same key safe without read-modify-write races.\n * The shard column is internal: transactional Mongo bumps pick a random shard in\n * [0, VOTE_SHARDS) so concurrent transactions rarely write the same document (see VOTE_SHARDS);\n * Postgres and non-transactional Mongo always bump shard 0. Readers sum across shards.\n *\n * When `transactionID` names an open Payload transaction, the write joins it: a bump failure\n * thrown from the submission hook rolls back the submission create (no undercount), and an\n * aborted create rolls back the joined bump (no overcount). Residual risk on transactional\n * Mongo: two concurrent bumps that land on the same shard still abort one transaction\n * (WriteConflict, labelled TransientTransactionError); the losing submission rolls back whole,\n * so counts stay consistent and the client can safely resubmit. Without a transaction (e.g.\n * Mongo with transactions disabled) the write lands on the root handle immediately; a later\n * recount from stored submissions is the healer for any drift that window allows.\n */\n// biome-ignore lint/complexity/useMaxParams: write primitive signature (payload, key, by, transactionID)\nexport async function bumpPollVote(\n\tpayload: Payload,\n\tkey: { form: string; field: string; value: string },\n\tby: number,\n\ttransactionID?: number | string\n): Promise<void> {\n\tif (payload.db.name === 'mongoose') {\n\t\tconst db = payload.db as unknown as MongoDb\n\t\tconst model = db.collections[POLL_VOTES_SLUG]\n\t\tif (!model) throw new Error(`form-builder: mongoose collection \"${POLL_VOTES_SLUG}\" not found`)\n\t\tconst session = transactionID !== undefined ? db.sessions?.[transactionID] : undefined\n\t\tconst shard = session ? Math.floor(Math.random() * VOTE_SHARDS) : 0\n\t\tconst shardedKey = { ...key, shard }\n\t\tconst update = { $inc: { count: by }, $setOnInsert: shardedKey }\n\t\tconst options = session ? { upsert: true, session } : { upsert: true }\n\t\ttry {\n\t\t\tawait model.collection.updateOne(shardedKey, update, options)\n\t\t} catch (error) {\n\t\t\t// Concurrent first inserts for a new key can race the upsert into E11000; outside a\n\t\t\t// transaction the row now exists, so one retry takes the $inc branch. Inside a\n\t\t\t// transaction the error propagates and Payload's rollback/retry semantics apply.\n\t\t\tconst duplicate = (error as { code?: unknown } | null)?.code === 11000\n\t\t\tif (session || !duplicate) throw error\n\t\t\tawait model.collection.updateOne(shardedKey, update, options)\n\t\t}\n\t\treturn\n\t}\n\tconst { sql } = await importPostgresSql()\n\tconst db = payload.db as unknown as PgDb\n\tconst tableName = db.tableNameMap.get(PG_TABLE_KEY)\n\tif (!tableName) throw new Error(`form-builder: drizzle table \"${PG_TABLE_KEY}\" not found`)\n\tconst table = db.tables[tableName]\n\tif (!table) throw new Error(`form-builder: drizzle table object for \"${tableName}\" not found`)\n\tconst txn = transactionID !== undefined ? db.sessions?.[transactionID]?.db : undefined\n\tawait (txn ?? db.drizzle)\n\t\t.insert(table)\n\t\t.values({ ...key, shard: 0, count: by })\n\t\t.onConflictDoUpdate({\n\t\t\ttarget: [table.form, table.field, table.value, table.shard],\n\t\t\tset: { count: sql`${table.count} + ${by}` },\n\t\t})\n}\n"],"mappings":";;AAGA,MAAM,eAAe;;;;;;;;;AAYrB,MAAM,0BAAsD;CAE3D,OAAO;;;;EADW,CAAC,eAAe,aAAa,EAAE,KAAK,GAE4B;;AAEnF;;;;;;;;;;;;;;;;;;AA2CA,eAAsB,aACrB,SACA,KACA,IACA,eACgB;CAChB,IAAI,QAAQ,GAAG,SAAS,YAAY;EACnC,MAAM,KAAK,QAAQ;EACnB,MAAM,QAAQ,GAAG,YAAY;EAC7B,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,sCAAsC,gBAAgB,YAAY;EAC9F,MAAM,UAAU,kBAAkB,KAAA,IAAY,GAAG,WAAW,iBAAiB,KAAA;EAC7E,MAAM,QAAQ,UAAU,KAAK,MAAM,KAAK,OAAO,IAAA,CAAe,IAAI;EAClE,MAAM,aAAa;GAAE,GAAG;GAAK;EAAM;EACnC,MAAM,SAAS;GAAE,MAAM,EAAE,OAAO,GAAG;GAAG,cAAc;EAAW;EAC/D,MAAM,UAAU,UAAU;GAAE,QAAQ;GAAM;EAAQ,IAAI,EAAE,QAAQ,KAAK;EACrE,IAAI;GACH,MAAM,MAAM,WAAW,UAAU,YAAY,QAAQ,OAAO;EAC7D,SAAS,OAAO;GAIf,MAAM,YAAa,OAAqC,SAAS;GACjE,IAAI,WAAW,CAAC,WAAW,MAAM;GACjC,MAAM,MAAM,WAAW,UAAU,YAAY,QAAQ,OAAO;EAC7D;EACA;CACD;CACA,MAAM,EAAE,QAAQ,MAAM,kBAAkB;CACxC,MAAM,KAAK,QAAQ;CACnB,MAAM,YAAY,GAAG,aAAa,IAAI,YAAY;CAClD,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,gCAAgC,aAAa,YAAY;CACzF,MAAM,QAAQ,GAAG,OAAO;CACxB,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,2CAA2C,UAAU,YAAY;CAE7F,QADY,kBAAkB,KAAA,IAAY,GAAG,WAAW,gBAAgB,KAAK,KAAA,MAC/D,GAAG,SACf,OAAO,KAAK,EACZ,OAAO;EAAE,GAAG;EAAK,OAAO;EAAG,OAAO;CAAG,CAAC,EACtC,mBAAmB;EACnB,QAAQ;GAAC,MAAM;GAAM,MAAM;GAAO,MAAM;GAAO,MAAM;EAAK;EAC1D,KAAK,EAAE,OAAO,GAAG,GAAG,MAAM,MAAM,KAAK,KAAK;CAC3C,CAAC;AACH"}
1
+ {"version":3,"file":"bumpPollVote.js","names":[],"sources":["../../../src/poll/votes/bumpPollVote.ts"],"sourcesContent":["import type { Payload } from 'payload'\nimport toSnakeCase from 'to-snake-case'\nimport { pluginSlugsOf } from '../../plugin/collectionSlugs'\nimport { VOTE_SHARDS } from './votesCollection'\n\ntype PostgresSqlModule = { sql: typeof import('@payloadcms/db-postgres')['sql'] }\n\n/**\n * Loads the optional Postgres peer without a literal specifier: bundlers (Turbopack, webpack,\n * Vite) resolve literal dynamic imports at build time, which fails a Mongo host's build even\n * though this branch is unreachable there. The function-built specifier is opaque to their\n * analyzers, the ignore comments cover bundlers that would still warn, and the type-only\n * reference above erases at compile time, so the package is touched only when the Postgres\n * branch actually runs.\n */\nconst importPostgresSql = (): Promise<PostgresSqlModule> => {\n\tconst specifier = ['@payloadcms', 'db-postgres'].join('/')\n\treturn import(\n\t\t/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ specifier\n\t) as Promise<PostgresSqlModule>\n}\n\n// payload.db raw-access shapes are intentionally loose; these narrow casts reach the\n// Mongoose driver collection / Drizzle instance for atomic upserts (no public typed API).\ntype MongoDb = {\n\tname: 'mongoose'\n\tcollections: Record<\n\t\tstring,\n\t\t{ collection: { updateOne: (f: object, u: object, o: object) => Promise<unknown> } }\n\t>\n\tsessions?: Record<number | string, unknown>\n}\ntype PgInsert = {\n\tinsert: (t: unknown) => {\n\t\tvalues: (v: unknown) => { onConflictDoUpdate: (c: unknown) => Promise<unknown> }\n\t}\n}\ntype PgDb = {\n\tname: 'postgres'\n\tdrizzle: PgInsert\n\tsessions?: Record<number | string, { db: PgInsert }>\n\ttables: Record<string, Record<string, unknown>>\n\ttableNameMap: Map<string, string>\n}\n\n/**\n * Atomically bumps one (form, field, value) tally by `by` in a single upsert-increment\n * statement (Mongo `$inc` + upsert, Postgres `INSERT ... ON CONFLICT DO UPDATE`); the unique\n * compound index makes concurrent bumps for the same key safe without read-modify-write races.\n * The shard column is internal: transactional Mongo bumps pick a random shard in\n * [0, VOTE_SHARDS) so concurrent transactions rarely write the same document (see VOTE_SHARDS);\n * Postgres and non-transactional Mongo always bump shard 0. Readers sum across shards.\n *\n * When `transactionID` names an open Payload transaction, the write joins it: a bump failure\n * thrown from the submission hook rolls back the submission create (no undercount), and an\n * aborted create rolls back the joined bump (no overcount). Residual risk on transactional\n * Mongo: two concurrent bumps that land on the same shard still abort one transaction\n * (WriteConflict, labelled TransientTransactionError); the losing submission rolls back whole,\n * so counts stay consistent and the client can safely resubmit. Without a transaction (e.g.\n * Mongo with transactions disabled) the write lands on the root handle immediately; a later\n * recount from stored submissions is the healer for any drift that window allows.\n */\n// biome-ignore lint/complexity/useMaxParams: write primitive signature (payload, key, by, transactionID)\nexport async function bumpPollVote(\n\tpayload: Payload,\n\tkey: { form: string; field: string; value: string },\n\tby: number,\n\ttransactionID?: number | string\n): Promise<void> {\n\tconst slug = pluginSlugsOf(payload).pollVotes\n\tif (payload.db.name === 'mongoose') {\n\t\tconst db = payload.db as unknown as MongoDb\n\t\tconst model = db.collections[slug]\n\t\tif (!model) throw new Error(`form-builder: mongoose collection \"${slug}\" not found`)\n\t\tconst session = transactionID !== undefined ? db.sessions?.[transactionID] : undefined\n\t\tconst shard = session ? Math.floor(Math.random() * VOTE_SHARDS) : 0\n\t\tconst shardedKey = { ...key, shard }\n\t\tconst update = { $inc: { count: by }, $setOnInsert: shardedKey }\n\t\tconst options = session ? { upsert: true, session } : { upsert: true }\n\t\ttry {\n\t\t\tawait model.collection.updateOne(shardedKey, update, options)\n\t\t} catch (error) {\n\t\t\t// Concurrent first inserts for a new key can race the upsert into E11000; outside a\n\t\t\t// transaction the row now exists, so one retry takes the $inc branch. Inside a\n\t\t\t// transaction the error propagates and Payload's rollback/retry semantics apply.\n\t\t\tconst duplicate = (error as { code?: unknown } | null)?.code === 11000\n\t\t\tif (session || !duplicate) throw error\n\t\t\tawait model.collection.updateOne(shardedKey, update, options)\n\t\t}\n\t\treturn\n\t}\n\tconst { sql } = await importPostgresSql()\n\tconst db = payload.db as unknown as PgDb\n\t// Payload keys every collection's table by its snake-cased slug, whatever `dbName` renames it to.\n\tconst tableKey = toSnakeCase(slug)\n\tconst tableName = db.tableNameMap.get(tableKey)\n\tif (!tableName) throw new Error(`form-builder: drizzle table \"${tableKey}\" not found`)\n\tconst table = db.tables[tableName]\n\tif (!table) throw new Error(`form-builder: drizzle table object for \"${tableName}\" not found`)\n\tconst txn = transactionID !== undefined ? db.sessions?.[transactionID]?.db : undefined\n\tawait (txn ?? db.drizzle)\n\t\t.insert(table)\n\t\t.values({ ...key, shard: 0, count: by })\n\t\t.onConflictDoUpdate({\n\t\t\ttarget: [table.form, table.field, table.value, table.shard],\n\t\t\tset: { count: sql`${table.count} + ${by}` },\n\t\t})\n}\n"],"mappings":";;;;;;;;;;;;AAeA,MAAM,0BAAsD;CAE3D,OAAO;;;;EADW,CAAC,eAAe,aAAa,EAAE,KAAK,GAE4B;;AAEnF;;;;;;;;;;;;;;;;;;AA2CA,eAAsB,aACrB,SACA,KACA,IACA,eACgB;CAChB,MAAM,OAAO,cAAc,OAAO,EAAE;CACpC,IAAI,QAAQ,GAAG,SAAS,YAAY;EACnC,MAAM,KAAK,QAAQ;EACnB,MAAM,QAAQ,GAAG,YAAY;EAC7B,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,sCAAsC,KAAK,YAAY;EACnF,MAAM,UAAU,kBAAkB,KAAA,IAAY,GAAG,WAAW,iBAAiB,KAAA;EAC7E,MAAM,QAAQ,UAAU,KAAK,MAAM,KAAK,OAAO,IAAA,CAAe,IAAI;EAClE,MAAM,aAAa;GAAE,GAAG;GAAK;EAAM;EACnC,MAAM,SAAS;GAAE,MAAM,EAAE,OAAO,GAAG;GAAG,cAAc;EAAW;EAC/D,MAAM,UAAU,UAAU;GAAE,QAAQ;GAAM;EAAQ,IAAI,EAAE,QAAQ,KAAK;EACrE,IAAI;GACH,MAAM,MAAM,WAAW,UAAU,YAAY,QAAQ,OAAO;EAC7D,SAAS,OAAO;GAIf,MAAM,YAAa,OAAqC,SAAS;GACjE,IAAI,WAAW,CAAC,WAAW,MAAM;GACjC,MAAM,MAAM,WAAW,UAAU,YAAY,QAAQ,OAAO;EAC7D;EACA;CACD;CACA,MAAM,EAAE,QAAQ,MAAM,kBAAkB;CACxC,MAAM,KAAK,QAAQ;CAEnB,MAAM,WAAW,YAAY,IAAI;CACjC,MAAM,YAAY,GAAG,aAAa,IAAI,QAAQ;CAC9C,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,gCAAgC,SAAS,YAAY;CACrF,MAAM,QAAQ,GAAG,OAAO;CACxB,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,2CAA2C,UAAU,YAAY;CAE7F,QADY,kBAAkB,KAAA,IAAY,GAAG,WAAW,gBAAgB,KAAK,KAAA,MAC/D,GAAG,SACf,OAAO,KAAK,EACZ,OAAO;EAAE,GAAG;EAAK,OAAO;EAAG,OAAO;CAAG,CAAC,EACtC,mBAAmB;EACnB,QAAQ;GAAC,MAAM;GAAM,MAAM;GAAO,MAAM;GAAO,MAAM;EAAK;EAC1D,KAAK,EAAE,OAAO,GAAG,GAAG,MAAM,MAAM,KAAK,KAAK;CAC3C,CAAC;AACH"}
@@ -1,9 +1,9 @@
1
+ import { pluginSlugsOf } from "../../plugin/collectionSlugs.js";
1
2
  import { pollConfigOf } from "../../form/pollState.js";
2
- import { aggregateFieldResponses } from "../../aggregation/aggregateResponses.js";
3
- import { POLL_VOTES_SLUG } from "./votesCollection.js";
4
- import { FORMS_SLUG } from "../../collections/forms.js";
3
+ import "./votesCollection.js";
5
4
  import { bumpPollVote } from "./bumpPollVote.js";
6
5
  import { transactionIDOf } from "./voteTallyHook.js";
6
+ import { aggregateFieldResponses } from "../../aggregation/aggregateResponses.js";
7
7
  //#region src/poll/votes/recountPollVotes.ts
8
8
  const RECOUNT_MAX_SUBMISSIONS = 1e5;
9
9
  /**
@@ -18,7 +18,7 @@ const RECOUNT_MAX_SUBMISSIONS = 1e5;
18
18
  const recountPollVotes = async (args) => {
19
19
  const { payload, formId, req } = args;
20
20
  const form = await payload.findByID({
21
- collection: FORMS_SLUG,
21
+ collection: pluginSlugsOf(payload).forms,
22
22
  id: formId,
23
23
  depth: 0,
24
24
  overrideAccess: true,
@@ -37,7 +37,7 @@ const recountPollVotes = async (args) => {
37
37
  });
38
38
  if (aggregation?.truncated) throw new Error(`form-builder: recount for form ${String(formId)} aborted: more than ${RECOUNT_MAX_SUBMISSIONS} submissions, the scan was truncated and a rebuilt tally would be partial.`);
39
39
  await payload.delete({
40
- collection: POLL_VOTES_SLUG,
40
+ collection: pluginSlugsOf(payload).pollVotes,
41
41
  where: { and: [{ form: { equals: String(formId) } }, { field: { equals: resultsField } }] },
42
42
  overrideAccess: true,
43
43
  req
@@ -1 +1 @@
1
- {"version":3,"file":"recountPollVotes.js","names":[],"sources":["../../../src/poll/votes/recountPollVotes.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { aggregateFieldResponses } from '../../aggregation/aggregateResponses'\nimport { FORMS_SLUG } from '../../collections/forms'\nimport { pollConfigOf } from '../../form/pollState'\nimport { bumpPollVote } from './bumpPollVote'\nimport { POLL_VOTES_SLUG, RESPONDENTS_VALUE } from './votesCollection'\nimport { transactionIDOf } from './voteTallyHook'\n\nconst RECOUNT_MAX_SUBMISSIONS = 100_000\n\n/**\n * Rebuild a form's tally rows from persisted submissions (drift healing after out-of-band DB\n * changes, or backfill after enabling a poll late). Scans first and refuses a truncated\n * aggregation, then deletes the form's tally rows for the results field and replays the scan\n * into fresh rows, so the store is never left holding a knowingly partial count. Writes join\n * the request's transaction when one is open. Refuses a form with `persistSubmissions` off:\n * its submissions are pruned, so the tally store is the only record of its votes and a rebuild\n * would wipe them.\n */\nexport const recountPollVotes = async (args: {\n\tpayload: Payload\n\tformId: number | string\n\treq?: PayloadRequest\n}): Promise<void> => {\n\tconst { payload, formId, req } = args\n\tconst form = await payload\n\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t.catch(() => null)\n\tconst poll = form ? pollConfigOf(form.poll) : undefined\n\tconst resultsField =\n\t\ttypeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t? poll.resultsField\n\t\t\t: undefined\n\tif (!resultsField) {\n\t\treturn\n\t}\n\tif (form?.persistSubmissions === false) {\n\t\tthrow new Error(\n\t\t\t`form-builder: recountPollVotes cannot rebuild form \"${String(formId)}\": it does not persist submissions, so the tally store is the only record of its votes.`\n\t\t)\n\t}\n\tconst aggregation = await aggregateFieldResponses({\n\t\tpayload,\n\t\tformId,\n\t\tfield: resultsField,\n\t\treq,\n\t\tmaxSubmissions: RECOUNT_MAX_SUBMISSIONS,\n\t})\n\tif (aggregation?.truncated) {\n\t\tthrow new Error(\n\t\t\t`form-builder: recount for form ${String(formId)} aborted: more than ${RECOUNT_MAX_SUBMISSIONS} submissions, the scan was truncated and a rebuilt tally would be partial.`\n\t\t)\n\t}\n\tawait payload.delete({\n\t\tcollection: POLL_VOTES_SLUG,\n\t\twhere: {\n\t\t\tand: [{ form: { equals: String(formId) } }, { field: { equals: resultsField } }],\n\t\t},\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\tif (!aggregation) {\n\t\treturn\n\t}\n\tconst transactionID = transactionIDOf(req?.transactionID)\n\tconst base = { form: String(formId), field: resultsField }\n\tfor (const bucket of aggregation.buckets) {\n\t\tif (bucket.count > 0) {\n\t\t\tawait bumpPollVote(payload, { ...base, value: bucket.value }, bucket.count, transactionID)\n\t\t}\n\t}\n\tif (aggregation.total > 0) {\n\t\tawait bumpPollVote(\n\t\t\tpayload,\n\t\t\t{ ...base, value: RESPONDENTS_VALUE },\n\t\t\taggregation.total,\n\t\t\ttransactionID\n\t\t)\n\t}\n}\n"],"mappings":";;;;;;;AAQA,MAAM,0BAA0B;;;;;;;;;;AAWhC,MAAa,mBAAmB,OAAO,SAIlB;CACpB,MAAM,EAAE,SAAS,QAAQ,QAAQ;CACjC,MAAM,OAAO,MAAM,QACjB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,MAAM,OAAO,OAAO,aAAa,KAAK,IAAI,IAAI,KAAA;CAC9C,MAAM,eACL,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IAClE,KAAK,eACL,KAAA;CACJ,IAAI,CAAC,cACJ;CAED,IAAI,MAAM,uBAAuB,OAChC,MAAM,IAAI,MACT,uDAAuD,OAAO,MAAM,EAAE,wFACvE;CAED,MAAM,cAAc,MAAM,wBAAwB;EACjD;EACA;EACA,OAAO;EACP;EACA,gBAAgB;CACjB,CAAC;CACD,IAAI,aAAa,WAChB,MAAM,IAAI,MACT,kCAAkC,OAAO,MAAM,EAAE,sBAAsB,wBAAwB,2EAChG;CAED,MAAM,QAAQ,OAAO;EACpB,YAAY;EACZ,OAAO,EACN,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,OAAO,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,aAAa,EAAE,CAAC,EAChF;EACA,gBAAgB;EAChB;CACD,CAAC;CACD,IAAI,CAAC,aACJ;CAED,MAAM,gBAAgB,gBAAgB,KAAK,aAAa;CACxD,MAAM,OAAO;EAAE,MAAM,OAAO,MAAM;EAAG,OAAO;CAAa;CACzD,KAAK,MAAM,UAAU,YAAY,SAChC,IAAI,OAAO,QAAQ,GAClB,MAAM,aAAa,SAAS;EAAE,GAAG;EAAM,OAAO,OAAO;CAAM,GAAG,OAAO,OAAO,aAAa;CAG3F,IAAI,YAAY,QAAQ,GACvB,MAAM,aACL,SACA;EAAE,GAAG;EAAM,OAAA;CAAyB,GACpC,YAAY,OACZ,aACD;AAEF"}
1
+ {"version":3,"file":"recountPollVotes.js","names":[],"sources":["../../../src/poll/votes/recountPollVotes.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { aggregateFieldResponses } from '../../aggregation/aggregateResponses'\nimport { pollConfigOf } from '../../form/pollState'\nimport { pluginSlugsOf } from '../../plugin/collectionSlugs'\nimport { bumpPollVote } from './bumpPollVote'\nimport { RESPONDENTS_VALUE } from './votesCollection'\nimport { transactionIDOf } from './voteTallyHook'\n\nconst RECOUNT_MAX_SUBMISSIONS = 100_000\n\n/**\n * Rebuild a form's tally rows from persisted submissions (drift healing after out-of-band DB\n * changes, or backfill after enabling a poll late). Scans first and refuses a truncated\n * aggregation, then deletes the form's tally rows for the results field and replays the scan\n * into fresh rows, so the store is never left holding a knowingly partial count. Writes join\n * the request's transaction when one is open. Refuses a form with `persistSubmissions` off:\n * its submissions are pruned, so the tally store is the only record of its votes and a rebuild\n * would wipe them.\n */\nexport const recountPollVotes = async (args: {\n\tpayload: Payload\n\tformId: number | string\n\treq?: PayloadRequest\n}): Promise<void> => {\n\tconst { payload, formId, req } = args\n\tconst form = await payload\n\t\t.findByID({\n\t\t\tcollection: pluginSlugsOf(payload).forms,\n\t\t\tid: formId,\n\t\t\tdepth: 0,\n\t\t\toverrideAccess: true,\n\t\t\treq,\n\t\t})\n\t\t.catch(() => null)\n\tconst poll = form ? pollConfigOf(form.poll) : undefined\n\tconst resultsField =\n\t\ttypeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t? poll.resultsField\n\t\t\t: undefined\n\tif (!resultsField) {\n\t\treturn\n\t}\n\tif (form?.persistSubmissions === false) {\n\t\tthrow new Error(\n\t\t\t`form-builder: recountPollVotes cannot rebuild form \"${String(formId)}\": it does not persist submissions, so the tally store is the only record of its votes.`\n\t\t)\n\t}\n\tconst aggregation = await aggregateFieldResponses({\n\t\tpayload,\n\t\tformId,\n\t\tfield: resultsField,\n\t\treq,\n\t\tmaxSubmissions: RECOUNT_MAX_SUBMISSIONS,\n\t})\n\tif (aggregation?.truncated) {\n\t\tthrow new Error(\n\t\t\t`form-builder: recount for form ${String(formId)} aborted: more than ${RECOUNT_MAX_SUBMISSIONS} submissions, the scan was truncated and a rebuilt tally would be partial.`\n\t\t)\n\t}\n\tawait payload.delete({\n\t\tcollection: pluginSlugsOf(payload).pollVotes,\n\t\twhere: {\n\t\t\tand: [{ form: { equals: String(formId) } }, { field: { equals: resultsField } }],\n\t\t},\n\t\toverrideAccess: true,\n\t\treq,\n\t})\n\tif (!aggregation) {\n\t\treturn\n\t}\n\tconst transactionID = transactionIDOf(req?.transactionID)\n\tconst base = { form: String(formId), field: resultsField }\n\tfor (const bucket of aggregation.buckets) {\n\t\tif (bucket.count > 0) {\n\t\t\tawait bumpPollVote(payload, { ...base, value: bucket.value }, bucket.count, transactionID)\n\t\t}\n\t}\n\tif (aggregation.total > 0) {\n\t\tawait bumpPollVote(\n\t\t\tpayload,\n\t\t\t{ ...base, value: RESPONDENTS_VALUE },\n\t\t\taggregation.total,\n\t\t\ttransactionID\n\t\t)\n\t}\n}\n"],"mappings":";;;;;;;AAQA,MAAM,0BAA0B;;;;;;;;;;AAWhC,MAAa,mBAAmB,OAAO,SAIlB;CACpB,MAAM,EAAE,SAAS,QAAQ,QAAQ;CACjC,MAAM,OAAO,MAAM,QACjB,SAAS;EACT,YAAY,cAAc,OAAO,EAAE;EACnC,IAAI;EACJ,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,MAAM,OAAO,OAAO,aAAa,KAAK,IAAI,IAAI,KAAA;CAC9C,MAAM,eACL,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IAClE,KAAK,eACL,KAAA;CACJ,IAAI,CAAC,cACJ;CAED,IAAI,MAAM,uBAAuB,OAChC,MAAM,IAAI,MACT,uDAAuD,OAAO,MAAM,EAAE,wFACvE;CAED,MAAM,cAAc,MAAM,wBAAwB;EACjD;EACA;EACA,OAAO;EACP;EACA,gBAAgB;CACjB,CAAC;CACD,IAAI,aAAa,WAChB,MAAM,IAAI,MACT,kCAAkC,OAAO,MAAM,EAAE,sBAAsB,wBAAwB,2EAChG;CAED,MAAM,QAAQ,OAAO;EACpB,YAAY,cAAc,OAAO,EAAE;EACnC,OAAO,EACN,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,OAAO,MAAM,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,aAAa,EAAE,CAAC,EAChF;EACA,gBAAgB;EAChB;CACD,CAAC;CACD,IAAI,CAAC,aACJ;CAED,MAAM,gBAAgB,gBAAgB,KAAK,aAAa;CACxD,MAAM,OAAO;EAAE,MAAM,OAAO,MAAM;EAAG,OAAO;CAAa;CACzD,KAAK,MAAM,UAAU,YAAY,SAChC,IAAI,OAAO,QAAQ,GAClB,MAAM,aAAa,SAAS;EAAE,GAAG;EAAM,OAAO,OAAO;CAAM,GAAG,OAAO,OAAO,aAAa;CAG3F,IAAI,YAAY,QAAQ,GACvB,MAAM,aACL,SACA;EAAE,GAAG;EAAM,OAAA;CAAyB,GACpC,YAAY,OACZ,aACD;AAEF"}
@@ -1,8 +1,8 @@
1
+ import { pluginSlugsOf } from "../../plugin/collectionSlugs.js";
1
2
  import { pollConfigOf } from "../../form/pollState.js";
2
- import "./votesCollection.js";
3
- import { FORMS_SLUG } from "../../collections/forms.js";
4
3
  import { formIdOf } from "../../submissions/formIdOf.js";
5
4
  import { answerValues } from "./answerValues.js";
5
+ import "./votesCollection.js";
6
6
  import { bumpPollVote } from "./bumpPollVote.js";
7
7
  //#region src/poll/votes/voteTallyHook.ts
8
8
  const isComplete = (doc) => doc.status == null || doc.status === "complete";
@@ -28,7 +28,7 @@ const makeVoteTallyHook = () => async ({ doc, previousDoc, operation, req }) =>
28
28
  const formId = formIdOf(doc.form);
29
29
  if (formId == null) return doc;
30
30
  const form = await req.payload.findByID({
31
- collection: FORMS_SLUG,
31
+ collection: pluginSlugsOf(req.payload).forms,
32
32
  id: formId,
33
33
  depth: 0,
34
34
  overrideAccess: true,
@@ -1 +1 @@
1
- {"version":3,"file":"voteTallyHook.js","names":[],"sources":["../../../src/poll/votes/voteTallyHook.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\nimport { FORMS_SLUG } from '../../collections/forms'\nimport { pollConfigOf } from '../../form/pollState'\nimport { formIdOf } from '../../submissions/formIdOf'\nimport { answerValues } from './answerValues'\nimport { bumpPollVote } from './bumpPollVote'\nimport { RESPONDENTS_VALUE } from './votesCollection'\n\nconst isComplete = (doc: { status?: unknown }): boolean =>\n\tdoc.status == null || doc.status === 'complete'\n\n/** Narrow a request's loosely typed `transactionID` to the value `bumpPollVote` accepts. */\nexport const transactionIDOf = (transactionID: unknown): number | string | undefined =>\n\ttypeof transactionID === 'number' || typeof transactionID === 'string' ? transactionID : undefined\n\n/**\n * Vote counting: a submission entering `complete` bumps one tally per answer value for the poll\n * results field, plus the shared respondents row. Votes are permanent by default: no decrement on\n * delete, edit, or prune, which is what lets a pruned submission's vote survive. The one\n * exception is an `allowChange` poll, where an update of an already-complete submission moves the\n * tally with the changed answer (decrement removed values, increment added ones, respondents\n * untouched); delete and prune stay permanent even then. Two concurrent changes of the same\n * submission can each read the same previous answer and double-adjust; the client's double-submit\n * guard makes that rare and `recountPollVotes` is the healer, matching `bumpPollVote`'s residual\n * drift note. Bump failures propagate so the enclosing transaction rolls the submission back\n * rather than silently undercounting; registered before the swallow-all dispatch hook.\n */\nexport const makeVoteTallyHook =\n\t(): CollectionAfterChangeHook =>\n\tasync ({ doc, previousDoc, operation, req }) => {\n\t\tif (operation !== 'create' && operation !== 'update') return doc\n\t\tconst entering =\n\t\t\t(operation === 'create' && isComplete(doc)) ||\n\t\t\t(operation === 'update' && isComplete(doc) && !isComplete(previousDoc ?? {}))\n\t\tconst changing =\n\t\t\toperation === 'update' && isComplete(doc) && isComplete(previousDoc ?? {}) && !entering\n\t\tif (!entering && !changing) return doc\n\t\tconst formId = formIdOf(doc.form)\n\t\tif (formId == null) return doc\n\t\tconst form = await req.payload\n\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t.catch(() => null)\n\t\tconst poll = form?.pollEnabled === true ? pollConfigOf(form.poll) : undefined\n\t\tconst resultsField =\n\t\t\ttypeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tif (!resultsField) return doc\n\t\tconst transactionID = transactionIDOf(req.transactionID)\n\t\tconst base = { form: String(formId), field: resultsField }\n\t\tif (changing) {\n\t\t\tif (poll?.allowChange !== true) return doc\n\t\t\tconst previous = answerValues(previousDoc?.values, resultsField)\n\t\t\tconst next = answerValues(doc.values, resultsField)\n\t\t\tconst removed = previous.filter((value) => !next.includes(value))\n\t\t\tconst added = next.filter((value) => !previous.includes(value))\n\t\t\tfor (const value of removed) {\n\t\t\t\tawait bumpPollVote(req.payload, { ...base, value }, -1, transactionID)\n\t\t\t}\n\t\t\tfor (const value of added) {\n\t\t\t\tawait bumpPollVote(req.payload, { ...base, value }, 1, transactionID)\n\t\t\t}\n\t\t\treturn doc\n\t\t}\n\t\tconst votes = answerValues(doc.values, resultsField)\n\t\tif (votes.length === 0) return doc\n\t\tfor (const value of votes) {\n\t\t\tawait bumpPollVote(req.payload, { ...base, value }, 1, transactionID)\n\t\t}\n\t\tawait bumpPollVote(req.payload, { ...base, value: RESPONDENTS_VALUE }, 1, transactionID)\n\t\treturn doc\n\t}\n"],"mappings":";;;;;;;AAQA,MAAM,cAAc,QACnB,IAAI,UAAU,QAAQ,IAAI,WAAW;;AAGtC,MAAa,mBAAmB,kBAC/B,OAAO,kBAAkB,YAAY,OAAO,kBAAkB,WAAW,gBAAgB,KAAA;;;;;;;;;;;;;AAc1F,MAAa,0BAEZ,OAAO,EAAE,KAAK,aAAa,WAAW,UAAU;CAC/C,IAAI,cAAc,YAAY,cAAc,UAAU,OAAO;CAC7D,MAAM,WACJ,cAAc,YAAY,WAAW,GAAG,KACxC,cAAc,YAAY,WAAW,GAAG,KAAK,CAAC,WAAW,eAAe,CAAC,CAAC;CAC5E,MAAM,WACL,cAAc,YAAY,WAAW,GAAG,KAAK,WAAW,eAAe,CAAC,CAAC,KAAK,CAAC;CAChF,IAAI,CAAC,YAAY,CAAC,UAAU,OAAO;CACnC,MAAM,SAAS,SAAS,IAAI,IAAI;CAChC,IAAI,UAAU,MAAM,OAAO;CAC3B,MAAM,OAAO,MAAM,IAAI,QACrB,SAAS;EAAE,YAAY;EAAY,IAAI;EAAQ,OAAO;EAAG,gBAAgB;EAAM;CAAI,CAAC,EACpF,YAAY,IAAI;CAClB,MAAM,OAAO,MAAM,gBAAgB,OAAO,aAAa,KAAK,IAAI,IAAI,KAAA;CACpE,MAAM,eACL,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IAClE,KAAK,eACL,KAAA;CACJ,IAAI,CAAC,cAAc,OAAO;CAC1B,MAAM,gBAAgB,gBAAgB,IAAI,aAAa;CACvD,MAAM,OAAO;EAAE,MAAM,OAAO,MAAM;EAAG,OAAO;CAAa;CACzD,IAAI,UAAU;EACb,IAAI,MAAM,gBAAgB,MAAM,OAAO;EACvC,MAAM,WAAW,aAAa,aAAa,QAAQ,YAAY;EAC/D,MAAM,OAAO,aAAa,IAAI,QAAQ,YAAY;EAClD,MAAM,UAAU,SAAS,QAAQ,UAAU,CAAC,KAAK,SAAS,KAAK,CAAC;EAChE,MAAM,QAAQ,KAAK,QAAQ,UAAU,CAAC,SAAS,SAAS,KAAK,CAAC;EAC9D,KAAK,MAAM,SAAS,SACnB,MAAM,aAAa,IAAI,SAAS;GAAE,GAAG;GAAM;EAAM,GAAG,IAAI,aAAa;EAEtE,KAAK,MAAM,SAAS,OACnB,MAAM,aAAa,IAAI,SAAS;GAAE,GAAG;GAAM;EAAM,GAAG,GAAG,aAAa;EAErE,OAAO;CACR;CACA,MAAM,QAAQ,aAAa,IAAI,QAAQ,YAAY;CACnD,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,KAAK,MAAM,SAAS,OACnB,MAAM,aAAa,IAAI,SAAS;EAAE,GAAG;EAAM;CAAM,GAAG,GAAG,aAAa;CAErE,MAAM,aAAa,IAAI,SAAS;EAAE,GAAG;EAAM,OAAA;CAAyB,GAAG,GAAG,aAAa;CACvF,OAAO;AACR"}
1
+ {"version":3,"file":"voteTallyHook.js","names":[],"sources":["../../../src/poll/votes/voteTallyHook.ts"],"sourcesContent":["import type { CollectionAfterChangeHook } from 'payload'\nimport { pollConfigOf } from '../../form/pollState'\nimport { pluginSlugsOf } from '../../plugin/collectionSlugs'\nimport { formIdOf } from '../../submissions/formIdOf'\nimport { answerValues } from './answerValues'\nimport { bumpPollVote } from './bumpPollVote'\nimport { RESPONDENTS_VALUE } from './votesCollection'\n\nconst isComplete = (doc: { status?: unknown }): boolean =>\n\tdoc.status == null || doc.status === 'complete'\n\n/** Narrow a request's loosely typed `transactionID` to the value `bumpPollVote` accepts. */\nexport const transactionIDOf = (transactionID: unknown): number | string | undefined =>\n\ttypeof transactionID === 'number' || typeof transactionID === 'string' ? transactionID : undefined\n\n/**\n * Vote counting: a submission entering `complete` bumps one tally per answer value for the poll\n * results field, plus the shared respondents row. Votes are permanent by default: no decrement on\n * delete, edit, or prune, which is what lets a pruned submission's vote survive. The one\n * exception is an `allowChange` poll, where an update of an already-complete submission moves the\n * tally with the changed answer (decrement removed values, increment added ones, respondents\n * untouched); delete and prune stay permanent even then. Two concurrent changes of the same\n * submission can each read the same previous answer and double-adjust; the client's double-submit\n * guard makes that rare and `recountPollVotes` is the healer, matching `bumpPollVote`'s residual\n * drift note. Bump failures propagate so the enclosing transaction rolls the submission back\n * rather than silently undercounting; registered before the swallow-all dispatch hook.\n */\nexport const makeVoteTallyHook =\n\t(): CollectionAfterChangeHook =>\n\tasync ({ doc, previousDoc, operation, req }) => {\n\t\tif (operation !== 'create' && operation !== 'update') return doc\n\t\tconst entering =\n\t\t\t(operation === 'create' && isComplete(doc)) ||\n\t\t\t(operation === 'update' && isComplete(doc) && !isComplete(previousDoc ?? {}))\n\t\tconst changing =\n\t\t\toperation === 'update' && isComplete(doc) && isComplete(previousDoc ?? {}) && !entering\n\t\tif (!entering && !changing) return doc\n\t\tconst formId = formIdOf(doc.form)\n\t\tif (formId == null) return doc\n\t\tconst form = await req.payload\n\t\t\t.findByID({\n\t\t\t\tcollection: pluginSlugsOf(req.payload).forms,\n\t\t\t\tid: formId,\n\t\t\t\tdepth: 0,\n\t\t\t\toverrideAccess: true,\n\t\t\t\treq,\n\t\t\t})\n\t\t\t.catch(() => null)\n\t\tconst poll = form?.pollEnabled === true ? pollConfigOf(form.poll) : undefined\n\t\tconst resultsField =\n\t\t\ttypeof poll?.resultsField === 'string' && poll.resultsField.length > 0\n\t\t\t\t? poll.resultsField\n\t\t\t\t: undefined\n\t\tif (!resultsField) return doc\n\t\tconst transactionID = transactionIDOf(req.transactionID)\n\t\tconst base = { form: String(formId), field: resultsField }\n\t\tif (changing) {\n\t\t\tif (poll?.allowChange !== true) return doc\n\t\t\tconst previous = answerValues(previousDoc?.values, resultsField)\n\t\t\tconst next = answerValues(doc.values, resultsField)\n\t\t\tconst removed = previous.filter((value) => !next.includes(value))\n\t\t\tconst added = next.filter((value) => !previous.includes(value))\n\t\t\tfor (const value of removed) {\n\t\t\t\tawait bumpPollVote(req.payload, { ...base, value }, -1, transactionID)\n\t\t\t}\n\t\t\tfor (const value of added) {\n\t\t\t\tawait bumpPollVote(req.payload, { ...base, value }, 1, transactionID)\n\t\t\t}\n\t\t\treturn doc\n\t\t}\n\t\tconst votes = answerValues(doc.values, resultsField)\n\t\tif (votes.length === 0) return doc\n\t\tfor (const value of votes) {\n\t\t\tawait bumpPollVote(req.payload, { ...base, value }, 1, transactionID)\n\t\t}\n\t\tawait bumpPollVote(req.payload, { ...base, value: RESPONDENTS_VALUE }, 1, transactionID)\n\t\treturn doc\n\t}\n"],"mappings":";;;;;;;AAQA,MAAM,cAAc,QACnB,IAAI,UAAU,QAAQ,IAAI,WAAW;;AAGtC,MAAa,mBAAmB,kBAC/B,OAAO,kBAAkB,YAAY,OAAO,kBAAkB,WAAW,gBAAgB,KAAA;;;;;;;;;;;;;AAc1F,MAAa,0BAEZ,OAAO,EAAE,KAAK,aAAa,WAAW,UAAU;CAC/C,IAAI,cAAc,YAAY,cAAc,UAAU,OAAO;CAC7D,MAAM,WACJ,cAAc,YAAY,WAAW,GAAG,KACxC,cAAc,YAAY,WAAW,GAAG,KAAK,CAAC,WAAW,eAAe,CAAC,CAAC;CAC5E,MAAM,WACL,cAAc,YAAY,WAAW,GAAG,KAAK,WAAW,eAAe,CAAC,CAAC,KAAK,CAAC;CAChF,IAAI,CAAC,YAAY,CAAC,UAAU,OAAO;CACnC,MAAM,SAAS,SAAS,IAAI,IAAI;CAChC,IAAI,UAAU,MAAM,OAAO;CAC3B,MAAM,OAAO,MAAM,IAAI,QACrB,SAAS;EACT,YAAY,cAAc,IAAI,OAAO,EAAE;EACvC,IAAI;EACJ,OAAO;EACP,gBAAgB;EAChB;CACD,CAAC,EACA,YAAY,IAAI;CAClB,MAAM,OAAO,MAAM,gBAAgB,OAAO,aAAa,KAAK,IAAI,IAAI,KAAA;CACpE,MAAM,eACL,OAAO,MAAM,iBAAiB,YAAY,KAAK,aAAa,SAAS,IAClE,KAAK,eACL,KAAA;CACJ,IAAI,CAAC,cAAc,OAAO;CAC1B,MAAM,gBAAgB,gBAAgB,IAAI,aAAa;CACvD,MAAM,OAAO;EAAE,MAAM,OAAO,MAAM;EAAG,OAAO;CAAa;CACzD,IAAI,UAAU;EACb,IAAI,MAAM,gBAAgB,MAAM,OAAO;EACvC,MAAM,WAAW,aAAa,aAAa,QAAQ,YAAY;EAC/D,MAAM,OAAO,aAAa,IAAI,QAAQ,YAAY;EAClD,MAAM,UAAU,SAAS,QAAQ,UAAU,CAAC,KAAK,SAAS,KAAK,CAAC;EAChE,MAAM,QAAQ,KAAK,QAAQ,UAAU,CAAC,SAAS,SAAS,KAAK,CAAC;EAC9D,KAAK,MAAM,SAAS,SACnB,MAAM,aAAa,IAAI,SAAS;GAAE,GAAG;GAAM;EAAM,GAAG,IAAI,aAAa;EAEtE,KAAK,MAAM,SAAS,OACnB,MAAM,aAAa,IAAI,SAAS;GAAE,GAAG;GAAM;EAAM,GAAG,GAAG,aAAa;EAErE,OAAO;CACR;CACA,MAAM,QAAQ,aAAa,IAAI,QAAQ,YAAY;CACnD,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,KAAK,MAAM,SAAS,OACnB,MAAM,aAAa,IAAI,SAAS;EAAE,GAAG;EAAM;CAAM,GAAG,GAAG,aAAa;CAErE,MAAM,aAAa,IAAI,SAAS;EAAE,GAAG;EAAM,OAAA;CAAyB,GAAG,GAAG,aAAa;CACvF,OAAO;AACR"}
@@ -1,7 +1,11 @@
1
1
  import { CollectionConfig } from "payload";
2
2
 
3
3
  //#region src/poll/votes/votesCollection.d.ts
4
- declare const POLL_VOTES_SLUG = "form-poll-votes";
4
+ /**
5
+ * Default slug of the tally collection. A host may rename it via `poll.votes.overrides.slug`;
6
+ * read the live slug with `collectionSlugsOf(payload).pollVotes`.
7
+ */
8
+ declare const POLL_VOTES_SLUG: "form-poll-votes";
5
9
  /** Reserved tally `value` marking the per-field respondents counter (empty answers are never counted, so '' is free). */
6
10
  declare const RESPONDENTS_VALUE = "";
7
11
  /**
@@ -1,8 +1,13 @@
1
1
  import { keys } from "../../translations/keys.js";
2
2
  import { labelForKey } from "../../translations/server.js";
3
+ import { DEFAULT_COLLECTION_SLUGS } from "../../plugin/collectionSlugs.js";
3
4
  import { isLoggedIn } from "../../plugin/access.js";
4
5
  //#region src/poll/votes/votesCollection.ts
5
- const POLL_VOTES_SLUG = "form-poll-votes";
6
+ /**
7
+ * Default slug of the tally collection. A host may rename it via `poll.votes.overrides.slug`;
8
+ * read the live slug with `collectionSlugsOf(payload).pollVotes`.
9
+ */
10
+ const POLL_VOTES_SLUG = DEFAULT_COLLECTION_SLUGS.pollVotes;
6
11
  /** Reserved tally `value` marking the per-field respondents counter (empty answers are never counted, so '' is free). */
7
12
  const RESPONDENTS_VALUE = "";
8
13
  /**
@@ -57,7 +62,7 @@ const buildPollVotesCollection = (args) => {
57
62
  ];
58
63
  return {
59
64
  ...args.overrides ?? {},
60
- slug: POLL_VOTES_SLUG,
65
+ slug: args.slug ?? POLL_VOTES_SLUG,
61
66
  labels: {
62
67
  singular: labelForKey(keys.collectionPollVoteSingular),
63
68
  plural: labelForKey(keys.collectionPollVotePlural),
@@ -1 +1 @@
1
- {"version":3,"file":"votesCollection.js","names":[],"sources":["../../../src/poll/votes/votesCollection.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport { isLoggedIn } from '../../plugin/access'\nimport type { CollectionOverrides } from '../../plugin/collectionOverrides'\nimport { keys } from '../../translations/keys'\nimport { labelForKey } from '../../translations/server'\n\nexport const POLL_VOTES_SLUG = 'form-poll-votes'\n\n/** Reserved tally `value` marking the per-field respondents counter (empty answers are never counted, so '' is free). */\nexport const RESPONDENTS_VALUE = ''\n\n/**\n * Shard count for hot tally counters. Mongo aborts one of two transactions that write the same\n * document (WriteConflict/TransientTransactionError), so concurrent transactional votes for the\n * same (form, field, value) would deterministically fail one submission. Spreading each\n * transactional bump across VOTE_SHARDS rows makes concurrent bumps land on different shards\n * with high probability, so transactions stop contending. Postgres row locks serialize\n * ON CONFLICT updates without aborting and non-transactional Mongo $inc is conflict-free, so\n * both always use shard 0. Reads sum counts across shards.\n */\nexport const VOTE_SHARDS = 8\n\n/**\n * Append-only aggregate tallies: up to VOTE_SHARDS rows per (form, field, value) with running\n * counts that readers sum, plus respondents rows per (form, field) under RESPONDENTS_VALUE.\n * Rows carry no per-voter data and are written via atomic upsert-increments (see bumpPollVote),\n * made concurrency-safe by the unique compound index. Ids are stored as strings for cross-DB\n * parity.\n *\n * `indexes` is intentionally not part of the override surface: the unique compound index is\n * load-bearing for the atomic upsert-increment, so it is fixed rather than spread with the rest.\n */\nexport const buildPollVotesCollection = (args: {\n\toverrides?: CollectionOverrides\n}): CollectionConfig => {\n\tconst defaultFields: CollectionConfig['fields'] = [\n\t\t{ name: 'form', type: 'text', required: true, index: true },\n\t\t{ name: 'field', type: 'text', required: true },\n\t\t{ name: 'value', type: 'text' },\n\t\t{ name: 'shard', type: 'number', required: true, defaultValue: 0 },\n\t\t{ name: 'count', type: 'number', required: true, defaultValue: 0 },\n\t]\n\n\treturn {\n\t\t...(args.overrides ?? {}),\n\t\tslug: POLL_VOTES_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionPollVoteSingular),\n\t\t\tplural: labelForKey(keys.collectionPollVotePlural),\n\t\t\t...(args.overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: { hidden: true, ...(args.overrides?.admin ?? {}) },\n\t\taccess: { read: isLoggedIn, ...(args.overrides?.access ?? {}) },\n\t\tindexes: [{ fields: ['form', 'field', 'value', 'shard'], unique: true }],\n\t\tfields: args.overrides?.fields ? args.overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":";;;;AAMA,MAAa,kBAAkB;;AAG/B,MAAa,oBAAoB;;;;;;;;;;AAWjC,MAAa,cAAc;;;;;;;;;;;AAY3B,MAAa,4BAA4B,SAEjB;CACvB,MAAM,gBAA4C;EACjD;GAAE,MAAM;GAAQ,MAAM;GAAQ,UAAU;GAAM,OAAO;EAAK;EAC1D;GAAE,MAAM;GAAS,MAAM;GAAQ,UAAU;EAAK;EAC9C;GAAE,MAAM;GAAS,MAAM;EAAO;EAC9B;GAAE,MAAM;GAAS,MAAM;GAAU,UAAU;GAAM,cAAc;EAAE;EACjE;GAAE,MAAM;GAAS,MAAM;GAAU,UAAU;GAAM,cAAc;EAAE;CAClE;CAEA,OAAO;EACN,GAAI,KAAK,aAAa,CAAC;EACvB,MAAM;EACN,QAAQ;GACP,UAAU,YAAY,KAAK,0BAA0B;GACrD,QAAQ,YAAY,KAAK,wBAAwB;GACjD,GAAI,KAAK,WAAW,UAAU,CAAC;EAChC;EACA,OAAO;GAAE,QAAQ;GAAM,GAAI,KAAK,WAAW,SAAS,CAAC;EAAG;EACxD,QAAQ;GAAE,MAAM;GAAY,GAAI,KAAK,WAAW,UAAU,CAAC;EAAG;EAC9D,SAAS,CAAC;GAAE,QAAQ;IAAC;IAAQ;IAAS;IAAS;GAAO;GAAG,QAAQ;EAAK,CAAC;EACvE,QAAQ,KAAK,WAAW,SAAS,KAAK,UAAU,OAAO,EAAE,cAAc,CAAC,IAAI;CAC7E;AACD"}
1
+ {"version":3,"file":"votesCollection.js","names":[],"sources":["../../../src/poll/votes/votesCollection.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\nimport { isLoggedIn } from '../../plugin/access'\nimport type { CollectionOverrides } from '../../plugin/collectionOverrides'\nimport { DEFAULT_COLLECTION_SLUGS } from '../../plugin/collectionSlugs'\nimport { keys } from '../../translations/keys'\nimport { labelForKey } from '../../translations/server'\n\n/**\n * Default slug of the tally collection. A host may rename it via `poll.votes.overrides.slug`;\n * read the live slug with `collectionSlugsOf(payload).pollVotes`.\n */\nexport const POLL_VOTES_SLUG = DEFAULT_COLLECTION_SLUGS.pollVotes\n\n/** Reserved tally `value` marking the per-field respondents counter (empty answers are never counted, so '' is free). */\nexport const RESPONDENTS_VALUE = ''\n\n/**\n * Shard count for hot tally counters. Mongo aborts one of two transactions that write the same\n * document (WriteConflict/TransientTransactionError), so concurrent transactional votes for the\n * same (form, field, value) would deterministically fail one submission. Spreading each\n * transactional bump across VOTE_SHARDS rows makes concurrent bumps land on different shards\n * with high probability, so transactions stop contending. Postgres row locks serialize\n * ON CONFLICT updates without aborting and non-transactional Mongo $inc is conflict-free, so\n * both always use shard 0. Reads sum counts across shards.\n */\nexport const VOTE_SHARDS = 8\n\n/**\n * Append-only aggregate tallies: up to VOTE_SHARDS rows per (form, field, value) with running\n * counts that readers sum, plus respondents rows per (form, field) under RESPONDENTS_VALUE.\n * Rows carry no per-voter data and are written via atomic upsert-increments (see bumpPollVote),\n * made concurrency-safe by the unique compound index. Ids are stored as strings for cross-DB\n * parity.\n *\n * `indexes` is intentionally not part of the override surface: the unique compound index is\n * load-bearing for the atomic upsert-increment, so it is fixed rather than spread with the rest.\n */\nexport const buildPollVotesCollection = (args: {\n\toverrides?: CollectionOverrides\n\t/** The resolved tally slug (`poll.votes.overrides.slug`); the default when omitted. */\n\tslug?: string\n}): CollectionConfig => {\n\tconst defaultFields: CollectionConfig['fields'] = [\n\t\t{ name: 'form', type: 'text', required: true, index: true },\n\t\t{ name: 'field', type: 'text', required: true },\n\t\t{ name: 'value', type: 'text' },\n\t\t{ name: 'shard', type: 'number', required: true, defaultValue: 0 },\n\t\t{ name: 'count', type: 'number', required: true, defaultValue: 0 },\n\t]\n\n\treturn {\n\t\t...(args.overrides ?? {}),\n\t\tslug: args.slug ?? POLL_VOTES_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionPollVoteSingular),\n\t\t\tplural: labelForKey(keys.collectionPollVotePlural),\n\t\t\t...(args.overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: { hidden: true, ...(args.overrides?.admin ?? {}) },\n\t\taccess: { read: isLoggedIn, ...(args.overrides?.access ?? {}) },\n\t\tindexes: [{ fields: ['form', 'field', 'value', 'shard'], unique: true }],\n\t\tfields: args.overrides?.fields ? args.overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":";;;;;;;;;AAWA,MAAa,kBAAkB,yBAAyB;;AAGxD,MAAa,oBAAoB;;;;;;;;;;AAWjC,MAAa,cAAc;;;;;;;;;;;AAY3B,MAAa,4BAA4B,SAIjB;CACvB,MAAM,gBAA4C;EACjD;GAAE,MAAM;GAAQ,MAAM;GAAQ,UAAU;GAAM,OAAO;EAAK;EAC1D;GAAE,MAAM;GAAS,MAAM;GAAQ,UAAU;EAAK;EAC9C;GAAE,MAAM;GAAS,MAAM;EAAO;EAC9B;GAAE,MAAM;GAAS,MAAM;GAAU,UAAU;GAAM,cAAc;EAAE;EACjE;GAAE,MAAM;GAAS,MAAM;GAAU,UAAU;GAAM,cAAc;EAAE;CAClE;CAEA,OAAO;EACN,GAAI,KAAK,aAAa,CAAC;EACvB,MAAM,KAAK,QAAQ;EACnB,QAAQ;GACP,UAAU,YAAY,KAAK,0BAA0B;GACrD,QAAQ,YAAY,KAAK,wBAAwB;GACjD,GAAI,KAAK,WAAW,UAAU,CAAC;EAChC;EACA,OAAO;GAAE,QAAQ;GAAM,GAAI,KAAK,WAAW,SAAS,CAAC;EAAG;EACxD,QAAQ;GAAE,MAAM;GAAY,GAAI,KAAK,WAAW,UAAU,CAAC;EAAG;EAC9D,SAAS,CAAC;GAAE,QAAQ;IAAC;IAAQ;IAAS;IAAS;GAAO;GAAG,QAAQ;EAAK,CAAC;EACvE,QAAQ,KAAK,WAAW,SAAS,KAAK,UAAU,OAAO,EAAE,cAAc,CAAC,IAAI;CAC7E;AACD"}
@@ -5,6 +5,7 @@ import { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSetting
5
5
  import { FormAdapters } from "./adapters.js";
6
6
  import { RendererTranslate } from "./contract.js";
7
7
  import { FormEventSink } from "../events/types.js";
8
+ import { FormBuilderCollectionSlugs } from "../plugin/collectionSlugs.js";
8
9
  import { AnyValidationRuleDefinition } from "../validation/types.js";
9
10
  import { BackButtonRenderProps, NextButtonRenderProps, SubmitButtonRenderProps } from "./FormControls.js";
10
11
  import { FormPresentation } from "./presentation/types.js";
@@ -30,12 +31,19 @@ type FormSuccessResult = {
30
31
  response?: FormSuccessResponse; /** The submitted answers (reserved keys like the honeypot and captcha token excluded), e.g. for `<Poll>` to track the voter's pick. */
31
32
  values?: SubmissionValue[];
32
33
  };
34
+ /**
35
+ * The plugin's collection slugs as the browser needs them, for a host that renamed them through
36
+ * `overrides.forms.slug` / `overrides.formSubmissions.slug`. Pass the same values the server was
37
+ * given; each omitted key keeps its default.
38
+ */
39
+ type FormCollections = Partial<Pick<FormBuilderCollectionSlugs, 'forms' | 'formSubmissions'>>;
33
40
  type FormProps = {
34
41
  form: FormDocument;
35
42
  fieldTypes?: AnyFormFieldDefinition[];
36
43
  rules?: AnyValidationRuleDefinition[];
37
44
  renderers?: RenderersConfig;
38
- apiRoute?: string;
45
+ apiRoute?: string; /** Renamed plugin collections: submits post to `formSubmissions`, `<Poll>` reads results from `forms`. */
46
+ collections?: FormCollections;
39
47
  onSubmit?: SubmitHandler;
40
48
  /**
41
49
  * Called after a successful submission with the submission id and the resolved success response
@@ -62,13 +70,16 @@ type FormProps = {
62
70
  */
63
71
  calcFunctions?: Record<string, (args: number[]) => number>;
64
72
  events?: FormEventSink;
65
- t?: RendererTranslate;
73
+ t?: RendererTranslate; /** The visitor's locale for renderer strings and value formatting, e.g. `en-US` (`'en'` when absent). */
74
+ locale?: string;
66
75
  /**
67
- * The visitor's locale: drives renderer strings and value formatting (`'en'` when absent) and,
68
- * when passed, is sent with the submission so the server stores it and the post-submit actions
69
- * (confirmation emails included) render in it. Absent, the submission takes the host's default locale.
76
+ * The Payload content locale to submit under, one of the host's `localization` codes (`en`, `de`),
77
+ * sent as `?locale=` so the server stores it on the submission and the post-submit actions
78
+ * (confirmation emails included) render in it. A `<Poll>` also reads its results in it. Separate
79
+ * from `locale` because a formatting tag like `en-US` is usually not a content locale code.
80
+ * Absent, the submission takes the host's default locale.
70
81
  */
71
- locale?: string;
82
+ submissionLocale?: string;
72
83
  layout?: boolean; /** Submit button label. Precedence: this prop, then the form's `buttons.submitLabel`, then the translated default. */
73
84
  submitLabel?: string; /** "Next" button label for multi-step forms. Precedence: this prop, then the form's `buttons.nextLabel`, then the translated default. */
74
85
  nextLabel?: string; /** "Back" button label for multi-step forms. Precedence: this prop, then the form's `buttons.prevLabel`, then the translated default. */
@@ -118,6 +129,7 @@ declare const Form: ({
118
129
  rules,
119
130
  renderers,
120
131
  apiRoute,
132
+ collections,
121
133
  onSubmit,
122
134
  onSuccess,
123
135
  onError,
@@ -127,6 +139,7 @@ declare const Form: ({
127
139
  events,
128
140
  t,
129
141
  locale: localeProp,
142
+ submissionLocale,
130
143
  layout,
131
144
  submitLabel,
132
145
  nextLabel,
@@ -153,5 +166,5 @@ declare const Form: ({
153
166
  adapters
154
167
  }: FormProps) => import("react/jsx-runtime").JSX.Element;
155
168
  //#endregion
156
- export { Form, FormProps, FormSuccessResponse, FormSuccessResult };
169
+ export { Form, FormCollections, FormProps, FormSuccessResponse, FormSuccessResult };
157
170
  //# sourceMappingURL=Form.d.ts.map
@@ -3,9 +3,9 @@ import { keys } from "../translations/keys.js";
3
3
  import { interpolate } from "../recall/interpolate.js";
4
4
  import { serializeBody } from "../actions/body/serializeBody.js";
5
5
  import { noopEventSink } from "../events/noopSink.js";
6
+ import { CAPTCHA_TOKEN_KEY, CONTEXT_KEY, HONEYPOT_VALUE_KEY } from "../spam/constants.js";
6
7
  import { fieldKey, isNamedField } from "../fields/fieldKey.js";
7
8
  import { calcExpressionOf, computeCalcFields } from "../calc/computeCalcFields.js";
8
- import { CAPTCHA_TOKEN_KEY, CONTEXT_KEY, HONEYPOT_VALUE_KEY } from "../spam/constants.js";
9
9
  import { evaluateCondition } from "../conditions/evaluate.js";
10
10
  import { resolveMessage } from "../validation/message.js";
11
11
  import { en } from "../translations/en.js";
@@ -61,7 +61,7 @@ const focusFirstIn = (root, selector) => {
61
61
  /** A stored button label counts only when it is a non-empty string; anything else falls through. */
62
62
  const storedLabel = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
63
63
  /** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */
64
- const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, converters, successBehavior = "replace", calcFunctions, events, t, locale: localeProp, layout, submitLabel, nextLabel, prevLabel, closeLabel, successMessage, presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, context, children, header, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName, adapters }) => {
64
+ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, collections, onSubmit, onSuccess, onError, converters, successBehavior = "replace", calcFunctions, events, t, locale: localeProp, submissionLocale, layout, submitLabel, nextLabel, prevLabel, closeLabel, successMessage, presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, context, children, header, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName, adapters }) => {
65
65
  const locale = localeProp ?? "en";
66
66
  const honeypotName = honeypot === false ? null : honeypot?.name ?? "website";
67
67
  const honeypotRef = useRef(null);
@@ -141,10 +141,15 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
141
141
  const allStepIds = useMemo(() => flow ? flow.steps.map((flowStep) => flowStep.id) : [DEFAULT_STEP_ID], [flow]);
142
142
  const formRef = useRef(null);
143
143
  const pendingFocusRef = useRef(null);
144
+ const focusSeqRef = useRef(0);
144
145
  const [focusNonce, setFocusNonce] = useState(0);
145
146
  const requestFocus = (intent) => {
146
- pendingFocusRef.current = intent;
147
- setFocusNonce((nonce) => nonce + 1);
147
+ focusSeqRef.current += 1;
148
+ pendingFocusRef.current = {
149
+ intent,
150
+ nonce: focusSeqRef.current
151
+ };
152
+ setFocusNonce(focusSeqRef.current);
148
153
  };
149
154
  const dispatch = useCallback((action) => {
150
155
  if (action.type === "SET_VALUE" && !startedRef.current) {
@@ -350,9 +355,10 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
350
355
  });
351
356
  };
352
357
  useEffect(() => {
353
- const intent = pendingFocusRef.current;
354
- if (!intent) return;
358
+ const pending = pendingFocusRef.current;
359
+ if (!pending || pending.nonce !== focusNonce) return;
355
360
  pendingFocusRef.current = null;
361
+ const { intent } = pending;
356
362
  if (intent === "firstInvalid") {
357
363
  focusFirstIn(formRef.current, "[aria-invalid=\"true\"]");
358
364
  return;
@@ -439,15 +445,15 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
439
445
  field: CONTEXT_KEY,
440
446
  value: context
441
447
  });
442
- const result = onSubmit ? await onSubmit({
448
+ const input = {
443
449
  formId: form.id,
444
450
  values,
445
- ...localeProp ? { locale: localeProp } : {}
446
- }) : await submitForm({
447
- formId: form.id,
448
- values,
449
- apiRoute,
450
- locale: localeProp
451
+ ...submissionLocale ? { locale: submissionLocale } : {},
452
+ ...collections?.formSubmissions ? { collection: collections.formSubmissions } : {}
453
+ };
454
+ const result = onSubmit ? await onSubmit(input) : await submitForm({
455
+ ...input,
456
+ apiRoute
451
457
  });
452
458
  submittingRef.current = false;
453
459
  if (result.ok) {