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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/actions/buildActionBlocks.js +12 -9
  3. package/dist/actions/buildActionBlocks.js.map +1 -1
  4. package/dist/actions/builtin/confirmation.js +13 -68
  5. package/dist/actions/builtin/confirmation.js.map +1 -1
  6. package/dist/actions/builtin/emailAction.d.ts +17 -0
  7. package/dist/actions/builtin/emailAction.js +83 -0
  8. package/dist/actions/builtin/emailAction.js.map +1 -0
  9. package/dist/actions/builtin/emailTeam.js +14 -74
  10. package/dist/actions/builtin/emailTeam.js.map +1 -1
  11. package/dist/actions/builtin/index.d.ts +9 -12
  12. package/dist/actions/builtin/index.js +11 -11
  13. package/dist/actions/builtin/index.js.map +1 -1
  14. package/dist/actions/defineAction.d.ts +2 -2
  15. package/dist/actions/defineAction.js.map +1 -1
  16. package/dist/actions/emailRecipients.js +75 -31
  17. package/dist/actions/emailRecipients.js.map +1 -1
  18. package/dist/actions/registry.d.ts +2 -1
  19. package/dist/actions/registry.js +2 -6
  20. package/dist/actions/registry.js.map +1 -1
  21. package/dist/actions/task.js +5 -3
  22. package/dist/actions/task.js.map +1 -1
  23. package/dist/calc/evaluate.js +1 -2
  24. package/dist/calc/evaluate.js.map +1 -1
  25. package/dist/calc/normalizeCalc.js +1 -2
  26. package/dist/calc/normalizeCalc.js.map +1 -1
  27. package/dist/calc/types.js.map +1 -1
  28. package/dist/collections/buttonFields.js +1 -1
  29. package/dist/collections/formSubmissions.js +2 -1
  30. package/dist/collections/formSubmissions.js.map +1 -1
  31. package/dist/collections/forms.js +30 -99
  32. package/dist/collections/forms.js.map +1 -1
  33. package/dist/collections/formsEndpoints.js +101 -0
  34. package/dist/collections/formsEndpoints.js.map +1 -0
  35. package/dist/conditions/evaluate.js +1 -1
  36. package/dist/conditions/evaluate.js.map +1 -1
  37. package/dist/conditions/fieldTypes.d.ts +3 -1
  38. package/dist/conditions/fieldTypes.js +4 -1
  39. package/dist/conditions/fieldTypes.js.map +1 -1
  40. package/dist/conditions/normalizeConditions.js +18 -5
  41. package/dist/conditions/normalizeConditions.js.map +1 -1
  42. package/dist/consent/captureConsent.d.ts +7 -0
  43. package/dist/consent/captureConsent.js +7 -0
  44. package/dist/consent/captureConsent.js.map +1 -1
  45. package/dist/consent/consentSourcesField.d.ts +4 -0
  46. package/dist/consent/consentSourcesField.js +5 -1
  47. package/dist/consent/consentSourcesField.js.map +1 -1
  48. package/dist/consent/resolveConsentEntries.d.ts +1 -1
  49. package/dist/consent/resolveConsentEntries.js +1 -4
  50. package/dist/consent/resolveConsentEntries.js.map +1 -1
  51. package/dist/consent/resolveConsentSourcesRequest.js +0 -1
  52. package/dist/consent/resolveConsentSourcesRequest.js.map +1 -1
  53. package/dist/consent/resolveConsentStatements.d.ts +1 -1
  54. package/dist/consent/resolveConsentStatements.js.map +1 -1
  55. package/dist/consent/types.d.ts +9 -1
  56. package/dist/email/departmentsField.js +1 -1
  57. package/dist/exports/react.d.ts +3 -2
  58. package/dist/exports/react.js +3 -2
  59. package/dist/exports/rsc.js +1 -1
  60. package/dist/exports/types.d.ts +1 -1
  61. package/dist/fields/builtin/email.js +1 -1
  62. package/dist/fields/builtin/email.js.map +1 -1
  63. package/dist/fields/builtin/message.js +1 -1
  64. package/dist/fields/builtin/repeater.js +1 -1
  65. package/dist/fields/builtin/select.js +1 -1
  66. package/dist/fields/registry.d.ts +2 -1
  67. package/dist/fields/registry.js +2 -6
  68. package/dist/fields/registry.js.map +1 -1
  69. package/dist/fields/sharedConfig.js +1 -1
  70. package/dist/flow/normalizeFlow.js +7 -3
  71. package/dist/flow/normalizeFlow.js.map +1 -1
  72. package/dist/index.d.ts +4 -201
  73. package/dist/index.js +11 -4
  74. package/dist/index.js.map +1 -1
  75. package/dist/options.d.ts +228 -0
  76. package/dist/plugin/applyRegistryConfig.d.ts +11 -0
  77. package/dist/plugin/applyRegistryConfig.js +20 -0
  78. package/dist/plugin/applyRegistryConfig.js.map +1 -0
  79. package/dist/plugin/registerCollections.js +2 -1
  80. package/dist/plugin/registerCollections.js.map +1 -1
  81. package/dist/poll/buildPollOptionSourceFields.js +2 -2
  82. package/dist/poll/buildPollOptionSourceFields.js.map +1 -1
  83. package/dist/poll/definePollOptionSource.d.ts +2 -2
  84. package/dist/poll/definePollOptionSource.js.map +1 -1
  85. package/dist/poll/pollTypeRegistry.js +2 -4
  86. package/dist/poll/pollTypeRegistry.js.map +1 -1
  87. package/dist/poll/registry.d.ts +2 -1
  88. package/dist/poll/registry.js +2 -6
  89. package/dist/poll/registry.js.map +1 -1
  90. package/dist/poll/resolvePollOptionsRequest.js +0 -1
  91. package/dist/poll/resolvePollOptionsRequest.js.map +1 -1
  92. package/dist/prefill/valuesFromSearchParams.js +2 -7
  93. package/dist/prefill/valuesFromSearchParams.js.map +1 -1
  94. package/dist/react/Form.d.ts +3 -1
  95. package/dist/react/Form.js +93 -155
  96. package/dist/react/Form.js.map +1 -1
  97. package/dist/react/FormContext.d.ts +5 -1
  98. package/dist/react/FormContext.js.map +1 -1
  99. package/dist/react/FormFields.d.ts +18 -0
  100. package/dist/react/FormFields.js +114 -0
  101. package/dist/react/FormFields.js.map +1 -0
  102. package/dist/react/FormLayout.d.ts +4 -2
  103. package/dist/react/FormLayout.js +3 -2
  104. package/dist/react/FormLayout.js.map +1 -1
  105. package/dist/react/Poll.js +13 -3
  106. package/dist/react/Poll.js.map +1 -1
  107. package/dist/react/presentation/DialogSurface.js +1 -4
  108. package/dist/react/presentation/DialogSurface.js.map +1 -1
  109. package/dist/react/renderers/calculation.js +1 -3
  110. package/dist/react/renderers/calculation.js.map +1 -1
  111. package/dist/react/renderers/checkbox.js +1 -3
  112. package/dist/react/renderers/checkbox.js.map +1 -1
  113. package/dist/react/renderers/consent.js +2 -3
  114. package/dist/react/renderers/consent.js.map +1 -1
  115. package/dist/react/renderers/date.js +1 -3
  116. package/dist/react/renderers/date.js.map +1 -1
  117. package/dist/react/renderers/email.js +1 -3
  118. package/dist/react/renderers/email.js.map +1 -1
  119. package/dist/react/renderers/file.js +6 -7
  120. package/dist/react/renderers/file.js.map +1 -1
  121. package/dist/react/renderers/number.js +1 -3
  122. package/dist/react/renderers/number.js.map +1 -1
  123. package/dist/react/renderers/region.js +1 -3
  124. package/dist/react/renderers/region.js.map +1 -1
  125. package/dist/react/renderers/repeater.js +20 -4
  126. package/dist/react/renderers/repeater.js.map +1 -1
  127. package/dist/react/renderers/select.js +1 -3
  128. package/dist/react/renderers/select.js.map +1 -1
  129. package/dist/react/renderers/text.js +1 -3
  130. package/dist/react/renderers/text.js.map +1 -1
  131. package/dist/react/renderers/textarea.js +1 -3
  132. package/dist/react/renderers/textarea.js.map +1 -1
  133. package/dist/spam/constants.js +7 -1
  134. package/dist/spam/constants.js.map +1 -1
  135. package/dist/spam/resolveSpam.js +2 -1
  136. package/dist/spam/resolveSpam.js.map +1 -1
  137. package/dist/spam/spamGuard.js +22 -6
  138. package/dist/spam/spamGuard.js.map +1 -1
  139. package/dist/spam/types.d.ts +17 -2
  140. package/dist/submissions/coerceBoolean.js +14 -0
  141. package/dist/submissions/coerceBoolean.js.map +1 -0
  142. package/dist/submissions/runSubmission.js +7 -7
  143. package/dist/submissions/runSubmission.js.map +1 -1
  144. package/dist/submissions/validateSubmission.js +3 -2
  145. package/dist/submissions/validateSubmission.js.map +1 -1
  146. package/dist/translations/de.js +10 -0
  147. package/dist/translations/de.js.map +1 -1
  148. package/dist/translations/en.js +10 -0
  149. package/dist/translations/en.js.map +1 -1
  150. package/dist/translations/keys.d.ts +10 -0
  151. package/dist/translations/keys.js +10 -0
  152. package/dist/translations/keys.js.map +1 -1
  153. package/dist/translations/server.js +8 -1
  154. package/dist/translations/server.js.map +1 -1
  155. package/dist/uploads/captureFileRef.d.ts +10 -3
  156. package/dist/uploads/captureFileRef.js +18 -9
  157. package/dist/uploads/captureFileRef.js.map +1 -1
  158. package/dist/validation/builtin/email.js +1 -1
  159. package/dist/validation/builtin/email.js.map +1 -1
  160. package/dist/validation/builtin/notAlreadySubmitted.js +1 -0
  161. package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
  162. package/dist/validation/emailPattern.js +10 -0
  163. package/dist/validation/emailPattern.js.map +1 -0
  164. package/dist/validation/registry.d.ts +2 -1
  165. package/dist/validation/registry.js +2 -9
  166. package/dist/validation/registry.js.map +1 -1
  167. package/package.json +4 -4
@@ -1,33 +1,30 @@
1
1
  import { fieldNamesOfType } from "../fields/fieldNamesOfType.js";
2
- import { localizedIf } from "../fields/localizedIf.js";
3
2
  import { keys } from "../translations/keys.js";
4
- import { asTranslate, labelFor, labelForKey } from "../translations/server.js";
5
- import { resolveFromAddressesRequest } from "../actions/fromAddresses.js";
3
+ import { asTranslate, labelForKey, resolveDefinitionLabel } from "../translations/server.js";
4
+ import { localizedIf } from "../fields/localizedIf.js";
6
5
  import { validateUrl } from "../validation/validateUrl.js";
7
6
  import { normalizeCalc } from "../calc/normalizeCalc.js";
8
7
  import { isLoggedIn } from "../plugin/access.js";
9
8
  import { buildActionBlocks } from "../actions/buildActionBlocks.js";
10
- import { resolvePollTypes } from "../poll/pollTypeRegistry.js";
11
- import { enqueuePollClose } from "../poll/closeJob.js";
12
- import { resolveFormResultsRequest } from "../aggregation/resolveResultsRequest.js";
13
9
  import { buildConditionTypeMap } from "../conditions/conditionType.js";
14
- import { normalizeFormConditions } from "../conditions/normalizeConditions.js";
15
- import { resolveConsentSourcesRequest } from "../consent/resolveConsentSourcesRequest.js";
10
+ import { buildOperandTypes, normalizeFormConditions, normalizeWhere } from "../conditions/normalizeConditions.js";
16
11
  import { resolveConsentStatements } from "../consent/resolveConsentStatements.js";
17
- import { resolveDepartmentsRequest } from "../email/departments.js";
18
12
  import { buildFieldBlocks } from "../fields/buildFieldBlocks.js";
19
13
  import { normalizeFlow } from "../flow/normalizeFlow.js";
20
14
  import { END_OF_FORM } from "../flow/types.js";
21
15
  import { buildPollOptionSourceFields } from "../poll/buildPollOptionSourceFields.js";
16
+ import { resolvePollTypes } from "../poll/pollTypeRegistry.js";
17
+ import { enqueuePollClose } from "../poll/closeJob.js";
22
18
  import { pollOutcomeBeforeChange } from "../poll/outcomeBeforeChange.js";
23
19
  import { buildDefaultOutcomeFields } from "../poll/outcomeFields.js";
24
- import { resolvePollCloseRequest } from "../poll/resolvePollCloseRequest.js";
25
- import { resolvePollOptionsRequest } from "../poll/resolvePollOptionsRequest.js";
26
20
  import { buildValidateResultsField, pollEligibleTypes } from "../poll/resultsField.js";
27
21
  import { buildDefaultButtonFields } from "./buttonFields.js";
22
+ import { buildFormsEndpoints } from "./formsEndpoints.js";
28
23
  import { ValidationError } from "payload";
29
24
  //#region src/collections/forms.ts
30
25
  const FORMS_SLUG = "forms";
26
+ /** `req.context` key under which `consentAfterRead` tracks the form ids it is currently resolving, to break re-entrant reads. */
27
+ const CONSENT_AFTER_READ_GUARD = "formBuilderConsentAfterReadInFlight";
31
28
  /**
32
29
  * Require the title in the default locale (and on hosts without localization), but let it be empty in
33
30
  * other locales so a localized form falls back to the default-locale title rather than forcing a
@@ -87,7 +84,7 @@ const stampFileCollections = (rows, slug) => {
87
84
  if (Array.isArray(subFields)) stampFileCollections(subFields, slug);
88
85
  }
89
86
  };
90
- const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSources, actionRegistry = /* @__PURE__ */ new Map(), localizeContent = true, richText, uploadsCollectionSlug, resultsAccess, pollSourceRegistry, pollTypeRegistry, outcomeFields, buttons, response, fromAddresses, departments, redirectRelationships }) => {
87
+ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSources, consentResolveOnRead, actionRegistry = /* @__PURE__ */ new Map(), localizeContent = true, richText, uploadsCollectionSlug, resultsAccess, pollSourceRegistry, pollTypeRegistry, outcomeFields, buttons, response, fromAddresses, departments, redirectRelationships }) => {
91
88
  const conditionTypes = buildConditionTypeMap(registry);
92
89
  const pollTypes = pollTypeRegistry ?? resolvePollTypes();
93
90
  const pollResultsTypes = pollEligibleTypes(registry);
@@ -110,7 +107,8 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
110
107
  if (typeof field.name === "string" && field.name.length > 0) return field.name;
111
108
  return bareTypes.has(field.blockType) && field.id != null ? String(field.id) : void 0;
112
109
  }).filter((key) => key !== void 0);
113
- const normalizedFlow = normalizeFlow(data.flow, fieldKeys);
110
+ const operandTypes = buildOperandTypes(normalized, conditionTypes);
111
+ const normalizedFlow = normalizeFlow(data.flow, fieldKeys, (w) => normalizeWhere(w, operandTypes));
114
112
  if (providedFlowStepCount(data.flow) > 0 && normalizedFlow === void 0) throw new ValidationError({
115
113
  collection: FORMS_SLUG,
116
114
  errors: [{
@@ -152,7 +150,6 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
152
150
  });
153
151
  return doc;
154
152
  };
155
- const pollTypeOptionLabel = (label) => typeof label === "string" ? labelFor(label) : label;
156
153
  const fieldsField = {
157
154
  name: "fields",
158
155
  type: "blocks",
@@ -321,7 +318,7 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
321
318
  description: labelForKey(keys.pollTypeDescription)
322
319
  },
323
320
  options: orderedPollTypes.filter((strategy) => strategy.type !== "source" || hasOptionSources).map((strategy) => ({
324
- label: pollTypeOptionLabel(strategy.label),
321
+ label: resolveDefinitionLabel(strategy.label),
325
322
  value: strategy.type
326
323
  }))
327
324
  },
@@ -412,7 +409,15 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
412
409
  ]
413
410
  }
414
411
  ];
415
- const consentAfterRead = consentSources ? async ({ doc, req }) => {
412
+ const consentAfterRead = consentSources && consentResolveOnRead !== false ? async ({ doc, req }) => {
413
+ const id = doc.id;
414
+ const key = id == null ? void 0 : String(id);
415
+ if (key !== void 0 && req.context) {
416
+ const inFlight = req.context[CONSENT_AFTER_READ_GUARD] ?? /* @__PURE__ */ new Set();
417
+ if (inFlight.has(key)) return doc;
418
+ inFlight.add(key);
419
+ req.context[CONSENT_AFTER_READ_GUARD] = inFlight;
420
+ }
416
421
  try {
417
422
  const statements = await resolveConsentStatements({
418
423
  payload: req.payload,
@@ -423,92 +428,18 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
423
428
  if (Object.keys(statements).length > 0) doc.consentStatements = statements;
424
429
  } catch (error) {
425
430
  req.payload.logger?.warn(`form-builder consent afterRead: ${String(error)}`);
431
+ } finally {
432
+ if (key !== void 0 && req.context) req.context[CONSENT_AFTER_READ_GUARD]?.delete(key);
426
433
  }
427
434
  return doc;
428
435
  } : void 0;
429
- const defaultEndpoints = [
430
- {
431
- path: "/:id/results",
432
- method: "get",
433
- handler: async (req) => {
434
- const field = typeof req.query?.field === "string" ? req.query.field : void 0;
435
- const { status, body } = await resolveFormResultsRequest({
436
- payload: req.payload,
437
- formId: req.routeParams?.id,
438
- field,
439
- isAuthed: Boolean(req.user),
440
- req,
441
- access: resultsAccess,
442
- eligibleTypes: pollResultsTypes
443
- });
444
- return Response.json(body, { status });
445
- }
446
- },
447
- {
448
- path: "/:id/poll-options",
449
- method: "get",
450
- handler: async (req) => {
451
- const { status, body } = await resolvePollOptionsRequest({
452
- payload: req.payload,
453
- formId: req.routeParams?.id,
454
- isAuthed: Boolean(req.user),
455
- req
456
- });
457
- return Response.json(body, { status });
458
- }
459
- },
460
- {
461
- path: "/:id/close",
462
- method: "post",
463
- handler: async (req) => {
464
- const { status, body } = await resolvePollCloseRequest({
465
- payload: req.payload,
466
- formId: req.routeParams?.id,
467
- isAuthed: Boolean(req.user),
468
- req
469
- });
470
- return Response.json(body, { status });
471
- }
472
- },
473
- ...consentSources ? [{
474
- path: "/:id/consent-sources",
475
- method: "get",
476
- handler: async (req) => {
477
- const { status, body } = await resolveConsentSourcesRequest({
478
- payload: req.payload,
479
- formId: req.routeParams?.id,
480
- isAuthed: Boolean(req.user),
481
- req,
482
- resolver: consentSources
483
- });
484
- return Response.json(body, { status });
485
- }
486
- }] : [],
487
- ...fromAddresses ? [{
488
- path: "/:id/from-addresses",
489
- method: "get",
490
- handler: async (req) => {
491
- const { status, body } = await resolveFromAddressesRequest({
492
- isAuthed: Boolean(req.user),
493
- req,
494
- resolver: fromAddresses
495
- });
496
- return Response.json(body, { status });
497
- }
498
- }] : [],
499
- ...departments ? [{
500
- path: "/:id/departments",
501
- method: "get",
502
- handler: async (req) => {
503
- const { status, body } = await resolveDepartmentsRequest({
504
- isAuthed: Boolean(req.user),
505
- req,
506
- resolver: departments
507
- });
508
- return Response.json(body, { status });
509
- }
510
- }] : []
511
- ];
436
+ const defaultEndpoints = buildFormsEndpoints({
437
+ resultsAccess,
438
+ pollResultsTypes,
439
+ consentSources,
440
+ fromAddresses,
441
+ departments
442
+ });
512
443
  return {
513
444
  ...overrides ?? {},
514
445
  slug: FORMS_SLUG,
@@ -1 +1 @@
1
- {"version":3,"file":"forms.js","names":[],"sources":["../../src/collections/forms.ts"],"sourcesContent":["import {\n\ttype CollectionAfterChangeHook,\n\ttype CollectionAfterReadHook,\n\ttype CollectionBeforeValidateHook,\n\ttype CollectionConfig,\n\ttype CollectionSlug,\n\ttype Field,\n\ttype PayloadRequest,\n\ttype TextFieldSingleValidation,\n\tValidationError,\n} from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport { buildActionBlocks } from '../actions/buildActionBlocks'\nimport type { FromAddressesResolver } from '../actions/fromAddresses'\nimport { resolveFromAddressesRequest } from '../actions/fromAddresses'\nimport type { ActionRegistry } from '../actions/registry'\nimport {\n\ttype FormResultsAccess,\n\tresolveFormResultsRequest,\n} from '../aggregation/resolveResultsRequest'\nimport { normalizeCalc } from '../calc/normalizeCalc'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport { type FieldRow, normalizeFormConditions } from '../conditions/normalizeConditions'\nimport { resolveConsentSourcesRequest } from '../consent/resolveConsentSourcesRequest'\nimport { resolveConsentStatements } from '../consent/resolveConsentStatements'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport { type DepartmentEmailsResolver, resolveDepartmentsRequest } from '../email/departments'\nimport { buildFieldBlocks } from '../fields/buildFieldBlocks'\nimport { fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport { localizedIf } from '../fields/localizedIf'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { normalizeFlow } from '../flow/normalizeFlow'\nimport { END_OF_FORM } from '../flow/types'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport { buildPollOptionSourceFields } from '../poll/buildPollOptionSourceFields'\nimport { enqueuePollClose } from '../poll/closeJob'\nimport { pollOutcomeBeforeChange } from '../poll/outcomeBeforeChange'\nimport { buildDefaultOutcomeFields, type OutcomeFieldsOverride } from '../poll/outcomeFields'\nimport { type PollTypeRegistry, resolvePollTypes } from '../poll/pollTypeRegistry'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { resolvePollCloseRequest } from '../poll/resolvePollCloseRequest'\nimport { resolvePollOptionsRequest } from '../poll/resolvePollOptionsRequest'\nimport { buildValidateResultsField, pollEligibleTypes } from '../poll/resultsField'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelFor, labelForKey } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { validateUrl } from '../validation/validateUrl'\nimport { type ButtonsOption, buildDefaultButtonFields } from './buttonFields'\nimport type { ResponseOption } from './redirectFields'\n\nexport const FORMS_SLUG = 'forms'\n\n/**\n * Require the title in the default locale (and on hosts without localization), but let it be empty in\n * other locales so a localized form falls back to the default-locale title rather than forcing a\n * translation for every locale. Payload's field-level `required` cannot express this: it enforces per\n * write-locale, which would make a title mandatory in every locale and break the documented fallback.\n */\nconst validateFormTitle: TextFieldSingleValidation = (value, { req }) => {\n\tconst localization = req.payload.config.localization\n\tconst defaultLocale = localization ? localization.defaultLocale : undefined\n\tconst enforced =\n\t\tdefaultLocale === undefined || req.locale === undefined || req.locale === defaultLocale\n\tif (enforced && (typeof value !== 'string' || value.trim().length === 0)) {\n\t\treturn req.t('validation:required')\n\t}\n\treturn true\n}\n\n/**\n * Field-level validation for the raw flow JSON: step ids must be non-empty, unique, and not the\n * reserved `END_OF_FORM` sentinel, and every explicit step-id reference (a string `next`, a\n * transition `to`) must resolve to a known step. `next: null` (explicit end of form) and an\n * absent `next` (fall through to the next step in array order) are always valid. On full-document\n * saves the beforeValidate pass has already run `normalizeFlow` and laundered dangling\n * references, so the unknown-target checks mainly protect partial API updates that send `flow`\n * without `fields`.\n */\nconst validateFlow = (raw: unknown): string | true => {\n\tif (raw === null || raw === undefined) return true\n\tconst r = raw as Record<string, unknown>\n\tif (!Array.isArray(r.steps)) return true\n\tconst steps = r.steps as Array<Record<string, unknown>>\n\tconst emptyIdStep = steps.find((s) => typeof s?.id !== 'string' || s.id.length === 0)\n\tif (emptyIdStep) return 'Flow: every step must have a non-empty ID'\n\tif (steps.some((s) => s.id === END_OF_FORM)) {\n\t\treturn `Flow: step ID \"${END_OF_FORM}\" is reserved`\n\t}\n\tconst ids = steps.map((s) => s.id as string)\n\tif (new Set(ids).size !== ids.length) {\n\t\treturn 'Flow: duplicate step IDs found'\n\t}\n\tconst idSet = new Set(ids)\n\tfor (const step of steps) {\n\t\tconst id = step.id as string\n\t\tif (typeof step.next === 'string' && step.next.length > 0 && !idSet.has(step.next)) {\n\t\t\treturn `Flow: step \"${id}\" references unknown next step \"${step.next}\"`\n\t\t}\n\t\tif (Array.isArray(step.transitions)) {\n\t\t\tfor (const t of step.transitions as Array<Record<string, unknown>>) {\n\t\t\t\tif (typeof t?.to === 'string' && t.to.length > 0 && !idSet.has(t.to)) {\n\t\t\t\t\treturn `Flow: step \"${id}\" has a transition to unknown step \"${t.to}\"`\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n/** How many steps the caller actually submitted, before normalization strips/collapses the flow. */\nconst providedFlowStepCount = (raw: unknown): number => {\n\tif (raw === null || typeof raw !== 'object') return 0\n\tconst steps = (raw as { steps?: unknown }).steps\n\treturn Array.isArray(steps) ? steps.length : 0\n}\n\n/**\n * Stamp the configured uploads collection slug onto every `file` block (including repeater\n * sub-fields) on each save. The block carries a hidden `uploadsCollection` field so the slug\n * reaches the client renderer through the form document; the server always overwrites it from\n * plugin config, so the stored value is never author- or client-controlled.\n */\nconst stampFileCollections = (rows: FieldRow[], slug: string): void => {\n\tfor (const row of rows) {\n\t\tif (row.blockType === 'file') {\n\t\t\t;(row as { uploadsCollection?: string }).uploadsCollection = slug\n\t\t}\n\t\tconst subFields = (row as { subFields?: unknown }).subFields\n\t\tif (Array.isArray(subFields)) {\n\t\t\tstampFileCollections(subFields as FieldRow[], slug)\n\t\t}\n\t}\n}\n\ntype BuildFormsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/**\n\t * The plugin `consent.sources` option. Present: the `/:id/consent-sources` endpoint backing the\n\t * consent field's source select is registered. Absent: neither it nor the consent field type exists.\n\t */\n\tconsentSources?: ConsentSourcesResolver\n\tactionRegistry?: ActionRegistry\n\tlocalizeContent?: boolean\n\t/** The plugin `richText` option; `editor` overrides the response message's richText editor. */\n\trichText?: RichTextBodyOption\n\t/** The host-owned uploads collection slug from plugin config; absent when uploads are disabled. */\n\tuploadsCollectionSlug?: string\n\t/** Host seam gating anonymous results reads (plugin option `results.access`). */\n\tresultsAccess?: FormResultsAccess\n\t/** Registered poll option sources (plugin option `poll.sources`); empty registry means no source fields. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Registered poll outcome strategies (`poll.types`); drives the poll `type` select options. Defaults to the built-ins. */\n\tpollTypeRegistry?: PollTypeRegistry\n\t/** The plugin `poll.outcomeFields` seam; composes the outcome group from the two default fields. */\n\toutcomeFields?: OutcomeFieldsOverride\n\t/** The plugin `buttons` option; `fields` composes the `{ submit, prev, next }` labels from the localized defaults. */\n\tbuttons?: ButtonsOption\n\t/** The plugin `response` option; `redirect.fields` composes the `response.redirect` group from its default fields. */\n\tresponse?: ResponseOption\n\t/**\n\t * The plugin `email.fromAddresses` option. Present: both email actions gain a `from` select and\n\t * the `/:id/from-addresses` endpoint is registered. Absent: neither exists.\n\t */\n\tfromAddresses?: FromAddressesResolver\n\t/**\n\t * The plugin `email.departments` option. Present: the `emailTeam` `to` becomes a department select\n\t * and the `/:id/departments` endpoint backing it is registered. Absent: `to` stays a plain field.\n\t */\n\tdepartments?: DepartmentEmailsResolver\n\t/**\n\t * The plugin `redirectRelationships` option. Non-empty: `response.redirect` gains a polymorphic\n\t * `reference` relationship field so an author can redirect to an internal document instead of\n\t * (or alongside) a URL. Absent or empty: no `reference` field exists, matching today's URL-only\n\t * redirect.\n\t */\n\tredirectRelationships?: CollectionSlug[]\n\toverrides?: CollectionOverrides\n}\n\nexport const buildFormsCollection = ({\n\toverrides,\n\tregistry,\n\truleRegistry,\n\tconsentSources,\n\tactionRegistry = new Map(),\n\tlocalizeContent = true,\n\trichText,\n\tuploadsCollectionSlug,\n\tresultsAccess,\n\tpollSourceRegistry,\n\tpollTypeRegistry,\n\toutcomeFields,\n\tbuttons,\n\tresponse,\n\tfromAddresses,\n\tdepartments,\n\tredirectRelationships,\n}: BuildFormsCollectionArgs): CollectionConfig => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst pollTypes = pollTypeRegistry ?? resolvePollTypes()\n\tconst pollResultsTypes = pollEligibleTypes(registry)\n\tconst bareTypes = new Set(\n\t\t[...registry.values()].filter((d) => d.bare === true).map((d) => d.type)\n\t)\n\t// Handed to the FlowBuilder so it can list bare (nameless) blocks in the step picker: slug ->\n\t// label (an i18n key or literal, resolved client-side).\n\tconst bareTypeLabels = Object.fromEntries(\n\t\t[...registry.values()].filter((d) => d.bare === true).map((d) => [d.type, d.label])\n\t)\n\tconst FLOW_BUILDER_REF = '@10x-media/form-builder/client#FlowBuilder'\n\tconst FIELD_NAME_SELECT_REF = '@10x-media/form-builder/client#FieldNameSelect'\n\tconst FLOW_STEPS_CELL_REF = '@10x-media/form-builder/client#FlowStepsCell'\n\tconst FIELD_COUNT_CELL_REF = '@10x-media/form-builder/client#FieldCountCell'\n\tconst CLOSE_POLL_BUTTON_REF = '@10x-media/form-builder/client#ClosePollButton'\n\t// The `source` outcome strategy delegates to the poll's option source, so it is useless without one:\n\t// offer it in the author-facing select only when option sources are registered. The strategy stays in\n\t// the registry regardless, so `resolvePollOutcome` and host code can still address it.\n\tconst hasOptionSources = (pollSourceRegistry?.size ?? 0) > 0\n\t// `mostVoted` leads the author-facing select because it is the default: an enabled poll works\n\t// immediately, with no winner to hand-pick. Only the select order changes; the registry keeps its\n\t// own order (host strategies stay after the built-ins).\n\tconst orderedPollTypes = [\n\t\t...[...pollTypes.values()].filter((strategy) => strategy.type === 'mostVoted'),\n\t\t...[...pollTypes.values()].filter((strategy) => strategy.type !== 'mostVoted'),\n\t]\n\n\tconst beforeValidate: CollectionBeforeValidateHook = ({ data, req }) => {\n\t\tif (data && Array.isArray(data.fields)) {\n\t\t\tconst normalized: FieldRow[] = normalizeFormConditions(\n\t\t\t\tdata.fields as FieldRow[],\n\t\t\t\tconditionTypes\n\t\t\t)\n\t\t\tfor (const field of normalized) {\n\t\t\t\tif ('expression' in field) {\n\t\t\t\t\tfield.expression = normalizeCalc(field.expression)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (uploadsCollectionSlug) {\n\t\t\t\tstampFileCollections(normalized, uploadsCollectionSlug)\n\t\t\t}\n\t\t\tdata.fields = normalized\n\t\t\t// Flow step assignments store field keys: machine names for named fields, block row ids\n\t\t\t// for bare (nameless) blocks. Mirrors `fieldKey` over the raw rows.\n\t\t\tconst fieldKeys = normalized\n\t\t\t\t.map((field: FieldRow) => {\n\t\t\t\t\tif (typeof field.name === 'string' && field.name.length > 0) {\n\t\t\t\t\t\treturn field.name\n\t\t\t\t\t}\n\t\t\t\t\treturn bareTypes.has(field.blockType) && field.id != null ? String(field.id) : undefined\n\t\t\t\t})\n\t\t\t\t.filter((key): key is string => key !== undefined)\n\t\t\tconst normalizedFlow = normalizeFlow(data.flow, fieldKeys)\n\t\t\t// A flow the author built but that collapses to fewer than two valid steps would\n\t\t\t// otherwise vanish silently. Surface it instead of discarding their work.\n\t\t\tif (providedFlowStepCount(data.flow) > 0 && normalizedFlow === undefined) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tpath: 'flow',\n\t\t\t\t\t\t\t\tmessage: 'A flow needs at least two steps. Add another step or remove the flow.',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t\tdata.flow = normalizedFlow\n\t\t\t// A poll needs a field whose answers are the votes. With the poll enabled and no vote field\n\t\t\t// chosen (and the outcome not coming from an option source), bind one: auto-fill the sole\n\t\t\t// eligible field for a one-choice form, otherwise surface a clear error rather than storing a\n\t\t\t// poll that can never aggregate. The optionSource/`source` exemption preserves domain-driven\n\t\t\t// polls that have no on-form vote field. `buildValidateResultsField` still checks a set value;\n\t\t\t// this is the enforcer for enabled polls.\n\t\t\tif (data.pollEnabled === true) {\n\t\t\t\tconst poll =\n\t\t\t\t\tdata.poll != null && typeof data.poll === 'object'\n\t\t\t\t\t\t? (data.poll as Record<string, unknown>)\n\t\t\t\t\t\t: {}\n\t\t\t\tconst resultsField = typeof poll.resultsField === 'string' ? poll.resultsField.trim() : ''\n\t\t\t\tconst optionSource = typeof poll.optionSource === 'string' ? poll.optionSource.trim() : ''\n\t\t\t\tconst sourceDriven = optionSource.length > 0 || poll.type === 'source'\n\t\t\t\tif (resultsField.length === 0 && !sourceDriven) {\n\t\t\t\t\tconst eligible = fieldNamesOfType(data.fields, pollResultsTypes)\n\t\t\t\t\tif (eligible.length === 1) {\n\t\t\t\t\t\tdata.poll = { ...poll, resultsField: eligible[0] }\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst messageKey =\n\t\t\t\t\t\t\teligible.length > 1 ? keys.pollVoteFieldChoose : keys.pollVoteFieldMissing\n\t\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\t\t\terrors: [{ path: 'poll.resultsField', message: asTranslate(req.t)(messageKey) }],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treq.t\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn data\n\t}\n\n\t// After every save, (re)schedule the poll's auto-close job when it applies (enabled + closesAt +\n\t// unresolved + non-manual strategy + a job runner present). Best-effort and non-throwing, so it\n\t// never affects the save; without a runner the resolve-on-read fallback in the results endpoint\n\t// heals the outcome instead.\n\tconst pollCloseAfterChange: CollectionAfterChangeHook = async ({ doc, req }) => {\n\t\tawait enqueuePollClose({ payload: req.payload, form: doc, req })\n\t\treturn doc\n\t}\n\n\t// Select-option label from a strategy's `label`: a string is resolved like a field label (i18n key\n\t// or literal), a per-locale record passes through unchanged.\n\tconst pollTypeOptionLabel = (label: string | Record<string, string>) =>\n\t\ttypeof label === 'string' ? labelFor(label) : label\n\n\tconst fieldsField: Field = {\n\t\tname: 'fields',\n\t\ttype: 'blocks',\n\t\tblocks: buildFieldBlocks({ registry, ruleRegistry, localize: localizeContent }),\n\t\tadmin: { components: { Cell: FIELD_COUNT_CELL_REF } },\n\t}\n\n\tconst flowField: Field = {\n\t\tname: 'flow',\n\t\ttype: 'json',\n\t\tvalidate: validateFlow,\n\t\tadmin: {\n\t\t\tcomponents: {\n\t\t\t\tCell: FLOW_STEPS_CELL_REF,\n\t\t\t\tField: { path: FLOW_BUILDER_REF, clientProps: { conditionTypes, bareTypeLabels } },\n\t\t\t},\n\t\t},\n\t\t// Narrows the generated TypeScript type from opaque JSON to FormFlow so callers\n\t\t// don't need a cast. Keep this in sync with src/flow/types.ts.\n\t\ttypescriptSchema: [\n\t\t\t() => ({\n\t\t\t\ttype: 'object' as const,\n\t\t\t\trequired: ['steps'],\n\t\t\t\tadditionalProperties: false,\n\t\t\t\tproperties: {\n\t\t\t\t\tsteps: {\n\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\trequired: ['id'],\n\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\tid: { type: 'string' as const },\n\t\t\t\t\t\t\t\ttitle: { type: 'string' as const },\n\t\t\t\t\t\t\t\tfields: { type: 'array' as const, items: { type: 'string' as const } },\n\t\t\t\t\t\t\t\tnext: { type: ['string', 'null'] as ('string' | 'null')[] },\n\t\t\t\t\t\t\t\ttransitions: {\n\t\t\t\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\t\t\t\trequired: ['to'],\n\t\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\tto: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\t\twhen: { type: 'object' as const, additionalProperties: true },\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t}\n\n\tconst actionsField: Field = {\n\t\tname: 'actions',\n\t\ttype: 'blocks',\n\t\tblocks: buildActionBlocks(actionRegistry),\n\t\tlabel: labelForKey(keys.configActions),\n\t\t// Action config can contain secrets (e.g. signedWebhook.secret). The collection\n\t\t// itself is publicly readable so forms can be rendered by anonymous clients, but\n\t\t// action config must never be exposed to anonymous callers.\n\t\taccess: { read: isLoggedIn },\n\t}\n\n\t// Optional polymorphic reference to an internal document, for `response.redirect` to point at\n\t// instead of (or alongside) a URL. Absent unless `redirectRelationships` is a non-empty array:\n\t// always polymorphic even for one slug (the consent `page` field's precedent), so a host adding\n\t// a second collection later never changes the stored shape. The plugin never resolves this to a\n\t// URL itself; `toFormDocument` passes the raw `{ relationTo, value }` through for the host to\n\t// resolve, since only the host knows its own routing.\n\tconst redirectReferenceField: Field[] =\n\t\tredirectRelationships && redirectRelationships.length > 0\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'reference',\n\t\t\t\t\t\ttype: 'relationship',\n\t\t\t\t\t\trelationTo: redirectRelationships,\n\t\t\t\t\t\tlabel: labelForKey(keys.responseRedirectReference),\n\t\t\t\t\t\tadmin: { description: labelForKey(keys.responseRedirectReferenceDescription) },\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []\n\n\t// The redirect group's fields: the `url` text field plus the optional polymorphic `reference`.\n\t// The `response.redirect.fields` seam composes them (mirroring `buttons.fields`), so a host can\n\t// prepend a custom link field, swap `url` for their own picker, or filter; the default (no\n\t// override) is the same `[url, ...reference]` as before. Built-in redirect handling still reads\n\t// `redirect.url`/`redirect.reference`, so a host replacing `url` owns resolving their field to a\n\t// destination in their frontend.\n\tconst urlField: Field = {\n\t\tname: 'url',\n\t\ttype: 'text',\n\t\tlabel: labelForKey(keys.responseUrl),\n\t\tvalidate: validateUrl,\n\t}\n\tconst defaultRedirectFields: Field[] = [urlField, ...redirectReferenceField]\n\tconst redirectFields = response?.redirect?.fields\n\t\t? response.redirect.fields({ defaultFields: defaultRedirectFields })\n\t\t: defaultRedirectFields\n\n\t// What the visitor sees after a successful submit. Publicly readable (unlike actions): the\n\t// client renderer needs message/redirect. `type`/`url` are behavior, never localized;\n\t// `message` is visitor-facing content and follows `localizeContent`.\n\t// `type` is defaulted and not clearable rather than `required`: a required member would make\n\t// the whole group required in generated types, breaking typed `payload.create` calls that\n\t// omit `response`. Consumers treat a missing type as 'message'.\n\tconst responseField: Field = {\n\t\tname: 'response',\n\t\ttype: 'group',\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'type',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'message',\n\t\t\t\tlabel: labelForKey(keys.responseType),\n\t\t\t\tadmin: { isClearable: false },\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: labelForKey(keys.responseTypeMessage), value: 'message' },\n\t\t\t\t\t{ label: labelForKey(keys.responseTypeRedirect), value: 'redirect' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'message',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelForKey(keys.responseMessage),\n\t\t\t\t// Unset type (docs predating this field) means 'message', matching the client fallback.\n\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.type !== 'redirect' },\n\t\t\t\t...(richText?.editor ? { editor: richText.editor } : {}),\n\t\t\t\t...localizedIf(localizeContent),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'redirect',\n\t\t\t\ttype: 'group',\n\t\t\t\tlabel: labelForKey(keys.responseRedirect),\n\t\t\t\tadmin: {\n\t\t\t\t\tcondition: (_data, siblingData) => siblingData?.type === 'redirect',\n\t\t\t\t\thideGutter: true,\n\t\t\t\t},\n\t\t\t\tfields: redirectFields,\n\t\t\t},\n\t\t],\n\t}\n\n\tconst defaultOutcomeFields = buildDefaultOutcomeFields()\n\tconst defaultButtonFields = buildDefaultButtonFields(localizeContent)\n\t// Half-width copy of a default field for the prev/next row; merges width into whatever admin the\n\t// field already carries (a host's `buttons.fields` override may add its own) rather than replacing\n\t// it. The cast is safe: `width` is a valid admin option on every field variant, but spreading a\n\t// `Field`-typed value's `admin` back into a union-typed object literal loses the discriminant TS\n\t// needs to check it structurally.\n\tconst halfWidth = (field: Field): Field =>\n\t\t({ ...field, admin: { ...field.admin, width: '50%' } }) as Field\n\t// The three button-label fields sit at the document root now (no `buttons` group): `submit` at\n\t// the bottom of the Fields tab, `prev`/`next` in a row on the Flow tab. The `buttons.fields` seam\n\t// composes each slot from the already-localized defaults, so a host can wrap a default in a row\n\t// with its own field (e.g. an icon select) or replace it; `toFormDocument` reassembles the three\n\t// labels into `FormDocument.buttons` for the client.\n\tconst composedButtons = buttons?.fields\n\t\t? buttons.fields({ defaultFields: defaultButtonFields })\n\t\t: defaultButtonFields\n\tconst submitField = composedButtons.submit\n\tconst prevNextRow: Field = {\n\t\ttype: 'row',\n\t\t// The prev/next labels only matter once the flow has a step; the Flow tab itself is gated on\n\t\t// `multistep`. `data` (1st arg) is the whole document, so `flow.steps` reads the stored flow.\n\t\tadmin: {\n\t\t\tcondition: (data) => {\n\t\t\t\tconst steps = (data as { flow?: { steps?: unknown } })?.flow?.steps\n\t\t\t\treturn Array.isArray(steps) && steps.length >= 1\n\t\t\t},\n\t\t},\n\t\tfields: [halfWidth(composedButtons.prev), halfWidth(composedButtons.next)],\n\t}\n\n\t// Poll lifecycle config; identifiers and behavior, never localized. Lives inside the conditional\n\t// Poll tab (gated on the top-level `pollEnabled` flag), so the per-field enabled conditions are\n\t// gone; `label: false` suppresses the group header the tab label already provides.\n\t// `resultsVisibility` is defaulted and not clearable rather than `required` for the same\n\t// generated-types reason as `response.type`.\n\tconst pollGroupField: Field = {\n\t\tname: 'poll',\n\t\ttype: 'group',\n\t\tlabel: false,\n\t\tfields: [\n\t\t\t// Authored by picking from the form's poll-eligible fields; the stored value stays a plain\n\t\t\t// text field name. Select options and server validate share `pollResultsTypes`, so they\n\t\t\t// cannot drift.\n\t\t\t{\n\t\t\t\tname: 'resultsField',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.pollResultsField),\n\t\t\t\tvalidate: buildValidateResultsField(pollResultsTypes),\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: {\n\t\t\t\t\t\tField: {\n\t\t\t\t\t\t\tpath: FIELD_NAME_SELECT_REF,\n\t\t\t\t\t\t\tclientProps: {\n\t\t\t\t\t\t\t\ttypes: pollResultsTypes,\n\t\t\t\t\t\t\t\tdescriptionKey: keys.pollResultsFieldDescription,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// How the winning value(s) get decided. `mostVoted` (default) and `source` auto-resolve on close\n\t\t\t// (via the scheduled job or the results-read fallback); `manual` leaves it to an admin. Options\n\t\t\t// come from the registered `poll.types` strategies, so a host strategy appears here too, with\n\t\t\t// `mostVoted` first so an enabled poll works with no winner to hand-pick.\n\t\t\t{\n\t\t\t\tname: 'type',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'mostVoted',\n\t\t\t\tlabel: labelForKey(keys.pollType),\n\t\t\t\tadmin: { isClearable: false, description: labelForKey(keys.pollTypeDescription) },\n\t\t\t\toptions: orderedPollTypes\n\t\t\t\t\t.filter((strategy) => strategy.type !== 'source' || hasOptionSources)\n\t\t\t\t\t.map((strategy) => ({\n\t\t\t\t\t\tlabel: pollTypeOptionLabel(strategy.label),\n\t\t\t\t\t\tvalue: strategy.type,\n\t\t\t\t\t})),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'resultsVisibility',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'afterVote',\n\t\t\t\tlabel: labelForKey(keys.pollResultsVisibility),\n\t\t\t\tadmin: { isClearable: false },\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: labelForKey(keys.pollVisibilityAfterVote), value: 'afterVote' },\n\t\t\t\t\t{ label: labelForKey(keys.pollVisibilityAfterClose), value: 'afterClose' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'closesAt',\n\t\t\t\ttype: 'date',\n\t\t\t\tlabel: labelForKey(keys.pollClosesAt),\n\t\t\t\tadmin: { date: { pickerAppearance: 'dayAndTime' } },\n\t\t\t},\n\t\t\t// Close / reopen the poll from the admin. The button toggles on the live `closesAt` and saves the\n\t\t\t// whole document (persisting an unsaved winner alongside `closesAt`) rather than calling the close\n\t\t\t// endpoint, so there is no DB-vs-form-state mismatch. Always mounted inside the (pollEnabled-gated)\n\t\t\t// Poll tab now; the button owns both states, so it carries no `admin.condition` of its own.\n\t\t\t{\n\t\t\t\tname: 'closePoll',\n\t\t\t\ttype: 'ui',\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: { Field: CLOSE_POLL_BUTTON_REF },\n\t\t\t\t},\n\t\t\t},\n\t\t\t...buildPollOptionSourceFields(pollSourceRegistry ?? new Map()),\n\t\t\t// `winningValues` is recorded either by an admin picking from the poll's effective options\n\t\t\t// (served by `/:id/poll-options`) or by `resolvePollOutcome` (host domain logic); more than\n\t\t\t// one value records a tie. The `pollOutcomeBeforeChange` hook validates both paths and owns\n\t\t\t// the `resolvedAt` stamp. `resolvedAt` itself stays fully locked: field-level create/update\n\t\t\t// access blocks every non-override caller write (Payload silently drops the denied value\n\t\t\t// rather than erroring) while the hook's stamp, applied after access filtering, still\n\t\t\t// persists. The `poll.outcomeFields` seam receives both defaults and its return becomes the\n\t\t\t// group's fields verbatim, so a host can swap `winningValues` for its own component; the hook\n\t\t\t// still validates every stored value against the effective options, so no swap bypasses it.\n\t\t\t// `hideGutter` stays: outcome is still a group nested inside the poll group.\n\t\t\t{\n\t\t\t\tname: 'outcome',\n\t\t\t\ttype: 'group',\n\t\t\t\tlabel: labelForKey(keys.pollOutcome),\n\t\t\t\tadmin: { hideGutter: true },\n\t\t\t\tfields: outcomeFields\n\t\t\t\t\t? outcomeFields({ defaultFields: defaultOutcomeFields })\n\t\t\t\t\t: [defaultOutcomeFields.winningValues, defaultOutcomeFields.resolvedAt],\n\t\t\t},\n\t\t],\n\t}\n\n\tconst defaultFields: Field[] = [\n\t\t// The document title is the only visitor-facing text the collection renders itself: it is\n\t\t// always required (drives `useAsTitle` in the admin list/relationship views) and is passed\n\t\t// through `toFormDocument` as `FormDocument.title`. Whether and how a host renders it above\n\t\t// the fields is entirely the host's call; the plugin does not gate or duplicate it.\n\t\t{\n\t\t\tname: 'title',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelForKey(keys.fieldTitle),\n\t\t\t// Visitor-facing content, so it follows `localizeContent` like the response message and\n\t\t\t// consent statement: hosts with localization get a per-locale title (and a locale-aware\n\t\t\t// `useAsTitle`), hosts without it are unaffected since Payload strips the flag. `validate`\n\t\t\t// (not `required`) keeps it mandatory only in the default locale so other locales fall back.\n\t\t\t...localizedIf(localizeContent),\n\t\t\tvalidate: validateFormTitle,\n\t\t},\n\t\t// The two form-type flags: behavior, never localized. `multistep` gates the Flow tab and the\n\t\t// client's step navigation; `pollEnabled` gates the Poll tab and marks the form a poll.\n\t\t{\n\t\t\ttype: 'row',\n\t\t\tfields: [\n\t\t\t\t{\n\t\t\t\t\tname: 'multistep',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tdefaultValue: false,\n\t\t\t\t\tlabel: labelForKey(keys.formMultistep),\n\t\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'pollEnabled',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tdefaultValue: false,\n\t\t\t\t\tlabel: labelForKey(keys.formPollEnabled),\n\t\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t// Unnamed tabs are presentational: their fields stay at the document root. An unnamed tab's\n\t\t// `admin.condition` receives the whole document as its 2nd arg, so the Flow and Poll tabs gate\n\t\t// on the root-level flags. The poll group nests its config under `form.poll` as before.\n\t\t{\n\t\t\ttype: 'tabs',\n\t\t\ttabs: [\n\t\t\t\t{ label: labelForKey(keys.tabFields), fields: [fieldsField, submitField] },\n\t\t\t\t{\n\t\t\t\t\tlabel: labelForKey(keys.tabFlow),\n\t\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.multistep === true },\n\t\t\t\t\tfields: [flowField, prevNextRow],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: labelForKey(keys.pollGroup),\n\t\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.pollEnabled === true },\n\t\t\t\t\tfields: [pollGroupField],\n\t\t\t\t},\n\t\t\t\t{ label: labelForKey(keys.tabActions), fields: [actionsField] },\n\t\t\t\t{ label: labelForKey(keys.tabResponse), fields: [responseField] },\n\t\t\t],\n\t\t},\n\t]\n\n\t// When `consent.sources` is set, resolve the visitor-facing consent statements onto every read of\n\t// a form doc (REST, local API, relationship population), so a client/modal fetch renders consent,\n\t// not only the RSC path that calls `resolveConsentStatements` itself. Fails open: a resolver outage\n\t// must never break form/admin/relationship reads, and the renderer already tolerates a missing\n\t// statement. `resolveConsentStatements` returns `{}` without calling the resolver when the form has\n\t// no consent fields, and entries are cached per request+form, so the cost stays bounded.\n\tconst consentAfterRead: CollectionAfterReadHook | undefined = consentSources\n\t\t? async ({ doc, req }) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst statements = await resolveConsentStatements({\n\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\treq,\n\t\t\t\t\t\tform: doc,\n\t\t\t\t\t\tsources: consentSources,\n\t\t\t\t\t})\n\t\t\t\t\tif (Object.keys(statements).length > 0) {\n\t\t\t\t\t\t;(doc as Record<string, unknown>).consentStatements = statements\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\treq.payload.logger?.warn(`form-builder consent afterRead: ${String(error)}`)\n\t\t\t\t}\n\t\t\t\treturn doc\n\t\t\t}\n\t\t: undefined\n\n\tconst defaultEndpoints: CollectionConfig['endpoints'] = [\n\t\t{\n\t\t\tpath: '/:id/results',\n\t\t\tmethod: 'get',\n\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\tconst field = typeof req.query?.field === 'string' ? req.query.field : undefined\n\t\t\t\tconst { status, body } = await resolveFormResultsRequest({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\t\tfield,\n\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\treq,\n\t\t\t\t\taccess: resultsAccess,\n\t\t\t\t\teligibleTypes: pollResultsTypes,\n\t\t\t\t})\n\t\t\t\treturn Response.json(body, { status })\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tpath: '/:id/poll-options',\n\t\t\tmethod: 'get',\n\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\tconst { status, body } = await resolvePollOptionsRequest({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\treq,\n\t\t\t\t})\n\t\t\t\treturn Response.json(body, { status })\n\t\t\t},\n\t\t},\n\t\t// Trusted admin action (authenticated only): close a poll now and resolve its outcome. A POST\n\t\t// because it mutates; auth is `Boolean(req.user)` so an anonymous caller gets 403 from the helper.\n\t\t{\n\t\t\tpath: '/:id/close',\n\t\t\tmethod: 'post',\n\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\tconst { status, body } = await resolvePollCloseRequest({\n\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\treq,\n\t\t\t\t})\n\t\t\t\treturn Response.json(body, { status })\n\t\t\t},\n\t\t},\n\t\t...(consentSources\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: '/:id/consent-sources',\n\t\t\t\t\t\tmethod: 'get' as const,\n\t\t\t\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\t\t\t\tconst { status, body } = await resolveConsentSourcesRequest({\n\t\t\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\t\tresolver: consentSources,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\treturn Response.json(body, { status })\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []),\n\t\t// The route id is unused: the from-addresses set is request-scoped (e.g. per tenant), not\n\t\t// per-form. Registered as a doc-scoped route only so the admin field can reuse\n\t\t// EndpointOptionsSelect unmodified (see buildFromField).\n\t\t...(fromAddresses\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: '/:id/from-addresses',\n\t\t\t\t\t\tmethod: 'get' as const,\n\t\t\t\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\t\t\t\tconst { status, body } = await resolveFromAddressesRequest({\n\t\t\t\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\t\tresolver: fromAddresses,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\treturn Response.json(body, { status })\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []),\n\t\t// Same request-scoped, id-unused shape as from-addresses: registered doc-scoped only so the\n\t\t// recipient selects can fetch it (see RecipientsSelect / buildRecipientField).\n\t\t...(departments\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tpath: '/:id/departments',\n\t\t\t\t\t\tmethod: 'get' as const,\n\t\t\t\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\t\t\t\tconst { status, body } = await resolveDepartmentsRequest({\n\t\t\t\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\t\tresolver: departments,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\treturn Response.json(body, { status })\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []),\n\t]\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORMS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionFormSingular),\n\t\t\tplural: labelForKey(keys.collectionFormPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: {\n\t\t\tgroup: 'Forms',\n\t\t\tuseAsTitle: 'title',\n\t\t\tdefaultColumns: ['title', 'fields', 'flow', 'pollEnabled', 'updatedAt'],\n\t\t\t...(overrides?.admin ?? {}),\n\t\t},\n\t\taccess: { read: () => true, ...(overrides?.access ?? {}) },\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// beforeValidate normalizes conditions and flow; consumer hooks run after\n\t\t\tbeforeValidate: [beforeValidate, ...(overrides?.hooks?.beforeValidate ?? [])],\n\t\t\tbeforeChange: [pollOutcomeBeforeChange, ...(overrides?.hooks?.beforeChange ?? [])],\n\t\t\tafterChange: [pollCloseAfterChange, ...(overrides?.hooks?.afterChange ?? [])],\n\t\t\tafterRead: [\n\t\t\t\t...(consentAfterRead ? [consentAfterRead] : []),\n\t\t\t\t...(overrides?.hooks?.afterRead ?? []),\n\t\t\t],\n\t\t},\n\t\tendpoints: [\n\t\t\t...defaultEndpoints,\n\t\t\t...(Array.isArray(overrides?.endpoints) ? overrides.endpoints : []),\n\t\t],\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"forms.js","names":[],"sources":["../../src/collections/forms.ts"],"sourcesContent":["import {\n\ttype CollectionAfterChangeHook,\n\ttype CollectionAfterReadHook,\n\ttype CollectionBeforeValidateHook,\n\ttype CollectionConfig,\n\ttype CollectionSlug,\n\ttype Field,\n\ttype TextFieldSingleValidation,\n\tValidationError,\n} from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport { buildActionBlocks } from '../actions/buildActionBlocks'\nimport type { FromAddressesResolver } from '../actions/fromAddresses'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { FormResultsAccess } from '../aggregation/resolveResultsRequest'\nimport { normalizeCalc } from '../calc/normalizeCalc'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport {\n\tbuildOperandTypes,\n\ttype FieldRow,\n\tnormalizeFormConditions,\n\tnormalizeWhere,\n} from '../conditions/normalizeConditions'\nimport { resolveConsentStatements } from '../consent/resolveConsentStatements'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport type { DepartmentEmailsResolver } from '../email/departments'\nimport { buildFieldBlocks } from '../fields/buildFieldBlocks'\nimport { fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport { localizedIf } from '../fields/localizedIf'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { normalizeFlow } from '../flow/normalizeFlow'\nimport { END_OF_FORM } from '../flow/types'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport { buildPollOptionSourceFields } from '../poll/buildPollOptionSourceFields'\nimport { enqueuePollClose } from '../poll/closeJob'\nimport { pollOutcomeBeforeChange } from '../poll/outcomeBeforeChange'\nimport { buildDefaultOutcomeFields, type OutcomeFieldsOverride } from '../poll/outcomeFields'\nimport { type PollTypeRegistry, resolvePollTypes } from '../poll/pollTypeRegistry'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { buildValidateResultsField, pollEligibleTypes } from '../poll/resultsField'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelForKey, resolveDefinitionLabel } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { validateUrl } from '../validation/validateUrl'\nimport { type ButtonsOption, buildDefaultButtonFields } from './buttonFields'\nimport { buildFormsEndpoints } from './formsEndpoints'\nimport type { ResponseOption } from './redirectFields'\n\nexport const FORMS_SLUG = 'forms'\n\n/** `req.context` key under which `consentAfterRead` tracks the form ids it is currently resolving, to break re-entrant reads. */\nconst CONSENT_AFTER_READ_GUARD = 'formBuilderConsentAfterReadInFlight'\n\n/**\n * Require the title in the default locale (and on hosts without localization), but let it be empty in\n * other locales so a localized form falls back to the default-locale title rather than forcing a\n * translation for every locale. Payload's field-level `required` cannot express this: it enforces per\n * write-locale, which would make a title mandatory in every locale and break the documented fallback.\n */\nconst validateFormTitle: TextFieldSingleValidation = (value, { req }) => {\n\tconst localization = req.payload.config.localization\n\tconst defaultLocale = localization ? localization.defaultLocale : undefined\n\tconst enforced =\n\t\tdefaultLocale === undefined || req.locale === undefined || req.locale === defaultLocale\n\tif (enforced && (typeof value !== 'string' || value.trim().length === 0)) {\n\t\treturn req.t('validation:required')\n\t}\n\treturn true\n}\n\n/**\n * Field-level validation for the raw flow JSON: step ids must be non-empty, unique, and not the\n * reserved `END_OF_FORM` sentinel, and every explicit step-id reference (a string `next`, a\n * transition `to`) must resolve to a known step. `next: null` (explicit end of form) and an\n * absent `next` (fall through to the next step in array order) are always valid. On full-document\n * saves the beforeValidate pass has already run `normalizeFlow` and laundered dangling\n * references, so the unknown-target checks mainly protect partial API updates that send `flow`\n * without `fields`.\n */\nconst validateFlow = (raw: unknown): string | true => {\n\tif (raw === null || raw === undefined) return true\n\tconst r = raw as Record<string, unknown>\n\tif (!Array.isArray(r.steps)) return true\n\tconst steps = r.steps as Array<Record<string, unknown>>\n\tconst emptyIdStep = steps.find((s) => typeof s?.id !== 'string' || s.id.length === 0)\n\tif (emptyIdStep) return 'Flow: every step must have a non-empty ID'\n\tif (steps.some((s) => s.id === END_OF_FORM)) {\n\t\treturn `Flow: step ID \"${END_OF_FORM}\" is reserved`\n\t}\n\tconst ids = steps.map((s) => s.id as string)\n\tif (new Set(ids).size !== ids.length) {\n\t\treturn 'Flow: duplicate step IDs found'\n\t}\n\tconst idSet = new Set(ids)\n\tfor (const step of steps) {\n\t\tconst id = step.id as string\n\t\tif (typeof step.next === 'string' && step.next.length > 0 && !idSet.has(step.next)) {\n\t\t\treturn `Flow: step \"${id}\" references unknown next step \"${step.next}\"`\n\t\t}\n\t\tif (Array.isArray(step.transitions)) {\n\t\t\tfor (const t of step.transitions as Array<Record<string, unknown>>) {\n\t\t\t\tif (typeof t?.to === 'string' && t.to.length > 0 && !idSet.has(t.to)) {\n\t\t\t\t\treturn `Flow: step \"${id}\" has a transition to unknown step \"${t.to}\"`\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\n/** How many steps the caller actually submitted, before normalization strips/collapses the flow. */\nconst providedFlowStepCount = (raw: unknown): number => {\n\tif (raw === null || typeof raw !== 'object') return 0\n\tconst steps = (raw as { steps?: unknown }).steps\n\treturn Array.isArray(steps) ? steps.length : 0\n}\n\n/**\n * Stamp the configured uploads collection slug onto every `file` block (including repeater\n * sub-fields) on each save. The block carries a hidden `uploadsCollection` field so the slug\n * reaches the client renderer through the form document; the server always overwrites it from\n * plugin config, so the stored value is never author- or client-controlled.\n */\nconst stampFileCollections = (rows: FieldRow[], slug: string): void => {\n\tfor (const row of rows) {\n\t\tif (row.blockType === 'file') {\n\t\t\t;(row as { uploadsCollection?: string }).uploadsCollection = slug\n\t\t}\n\t\tconst subFields = (row as { subFields?: unknown }).subFields\n\t\tif (Array.isArray(subFields)) {\n\t\t\tstampFileCollections(subFields as FieldRow[], slug)\n\t\t}\n\t}\n}\n\ntype BuildFormsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/**\n\t * The plugin `consent.sources` option. Present: the `/:id/consent-sources` endpoint backing the\n\t * consent field's source select is registered. Absent: neither it nor the consent field type exists.\n\t */\n\tconsentSources?: ConsentSourcesResolver\n\t/** Plugin `consent.resolveOnRead` (default true); `false` skips the per-read consent afterRead hook. */\n\tconsentResolveOnRead?: boolean\n\tactionRegistry?: ActionRegistry\n\tlocalizeContent?: boolean\n\t/** The plugin `richText` option; `editor` overrides the response message's richText editor. */\n\trichText?: RichTextBodyOption\n\t/** The host-owned uploads collection slug from plugin config; absent when uploads are disabled. */\n\tuploadsCollectionSlug?: string\n\t/** Host seam gating anonymous results reads (plugin option `results.access`). */\n\tresultsAccess?: FormResultsAccess\n\t/** Registered poll option sources (plugin option `poll.sources`); empty registry means no source fields. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/** Registered poll outcome strategies (`poll.types`); drives the poll `type` select options. Defaults to the built-ins. */\n\tpollTypeRegistry?: PollTypeRegistry\n\t/** The plugin `poll.outcomeFields` seam; composes the outcome group from the two default fields. */\n\toutcomeFields?: OutcomeFieldsOverride\n\t/** The plugin `buttons` option; `fields` composes the `{ submit, prev, next }` labels from the localized defaults. */\n\tbuttons?: ButtonsOption\n\t/** The plugin `response` option; `redirect.fields` composes the `response.redirect` group from its default fields. */\n\tresponse?: ResponseOption\n\t/**\n\t * The plugin `email.fromAddresses` option. Present: both email actions gain a `from` select and\n\t * the `/:id/from-addresses` endpoint is registered. Absent: neither exists.\n\t */\n\tfromAddresses?: FromAddressesResolver\n\t/**\n\t * The plugin `email.departments` option. Present: the `emailTeam` `to` becomes a department select\n\t * and the `/:id/departments` endpoint backing it is registered. Absent: `to` stays a plain field.\n\t */\n\tdepartments?: DepartmentEmailsResolver\n\t/**\n\t * The plugin `redirectRelationships` option. Non-empty: `response.redirect` gains a polymorphic\n\t * `reference` relationship field so an author can redirect to an internal document instead of\n\t * (or alongside) a URL. Absent or empty: no `reference` field exists, matching today's URL-only\n\t * redirect.\n\t */\n\tredirectRelationships?: CollectionSlug[]\n\toverrides?: CollectionOverrides\n}\n\nexport const buildFormsCollection = ({\n\toverrides,\n\tregistry,\n\truleRegistry,\n\tconsentSources,\n\tconsentResolveOnRead,\n\tactionRegistry = new Map(),\n\tlocalizeContent = true,\n\trichText,\n\tuploadsCollectionSlug,\n\tresultsAccess,\n\tpollSourceRegistry,\n\tpollTypeRegistry,\n\toutcomeFields,\n\tbuttons,\n\tresponse,\n\tfromAddresses,\n\tdepartments,\n\tredirectRelationships,\n}: BuildFormsCollectionArgs): CollectionConfig => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst pollTypes = pollTypeRegistry ?? resolvePollTypes()\n\tconst pollResultsTypes = pollEligibleTypes(registry)\n\tconst bareTypes = new Set(\n\t\t[...registry.values()].filter((d) => d.bare === true).map((d) => d.type)\n\t)\n\t// Handed to the FlowBuilder so it can list bare (nameless) blocks in the step picker: slug ->\n\t// label (an i18n key or literal, resolved client-side).\n\tconst bareTypeLabels = Object.fromEntries(\n\t\t[...registry.values()].filter((d) => d.bare === true).map((d) => [d.type, d.label])\n\t)\n\tconst FLOW_BUILDER_REF = '@10x-media/form-builder/client#FlowBuilder'\n\tconst FIELD_NAME_SELECT_REF = '@10x-media/form-builder/client#FieldNameSelect'\n\tconst FLOW_STEPS_CELL_REF = '@10x-media/form-builder/client#FlowStepsCell'\n\tconst FIELD_COUNT_CELL_REF = '@10x-media/form-builder/client#FieldCountCell'\n\tconst CLOSE_POLL_BUTTON_REF = '@10x-media/form-builder/client#ClosePollButton'\n\t// The `source` outcome strategy delegates to the poll's option source, so it is useless without one:\n\t// offer it in the author-facing select only when option sources are registered. The strategy stays in\n\t// the registry regardless, so `resolvePollOutcome` and host code can still address it.\n\tconst hasOptionSources = (pollSourceRegistry?.size ?? 0) > 0\n\t// `mostVoted` leads the author-facing select because it is the default: an enabled poll works\n\t// immediately, with no winner to hand-pick. Only the select order changes; the registry keeps its\n\t// own order (host strategies stay after the built-ins).\n\tconst orderedPollTypes = [\n\t\t...[...pollTypes.values()].filter((strategy) => strategy.type === 'mostVoted'),\n\t\t...[...pollTypes.values()].filter((strategy) => strategy.type !== 'mostVoted'),\n\t]\n\n\tconst beforeValidate: CollectionBeforeValidateHook = ({ data, req }) => {\n\t\tif (data && Array.isArray(data.fields)) {\n\t\t\tconst normalized: FieldRow[] = normalizeFormConditions(\n\t\t\t\tdata.fields as FieldRow[],\n\t\t\t\tconditionTypes\n\t\t\t)\n\t\t\tfor (const field of normalized) {\n\t\t\t\tif ('expression' in field) {\n\t\t\t\t\tfield.expression = normalizeCalc(field.expression)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (uploadsCollectionSlug) {\n\t\t\t\tstampFileCollections(normalized, uploadsCollectionSlug)\n\t\t\t}\n\t\t\tdata.fields = normalized\n\t\t\t// Flow step assignments store field keys: machine names for named fields, block row ids\n\t\t\t// for bare (nameless) blocks. Mirrors `fieldKey` over the raw rows.\n\t\t\tconst fieldKeys = normalized\n\t\t\t\t.map((field: FieldRow) => {\n\t\t\t\t\tif (typeof field.name === 'string' && field.name.length > 0) {\n\t\t\t\t\t\treturn field.name\n\t\t\t\t\t}\n\t\t\t\t\treturn bareTypes.has(field.blockType) && field.id != null ? String(field.id) : undefined\n\t\t\t\t})\n\t\t\t\t.filter((key): key is string => key !== undefined)\n\t\t\t// Launder flow transition `when` clauses with the same operand-type rules as field conditions,\n\t\t\t// so a transition referencing a deleted field is dropped, not left as a route that always\n\t\t\t// matches at navigation time and force-routes the visitor.\n\t\t\tconst operandTypes = buildOperandTypes(normalized, conditionTypes)\n\t\t\tconst normalizedFlow = normalizeFlow(data.flow, fieldKeys, (w) =>\n\t\t\t\tnormalizeWhere(w, operandTypes)\n\t\t\t)\n\t\t\t// A flow the author built but that collapses to fewer than two valid steps would\n\t\t\t// otherwise vanish silently. Surface it instead of discarding their work.\n\t\t\tif (providedFlowStepCount(data.flow) > 0 && normalizedFlow === undefined) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t{\n\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\terrors: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tpath: 'flow',\n\t\t\t\t\t\t\t\tmessage: 'A flow needs at least two steps. Add another step or remove the flow.',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t\treq.t\n\t\t\t\t)\n\t\t\t}\n\t\t\tdata.flow = normalizedFlow\n\t\t\t// A poll needs a field whose answers are the votes. With the poll enabled and no vote field\n\t\t\t// chosen (and the outcome not coming from an option source), bind one: auto-fill the sole\n\t\t\t// eligible field for a one-choice form, otherwise surface a clear error rather than storing a\n\t\t\t// poll that can never aggregate. The optionSource/`source` exemption preserves domain-driven\n\t\t\t// polls that have no on-form vote field. `buildValidateResultsField` still checks a set value;\n\t\t\t// this is the enforcer for enabled polls.\n\t\t\tif (data.pollEnabled === true) {\n\t\t\t\tconst poll =\n\t\t\t\t\tdata.poll != null && typeof data.poll === 'object'\n\t\t\t\t\t\t? (data.poll as Record<string, unknown>)\n\t\t\t\t\t\t: {}\n\t\t\t\tconst resultsField = typeof poll.resultsField === 'string' ? poll.resultsField.trim() : ''\n\t\t\t\tconst optionSource = typeof poll.optionSource === 'string' ? poll.optionSource.trim() : ''\n\t\t\t\tconst sourceDriven = optionSource.length > 0 || poll.type === 'source'\n\t\t\t\tif (resultsField.length === 0 && !sourceDriven) {\n\t\t\t\t\tconst eligible = fieldNamesOfType(data.fields, pollResultsTypes)\n\t\t\t\t\tif (eligible.length === 1) {\n\t\t\t\t\t\tdata.poll = { ...poll, resultsField: eligible[0] }\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst messageKey =\n\t\t\t\t\t\t\teligible.length > 1 ? keys.pollVoteFieldChoose : keys.pollVoteFieldMissing\n\t\t\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tcollection: FORMS_SLUG,\n\t\t\t\t\t\t\t\terrors: [{ path: 'poll.resultsField', message: asTranslate(req.t)(messageKey) }],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treq.t\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn data\n\t}\n\n\t// After every save, (re)schedule the poll's auto-close job when it applies (enabled + closesAt +\n\t// unresolved + non-manual strategy + a job runner present). Best-effort and non-throwing, so it\n\t// never affects the save; without a runner the resolve-on-read fallback in the results endpoint\n\t// heals the outcome instead.\n\tconst pollCloseAfterChange: CollectionAfterChangeHook = async ({ doc, req }) => {\n\t\tawait enqueuePollClose({ payload: req.payload, form: doc, req })\n\t\treturn doc\n\t}\n\n\tconst fieldsField: Field = {\n\t\tname: 'fields',\n\t\ttype: 'blocks',\n\t\tblocks: buildFieldBlocks({ registry, ruleRegistry, localize: localizeContent }),\n\t\tadmin: { components: { Cell: FIELD_COUNT_CELL_REF } },\n\t}\n\n\tconst flowField: Field = {\n\t\tname: 'flow',\n\t\ttype: 'json',\n\t\tvalidate: validateFlow,\n\t\tadmin: {\n\t\t\tcomponents: {\n\t\t\t\tCell: FLOW_STEPS_CELL_REF,\n\t\t\t\tField: { path: FLOW_BUILDER_REF, clientProps: { conditionTypes, bareTypeLabels } },\n\t\t\t},\n\t\t},\n\t\t// Narrows the generated TypeScript type from opaque JSON to FormFlow so callers\n\t\t// don't need a cast. Keep this in sync with src/flow/types.ts.\n\t\ttypescriptSchema: [\n\t\t\t() => ({\n\t\t\t\ttype: 'object' as const,\n\t\t\t\trequired: ['steps'],\n\t\t\t\tadditionalProperties: false,\n\t\t\t\tproperties: {\n\t\t\t\t\tsteps: {\n\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\trequired: ['id'],\n\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\tid: { type: 'string' as const },\n\t\t\t\t\t\t\t\ttitle: { type: 'string' as const },\n\t\t\t\t\t\t\t\tfields: { type: 'array' as const, items: { type: 'string' as const } },\n\t\t\t\t\t\t\t\tnext: { type: ['string', 'null'] as ('string' | 'null')[] },\n\t\t\t\t\t\t\t\ttransitions: {\n\t\t\t\t\t\t\t\t\ttype: 'array' as const,\n\t\t\t\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\t\t\t\ttype: 'object' as const,\n\t\t\t\t\t\t\t\t\t\trequired: ['to'],\n\t\t\t\t\t\t\t\t\t\tadditionalProperties: true,\n\t\t\t\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t\t\t\tto: { type: 'string' as const },\n\t\t\t\t\t\t\t\t\t\t\twhen: { type: 'object' as const, additionalProperties: true },\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t],\n\t}\n\n\tconst actionsField: Field = {\n\t\tname: 'actions',\n\t\ttype: 'blocks',\n\t\tblocks: buildActionBlocks(actionRegistry),\n\t\tlabel: labelForKey(keys.configActions),\n\t\t// Action config can contain secrets (e.g. signedWebhook.secret). The collection\n\t\t// itself is publicly readable so forms can be rendered by anonymous clients, but\n\t\t// action config must never be exposed to anonymous callers.\n\t\taccess: { read: isLoggedIn },\n\t}\n\n\t// Optional polymorphic reference to an internal document, for `response.redirect` to point at\n\t// instead of (or alongside) a URL. Absent unless `redirectRelationships` is a non-empty array:\n\t// always polymorphic even for one slug (the consent `page` field's precedent), so a host adding\n\t// a second collection later never changes the stored shape. The plugin never resolves this to a\n\t// URL itself; `toFormDocument` passes the raw `{ relationTo, value }` through for the host to\n\t// resolve, since only the host knows its own routing.\n\tconst redirectReferenceField: Field[] =\n\t\tredirectRelationships && redirectRelationships.length > 0\n\t\t\t? [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'reference',\n\t\t\t\t\t\ttype: 'relationship',\n\t\t\t\t\t\trelationTo: redirectRelationships,\n\t\t\t\t\t\tlabel: labelForKey(keys.responseRedirectReference),\n\t\t\t\t\t\tadmin: { description: labelForKey(keys.responseRedirectReferenceDescription) },\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t: []\n\n\t// The redirect group's fields: the `url` text field plus the optional polymorphic `reference`.\n\t// The `response.redirect.fields` seam composes them (mirroring `buttons.fields`), so a host can\n\t// prepend a custom link field, swap `url` for their own picker, or filter; the default (no\n\t// override) is the same `[url, ...reference]` as before. Built-in redirect handling still reads\n\t// `redirect.url`/`redirect.reference`, so a host replacing `url` owns resolving their field to a\n\t// destination in their frontend.\n\tconst urlField: Field = {\n\t\tname: 'url',\n\t\ttype: 'text',\n\t\tlabel: labelForKey(keys.responseUrl),\n\t\tvalidate: validateUrl,\n\t}\n\tconst defaultRedirectFields: Field[] = [urlField, ...redirectReferenceField]\n\tconst redirectFields = response?.redirect?.fields\n\t\t? response.redirect.fields({ defaultFields: defaultRedirectFields })\n\t\t: defaultRedirectFields\n\n\t// What the visitor sees after a successful submit. Publicly readable (unlike actions): the\n\t// client renderer needs message/redirect. `type`/`url` are behavior, never localized;\n\t// `message` is visitor-facing content and follows `localizeContent`.\n\t// `type` is defaulted and not clearable rather than `required`: a required member would make\n\t// the whole group required in generated types, breaking typed `payload.create` calls that\n\t// omit `response`. Consumers treat a missing type as 'message'.\n\tconst responseField: Field = {\n\t\tname: 'response',\n\t\ttype: 'group',\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'type',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'message',\n\t\t\t\tlabel: labelForKey(keys.responseType),\n\t\t\t\tadmin: { isClearable: false },\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: labelForKey(keys.responseTypeMessage), value: 'message' },\n\t\t\t\t\t{ label: labelForKey(keys.responseTypeRedirect), value: 'redirect' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'message',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelForKey(keys.responseMessage),\n\t\t\t\t// Unset type (docs predating this field) means 'message', matching the client fallback.\n\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.type !== 'redirect' },\n\t\t\t\t...(richText?.editor ? { editor: richText.editor } : {}),\n\t\t\t\t...localizedIf(localizeContent),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'redirect',\n\t\t\t\ttype: 'group',\n\t\t\t\tlabel: labelForKey(keys.responseRedirect),\n\t\t\t\tadmin: {\n\t\t\t\t\tcondition: (_data, siblingData) => siblingData?.type === 'redirect',\n\t\t\t\t\thideGutter: true,\n\t\t\t\t},\n\t\t\t\tfields: redirectFields,\n\t\t\t},\n\t\t],\n\t}\n\n\tconst defaultOutcomeFields = buildDefaultOutcomeFields()\n\tconst defaultButtonFields = buildDefaultButtonFields(localizeContent)\n\t// Half-width copy of a default field for the prev/next row; merges width into whatever admin the\n\t// field already carries (a host's `buttons.fields` override may add its own) rather than replacing\n\t// it. The cast is safe: `width` is a valid admin option on every field variant, but spreading a\n\t// `Field`-typed value's `admin` back into a union-typed object literal loses the discriminant TS\n\t// needs to check it structurally.\n\tconst halfWidth = (field: Field): Field =>\n\t\t({ ...field, admin: { ...field.admin, width: '50%' } }) as Field\n\t// The three button-label fields sit at the document root now (no `buttons` group): `submit` at\n\t// the bottom of the Fields tab, `prev`/`next` in a row on the Flow tab. The `buttons.fields` seam\n\t// composes each slot from the already-localized defaults, so a host can wrap a default in a row\n\t// with its own field (e.g. an icon select) or replace it; `toFormDocument` reassembles the three\n\t// labels into `FormDocument.buttons` for the client.\n\tconst composedButtons = buttons?.fields\n\t\t? buttons.fields({ defaultFields: defaultButtonFields })\n\t\t: defaultButtonFields\n\tconst submitField = composedButtons.submit\n\tconst prevNextRow: Field = {\n\t\ttype: 'row',\n\t\t// The prev/next labels only matter once the flow has a step; the Flow tab itself is gated on\n\t\t// `multistep`. `data` (1st arg) is the whole document, so `flow.steps` reads the stored flow.\n\t\tadmin: {\n\t\t\tcondition: (data) => {\n\t\t\t\tconst steps = (data as { flow?: { steps?: unknown } })?.flow?.steps\n\t\t\t\treturn Array.isArray(steps) && steps.length >= 1\n\t\t\t},\n\t\t},\n\t\tfields: [halfWidth(composedButtons.prev), halfWidth(composedButtons.next)],\n\t}\n\n\t// Poll lifecycle config; identifiers and behavior, never localized. Lives inside the conditional\n\t// Poll tab (gated on the top-level `pollEnabled` flag), so the per-field enabled conditions are\n\t// gone; `label: false` suppresses the group header the tab label already provides.\n\t// `resultsVisibility` is defaulted and not clearable rather than `required` for the same\n\t// generated-types reason as `response.type`.\n\tconst pollGroupField: Field = {\n\t\tname: 'poll',\n\t\ttype: 'group',\n\t\tlabel: false,\n\t\tfields: [\n\t\t\t// Authored by picking from the form's poll-eligible fields; the stored value stays a plain\n\t\t\t// text field name. Select options and server validate share `pollResultsTypes`, so they\n\t\t\t// cannot drift.\n\t\t\t{\n\t\t\t\tname: 'resultsField',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.pollResultsField),\n\t\t\t\tvalidate: buildValidateResultsField(pollResultsTypes),\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: {\n\t\t\t\t\t\tField: {\n\t\t\t\t\t\t\tpath: FIELD_NAME_SELECT_REF,\n\t\t\t\t\t\t\tclientProps: {\n\t\t\t\t\t\t\t\ttypes: pollResultsTypes,\n\t\t\t\t\t\t\t\tdescriptionKey: keys.pollResultsFieldDescription,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// How the winning value(s) get decided. `mostVoted` (default) and `source` auto-resolve on close\n\t\t\t// (via the scheduled job or the results-read fallback); `manual` leaves it to an admin. Options\n\t\t\t// come from the registered `poll.types` strategies, so a host strategy appears here too, with\n\t\t\t// `mostVoted` first so an enabled poll works with no winner to hand-pick.\n\t\t\t{\n\t\t\t\tname: 'type',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'mostVoted',\n\t\t\t\tlabel: labelForKey(keys.pollType),\n\t\t\t\tadmin: { isClearable: false, description: labelForKey(keys.pollTypeDescription) },\n\t\t\t\toptions: orderedPollTypes\n\t\t\t\t\t.filter((strategy) => strategy.type !== 'source' || hasOptionSources)\n\t\t\t\t\t.map((strategy) => ({\n\t\t\t\t\t\tlabel: resolveDefinitionLabel(strategy.label),\n\t\t\t\t\t\tvalue: strategy.type,\n\t\t\t\t\t})),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'resultsVisibility',\n\t\t\t\ttype: 'select',\n\t\t\t\tdefaultValue: 'afterVote',\n\t\t\t\tlabel: labelForKey(keys.pollResultsVisibility),\n\t\t\t\tadmin: { isClearable: false },\n\t\t\t\toptions: [\n\t\t\t\t\t{ label: labelForKey(keys.pollVisibilityAfterVote), value: 'afterVote' },\n\t\t\t\t\t{ label: labelForKey(keys.pollVisibilityAfterClose), value: 'afterClose' },\n\t\t\t\t],\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'closesAt',\n\t\t\t\ttype: 'date',\n\t\t\t\tlabel: labelForKey(keys.pollClosesAt),\n\t\t\t\tadmin: { date: { pickerAppearance: 'dayAndTime' } },\n\t\t\t},\n\t\t\t// Close / reopen the poll from the admin. The button toggles on the live `closesAt` and saves the\n\t\t\t// whole document (persisting an unsaved winner alongside `closesAt`) rather than calling the close\n\t\t\t// endpoint, so there is no DB-vs-form-state mismatch. Always mounted inside the (pollEnabled-gated)\n\t\t\t// Poll tab now; the button owns both states, so it carries no `admin.condition` of its own.\n\t\t\t{\n\t\t\t\tname: 'closePoll',\n\t\t\t\ttype: 'ui',\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: { Field: CLOSE_POLL_BUTTON_REF },\n\t\t\t\t},\n\t\t\t},\n\t\t\t...buildPollOptionSourceFields(pollSourceRegistry ?? new Map()),\n\t\t\t// `winningValues` is recorded either by an admin picking from the poll's effective options\n\t\t\t// (served by `/:id/poll-options`) or by `resolvePollOutcome` (host domain logic); more than\n\t\t\t// one value records a tie. The `pollOutcomeBeforeChange` hook validates both paths and owns\n\t\t\t// the `resolvedAt` stamp. `resolvedAt` itself stays fully locked: field-level create/update\n\t\t\t// access blocks every non-override caller write (Payload silently drops the denied value\n\t\t\t// rather than erroring) while the hook's stamp, applied after access filtering, still\n\t\t\t// persists. The `poll.outcomeFields` seam receives both defaults and its return becomes the\n\t\t\t// group's fields verbatim, so a host can swap `winningValues` for its own component; the hook\n\t\t\t// still validates every stored value against the effective options, so no swap bypasses it.\n\t\t\t// `hideGutter` stays: outcome is still a group nested inside the poll group.\n\t\t\t{\n\t\t\t\tname: 'outcome',\n\t\t\t\ttype: 'group',\n\t\t\t\tlabel: labelForKey(keys.pollOutcome),\n\t\t\t\tadmin: { hideGutter: true },\n\t\t\t\tfields: outcomeFields\n\t\t\t\t\t? outcomeFields({ defaultFields: defaultOutcomeFields })\n\t\t\t\t\t: [defaultOutcomeFields.winningValues, defaultOutcomeFields.resolvedAt],\n\t\t\t},\n\t\t],\n\t}\n\n\tconst defaultFields: Field[] = [\n\t\t// The document title is the only visitor-facing text the collection renders itself: it is\n\t\t// always required (drives `useAsTitle` in the admin list/relationship views) and is passed\n\t\t// through `toFormDocument` as `FormDocument.title`. Whether and how a host renders it above\n\t\t// the fields is entirely the host's call; the plugin does not gate or duplicate it.\n\t\t{\n\t\t\tname: 'title',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelForKey(keys.fieldTitle),\n\t\t\t// Visitor-facing content, so it follows `localizeContent` like the response message and\n\t\t\t// consent statement: hosts with localization get a per-locale title (and a locale-aware\n\t\t\t// `useAsTitle`), hosts without it are unaffected since Payload strips the flag. `validate`\n\t\t\t// (not `required`) keeps it mandatory only in the default locale so other locales fall back.\n\t\t\t...localizedIf(localizeContent),\n\t\t\tvalidate: validateFormTitle,\n\t\t},\n\t\t// The two form-type flags: behavior, never localized. `multistep` gates the Flow tab and the\n\t\t// client's step navigation; `pollEnabled` gates the Poll tab and marks the form a poll.\n\t\t{\n\t\t\ttype: 'row',\n\t\t\tfields: [\n\t\t\t\t{\n\t\t\t\t\tname: 'multistep',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tdefaultValue: false,\n\t\t\t\t\tlabel: labelForKey(keys.formMultistep),\n\t\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'pollEnabled',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tdefaultValue: false,\n\t\t\t\t\tlabel: labelForKey(keys.formPollEnabled),\n\t\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t// Unnamed tabs are presentational: their fields stay at the document root. An unnamed tab's\n\t\t// `admin.condition` receives the whole document as its 2nd arg, so the Flow and Poll tabs gate\n\t\t// on the root-level flags. The poll group nests its config under `form.poll` as before.\n\t\t{\n\t\t\ttype: 'tabs',\n\t\t\ttabs: [\n\t\t\t\t{ label: labelForKey(keys.tabFields), fields: [fieldsField, submitField] },\n\t\t\t\t{\n\t\t\t\t\tlabel: labelForKey(keys.tabFlow),\n\t\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.multistep === true },\n\t\t\t\t\tfields: [flowField, prevNextRow],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: labelForKey(keys.pollGroup),\n\t\t\t\t\tadmin: { condition: (_data, siblingData) => siblingData?.pollEnabled === true },\n\t\t\t\t\tfields: [pollGroupField],\n\t\t\t\t},\n\t\t\t\t{ label: labelForKey(keys.tabActions), fields: [actionsField] },\n\t\t\t\t{ label: labelForKey(keys.tabResponse), fields: [responseField] },\n\t\t\t],\n\t\t},\n\t]\n\n\t// When `consent.sources` is set, resolve the visitor-facing consent statements onto every read of\n\t// a form doc (REST, local API, relationship population), so a client/modal fetch renders consent,\n\t// not only the RSC path that calls `resolveConsentStatements` itself. Fails open: a resolver outage\n\t// must never break form/admin/relationship reads, and the renderer already tolerates a missing\n\t// statement. `resolveConsentStatements` returns `{}` without calling the resolver when the form has\n\t// no consent fields, and entries are cached per request+form, so the cost stays bounded.\n\tconst consentAfterRead: CollectionAfterReadHook | undefined =\n\t\tconsentSources && consentResolveOnRead !== false\n\t\t\t? async ({ doc, req }) => {\n\t\t\t\t\tconst id = (doc as { id?: unknown }).id\n\t\t\t\t\tconst key = id == null ? undefined : String(id)\n\t\t\t\t\t// Re-entrance guard: a host resolver that reads this same form back (threading req) would\n\t\t\t\t\t// otherwise re-enter this hook and recurse. Payload runs collection afterRead concurrently\n\t\t\t\t\t// across list docs on one shared req.context, so this must be a per-id Set (a boolean would\n\t\t\t\t\t// make sibling docs skip), mutated in place and never reassigned after fan-out. Mirrors\n\t\t\t\t\t// @payloadcms/plugin-search's syncDocAsSearchIndex guard.\n\t\t\t\t\tif (key !== undefined && req.context) {\n\t\t\t\t\t\tconst inFlight =\n\t\t\t\t\t\t\t(req.context[CONSENT_AFTER_READ_GUARD] as Set<string> | undefined) ??\n\t\t\t\t\t\t\tnew Set<string>()\n\t\t\t\t\t\tif (inFlight.has(key)) {\n\t\t\t\t\t\t\treturn doc\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinFlight.add(key)\n\t\t\t\t\t\treq.context[CONSENT_AFTER_READ_GUARD] = inFlight\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst statements = await resolveConsentStatements({\n\t\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\tform: doc,\n\t\t\t\t\t\t\tsources: consentSources,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif (Object.keys(statements).length > 0) {\n\t\t\t\t\t\t\t;(doc as Record<string, unknown>).consentStatements = statements\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\treq.payload.logger?.warn(`form-builder consent afterRead: ${String(error)}`)\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (key !== undefined && req.context) {\n\t\t\t\t\t\t\t;(req.context[CONSENT_AFTER_READ_GUARD] as Set<string> | undefined)?.delete(key)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn doc\n\t\t\t\t}\n\t\t\t: undefined\n\n\tconst defaultEndpoints = buildFormsEndpoints({\n\t\tresultsAccess,\n\t\tpollResultsTypes,\n\t\tconsentSources,\n\t\tfromAddresses,\n\t\tdepartments,\n\t})\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORMS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionFormSingular),\n\t\t\tplural: labelForKey(keys.collectionFormPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: {\n\t\t\tgroup: 'Forms',\n\t\t\tuseAsTitle: 'title',\n\t\t\tdefaultColumns: ['title', 'fields', 'flow', 'pollEnabled', 'updatedAt'],\n\t\t\t...(overrides?.admin ?? {}),\n\t\t},\n\t\taccess: { read: () => true, ...(overrides?.access ?? {}) },\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// beforeValidate normalizes conditions and flow; consumer hooks run after\n\t\t\tbeforeValidate: [beforeValidate, ...(overrides?.hooks?.beforeValidate ?? [])],\n\t\t\tbeforeChange: [pollOutcomeBeforeChange, ...(overrides?.hooks?.beforeChange ?? [])],\n\t\t\tafterChange: [pollCloseAfterChange, ...(overrides?.hooks?.afterChange ?? [])],\n\t\t\tafterRead: [\n\t\t\t\t...(consentAfterRead ? [consentAfterRead] : []),\n\t\t\t\t...(overrides?.hooks?.afterRead ?? []),\n\t\t\t],\n\t\t},\n\t\tendpoints: [\n\t\t\t...defaultEndpoints,\n\t\t\t...(Array.isArray(overrides?.endpoints) ? overrides.endpoints : []),\n\t\t],\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ import { resolveFromAddressesRequest } from "../actions/fromAddresses.js";
2
+ import { resolveFormResultsRequest } from "../aggregation/resolveResultsRequest.js";
3
+ import { resolveConsentSourcesRequest } from "../consent/resolveConsentSourcesRequest.js";
4
+ import { resolveDepartmentsRequest } from "../email/departments.js";
5
+ import { resolvePollCloseRequest } from "../poll/resolvePollCloseRequest.js";
6
+ import { resolvePollOptionsRequest } from "../poll/resolvePollOptionsRequest.js";
7
+ //#region src/collections/formsEndpoints.ts
8
+ /**
9
+ * The forms collection's built-in endpoints: poll results/options/close, plus the optional
10
+ * consent-sources, from-addresses, and departments option endpoints that only exist when the
11
+ * matching plugin option is set. Each delegates to its request helper, which owns the auth check
12
+ * (anonymous callers get a 403 from the helper, not here). Extracted from `buildFormsCollection`
13
+ * so the collection builder stays focused on field and hook composition.
14
+ */
15
+ const buildFormsEndpoints = ({ resultsAccess, pollResultsTypes, consentSources, fromAddresses, departments }) => [
16
+ {
17
+ path: "/:id/results",
18
+ method: "get",
19
+ handler: async (req) => {
20
+ const field = typeof req.query?.field === "string" ? req.query.field : void 0;
21
+ const { status, body } = await resolveFormResultsRequest({
22
+ payload: req.payload,
23
+ formId: req.routeParams?.id,
24
+ field,
25
+ isAuthed: Boolean(req.user),
26
+ req,
27
+ access: resultsAccess,
28
+ eligibleTypes: pollResultsTypes
29
+ });
30
+ return Response.json(body, { status });
31
+ }
32
+ },
33
+ {
34
+ path: "/:id/poll-options",
35
+ method: "get",
36
+ handler: async (req) => {
37
+ const { status, body } = await resolvePollOptionsRequest({
38
+ payload: req.payload,
39
+ formId: req.routeParams?.id,
40
+ isAuthed: Boolean(req.user),
41
+ req
42
+ });
43
+ return Response.json(body, { status });
44
+ }
45
+ },
46
+ {
47
+ path: "/:id/close",
48
+ method: "post",
49
+ handler: async (req) => {
50
+ const { status, body } = await resolvePollCloseRequest({
51
+ payload: req.payload,
52
+ formId: req.routeParams?.id,
53
+ isAuthed: Boolean(req.user),
54
+ req
55
+ });
56
+ return Response.json(body, { status });
57
+ }
58
+ },
59
+ ...consentSources ? [{
60
+ path: "/:id/consent-sources",
61
+ method: "get",
62
+ handler: async (req) => {
63
+ const { status, body } = await resolveConsentSourcesRequest({
64
+ payload: req.payload,
65
+ formId: req.routeParams?.id,
66
+ isAuthed: Boolean(req.user),
67
+ req,
68
+ resolver: consentSources
69
+ });
70
+ return Response.json(body, { status });
71
+ }
72
+ }] : [],
73
+ ...fromAddresses ? [{
74
+ path: "/:id/from-addresses",
75
+ method: "get",
76
+ handler: async (req) => {
77
+ const { status, body } = await resolveFromAddressesRequest({
78
+ isAuthed: Boolean(req.user),
79
+ req,
80
+ resolver: fromAddresses
81
+ });
82
+ return Response.json(body, { status });
83
+ }
84
+ }] : [],
85
+ ...departments ? [{
86
+ path: "/:id/departments",
87
+ method: "get",
88
+ handler: async (req) => {
89
+ const { status, body } = await resolveDepartmentsRequest({
90
+ isAuthed: Boolean(req.user),
91
+ req,
92
+ resolver: departments
93
+ });
94
+ return Response.json(body, { status });
95
+ }
96
+ }] : []
97
+ ];
98
+ //#endregion
99
+ export { buildFormsEndpoints };
100
+
101
+ //# sourceMappingURL=formsEndpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formsEndpoints.js","names":[],"sources":["../../src/collections/formsEndpoints.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest } from 'payload'\nimport { type FromAddressesResolver, resolveFromAddressesRequest } from '../actions/fromAddresses'\nimport {\n\ttype FormResultsAccess,\n\tresolveFormResultsRequest,\n} from '../aggregation/resolveResultsRequest'\nimport { resolveConsentSourcesRequest } from '../consent/resolveConsentSourcesRequest'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport { type DepartmentEmailsResolver, resolveDepartmentsRequest } from '../email/departments'\nimport { resolvePollCloseRequest } from '../poll/resolvePollCloseRequest'\nimport { resolvePollOptionsRequest } from '../poll/resolvePollOptionsRequest'\n\ntype FormsEndpointDeps = {\n\t/** Host seam gating anonymous results reads (plugin option `results.access`). */\n\tresultsAccess?: FormResultsAccess\n\t/** The poll-eligible field-type names; the `/:id/results` endpoint restricts reads to these. */\n\tpollResultsTypes: string[]\n\t/** The plugin `consent.sources` option; present registers the `/:id/consent-sources` endpoint. */\n\tconsentSources?: ConsentSourcesResolver\n\t/** The plugin `email.fromAddresses` option; present registers the `/:id/from-addresses` endpoint. */\n\tfromAddresses?: FromAddressesResolver\n\t/** The plugin `email.departments` option; present registers the `/:id/departments` endpoint. */\n\tdepartments?: DepartmentEmailsResolver\n}\n\n/**\n * The forms collection's built-in endpoints: poll results/options/close, plus the optional\n * consent-sources, from-addresses, and departments option endpoints that only exist when the\n * matching plugin option is set. Each delegates to its request helper, which owns the auth check\n * (anonymous callers get a 403 from the helper, not here). Extracted from `buildFormsCollection`\n * so the collection builder stays focused on field and hook composition.\n */\nexport const buildFormsEndpoints = ({\n\tresultsAccess,\n\tpollResultsTypes,\n\tconsentSources,\n\tfromAddresses,\n\tdepartments,\n}: FormsEndpointDeps): Exclude<CollectionConfig['endpoints'], false | undefined> => [\n\t{\n\t\tpath: '/:id/results',\n\t\tmethod: 'get',\n\t\thandler: async (req: PayloadRequest) => {\n\t\t\tconst field = typeof req.query?.field === 'string' ? req.query.field : undefined\n\t\t\tconst { status, body } = await resolveFormResultsRequest({\n\t\t\t\tpayload: req.payload,\n\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\tfield,\n\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\treq,\n\t\t\t\taccess: resultsAccess,\n\t\t\t\teligibleTypes: pollResultsTypes,\n\t\t\t})\n\t\t\treturn Response.json(body, { status })\n\t\t},\n\t},\n\t{\n\t\tpath: '/:id/poll-options',\n\t\tmethod: 'get',\n\t\thandler: async (req: PayloadRequest) => {\n\t\t\tconst { status, body } = await resolvePollOptionsRequest({\n\t\t\t\tpayload: req.payload,\n\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\treq,\n\t\t\t})\n\t\t\treturn Response.json(body, { status })\n\t\t},\n\t},\n\t// Trusted admin action (authenticated only): close a poll now and resolve its outcome. A POST\n\t// because it mutates; auth is `Boolean(req.user)` so an anonymous caller gets 403 from the helper.\n\t{\n\t\tpath: '/:id/close',\n\t\tmethod: 'post',\n\t\thandler: async (req: PayloadRequest) => {\n\t\t\tconst { status, body } = await resolvePollCloseRequest({\n\t\t\t\tpayload: req.payload,\n\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\treq,\n\t\t\t})\n\t\t\treturn Response.json(body, { status })\n\t\t},\n\t},\n\t...(consentSources\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tpath: '/:id/consent-sources',\n\t\t\t\t\tmethod: 'get' as const,\n\t\t\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\t\t\tconst { status, body } = await resolveConsentSourcesRequest({\n\t\t\t\t\t\t\tpayload: req.payload,\n\t\t\t\t\t\t\tformId: req.routeParams?.id as number | string | undefined,\n\t\t\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\tresolver: consentSources,\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn Response.json(body, { status })\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t]\n\t\t: []),\n\t// The route id is unused: the from-addresses set is request-scoped (e.g. per tenant), not\n\t// per-form. Registered as a doc-scoped route only so the admin field can reuse\n\t// EndpointOptionsSelect unmodified (see buildFromField).\n\t...(fromAddresses\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tpath: '/:id/from-addresses',\n\t\t\t\t\tmethod: 'get' as const,\n\t\t\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\t\t\tconst { status, body } = await resolveFromAddressesRequest({\n\t\t\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\tresolver: fromAddresses,\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn Response.json(body, { status })\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t]\n\t\t: []),\n\t// Same request-scoped, id-unused shape as from-addresses: registered doc-scoped only so the\n\t// recipient selects can fetch it (see RecipientsSelect / buildRecipientField).\n\t...(departments\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tpath: '/:id/departments',\n\t\t\t\t\tmethod: 'get' as const,\n\t\t\t\t\thandler: async (req: PayloadRequest) => {\n\t\t\t\t\t\tconst { status, body } = await resolveDepartmentsRequest({\n\t\t\t\t\t\t\tisAuthed: Boolean(req.user),\n\t\t\t\t\t\t\treq,\n\t\t\t\t\t\t\tresolver: departments,\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn Response.json(body, { status })\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t]\n\t\t: []),\n]\n"],"mappings":";;;;;;;;;;;;;;AAgCA,MAAa,uBAAuB,EACnC,eACA,kBACA,gBACA,eACA,kBACmF;CACnF;EACC,MAAM;EACN,QAAQ;EACR,SAAS,OAAO,QAAwB;GACvC,MAAM,QAAQ,OAAO,IAAI,OAAO,UAAU,WAAW,IAAI,MAAM,QAAQ,KAAA;GACvE,MAAM,EAAE,QAAQ,SAAS,MAAM,0BAA0B;IACxD,SAAS,IAAI;IACb,QAAQ,IAAI,aAAa;IACzB;IACA,UAAU,QAAQ,IAAI,IAAI;IAC1B;IACA,QAAQ;IACR,eAAe;GAChB,CAAC;GACD,OAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;EACtC;CACD;CACA;EACC,MAAM;EACN,QAAQ;EACR,SAAS,OAAO,QAAwB;GACvC,MAAM,EAAE,QAAQ,SAAS,MAAM,0BAA0B;IACxD,SAAS,IAAI;IACb,QAAQ,IAAI,aAAa;IACzB,UAAU,QAAQ,IAAI,IAAI;IAC1B;GACD,CAAC;GACD,OAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;EACtC;CACD;CAGA;EACC,MAAM;EACN,QAAQ;EACR,SAAS,OAAO,QAAwB;GACvC,MAAM,EAAE,QAAQ,SAAS,MAAM,wBAAwB;IACtD,SAAS,IAAI;IACb,QAAQ,IAAI,aAAa;IACzB,UAAU,QAAQ,IAAI,IAAI;IAC1B;GACD,CAAC;GACD,OAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;EACtC;CACD;CACA,GAAI,iBACD,CACA;EACC,MAAM;EACN,QAAQ;EACR,SAAS,OAAO,QAAwB;GACvC,MAAM,EAAE,QAAQ,SAAS,MAAM,6BAA6B;IAC3D,SAAS,IAAI;IACb,QAAQ,IAAI,aAAa;IACzB,UAAU,QAAQ,IAAI,IAAI;IAC1B;IACA,UAAU;GACX,CAAC;GACD,OAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;EACtC;CACD,CACD,IACC,CAAC;CAIJ,GAAI,gBACD,CACA;EACC,MAAM;EACN,QAAQ;EACR,SAAS,OAAO,QAAwB;GACvC,MAAM,EAAE,QAAQ,SAAS,MAAM,4BAA4B;IAC1D,UAAU,QAAQ,IAAI,IAAI;IAC1B;IACA,UAAU;GACX,CAAC;GACD,OAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;EACtC;CACD,CACD,IACC,CAAC;CAGJ,GAAI,cACD,CACA;EACC,MAAM;EACN,QAAQ;EACR,SAAS,OAAO,QAAwB;GACvC,MAAM,EAAE,QAAQ,SAAS,MAAM,0BAA0B;IACxD,UAAU,QAAQ,IAAI,IAAI;IAC1B;IACA,UAAU;GACX,CAAC;GACD,OAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC;EACtC;CACD,CACD,IACC,CAAC;AACL"}
@@ -47,7 +47,7 @@ const evaluateOperator = (operator, answer, value) => {
47
47
  case "in": return toList(value).some((entry) => valuesEqual(answer, entry));
48
48
  case "not_in": return isNil(answer) || !toList(value).some((entry) => valuesEqual(answer, entry));
49
49
  case "exists": {
50
- const present = !isNil(answer) && answer !== "";
50
+ const present = !isNil(answer) && answer !== "" && !(Array.isArray(answer) && answer.length === 0);
51
51
  return value === true || value === "true" ? present : !present;
52
52
  }
53
53
  case "greater_than": return ordered(answer, value, (a, b) => a > b);
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate.js","names":[],"sources":["../../src/conditions/evaluate.ts"],"sourcesContent":["import type { Operator, Where } from 'payload'\nimport { transformWhereQuery } from 'payload/shared'\n\nconst isNil = (value: unknown): boolean => value === null || value === undefined\n\nconst toNumber = (value: unknown): number | undefined => {\n\tif (typeof value === 'number') {\n\t\treturn Number.isFinite(value) ? value : undefined\n\t}\n\tif (typeof value === 'string' && value.trim() !== '') {\n\t\tconst next = Number(value)\n\t\treturn Number.isNaN(next) ? undefined : next\n\t}\n\treturn undefined\n}\n\nconst toTime = (value: unknown): number | undefined => {\n\tif (value instanceof Date) {\n\t\treturn value.getTime()\n\t}\n\tif (typeof value === 'string' && value.trim() !== '') {\n\t\tconst time = Date.parse(value)\n\t\treturn Number.isNaN(time) ? undefined : time\n\t}\n\treturn undefined\n}\n\n/** Strict, then numeric, then string equality, mirroring Payload's coerce-by-type-then-compare. */\nconst valuesEqual = (answer: unknown, value: unknown): boolean => {\n\tif (answer === value) {\n\t\treturn true\n\t}\n\tif (isNil(answer) || isNil(value)) {\n\t\treturn false\n\t}\n\tconst a = toNumber(answer)\n\tconst b = toNumber(value)\n\tif (a !== undefined && b !== undefined) {\n\t\treturn a === b\n\t}\n\treturn String(answer) === String(value)\n}\n\nconst toList = (value: unknown): unknown[] => {\n\tif (Array.isArray(value)) {\n\t\treturn value\n\t}\n\tif (typeof value === 'string') {\n\t\treturn value.split(',').map((entry) => entry.trim())\n\t}\n\treturn [value]\n}\n\nconst ordered = (\n\tanswer: unknown,\n\tvalue: unknown,\n\tcompare: (a: number, b: number) => boolean\n): boolean => {\n\tconst a = toNumber(answer) ?? toTime(answer)\n\tconst b = toNumber(value) ?? toTime(value)\n\tif (a === undefined || b === undefined) {\n\t\treturn false\n\t}\n\treturn compare(a, b)\n}\n\n/** Case-insensitive: every space-separated word in `value` is a substring of `answer` (Payload `like`). */\nconst everyWord = (answer: unknown, value: unknown): boolean => {\n\tconst haystack = String(answer).toLowerCase()\n\treturn String(value)\n\t\t.toLowerCase()\n\t\t.split(' ')\n\t\t.filter((word) => word.length > 0)\n\t\t.every((word) => haystack.includes(word))\n}\n\nconst evaluateOperator = (operator: Operator, answer: unknown, value: unknown): boolean => {\n\tswitch (operator) {\n\t\tcase 'equals':\n\t\t\treturn isNil(value) ? isNil(answer) : valuesEqual(answer, value)\n\t\tcase 'not_equals':\n\t\t\treturn isNil(value) ? !isNil(answer) : isNil(answer) || !valuesEqual(answer, value)\n\t\tcase 'in':\n\t\t\treturn toList(value).some((entry) => valuesEqual(answer, entry))\n\t\tcase 'not_in':\n\t\t\treturn isNil(answer) || !toList(value).some((entry) => valuesEqual(answer, entry))\n\t\tcase 'exists': {\n\t\t\tconst present = !isNil(answer) && answer !== ''\n\t\t\treturn value === true || value === 'true' ? present : !present\n\t\t}\n\t\tcase 'greater_than':\n\t\t\treturn ordered(answer, value, (a, b) => a > b)\n\t\tcase 'greater_than_equal':\n\t\t\treturn ordered(answer, value, (a, b) => a >= b)\n\t\tcase 'less_than':\n\t\t\treturn ordered(answer, value, (a, b) => a < b)\n\t\tcase 'less_than_equal':\n\t\t\treturn ordered(answer, value, (a, b) => a <= b)\n\t\tcase 'like':\n\t\t\treturn isNil(answer) ? false : everyWord(answer, value)\n\t\tcase 'not_like':\n\t\t\treturn isNil(answer) ? true : !everyWord(answer, value)\n\t\tcase 'contains':\n\t\t\treturn isNil(answer)\n\t\t\t\t? false\n\t\t\t\t: String(answer).toLowerCase().includes(String(value).toLowerCase())\n\t\tdefault:\n\t\t\treturn false\n\t}\n}\n\n/**\n * Evaluate one `Where` node: every top-level key is ANDed. `or` is a disjunction of sub-conditions\n * (an empty `or` matches), `and` is a conjunction, and any other key is a field path whose\n * `{ operator: value }` constraints are ANDed. Recurses, so nested `or`/`and` groups are supported.\n */\nconst evaluateWhere = (where: Where, answers: Record<string, unknown>): boolean =>\n\tObject.entries(where).every(([key, constraint]) => {\n\t\tif (key === 'or') {\n\t\t\tconst groups = (Array.isArray(constraint) ? constraint : []) as Where[]\n\t\t\treturn groups.length === 0 || groups.some((group) => evaluateWhere(group, answers))\n\t\t}\n\t\tif (key === 'and') {\n\t\t\tconst groups = (Array.isArray(constraint) ? constraint : []) as Where[]\n\t\t\treturn groups.every((group) => evaluateWhere(group, answers))\n\t\t}\n\t\tif (constraint == null || typeof constraint !== 'object' || Array.isArray(constraint)) {\n\t\t\treturn false\n\t\t}\n\t\treturn Object.entries(constraint as Record<string, unknown>).every(([operator, value]) =>\n\t\t\tevaluateOperator(operator as Operator, answers[key], value)\n\t\t)\n\t})\n\n/**\n * Evaluate a serializable `Where`-shaped condition against a flat map of (already coerced) form answers.\n * An absent or empty condition matches (returns true). Operator semantics mirror Payload's query\n * adapters: coerce then compare; `not_equals`/`not_in` are null-inclusive; `exists` treats `''`/absent\n * as not-existing; `like` is case-insensitive and space-splits (every word must be a substring), and\n * `not_like` is its logical negation (at least one word absent); `contains` is a single case-insensitive\n * substring; comma-delimited `in`/`not_in` string values are split and trimmed. Geo (`near`/`within`/\n * `intersects`) and `all` are out of scope and evaluate to false. Isomorphic: no `req`/DB access, so the\n * renderer reuses it client-side.\n */\nexport const evaluateCondition = (\n\twhere: Where | null | undefined,\n\tanswers: Record<string, unknown>\n): boolean => {\n\tif (!where || Object.keys(where).length === 0) {\n\t\treturn true\n\t}\n\treturn evaluateWhere(transformWhereQuery(where), answers)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"evaluate.js","names":[],"sources":["../../src/conditions/evaluate.ts"],"sourcesContent":["import type { Operator, Where } from 'payload'\nimport { transformWhereQuery } from 'payload/shared'\n\nconst isNil = (value: unknown): boolean => value === null || value === undefined\n\nconst toNumber = (value: unknown): number | undefined => {\n\tif (typeof value === 'number') {\n\t\treturn Number.isFinite(value) ? value : undefined\n\t}\n\tif (typeof value === 'string' && value.trim() !== '') {\n\t\tconst next = Number(value)\n\t\treturn Number.isNaN(next) ? undefined : next\n\t}\n\treturn undefined\n}\n\nconst toTime = (value: unknown): number | undefined => {\n\tif (value instanceof Date) {\n\t\treturn value.getTime()\n\t}\n\tif (typeof value === 'string' && value.trim() !== '') {\n\t\tconst time = Date.parse(value)\n\t\treturn Number.isNaN(time) ? undefined : time\n\t}\n\treturn undefined\n}\n\n/** Strict, then numeric, then string equality, mirroring Payload's coerce-by-type-then-compare. */\nconst valuesEqual = (answer: unknown, value: unknown): boolean => {\n\tif (answer === value) {\n\t\treturn true\n\t}\n\tif (isNil(answer) || isNil(value)) {\n\t\treturn false\n\t}\n\tconst a = toNumber(answer)\n\tconst b = toNumber(value)\n\tif (a !== undefined && b !== undefined) {\n\t\treturn a === b\n\t}\n\treturn String(answer) === String(value)\n}\n\nconst toList = (value: unknown): unknown[] => {\n\tif (Array.isArray(value)) {\n\t\treturn value\n\t}\n\tif (typeof value === 'string') {\n\t\treturn value.split(',').map((entry) => entry.trim())\n\t}\n\treturn [value]\n}\n\nconst ordered = (\n\tanswer: unknown,\n\tvalue: unknown,\n\tcompare: (a: number, b: number) => boolean\n): boolean => {\n\tconst a = toNumber(answer) ?? toTime(answer)\n\tconst b = toNumber(value) ?? toTime(value)\n\tif (a === undefined || b === undefined) {\n\t\treturn false\n\t}\n\treturn compare(a, b)\n}\n\n/** Case-insensitive: every space-separated word in `value` is a substring of `answer` (Payload `like`). */\nconst everyWord = (answer: unknown, value: unknown): boolean => {\n\tconst haystack = String(answer).toLowerCase()\n\treturn String(value)\n\t\t.toLowerCase()\n\t\t.split(' ')\n\t\t.filter((word) => word.length > 0)\n\t\t.every((word) => haystack.includes(word))\n}\n\nconst evaluateOperator = (operator: Operator, answer: unknown, value: unknown): boolean => {\n\tswitch (operator) {\n\t\tcase 'equals':\n\t\t\treturn isNil(value) ? isNil(answer) : valuesEqual(answer, value)\n\t\tcase 'not_equals':\n\t\t\treturn isNil(value) ? !isNil(answer) : isNil(answer) || !valuesEqual(answer, value)\n\t\tcase 'in':\n\t\t\treturn toList(value).some((entry) => valuesEqual(answer, entry))\n\t\tcase 'not_in':\n\t\t\treturn isNil(answer) || !toList(value).some((entry) => valuesEqual(answer, entry))\n\t\tcase 'exists': {\n\t\t\t// Empty string and empty array both count as absent, so a repeater with no rows and an\n\t\t\t// unfilled text field are treated the same by a presence check.\n\t\t\tconst present =\n\t\t\t\t!isNil(answer) && answer !== '' && !(Array.isArray(answer) && answer.length === 0)\n\t\t\treturn value === true || value === 'true' ? present : !present\n\t\t}\n\t\tcase 'greater_than':\n\t\t\treturn ordered(answer, value, (a, b) => a > b)\n\t\tcase 'greater_than_equal':\n\t\t\treturn ordered(answer, value, (a, b) => a >= b)\n\t\tcase 'less_than':\n\t\t\treturn ordered(answer, value, (a, b) => a < b)\n\t\tcase 'less_than_equal':\n\t\t\treturn ordered(answer, value, (a, b) => a <= b)\n\t\tcase 'like':\n\t\t\treturn isNil(answer) ? false : everyWord(answer, value)\n\t\tcase 'not_like':\n\t\t\treturn isNil(answer) ? true : !everyWord(answer, value)\n\t\tcase 'contains':\n\t\t\treturn isNil(answer)\n\t\t\t\t? false\n\t\t\t\t: String(answer).toLowerCase().includes(String(value).toLowerCase())\n\t\tdefault:\n\t\t\treturn false\n\t}\n}\n\n/**\n * Evaluate one `Where` node: every top-level key is ANDed. `or` is a disjunction of sub-conditions\n * (an empty `or` matches), `and` is a conjunction, and any other key is a field path whose\n * `{ operator: value }` constraints are ANDed. Recurses, so nested `or`/`and` groups are supported.\n */\nconst evaluateWhere = (where: Where, answers: Record<string, unknown>): boolean =>\n\tObject.entries(where).every(([key, constraint]) => {\n\t\tif (key === 'or') {\n\t\t\tconst groups = (Array.isArray(constraint) ? constraint : []) as Where[]\n\t\t\treturn groups.length === 0 || groups.some((group) => evaluateWhere(group, answers))\n\t\t}\n\t\tif (key === 'and') {\n\t\t\tconst groups = (Array.isArray(constraint) ? constraint : []) as Where[]\n\t\t\treturn groups.every((group) => evaluateWhere(group, answers))\n\t\t}\n\t\tif (constraint == null || typeof constraint !== 'object' || Array.isArray(constraint)) {\n\t\t\treturn false\n\t\t}\n\t\treturn Object.entries(constraint as Record<string, unknown>).every(([operator, value]) =>\n\t\t\tevaluateOperator(operator as Operator, answers[key], value)\n\t\t)\n\t})\n\n/**\n * Evaluate a serializable `Where`-shaped condition against a flat map of (already coerced) form answers.\n * An absent or empty condition matches (returns true). Operator semantics mirror Payload's query\n * adapters: coerce then compare; `not_equals`/`not_in` are null-inclusive; `exists` treats `''`/absent\n * as not-existing; `like` is case-insensitive and space-splits (every word must be a substring), and\n * `not_like` is its logical negation (at least one word absent); `contains` is a single case-insensitive\n * substring; comma-delimited `in`/`not_in` string values are split and trimmed. Geo (`near`/`within`/\n * `intersects`) and `all` are out of scope and evaluate to false. Isomorphic: no `req`/DB access, so the\n * renderer reuses it client-side.\n */\nexport const evaluateCondition = (\n\twhere: Where | null | undefined,\n\tanswers: Record<string, unknown>\n): boolean => {\n\tif (!where || Object.keys(where).length === 0) {\n\t\treturn true\n\t}\n\treturn evaluateWhere(transformWhereQuery(where), answers)\n}\n"],"mappings":"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"}
@@ -2,8 +2,10 @@
2
2
  /**
3
3
  * The condition input families the builder supports. A deliberate subset of Payload's field-condition
4
4
  * types, restricted to the operators `evaluateCondition` implements (no geo/relationship in v1).
5
+ * `presence` is for values that only support an existence check (a file upload, a repeater), never a
6
+ * scalar comparison.
5
7
  */
6
- type ConditionFieldType = 'text' | 'number' | 'select' | 'checkbox' | 'date';
8
+ type ConditionFieldType = 'text' | 'number' | 'select' | 'checkbox' | 'date' | 'presence';
7
9
  //#endregion
8
10
  export { ConditionFieldType };
9
11
  //# sourceMappingURL=fieldTypes.d.ts.map