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

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 (182) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/actions/assertNoBlockCollision.js +18 -0
  3. package/dist/actions/assertNoBlockCollision.js.map +1 -0
  4. package/dist/actions/builtin/confirmation.js +2 -1
  5. package/dist/actions/builtin/confirmation.js.map +1 -1
  6. package/dist/actions/builtin/emailAction.d.ts +3 -1
  7. package/dist/actions/builtin/emailAction.js +43 -12
  8. package/dist/actions/builtin/emailAction.js.map +1 -1
  9. package/dist/actions/builtin/emailTeam.js +11 -5
  10. package/dist/actions/builtin/emailTeam.js.map +1 -1
  11. package/dist/actions/defineAction.d.ts +3 -1
  12. package/dist/actions/defineAction.js.map +1 -1
  13. package/dist/actions/dispatch.js +1 -1
  14. package/dist/actions/dispatch.js.map +1 -1
  15. package/dist/actions/emailRecipients.js +34 -7
  16. package/dist/actions/emailRecipients.js.map +1 -1
  17. package/dist/actions/recipientSources.d.ts +34 -0
  18. package/dist/actions/recipientSources.js +11 -0
  19. package/dist/actions/recipientSources.js.map +1 -0
  20. package/dist/actions/task.js +20 -0
  21. package/dist/actions/task.js.map +1 -1
  22. package/dist/aggregation/resolveResultsRequest.d.ts +6 -0
  23. package/dist/aggregation/resolveResultsRequest.js +32 -3
  24. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  25. package/dist/calc/computeCalcFields.d.ts +5 -3
  26. package/dist/calc/computeCalcFields.js +4 -3
  27. package/dist/calc/computeCalcFields.js.map +1 -1
  28. package/dist/calc/evaluate.d.ts +26 -3
  29. package/dist/calc/evaluate.js +33 -11
  30. package/dist/calc/evaluate.js.map +1 -1
  31. package/dist/calc/formatCalc.d.ts +11 -0
  32. package/dist/calc/formatCalc.js +35 -0
  33. package/dist/calc/formatCalc.js.map +1 -0
  34. package/dist/calc/formatCalcValue.d.ts +17 -0
  35. package/dist/calc/formatCalcValue.js +17 -0
  36. package/dist/calc/formatCalcValue.js.map +1 -0
  37. package/dist/calc/normalizeCalc.d.ts +17 -3
  38. package/dist/calc/normalizeCalc.js +39 -17
  39. package/dist/calc/normalizeCalc.js.map +1 -1
  40. package/dist/calc/registry.d.ts +43 -0
  41. package/dist/calc/registry.js +27 -0
  42. package/dist/calc/registry.js.map +1 -0
  43. package/dist/calc/resolveCalcContext.d.ts +36 -0
  44. package/dist/calc/resolveCalcContext.js +87 -0
  45. package/dist/calc/resolveCalcContext.js.map +1 -0
  46. package/dist/calc/types.d.ts +18 -3
  47. package/dist/calc/types.js +2 -1
  48. package/dist/calc/types.js.map +1 -1
  49. package/dist/client/CalcExpressionBuilder.d.ts +20 -0
  50. package/dist/client/CalcExpressionBuilder.js +890 -0
  51. package/dist/client/CalcExpressionBuilder.js.map +1 -0
  52. package/dist/client/RecipientsSelect.d.ts +5 -1
  53. package/dist/client/RecipientsSelect.js +22 -3
  54. package/dist/client/RecipientsSelect.js.map +1 -1
  55. package/dist/client/admin.css +282 -0
  56. package/dist/collections/formSubmissions.js +34 -16
  57. package/dist/collections/formSubmissions.js.map +1 -1
  58. package/dist/collections/forms.js +59 -27
  59. package/dist/collections/forms.js.map +1 -1
  60. package/dist/collections/formsEndpoints.js +4 -2
  61. package/dist/collections/formsEndpoints.js.map +1 -1
  62. package/dist/collections/settingsFields.d.ts +31 -0
  63. package/dist/collections/settingsFields.js +45 -0
  64. package/dist/collections/settingsFields.js.map +1 -0
  65. package/dist/context/formContext.d.ts +30 -0
  66. package/dist/context/formContext.js +54 -0
  67. package/dist/context/formContext.js.map +1 -0
  68. package/dist/exports/client.d.ts +2 -1
  69. package/dist/exports/client.js +2 -1
  70. package/dist/exports/react.d.ts +7 -3
  71. package/dist/exports/react.js +6 -2
  72. package/dist/exports/rsc.d.ts +2 -1
  73. package/dist/exports/rsc.js +3 -2
  74. package/dist/exports/types.d.ts +1 -1
  75. package/dist/fields/buildFieldBlocks.js +2 -1
  76. package/dist/fields/buildFieldBlocks.js.map +1 -1
  77. package/dist/fields/builtin/calculation.d.ts +17 -0
  78. package/dist/fields/builtin/calculation.js +170 -115
  79. package/dist/fields/builtin/calculation.js.map +1 -1
  80. package/dist/fields/builtin/checkbox.js +15 -0
  81. package/dist/fields/builtin/checkbox.js.map +1 -1
  82. package/dist/fields/builtin/email.js +7 -0
  83. package/dist/fields/builtin/email.js.map +1 -1
  84. package/dist/fields/builtin/index.d.ts +6 -2
  85. package/dist/fields/builtin/index.js +9 -4
  86. package/dist/fields/builtin/index.js.map +1 -1
  87. package/dist/fields/builtin/select.js +20 -1
  88. package/dist/fields/builtin/select.js.map +1 -1
  89. package/dist/fields/builtin/text.js +7 -0
  90. package/dist/fields/builtin/text.js.map +1 -1
  91. package/dist/fields/instanceOptions.js +1 -1
  92. package/dist/fields/instanceOptions.js.map +1 -1
  93. package/dist/fields/sharedConfig.js +15 -11
  94. package/dist/fields/sharedConfig.js.map +1 -1
  95. package/dist/fields/types.d.ts +8 -2
  96. package/dist/form/toFormDocument.d.ts +7 -0
  97. package/dist/form/toFormDocument.js +2 -1
  98. package/dist/form/toFormDocument.js.map +1 -1
  99. package/dist/form/types.d.ts +7 -0
  100. package/dist/index.d.ts +14 -4
  101. package/dist/index.js +40 -9
  102. package/dist/index.js.map +1 -1
  103. package/dist/options.d.ts +42 -0
  104. package/dist/plugin/registerCollections.js +29 -19
  105. package/dist/plugin/registerCollections.js.map +1 -1
  106. package/dist/poll/closeJob.d.ts +4 -3
  107. package/dist/poll/closeJob.js +6 -4
  108. package/dist/poll/closeJob.js.map +1 -1
  109. package/dist/poll/pollTypeRegistry.js +1 -1
  110. package/dist/poll/resolvePollCloseRequest.js +2 -1
  111. package/dist/poll/resolvePollCloseRequest.js.map +1 -1
  112. package/dist/poll/resolvePollOutcome.d.ts +6 -0
  113. package/dist/poll/resolvePollOutcome.js +17 -3
  114. package/dist/poll/resolvePollOutcome.js.map +1 -1
  115. package/dist/poll/votes/aggregateFromVotes.d.ts +27 -0
  116. package/dist/poll/votes/aggregateFromVotes.js +53 -0
  117. package/dist/poll/votes/aggregateFromVotes.js.map +1 -0
  118. package/dist/poll/votes/bumpPollVote.js +72 -0
  119. package/dist/poll/votes/bumpPollVote.js.map +1 -0
  120. package/dist/poll/votes/recountPollVotes.d.ts +20 -0
  121. package/dist/poll/votes/recountPollVotes.js +63 -0
  122. package/dist/poll/votes/recountPollVotes.js.map +1 -0
  123. package/dist/poll/votes/voteTallyHook.js +58 -0
  124. package/dist/poll/votes/voteTallyHook.js.map +1 -0
  125. package/dist/poll/votes/votesCollection.d.ts +19 -0
  126. package/dist/poll/votes/votesCollection.js +89 -0
  127. package/dist/poll/votes/votesCollection.js.map +1 -0
  128. package/dist/react/Form.d.ts +15 -1
  129. package/dist/react/Form.js +24 -3
  130. package/dist/react/Form.js.map +1 -1
  131. package/dist/react/FormContext.d.ts +8 -1
  132. package/dist/react/FormContext.js.map +1 -1
  133. package/dist/react/primitives/Checkbox.d.ts +4 -2
  134. package/dist/react/primitives/Checkbox.js +3 -2
  135. package/dist/react/primitives/Checkbox.js.map +1 -1
  136. package/dist/react/primitives/ChoiceGroup.d.ts +40 -0
  137. package/dist/react/primitives/ChoiceGroup.js +33 -0
  138. package/dist/react/primitives/ChoiceGroup.js.map +1 -0
  139. package/dist/react/primitives/FieldShell.d.ts +11 -1
  140. package/dist/react/primitives/FieldShell.js +43 -33
  141. package/dist/react/primitives/FieldShell.js.map +1 -1
  142. package/dist/react/primitives/Input.d.ts +3 -1
  143. package/dist/react/primitives/Input.js +2 -1
  144. package/dist/react/primitives/Input.js.map +1 -1
  145. package/dist/react/renderers/calculation.js +6 -1
  146. package/dist/react/renderers/calculation.js.map +1 -1
  147. package/dist/react/renderers/checkbox.js +2 -1
  148. package/dist/react/renderers/checkbox.js.map +1 -1
  149. package/dist/react/renderers/email.js +3 -1
  150. package/dist/react/renderers/email.js.map +1 -1
  151. package/dist/react/renderers/select.js +20 -2
  152. package/dist/react/renderers/select.js.map +1 -1
  153. package/dist/react/renderers/text.js +3 -1
  154. package/dist/react/renderers/text.js.map +1 -1
  155. package/dist/react/useCalcValues.d.ts +10 -0
  156. package/dist/react/useCalcValues.js +13 -0
  157. package/dist/react/useCalcValues.js.map +1 -0
  158. package/dist/recall/resolver.js +1 -1
  159. package/dist/recall/resolver.js.map +1 -1
  160. package/dist/spam/constants.js +7 -1
  161. package/dist/spam/constants.js.map +1 -1
  162. package/dist/submissions/formIdOf.js +13 -0
  163. package/dist/submissions/formIdOf.js.map +1 -0
  164. package/dist/submissions/runSubmission.js +3 -3
  165. package/dist/submissions/runSubmission.js.map +1 -1
  166. package/dist/submissions/validateSubmission.js +26 -2
  167. package/dist/submissions/validateSubmission.js.map +1 -1
  168. package/dist/submissions/verifyContext.js +35 -0
  169. package/dist/submissions/verifyContext.js.map +1 -0
  170. package/dist/translations/de.js +62 -3
  171. package/dist/translations/de.js.map +1 -1
  172. package/dist/translations/en.js +62 -3
  173. package/dist/translations/en.js.map +1 -1
  174. package/dist/translations/keys.d.ts +60 -1
  175. package/dist/translations/keys.js +60 -1
  176. package/dist/translations/keys.js.map +1 -1
  177. package/dist/validation/builtin/index.js +2 -0
  178. package/dist/validation/builtin/index.js.map +1 -1
  179. package/dist/validation/builtin/integer.js +15 -0
  180. package/dist/validation/builtin/integer.js.map +1 -0
  181. package/package.json +11 -5
  182. package/styles/form-builder.css +84 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CalcExpressionBuilder.js","names":["useTranslation"],"sources":["../../src/client/CalcExpressionBuilder.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tButton,\n\tFieldError,\n\tFieldLabel,\n\tReactSelect,\n\ttype ReactSelectOption,\n\tRenderCustomComponent,\n\tuseField,\n\tuseFormFields,\n} from '@payloadcms/ui'\nimport { reduceFieldsToValues } from 'payload/shared'\nimport { useMemo, useState } from 'react'\nimport { formatCalc } from '../calc/formatCalc'\nimport { normalizeCalc } from '../calc/normalizeCalc'\nimport {\n\tCALC_FNS,\n\tCALC_OPS,\n\ttype CalcExpression,\n\ttype CalcFn,\n\ttype CalcFnName,\n\ttype CalcOp,\n\tisCalcFn,\n} from '../calc/types'\nimport type { CalcSourceClientMeta } from '../fields/builtin/calculation'\nimport { keys, type TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport type { FieldRow } from './synthesizeClientField'\nimport { toStaticLabel } from './toStaticLabel'\nimport './admin.css'\n\nexport type CalcExpressionBuilderProps = {\n\tpath?: string\n\tfield?: { label?: unknown }\n\treadOnly?: boolean\n\t/** Registered calc-source metadata (key, label, modes), threaded as clientProps by `buildCalculationField`. */\n\tsources?: CalcSourceClientMeta[]\n\t/** Registered custom calc function names, so client-side normalization accepts what the server accepts. */\n\tfunctions?: string[]\n}\n\n/**\n * The editing view model: authors read and build expressions left-to-right, so the UI edits a\n * chain (first operand, then op+operand steps, then an optional wrapping unary function) and the\n * stored `CalcExpression` AST is derived by left-folding. The AST contract is untouched.\n */\nexport type CalcChain = { first: CalcOperand; steps: CalcChainStep[]; finish?: CalcFn }\nexport type CalcChainStep = { op: CalcOp; operand: CalcOperand }\nexport type CalcOperand =\n\t| { kind: 'ref'; field: string }\n\t| { kind: 'lit'; value: number }\n\t| { kind: 'weight'; field: string; weights?: Record<string, number>; source?: string }\n\t| { kind: 'source'; source: string }\n\t| { kind: 'group'; chain: CalcChain }\n\t| { kind: 'fn'; fn: CalcFnName; args: CalcChain[] }\n\t| { kind: 'neg'; operand: CalcOperand }\n\n/** Kinds a leaf operand card can switch between in place; containers come from Start with, loading, or JSON. */\ntype LeafKind = 'lit' | 'ref' | 'weight'\ntype StartKind = 'fn' | LeafKind\n\n/** The single-argument functions offered by the \"Then apply\" finisher; min/max are variadic and stay Function operands. */\nconst UNARY_FNS = CALC_FNS.filter((fn) => fn !== 'min' && fn !== 'max')\n\nconst seedOperand = (kind: LeafKind): CalcOperand => {\n\tswitch (kind) {\n\t\tcase 'ref':\n\t\t\treturn { kind: 'ref', field: '' }\n\t\tcase 'lit':\n\t\t\treturn { kind: 'lit', value: 0 }\n\t\tcase 'weight':\n\t\t\treturn { kind: 'weight', field: '', weights: {} }\n\t}\n}\n\nconst seedAst = (kind: StartKind): CalcExpression => {\n\tswitch (kind) {\n\t\tcase 'ref':\n\t\t\treturn { type: 'ref', field: '' }\n\t\tcase 'lit':\n\t\t\treturn { type: 'lit', value: 0 }\n\t\tcase 'weight':\n\t\t\treturn { type: 'weight', field: '', weights: {} }\n\t\tcase 'fn':\n\t\t\treturn { type: 'fn', fn: 'min', args: [{ type: 'lit', value: 0 }] }\n\t}\n}\n\nconst astToOperand = (node: CalcExpression): CalcOperand => {\n\tswitch (node.type) {\n\t\tcase 'ref':\n\t\t\treturn { kind: 'ref', field: node.field }\n\t\tcase 'lit':\n\t\t\treturn { kind: 'lit', value: node.value }\n\t\tcase 'source':\n\t\t\treturn { kind: 'source', source: node.source }\n\t\tcase 'weight':\n\t\t\treturn {\n\t\t\t\tkind: 'weight',\n\t\t\t\tfield: node.field,\n\t\t\t\t...(node.weights !== undefined ? { weights: node.weights } : {}),\n\t\t\t\t...(node.source !== undefined ? { source: node.source } : {}),\n\t\t\t}\n\t\tcase 'neg':\n\t\t\treturn { kind: 'neg', operand: astToOperand(node.operand) }\n\t\tcase 'fn':\n\t\t\treturn { kind: 'fn', fn: node.fn, args: node.args.map((arg) => toChain(arg, false)) }\n\t\tcase 'op':\n\t\t\treturn { kind: 'group', chain: toChain(node, false) }\n\t}\n}\n\nconst toChain = (expr: CalcExpression, allowFinish: boolean): CalcChain => {\n\tconst arg = expr.type === 'fn' ? expr.args[0] : undefined\n\tif (\n\t\tallowFinish &&\n\t\texpr.type === 'fn' &&\n\t\texpr.args.length === 1 &&\n\t\targ !== undefined &&\n\t\tisCalcFn(expr.fn) &&\n\t\t(UNARY_FNS as readonly CalcFn[]).includes(expr.fn)\n\t) {\n\t\treturn { ...toChain(arg, false), finish: expr.fn }\n\t}\n\tconst steps: CalcChainStep[] = []\n\tlet node = expr\n\twhile (node.type === 'op') {\n\t\tsteps.unshift({ op: node.op, operand: astToOperand(node.right) })\n\t\tnode = node.left\n\t}\n\treturn { first: astToOperand(node), steps }\n}\n\n/**\n * AST -> chain: the left spine of binary ops flattens into first + steps (a nested op on the right\n * becomes a `group`); a root unary fn wrapping the whole expression loads as `finish` (min/max stay\n * fn operands so the finisher select never holds a value it cannot offer). Total: every valid\n * `CalcExpression` loads without loss, and `chainToAst(astToChain(x))` is structurally `x`.\n */\nexport const astToChain = (expr: CalcExpression): CalcChain => toChain(expr, true)\n\nconst operandToAst = (operand: CalcOperand): CalcExpression => {\n\tswitch (operand.kind) {\n\t\tcase 'ref':\n\t\t\treturn { type: 'ref', field: operand.field }\n\t\tcase 'lit':\n\t\t\treturn { type: 'lit', value: operand.value }\n\t\tcase 'weight':\n\t\t\treturn {\n\t\t\t\ttype: 'weight',\n\t\t\t\tfield: operand.field,\n\t\t\t\t...(operand.weights !== undefined ? { weights: operand.weights } : {}),\n\t\t\t\t...(operand.source !== undefined ? { source: operand.source } : {}),\n\t\t\t}\n\t\tcase 'source':\n\t\t\treturn { type: 'source', source: operand.source }\n\t\tcase 'neg':\n\t\t\treturn { type: 'neg', operand: operandToAst(operand.operand) }\n\t\tcase 'fn':\n\t\t\treturn { type: 'fn', fn: operand.fn, args: operand.args.map(chainBodyToAst) }\n\t\tcase 'group':\n\t\t\treturn chainBodyToAst(operand.chain)\n\t}\n}\n\n/** Folds first + steps only. `finish` is root-only by design: nested chains (groups, fn args) never carry one, so it is deliberately ignored here. */\nconst chainBodyToAst = (chain: CalcChain): CalcExpression =>\n\tchain.steps.reduce<CalcExpression>(\n\t\t(left, step) => ({ type: 'op', op: step.op, left, right: operandToAst(step.operand) }),\n\t\toperandToAst(chain.first)\n\t)\n\n/** Chain -> AST: left-fold first + steps into the binary tree (left-assoc matches `-`/`/`/`%`), then wrap in `finish` when set. */\nexport const chainToAst = (chain: CalcChain): CalcExpression => {\n\tconst body = chainBodyToAst(chain)\n\treturn chain.finish ? { type: 'fn', fn: chain.finish, args: [body] } : body\n}\n\ntype ChoiceOption = { label: string; value: string }\ntype ChoiceSibling = { name: string; label: string; options: ChoiceOption[] }\ntype Siblings = {\n\tnumeric: ChoiceOption[]\n\tchoices: ChoiceSibling[]\n\tlabels: Record<string, string>\n}\n\nconst optionRows = (options: unknown): ChoiceOption[] => {\n\tif (!Array.isArray(options)) {\n\t\treturn []\n\t}\n\treturn options.flatMap((option) => {\n\t\tif (typeof option === 'string') {\n\t\t\treturn option.length > 0 ? [{ label: option, value: option }] : []\n\t\t}\n\t\tif (!option || typeof option !== 'object') {\n\t\t\treturn []\n\t\t}\n\t\tconst { label, value } = option as { label?: unknown; value?: unknown }\n\t\tif (typeof value !== 'string' || value.length === 0) {\n\t\t\treturn []\n\t\t}\n\t\treturn [{ label: typeof label === 'string' && label.length > 0 ? label : value, value }]\n\t})\n}\n\nconst siblingsFromData = (data: Record<string, unknown>, selfIndex: number): Siblings => {\n\tconst rows = Array.isArray(data.fields) ? (data.fields as unknown[]) : []\n\tconst siblings: Siblings = { numeric: [], choices: [], labels: {} }\n\trows.forEach((row, index) => {\n\t\tif (!row || typeof row !== 'object') {\n\t\t\treturn\n\t\t}\n\t\tconst { blockType, name, label, options } = row as FieldRow\n\t\tconst trimmed = typeof name === 'string' ? name.trim() : ''\n\t\tif (trimmed.length === 0) {\n\t\t\treturn\n\t\t}\n\t\tconst display = typeof label === 'string' && label.length > 0 ? label : trimmed\n\t\tsiblings.labels[trimmed] = display\n\t\t// Calculations fold in declaration order, so a calculation is only referenceable when it is\n\t\t// declared before this one; a forward (or self) reference would always evaluate to 0.\n\t\tconst referenceable =\n\t\t\tblockType === 'number' || (blockType === 'calculation' && index < selfIndex)\n\t\tif (referenceable) {\n\t\t\tsiblings.numeric.push({ label: display, value: trimmed })\n\t\t}\n\t\tif (blockType === 'select') {\n\t\t\tsiblings.choices.push({ name: trimmed, label: display, options: optionRows(options) })\n\t\t}\n\t})\n\treturn siblings\n}\n\nconst singleOption = (\n\tselected: ReactSelectOption | ReactSelectOption[] | null\n): ReactSelectOption | undefined => {\n\tconst chosen = Array.isArray(selected) ? selected[0] : selected\n\treturn chosen ?? undefined\n}\n\n/** Keeps a stored reference selectable even when it no longer matches any option, like FieldNameSelect. */\nconst withStored = (options: ChoiceOption[], stored: string): ChoiceOption[] =>\n\tstored.length > 0 && !options.some((option) => option.value === stored)\n\t\t? [...options, { label: stored, value: stored }]\n\t\t: options\n\ntype NumberInputProps = {\n\tvalue: number | undefined\n\tonCommit: (value: number) => void\n\tclassName: string\n\tid?: string\n\tdisabled?: boolean\n\t'aria-label'?: string\n}\n\n/**\n * Number input with a local draft: keystrokes commit only when the text parses to a finite number,\n * an invalid draft reverts to the committed value on blur, and an external value change resyncs\n * the draft.\n */\nconst NumberInput = ({\n\tvalue,\n\tonCommit,\n\tclassName,\n\tid,\n\tdisabled,\n\t'aria-label': ariaLabel,\n}: NumberInputProps) => {\n\tconst [draft, setDraft] = useState(value === undefined ? '' : String(value))\n\tconst [lastValue, setLastValue] = useState(value)\n\tif (value !== lastValue) {\n\t\tsetLastValue(value)\n\t\tif (draft.trim() === '' || Number(draft) !== value) {\n\t\t\tsetDraft(value === undefined ? '' : String(value))\n\t\t}\n\t}\n\treturn (\n\t\t<input\n\t\t\ttype=\"number\"\n\t\t\tclassName={className}\n\t\t\tid={id}\n\t\t\tdisabled={disabled}\n\t\t\taria-label={ariaLabel}\n\t\t\tvalue={draft}\n\t\t\tonChange={(event) => {\n\t\t\t\tconst text = event.target.value\n\t\t\t\tsetDraft(text)\n\t\t\t\tconst parsed = Number(text)\n\t\t\t\tif (text.trim() !== '' && Number.isFinite(parsed)) {\n\t\t\t\t\tonCommit(parsed)\n\t\t\t\t}\n\t\t\t}}\n\t\t\tonBlur={() => {\n\t\t\t\tconst parsed = Number(draft)\n\t\t\t\tif (draft.trim() === '' || !Number.isFinite(parsed)) {\n\t\t\t\t\tsetDraft(value === undefined ? '' : String(value))\n\t\t\t\t}\n\t\t\t}}\n\t\t/>\n\t)\n}\n\ntype BuilderCtx = {\n\tt: (key: TranslationKey) => string\n\tsiblings: Siblings\n\treadOnly: boolean\n\t/** Resolves a calc source key to its admin-locale display label (raw key when unregistered). */\n\tsourceLabelOf: (key: string) => string\n\t/** Scalar-capable sources as select options (`source:`-prefixed values). */\n\tscalarSourceOptions: ChoiceOption[]\n\t/** Weights-capable sources as select options (`source:`-prefixed values). */\n\tweightSourceOptions: ChoiceOption[]\n\t/** Registered custom calc function names, offered by the fn select after the built-ins. */\n\tcustomFnOptions: ChoiceOption[]\n}\n\nconst OP_OPTIONS: ChoiceOption[] = CALC_OPS.map((op) => ({ label: op, value: op }))\nconst FN_OPTIONS: ChoiceOption[] = CALC_FNS.map((fn) => ({ label: fn, value: fn }))\nconst UNARY_OPTIONS: ChoiceOption[] = UNARY_FNS.map((fn) => ({ label: fn, value: fn }))\n\n/** Source entries share kind/start selects with the fixed kinds, namespaced to avoid key collisions. */\nconst SOURCE_PREFIX = 'source:'\nconst sourceOptionValue = (key: string): string => `${SOURCE_PREFIX}${key}`\nconst sourceKeyOf = (value: string): string | undefined =>\n\tvalue.startsWith(SOURCE_PREFIX) ? value.slice(SOURCE_PREFIX.length) : undefined\n\n/** A source label is a plain string or a per-locale record; resolve it for the admin UI language. */\nconst resolveSourceLabel = (label: string | Record<string, string>, language: string): string =>\n\ttypeof label === 'string' ? label : (label[language] ?? label.en ?? Object.values(label)[0] ?? '')\n\ntype SelectOptionOrGroup = ChoiceOption | { label: string; options: ChoiceOption[] }\n\n/** Fixed kinds stay flat; sources join as a labeled group when any exist (react-select mixes both). */\nconst withSourceGroup = (\n\tbase: ChoiceOption[],\n\tsourceOptions: ChoiceOption[],\n\tgroupLabel: string\n): SelectOptionOrGroup[] =>\n\tsourceOptions.length > 0 ? [...base, { label: groupLabel, options: sourceOptions }] : base\n\ntype OperandCardProps = {\n\toperand: CalcOperand\n\tonChange: (next: CalcOperand) => void\n\tctx: BuilderCtx\n\tidPrefix: string\n}\n\nconst OperandCard = ({ operand, onChange, ctx, idPrefix }: OperandCardProps) => {\n\tconst { t, siblings, readOnly } = ctx\n\n\tif (operand.kind === 'group') {\n\t\treturn (\n\t\t\t<div className=\"fb-calc-operand fb-calc-operand--group\">\n\t\t\t\t<div className=\"fb-calc-operand__title\">{t(keys.calcBuilderGroup)}</div>\n\t\t\t\t<ChainEditor\n\t\t\t\t\tchain={operand.chain}\n\t\t\t\t\tonChange={(chain) => onChange({ ...operand, chain })}\n\t\t\t\t\tctx={ctx}\n\t\t\t\t\tidPrefix={`${idPrefix}-g`}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (operand.kind === 'fn') {\n\t\tconst fnOptions = withStored([...FN_OPTIONS, ...ctx.customFnOptions], operand.fn)\n\t\treturn (\n\t\t\t<div className=\"fb-calc-operand fb-calc-operand--fn\">\n\t\t\t\t<div className=\"fb-calc-operand__header\">\n\t\t\t\t\t<label className=\"fb-visually-hidden\" htmlFor={`${idPrefix}-fn`}>\n\t\t\t\t\t\t{t(keys.calcBuilderFunction)}\n\t\t\t\t\t</label>\n\t\t\t\t\t<ReactSelect\n\t\t\t\t\t\tclassName=\"fb-calc-operand__fn\"\n\t\t\t\t\t\tinputId={`${idPrefix}-fn`}\n\t\t\t\t\t\toptions={fnOptions}\n\t\t\t\t\t\tvalue={fnOptions.find((option) => option.value === operand.fn)}\n\t\t\t\t\t\tisClearable={false}\n\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\tonChange={(selected) => {\n\t\t\t\t\t\t\tconst chosen = singleOption(selected)\n\t\t\t\t\t\t\tif (chosen) {\n\t\t\t\t\t\t\t\tonChange({ ...operand, fn: String(chosen.value) })\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</div>\n\t\t\t\t<div className=\"fb-calc-fn__args\">\n\t\t\t\t\t{operand.args.map((arg, index) => (\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noArrayIndexKey: args are positional and never reordered\n\t\t\t\t\t\t<div key={index} className=\"fb-calc-fn__arg\">\n\t\t\t\t\t\t\t<div className=\"fb-calc-fn__arg-chain\">\n\t\t\t\t\t\t\t\t<ChainEditor\n\t\t\t\t\t\t\t\t\tchain={arg}\n\t\t\t\t\t\t\t\t\tonChange={(next) =>\n\t\t\t\t\t\t\t\t\t\tonChange({\n\t\t\t\t\t\t\t\t\t\t\t...operand,\n\t\t\t\t\t\t\t\t\t\t\targs: operand.args.map((a, i) => (i === index ? next : a)),\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\tctx={ctx}\n\t\t\t\t\t\t\t\t\tidPrefix={`${idPrefix}-a${index}`}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{operand.args.length > 1 ? (\n\t\t\t\t\t\t\t\t<div className=\"fb-calc-fn__arg-remove\">\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tbuttonStyle=\"icon-label\"\n\t\t\t\t\t\t\t\t\t\ticon=\"x\"\n\t\t\t\t\t\t\t\t\t\taria-label={t(keys.calcBuilderRemove)}\n\t\t\t\t\t\t\t\t\t\tmargin={false}\n\t\t\t\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\t\t\t\t\t\tonChange({\n\t\t\t\t\t\t\t\t\t\t\t\t...operand,\n\t\t\t\t\t\t\t\t\t\t\t\targs: operand.args.filter((_, i) => i !== index),\n\t\t\t\t\t\t\t\t\t\t\t})\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</div>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t))}\n\t\t\t\t\t<div className=\"fb-calc-fn__add\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tbuttonStyle=\"icon-label\"\n\t\t\t\t\t\t\ticon=\"plus\"\n\t\t\t\t\t\t\ticonStyle=\"with-border\"\n\t\t\t\t\t\t\ticonPosition=\"left\"\n\t\t\t\t\t\t\tmargin={false}\n\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\t\t\tonChange({\n\t\t\t\t\t\t\t\t\t...operand,\n\t\t\t\t\t\t\t\t\targs: [...operand.args, { first: { kind: 'lit', value: 0 }, steps: [] }],\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\t\t{t(keys.calcBuilderAddArgument)}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (operand.kind === 'neg') {\n\t\treturn (\n\t\t\t<div className=\"fb-calc-operand fb-calc-operand--neg\">\n\t\t\t\t<div className=\"fb-calc-operand__title\">{t(keys.calcBuilderNegate)}</div>\n\t\t\t\t<OperandCard\n\t\t\t\t\toperand={operand.operand}\n\t\t\t\t\tonChange={(next) => onChange({ kind: 'neg', operand: next })}\n\t\t\t\t\tctx={ctx}\n\t\t\t\t\tidPrefix={`${idPrefix}-n`}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t)\n\t}\n\n\tconst leafKindOptions: ChoiceOption[] = [\n\t\t{ label: t(keys.calcBuilderAnswer), value: 'ref' },\n\t\t{ label: t(keys.calcBuilderNumber), value: 'lit' },\n\t\t{ label: t(keys.calcBuilderWeights), value: 'weight' },\n\t]\n\tconst kindValue = operand.kind === 'source' ? sourceOptionValue(operand.source) : operand.kind\n\tconst kindSourceOptions =\n\t\toperand.kind === 'source' &&\n\t\t!ctx.scalarSourceOptions.some((option) => option.value === sourceOptionValue(operand.source))\n\t\t\t? [...ctx.scalarSourceOptions, { label: operand.source, value: kindValue }]\n\t\t\t: ctx.scalarSourceOptions\n\tconst kindFlat = [...leafKindOptions, ...kindSourceOptions]\n\tconst refOptions = operand.kind === 'ref' ? withStored(siblings.numeric, operand.field) : []\n\tconst choiceOptions =\n\t\toperand.kind === 'weight'\n\t\t\t? withStored(\n\t\t\t\t\tsiblings.choices.map((c) => ({ label: c.label, value: c.name })),\n\t\t\t\t\toperand.field\n\t\t\t\t)\n\t\t\t: []\n\tconst choice =\n\t\toperand.kind === 'weight' ? siblings.choices.find((c) => c.name === operand.field) : undefined\n\tconst weightSource = operand.kind === 'weight' ? operand.source : undefined\n\tconst weightValuesOptions: ChoiceOption[] = [\n\t\t{ label: t(keys.calcBuilderWeightManual), value: 'manual' },\n\t\t...(weightSource !== undefined &&\n\t\t!ctx.weightSourceOptions.some((option) => option.value === sourceOptionValue(weightSource))\n\t\t\t? [\n\t\t\t\t\t...ctx.weightSourceOptions,\n\t\t\t\t\t{ label: weightSource, value: sourceOptionValue(weightSource) },\n\t\t\t\t]\n\t\t\t: ctx.weightSourceOptions),\n\t]\n\n\treturn (\n\t\t<div className=\"fb-calc-operand\">\n\t\t\t<div className=\"fb-calc-operand__header\">\n\t\t\t\t<label className=\"fb-visually-hidden\" htmlFor={`${idPrefix}-kind`}>\n\t\t\t\t\t{t(keys.calcBuilderKind)}\n\t\t\t\t</label>\n\t\t\t\t<ReactSelect\n\t\t\t\t\tclassName=\"fb-calc-operand__kind\"\n\t\t\t\t\tinputId={`${idPrefix}-kind`}\n\t\t\t\t\toptions={\n\t\t\t\t\t\twithSourceGroup(\n\t\t\t\t\t\t\tleafKindOptions,\n\t\t\t\t\t\t\tkindSourceOptions,\n\t\t\t\t\t\t\tt(keys.calcBuilderSourcesGroup)\n\t\t\t\t\t\t) as ReactSelectOption[]\n\t\t\t\t\t}\n\t\t\t\t\tvalue={kindFlat.find((option) => option.value === kindValue)}\n\t\t\t\t\tisClearable={false}\n\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\tonChange={(selected) => {\n\t\t\t\t\t\tconst chosen = singleOption(selected)\n\t\t\t\t\t\tif (!chosen || chosen.value === kindValue) {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst sourceKey = sourceKeyOf(String(chosen.value))\n\t\t\t\t\t\tonChange(\n\t\t\t\t\t\t\tsourceKey !== undefined\n\t\t\t\t\t\t\t\t? { kind: 'source', source: sourceKey }\n\t\t\t\t\t\t\t\t: seedOperand(chosen.value as LeafKind)\n\t\t\t\t\t\t)\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t\t{operand.kind === 'source' ? (\n\t\t\t\t\t<span className=\"fb-calc-operand__source\">{ctx.sourceLabelOf(operand.source)}</span>\n\t\t\t\t) : null}\n\t\t\t\t{operand.kind === 'ref' ? (\n\t\t\t\t\trefOptions.length === 0 ? (\n\t\t\t\t\t\t<p className=\"fb-calc-hint\">{t(keys.calcBuilderNoNumericFields)}</p>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<label className=\"fb-visually-hidden\" htmlFor={`${idPrefix}-ref`}>\n\t\t\t\t\t\t\t\t{t(keys.calcBuilderPickField)}\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<ReactSelect\n\t\t\t\t\t\t\t\tclassName=\"fb-calc-operand__ref\"\n\t\t\t\t\t\t\t\tinputId={`${idPrefix}-ref`}\n\t\t\t\t\t\t\t\toptions={refOptions}\n\t\t\t\t\t\t\t\tvalue={refOptions.find((option) => option.value === operand.field)}\n\t\t\t\t\t\t\t\tisClearable={!readOnly}\n\t\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\t\tplaceholder={t(keys.calcBuilderPickField)}\n\t\t\t\t\t\t\t\tonChange={(selected) => {\n\t\t\t\t\t\t\t\t\tconst chosen = singleOption(selected)\n\t\t\t\t\t\t\t\t\tonChange({ kind: 'ref', field: chosen ? String(chosen.value) : '' })\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) : null}\n\t\t\t\t{operand.kind === 'lit' ? (\n\t\t\t\t\t<NumberInput\n\t\t\t\t\t\tclassName=\"fb-calc-operand__number\"\n\t\t\t\t\t\tid={`${idPrefix}-number`}\n\t\t\t\t\t\taria-label={t(keys.calcBuilderNumber)}\n\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\tvalue={operand.value}\n\t\t\t\t\t\tonCommit={(value) => onChange({ kind: 'lit', value })}\n\t\t\t\t\t/>\n\t\t\t\t) : null}\n\t\t\t\t{operand.kind === 'weight' ? (\n\t\t\t\t\tchoiceOptions.length === 0 ? (\n\t\t\t\t\t\t<p className=\"fb-calc-hint\">{t(keys.calcBuilderNoChoiceFields)}</p>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<label className=\"fb-visually-hidden\" htmlFor={`${idPrefix}-weight-field`}>\n\t\t\t\t\t\t\t\t{t(keys.calcBuilderPickField)}\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<ReactSelect\n\t\t\t\t\t\t\t\tclassName=\"fb-calc-operand__weight-field\"\n\t\t\t\t\t\t\t\tinputId={`${idPrefix}-weight-field`}\n\t\t\t\t\t\t\t\toptions={choiceOptions}\n\t\t\t\t\t\t\t\tvalue={choiceOptions.find((option) => option.value === operand.field)}\n\t\t\t\t\t\t\t\tisClearable={!readOnly}\n\t\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\t\tplaceholder={t(keys.calcBuilderPickField)}\n\t\t\t\t\t\t\t\tonChange={(selected) => {\n\t\t\t\t\t\t\t\t\tconst chosen = singleOption(selected)\n\t\t\t\t\t\t\t\t\tconst field = chosen ? String(chosen.value) : ''\n\t\t\t\t\t\t\t\t\tif (field !== operand.field) {\n\t\t\t\t\t\t\t\t\t\t// Inline weights are per-option and reset with the field; a source keeps\n\t\t\t\t\t\t\t\t\t\t// resolving (its resolver receives the newly chosen field).\n\t\t\t\t\t\t\t\t\t\tonChange(\n\t\t\t\t\t\t\t\t\t\t\toperand.source !== undefined\n\t\t\t\t\t\t\t\t\t\t\t\t? { kind: 'weight', field, source: operand.source }\n\t\t\t\t\t\t\t\t\t\t\t\t: { kind: 'weight', field, weights: {} }\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) : null}\n\t\t\t</div>\n\t\t\t{operand.kind === 'weight' && ctx.weightSourceOptions.length > 0 ? (\n\t\t\t\t<div className=\"fb-calc-operand__weight-values-row\">\n\t\t\t\t\t<label className=\"fb-calc-operand__weight-values-label\" htmlFor={`${idPrefix}-wv`}>\n\t\t\t\t\t\t{t(keys.calcBuilderWeightValues)}\n\t\t\t\t\t</label>\n\t\t\t\t\t<ReactSelect\n\t\t\t\t\t\tclassName=\"fb-calc-operand__weight-values\"\n\t\t\t\t\t\tinputId={`${idPrefix}-wv`}\n\t\t\t\t\t\toptions={weightValuesOptions}\n\t\t\t\t\t\tvalue={weightValuesOptions.find(\n\t\t\t\t\t\t\t(option) =>\n\t\t\t\t\t\t\t\toption.value ===\n\t\t\t\t\t\t\t\t(operand.source !== undefined ? sourceOptionValue(operand.source) : 'manual')\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tisClearable={false}\n\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\tonChange={(selected) => {\n\t\t\t\t\t\t\tconst chosen = singleOption(selected)\n\t\t\t\t\t\t\tif (!chosen) {\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst sourceKey = sourceKeyOf(String(chosen.value))\n\t\t\t\t\t\t\tif (sourceKey === undefined) {\n\t\t\t\t\t\t\t\tif (operand.source !== undefined) {\n\t\t\t\t\t\t\t\t\t// Back to Manual: drop the source, keep any inline weights (they were\n\t\t\t\t\t\t\t\t\t// preserved untouched while sourced) so the author's numbers return.\n\t\t\t\t\t\t\t\t\tonChange({\n\t\t\t\t\t\t\t\t\t\tkind: 'weight',\n\t\t\t\t\t\t\t\t\t\tfield: operand.field,\n\t\t\t\t\t\t\t\t\t\tweights: operand.weights ?? {},\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\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (sourceKey !== operand.source) {\n\t\t\t\t\t\t\t\tonChange({ ...operand, source: sourceKey })\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</div>\n\t\t\t) : null}\n\t\t\t{operand.kind === 'weight' && operand.source !== undefined ? (\n\t\t\t\t<p className=\"fb-calc-hint\">{t(keys.calcBuilderWeightsFromSource)}</p>\n\t\t\t) : null}\n\t\t\t{operand.kind === 'weight' && operand.source === undefined && choice ? (\n\t\t\t\t<div className=\"fb-calc-operand__weights\">\n\t\t\t\t\t{choice.options.map((option) => (\n\t\t\t\t\t\t<div key={option.value} className=\"fb-calc-operand__weight-row\">\n\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\tclassName=\"fb-calc-operand__weight-label\"\n\t\t\t\t\t\t\t\thtmlFor={`${idPrefix}-weight-${option.value}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{option.label}\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<NumberInput\n\t\t\t\t\t\t\t\tclassName=\"fb-calc-operand__weight\"\n\t\t\t\t\t\t\t\tid={`${idPrefix}-weight-${option.value}`}\n\t\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\t\tvalue={operand.weights?.[option.value]}\n\t\t\t\t\t\t\t\tonCommit={(value) =>\n\t\t\t\t\t\t\t\t\tonChange({\n\t\t\t\t\t\t\t\t\t\t...operand,\n\t\t\t\t\t\t\t\t\t\tweights: { ...(operand.weights ?? {}), [option.value]: value },\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</div>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t</div>\n\t)\n}\n\ntype ChainEditorProps = {\n\tchain: CalcChain\n\tonChange: (next: CalcChain) => void\n\t/** Root only: called when the only row is removed, clearing the expression back to Start with. */\n\tonEmptied?: () => void\n\t/**\n\t * Honest-affordance rule: an X must actually remove. The root's only-row X clears the whole\n\t * expression, so it stays; a nested chain's only row (fn arg, group) would merely reseed, so\n\t * its X is not rendered at all.\n\t */\n\tisRoot?: boolean\n\tctx: BuilderCtx\n\tidPrefix: string\n}\n\nconst ChainEditor = ({ chain, onChange, onEmptied, isRoot, ctx, idPrefix }: ChainEditorProps) => {\n\tconst { t, readOnly } = ctx\n\n\tconst removeRow = (row: number) => {\n\t\tif (row === 0) {\n\t\t\tconst [promoted, ...rest] = chain.steps\n\t\t\tif (!promoted) {\n\t\t\t\tonEmptied?.()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tonChange({ ...chain, first: promoted.operand, steps: rest })\n\t\t\treturn\n\t\t}\n\t\tonChange({ ...chain, steps: chain.steps.filter((_, i) => i !== row - 1) })\n\t}\n\n\tconst rows: { op?: CalcOp; operand: CalcOperand }[] = [\n\t\t{ operand: chain.first },\n\t\t...chain.steps.map((step) => ({ op: step.op, operand: step.operand })),\n\t]\n\n\treturn (\n\t\t<div className=\"fb-calc-chain\">\n\t\t\t{rows.map((row, index) => (\n\t\t\t\t// biome-ignore lint/suspicious/noArrayIndexKey: rows are positional and never reordered\n\t\t\t\t<div key={index} className={index > 0 ? 'fb-calc-row fb-calc-row--divided' : 'fb-calc-row'}>\n\t\t\t\t\t{index === 0 ? <div className=\"fb-calc-row__op-spacer\" aria-hidden=\"true\" /> : null}\n\t\t\t\t\t{index > 0 ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<label className=\"fb-visually-hidden\" htmlFor={`${idPrefix}-r${index}-op`}>\n\t\t\t\t\t\t\t\t{t(keys.calcBuilderMath)}\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<ReactSelect\n\t\t\t\t\t\t\t\tclassName=\"fb-calc-row__op\"\n\t\t\t\t\t\t\t\tinputId={`${idPrefix}-r${index}-op`}\n\t\t\t\t\t\t\t\toptions={OP_OPTIONS}\n\t\t\t\t\t\t\t\tvalue={OP_OPTIONS.find((option) => option.value === row.op)}\n\t\t\t\t\t\t\t\tisClearable={false}\n\t\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\t\tonChange={(selected) => {\n\t\t\t\t\t\t\t\t\tconst chosen = singleOption(selected)\n\t\t\t\t\t\t\t\t\tif (chosen) {\n\t\t\t\t\t\t\t\t\t\tonChange({\n\t\t\t\t\t\t\t\t\t\t\t...chain,\n\t\t\t\t\t\t\t\t\t\t\tsteps: chain.steps.map((step, i) =>\n\t\t\t\t\t\t\t\t\t\t\t\ti === index - 1 ? { ...step, op: chosen.value as CalcOp } : step\n\t\t\t\t\t\t\t\t\t\t\t),\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) : null}\n\t\t\t\t\t<div className=\"fb-calc-row__operand\">\n\t\t\t\t\t\t<OperandCard\n\t\t\t\t\t\t\toperand={row.operand}\n\t\t\t\t\t\t\tonChange={(operand) =>\n\t\t\t\t\t\t\t\tindex === 0\n\t\t\t\t\t\t\t\t\t? onChange({ ...chain, first: operand })\n\t\t\t\t\t\t\t\t\t: onChange({\n\t\t\t\t\t\t\t\t\t\t\t...chain,\n\t\t\t\t\t\t\t\t\t\t\tsteps: chain.steps.map((step, i) =>\n\t\t\t\t\t\t\t\t\t\t\t\ti === index - 1 ? { ...step, operand } : step\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tctx={ctx}\n\t\t\t\t\t\t\tidPrefix={`${idPrefix}-r${index}`}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t{rows.length > 1 || isRoot ? (\n\t\t\t\t\t\t<div className=\"fb-calc-row__remove\">\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tbuttonStyle=\"icon-label\"\n\t\t\t\t\t\t\t\ticon=\"x\"\n\t\t\t\t\t\t\t\taria-label={t(keys.calcBuilderRemove)}\n\t\t\t\t\t\t\t\tmargin={false}\n\t\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\t\tonClick={() => removeRow(index)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t))}\n\t\t\t<div className=\"fb-calc-chain__add\">\n\t\t\t\t<Button\n\t\t\t\t\tbuttonStyle=\"icon-label\"\n\t\t\t\t\ticon=\"plus\"\n\t\t\t\t\ticonStyle=\"with-border\"\n\t\t\t\t\ticonPosition=\"left\"\n\t\t\t\t\tmargin={false}\n\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\tonChange({\n\t\t\t\t\t\t\t...chain,\n\t\t\t\t\t\t\tsteps: [...chain.steps, { op: '*', operand: seedOperand('ref') }],\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t{t(keys.calcBuilderAddStep)}\n\t\t\t\t</Button>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n\n/**\n * Visual editor for the calculation field's `expression` AST, presented as a left-to-right chain.\n * Storage shape is unchanged and `validateExpression` stays the server gate; the AST remains\n * authorable via the API.\n */\nexport const CalcExpressionBuilder = (props: CalcExpressionBuilderProps) => {\n\tconst {\n\t\tcustomComponents: { Error: ErrorComponent, Label } = {},\n\t\tdisabled,\n\t\tpath,\n\t\tsetValue,\n\t\tshowError,\n\t\tvalue,\n\t} = useField<unknown>({ path: props.path })\n\tconst { t, i18n } = useTranslation()\n\tconst label = toStaticLabel(props.field?.label)\n\tconst readOnly = props.readOnly === true || disabled === true\n\n\tconst sources = useMemo(() => props.sources ?? [], [props.sources])\n\t// Mirrors the server-side allowed sets, so the builder accepts exactly the expressions\n\t// `buildValidateExpression` accepts (a sourced expression must not read as \"stored invalid\").\n\tconst allowed = useMemo(\n\t\t() => ({\n\t\t\tsources: new Set(sources.map((source) => source.key)),\n\t\t\tfunctions: new Set(props.functions ?? []),\n\t\t}),\n\t\t[sources, props.functions]\n\t)\n\tconst sourceLabelOf = (key: string): string => {\n\t\tconst meta = sources.find((source) => source.key === key)\n\t\treturn meta ? resolveSourceLabel(meta.label, i18n.language) : key\n\t}\n\tconst scalarSourceOptions: ChoiceOption[] = sources\n\t\t.filter((source) => source.scalar)\n\t\t.map((source) => ({ label: sourceLabelOf(source.key), value: sourceOptionValue(source.key) }))\n\tconst weightSourceOptions: ChoiceOption[] = sources\n\t\t.filter((source) => source.weights)\n\t\t.map((source) => ({ label: sourceLabelOf(source.key), value: sourceOptionValue(source.key) }))\n\tconst customFnOptions: ChoiceOption[] = (props.functions ?? []).map((fn) => ({\n\t\tlabel: fn,\n\t\tvalue: fn,\n\t}))\n\n\tconst expression = useMemo(() => normalizeCalc(value, allowed), [value, allowed])\n\tconst chain = useMemo(() => (expression ? astToChain(expression) : undefined), [expression])\n\tconst storedInvalid = expression === undefined && value != null && value !== ''\n\n\t// The expression path is `fields.<row>.expression`; that row is the calculation block being\n\t// edited, so it bounds which calculation siblings are referenceable (see siblingsFromData).\n\t// An unparseable path (never expected) offers every calculation rather than none.\n\tconst selfIndex = useMemo(() => {\n\t\tconst match = /^fields\\.(\\d+)\\./.exec(path)\n\t\treturn match ? Number(match[1]) : Number.MAX_SAFE_INTEGER\n\t}, [path])\n\n\tconst siblingsJson = useFormFields(([fields]) =>\n\t\tJSON.stringify(siblingsFromData(reduceFieldsToValues(fields, true), selfIndex))\n\t)\n\tconst siblings = useMemo(() => JSON.parse(siblingsJson) as Siblings, [siblingsJson])\n\n\tconst startOptions: ChoiceOption[] = [\n\t\t{ label: t(keys.calcBuilderAnswer), value: 'ref' },\n\t\t{ label: t(keys.calcBuilderNumber), value: 'lit' },\n\t\t{ label: t(keys.calcBuilderWeights), value: 'weight' },\n\t\t{ label: t(keys.calcBuilderFunction), value: 'fn' },\n\t]\n\tconst startDescriptions: { key: string; label: string; description: string }[] = [\n\t\t{\n\t\t\tkey: 'ref',\n\t\t\tlabel: t(keys.calcBuilderAnswer),\n\t\t\tdescription: t(keys.calcBuilderFieldDescription),\n\t\t},\n\t\t{\n\t\t\tkey: 'lit',\n\t\t\tlabel: t(keys.calcBuilderNumber),\n\t\t\tdescription: t(keys.calcBuilderNumberDescription),\n\t\t},\n\t\t{\n\t\t\tkey: 'weight',\n\t\t\tlabel: t(keys.calcBuilderWeights),\n\t\t\tdescription: t(keys.calcBuilderWeightsDescription),\n\t\t},\n\t\t{\n\t\t\tkey: 'fn',\n\t\t\tlabel: t(keys.calcBuilderFunction),\n\t\t\tdescription: t(keys.calcBuilderFunctionDescription),\n\t\t},\n\t]\n\n\tconst commitChain = (next: CalcChain) => {\n\t\tif (!readOnly) {\n\t\t\tsetValue(chainToAst(next))\n\t\t}\n\t}\n\n\tconst clear = () => {\n\t\tif (!readOnly) {\n\t\t\tsetValue(null)\n\t\t}\n\t}\n\n\t// An unfilled slot (blank ref) previews as \"?\" so a partial chain reads \"Price × ?\" instead of\n\t// trailing off; formatCalc itself stays pure.\n\tconst labelOf = (field: string) => (field.trim() === '' ? '?' : (siblings.labels[field] ?? field))\n\n\tconst idBase = `calc-${path.replace(/\\./g, '__')}`\n\tconst ctx: BuilderCtx = {\n\t\tt,\n\t\tsiblings,\n\t\treadOnly,\n\t\tsourceLabelOf,\n\t\tscalarSourceOptions,\n\t\tweightSourceOptions,\n\t\tcustomFnOptions,\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tclassName={['field-type', 'fb-calc-builder', showError && 'error', readOnly && 'read-only']\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(' ')}\n\t\t\tid={path ? `field-${path.replace(/\\./g, '__')}` : undefined}\n\t\t>\n\t\t\t<RenderCustomComponent\n\t\t\t\tCustomComponent={Label}\n\t\t\t\tFallback={<FieldLabel label={label} path={path} />}\n\t\t\t/>\n\t\t\t<div className=\"field-type__wrap\">\n\t\t\t\t<RenderCustomComponent\n\t\t\t\t\tCustomComponent={ErrorComponent}\n\t\t\t\t\tFallback={<FieldError path={path} showError={showError} />}\n\t\t\t\t/>\n\t\t\t\t{chain ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<ChainEditor\n\t\t\t\t\t\t\tchain={chain}\n\t\t\t\t\t\t\tonChange={commitChain}\n\t\t\t\t\t\t\tonEmptied={clear}\n\t\t\t\t\t\t\tisRoot\n\t\t\t\t\t\t\tctx={ctx}\n\t\t\t\t\t\t\tidPrefix={idBase}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<div className=\"fb-calc-finish\">\n\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\tclassName=\"fb-calc-finish__label\"\n\t\t\t\t\t\t\t\thtmlFor={`${idBase}-finish`}\n\t\t\t\t\t\t\t\ttitle={t(keys.calcBuilderThenApply)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{t(keys.calcBuilderThenApply)}\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<ReactSelect\n\t\t\t\t\t\t\t\tclassName=\"fb-calc-finish__select\"\n\t\t\t\t\t\t\t\tinputId={`${idBase}-finish`}\n\t\t\t\t\t\t\t\toptions={UNARY_OPTIONS}\n\t\t\t\t\t\t\t\tvalue={UNARY_OPTIONS.find((option) => option.value === chain.finish)}\n\t\t\t\t\t\t\t\tisClearable={!readOnly}\n\t\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\t\tonChange={(selected) => {\n\t\t\t\t\t\t\t\t\tconst chosen = singleOption(selected)\n\t\t\t\t\t\t\t\t\tcommitChain({\n\t\t\t\t\t\t\t\t\t\t...chain,\n\t\t\t\t\t\t\t\t\t\tfinish: chosen ? (chosen.value as CalcFn) : undefined,\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</div>\n\t\t\t\t\t</>\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"fb-calc-builder__empty\">\n\t\t\t\t\t\t{storedInvalid ? (\n\t\t\t\t\t\t\t<p className=\"fb-calc-builder__warning\">{t(keys.calcBuilderStoredInvalid)}</p>\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t<p className=\"fb-calc-builder__hint\">{t(keys.calcBuilderAddExpression)}</p>\n\t\t\t\t\t\t<label className=\"fb-visually-hidden\" htmlFor={`${idBase}-seed`}>\n\t\t\t\t\t\t\t{t(keys.calcBuilderStartWith)}\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<ReactSelect\n\t\t\t\t\t\t\tclassName=\"fb-calc-builder__seed\"\n\t\t\t\t\t\t\tinputId={`${idBase}-seed`}\n\t\t\t\t\t\t\toptions={\n\t\t\t\t\t\t\t\twithSourceGroup(\n\t\t\t\t\t\t\t\t\tstartOptions,\n\t\t\t\t\t\t\t\t\tscalarSourceOptions,\n\t\t\t\t\t\t\t\t\tt(keys.calcBuilderSourcesGroup)\n\t\t\t\t\t\t\t\t) as ReactSelectOption[]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\t\t\tplaceholder={t(keys.calcBuilderStartWith)}\n\t\t\t\t\t\t\tonChange={(selected) => {\n\t\t\t\t\t\t\t\tconst chosen = singleOption(selected)\n\t\t\t\t\t\t\t\tif (!chosen || readOnly) {\n\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tconst sourceKey = sourceKeyOf(String(chosen.value))\n\t\t\t\t\t\t\t\tsetValue(\n\t\t\t\t\t\t\t\t\tsourceKey !== undefined\n\t\t\t\t\t\t\t\t\t\t? { type: 'source', source: sourceKey }\n\t\t\t\t\t\t\t\t\t\t: seedAst(chosen.value as StartKind)\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\t<ul className=\"fb-calc-builder__kinds\">\n\t\t\t\t\t\t\t{startDescriptions.map((entry) => (\n\t\t\t\t\t\t\t\t<li key={entry.key}>\n\t\t\t\t\t\t\t\t\t<span className=\"fb-calc-builder__kind-label\">{entry.label}</span>{' '}\n\t\t\t\t\t\t\t\t\t{entry.description}\n\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\t<p className=\"fb-calc-builder__preview\" aria-live=\"polite\">\n\t\t\t\t\t{expression ? formatCalc(expression, labelOf, sourceLabelOf) : ''}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"],"mappings":"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"}
@@ -16,7 +16,11 @@ type RecipientsSelectProps = {
16
16
  endpoint?: string; /** Allow free-typed emails (default true). */
17
17
  allowCustom?: boolean; /** Offer the form's own fields as recipient tokens (default true). */
18
18
  fieldTokens?: boolean; /** Field types eligible as tokens (default `['email']`). */
19
- tokenFieldTypes?: string[];
19
+ tokenFieldTypes?: string[]; /** Registered recipient sources, offered as their own option group (labels resolved for the admin locale). */
20
+ sources?: {
21
+ value: string;
22
+ label: string | Record<string, string>;
23
+ }[];
20
24
  descriptionKey?: TranslationKey;
21
25
  };
22
26
  /**
@@ -10,6 +10,8 @@ import { FieldDescription, FieldError, FieldLabel, ReactSelect, RenderCustomComp
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
11
  import { useEffect, useMemo, useState } from "react";
12
12
  //#region src/client/RecipientsSelect.tsx
13
+ /** A source label is a plain string or a per-locale record; resolve it for the admin UI language. */
14
+ const resolveSourceLabel = (label, language) => typeof label === "string" ? label : label[language] ?? label.en ?? Object.values(label)[0] ?? "";
13
15
  const tokenOptionsFromData = (data, types) => {
14
16
  const rows = Array.isArray(data.fields) ? data.fields : [];
15
17
  const labels = /* @__PURE__ */ new Map();
@@ -33,7 +35,11 @@ const RecipientsSelect = (props) => {
33
35
  const allowCustom = props.allowCustom !== false;
34
36
  const fieldTokens = props.fieldTokens !== false;
35
37
  const { customComponents: { Description, Error: ErrorComponent, Label } = {}, disabled, path, setValue, showError, value } = useField({ path: props.path });
36
- const { t } = useTranslation$1();
38
+ const { t, i18n } = useTranslation$1();
39
+ const sourceOptions = useMemo(() => (props.sources ?? []).map((source) => ({
40
+ value: source.value,
41
+ label: resolveSourceLabel(source.label, i18n.language)
42
+ })), [props.sources, i18n.language]);
37
43
  const label = toStaticLabel(props.field?.label ?? props.label);
38
44
  const description = props.descriptionKey ? t(props.descriptionKey) : toStaticLabel(props.field?.admin?.description);
39
45
  const { id, collectionSlug } = useDocumentInfo();
@@ -77,9 +83,17 @@ const RecipientsSelect = (props) => {
77
83
  const stored = Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : typeof value === "string" && value.length > 0 ? [value] : [];
78
84
  const byValue = useMemo(() => {
79
85
  const map = /* @__PURE__ */ new Map();
80
- for (const option of [...presetOptions, ...tokenOptions]) map.set(option.value, option);
86
+ for (const option of [
87
+ ...presetOptions,
88
+ ...tokenOptions,
89
+ ...sourceOptions
90
+ ]) map.set(option.value, option);
81
91
  return map;
82
- }, [presetOptions, tokenOptions]);
92
+ }, [
93
+ presetOptions,
94
+ tokenOptions,
95
+ sourceOptions
96
+ ]);
83
97
  const selected = stored.map((entry) => byValue.get(entry) ?? {
84
98
  label: entry,
85
99
  value: entry
@@ -94,10 +108,15 @@ const RecipientsSelect = (props) => {
94
108
  label: t(keys.recipientsGroupFields),
95
109
  options: tokenOptions
96
110
  });
111
+ if (sourceOptions.length > 0) groups.push({
112
+ label: t(keys.recipientsGroupSources),
113
+ options: sourceOptions
114
+ });
97
115
  return groups;
98
116
  }, [
99
117
  presetOptions,
100
118
  tokenOptions,
119
+ sourceOptions,
101
120
  t
102
121
  ]);
103
122
  const handleChange = (selection) => {
@@ -1 +1 @@
1
- {"version":3,"file":"RecipientsSelect.js","names":["useTranslation"],"sources":["../../src/client/RecipientsSelect.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tFieldDescription,\n\tFieldError,\n\tFieldLabel,\n\tReactSelect,\n\ttype ReactSelectOption,\n\tRenderCustomComponent,\n\tuseConfig,\n\tuseDocumentInfo,\n\tuseField,\n\tuseFormFields,\n} from '@payloadcms/ui'\nimport { reduceFieldsToValues } from 'payload/shared'\nimport { type CSSProperties, useEffect, useMemo, useState } from 'react'\nimport { isPlausibleEmail } from '../actions/emailRecipients'\nimport { fieldNames, fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport { keys, type TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport {\n\tbuildEndpointOptionsUrl,\n\ttype EndpointOption,\n\tparseEndpointOptions,\n} from './endpointOptions'\nimport type { FieldRow } from './synthesizeClientField'\nimport { toStaticLabel } from './toStaticLabel'\n\nexport type RecipientsSelectProps = {\n\tpath?: string\n\tfield?: { label?: unknown; required?: boolean; admin?: { description?: unknown; width?: string } }\n\tlabel?: unknown\n\treadOnly?: boolean\n\t/** Endpoint subpath supplying preset address options (e.g. `'departments'`); omit for none. */\n\tendpoint?: string\n\t/** Allow free-typed emails (default true). */\n\tallowCustom?: boolean\n\t/** Offer the form's own fields as recipient tokens (default true). */\n\tfieldTokens?: boolean\n\t/** Field types eligible as tokens (default `['email']`). */\n\ttokenFieldTypes?: string[]\n\tdescriptionKey?: TranslationKey\n}\n\ntype FetchState =\n\t| { status: 'error' | 'idle' | 'loading' }\n\t| { status: 'loaded'; options: EndpointOption[] }\n\nconst tokenOptionsFromData = (\n\tdata: Record<string, unknown>,\n\ttypes?: string[]\n): ReactSelectOption[] => {\n\tconst rows = Array.isArray(data.fields) ? (data.fields as unknown[]) : []\n\tconst labels = new Map<string, string>()\n\tfor (const row of rows) {\n\t\tif (!row || typeof row !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tconst { name, label } = row as FieldRow\n\t\tif (typeof name === 'string' && typeof label === 'string' && label.length > 0) {\n\t\t\tlabels.set(name.trim(), label)\n\t\t}\n\t}\n\tconst names =\n\t\ttypes && types.length > 0 ? fieldNamesOfType(data.fields, types) : fieldNames(data.fields)\n\treturn names.map((name) => ({ label: labels.get(name) ?? name, value: `{{${name}}}` }))\n}\n\n/**\n * A creatable, multi-value recipient field: pick a department (preset options from `endpoint`), pick a\n * form-field token (`{{field}}`, offered when `fieldTokens`), or type any email and press Enter\n * (`allowCustom`). Renders as native Payload badges (drag-reorderable), stores a `string[]`, honors\n * `admin.width` and the standard field props. Unknown or legacy stored values stay selectable.\n */\nexport const RecipientsSelect = (props: RecipientsSelectProps) => {\n\tconst allowCustom = props.allowCustom !== false\n\tconst fieldTokens = props.fieldTokens !== false\n\tconst {\n\t\tcustomComponents: { Description, Error: ErrorComponent, Label } = {},\n\t\tdisabled,\n\t\tpath,\n\t\tsetValue,\n\t\tshowError,\n\t\tvalue,\n\t} = useField<string[] | string>({ path: props.path })\n\tconst { t } = useTranslation()\n\tconst label = toStaticLabel(props.field?.label ?? props.label)\n\tconst description = props.descriptionKey\n\t\t? t(props.descriptionKey)\n\t\t: toStaticLabel(props.field?.admin?.description)\n\tconst { id, collectionSlug } = useDocumentInfo()\n\tconst { config } = useConfig()\n\tconst apiRoute = config.routes.api\n\tconst readOnly = props.readOnly === true || disabled === true\n\tconst [state, setState] = useState<FetchState>({ status: 'idle' })\n\n\tuseEffect(() => {\n\t\tif (!props.endpoint || id == null || !collectionSlug) {\n\t\t\treturn\n\t\t}\n\t\tconst controller = new AbortController()\n\t\tconst url = buildEndpointOptionsUrl({ apiRoute, collectionSlug, id, endpoint: props.endpoint })\n\t\tsetState({ status: 'loading' })\n\t\tfetch(url, {\n\t\t\tcredentials: 'include',\n\t\t\theaders: { Accept: 'application/json' },\n\t\t\tsignal: controller.signal,\n\t\t})\n\t\t\t.then(async (response) => {\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Options request failed with ${response.status}`)\n\t\t\t\t}\n\t\t\t\tsetState({ status: 'loaded', options: parseEndpointOptions(await response.json()) })\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\tif (!controller.signal.aborted) {\n\t\t\t\t\tsetState({ status: 'error' })\n\t\t\t\t}\n\t\t\t})\n\t\treturn () => controller.abort()\n\t}, [apiRoute, collectionSlug, id, props.endpoint])\n\n\tconst presetOptions: ReactSelectOption[] = state.status === 'loaded' ? state.options : []\n\n\tconst tokenJson = useFormFields(([fields]) =>\n\t\tfieldTokens\n\t\t\t? JSON.stringify(\n\t\t\t\t\ttokenOptionsFromData(reduceFieldsToValues(fields, true), props.tokenFieldTypes)\n\t\t\t\t)\n\t\t\t: '[]'\n\t)\n\tconst tokenOptions = useMemo(() => JSON.parse(tokenJson) as ReactSelectOption[], [tokenJson])\n\n\tconst stored: string[] = Array.isArray(value)\n\t\t? value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n\t\t: typeof value === 'string' && value.length > 0\n\t\t\t? [value]\n\t\t\t: []\n\n\tconst byValue = useMemo(() => {\n\t\tconst map = new Map<string, ReactSelectOption>()\n\t\tfor (const option of [...presetOptions, ...tokenOptions]) {\n\t\t\tmap.set(option.value as string, option)\n\t\t}\n\t\treturn map\n\t}, [presetOptions, tokenOptions])\n\n\tconst selected = stored.map((entry) => byValue.get(entry) ?? { label: entry, value: entry })\n\n\tconst groupedOptions = useMemo(() => {\n\t\tconst groups: { label: string; options: ReactSelectOption[] }[] = []\n\t\tif (presetOptions.length > 0) {\n\t\t\tgroups.push({ label: t(keys.recipientsGroupDepartments), options: presetOptions })\n\t\t}\n\t\tif (tokenOptions.length > 0) {\n\t\t\tgroups.push({ label: t(keys.recipientsGroupFields), options: tokenOptions })\n\t\t}\n\t\treturn groups\n\t}, [presetOptions, tokenOptions, t])\n\n\tconst handleChange = (selection: ReactSelectOption | ReactSelectOption[] | null) => {\n\t\tif (readOnly) {\n\t\t\treturn\n\t\t}\n\t\tconst chosen = Array.isArray(selection) ? selection : selection ? [selection] : []\n\t\tconst entries = chosen\n\t\t\t.map((option) => String(option.value).trim())\n\t\t\t.filter((entry) => entry.length > 0 && (isPlausibleEmail(entry) || byValue.has(entry)))\n\t\tconst deduped = new Map<string, string>()\n\t\tfor (const entry of entries) {\n\t\t\tconst key = entry.toLowerCase()\n\t\t\tif (!deduped.has(key)) {\n\t\t\t\tdeduped.set(key, entry)\n\t\t\t}\n\t\t}\n\t\tsetValue(Array.from(deduped.values()))\n\t}\n\n\tconst fieldStyle = props.field?.admin?.width\n\t\t? ({ '--field-width': props.field.admin.width } as CSSProperties)\n\t\t: undefined\n\n\treturn (\n\t\t<div\n\t\t\tclassName={['field-type', showError && 'error', readOnly && 'read-only']\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(' ')}\n\t\t\tid={path ? `field-${path.replace(/\\./g, '__')}` : undefined}\n\t\t\tstyle={{ marginBlockEnd: '1rem', ...fieldStyle }}\n\t\t>\n\t\t\t<RenderCustomComponent\n\t\t\t\tCustomComponent={Label}\n\t\t\t\tFallback={<FieldLabel label={label} path={path} required={props.field?.required} />}\n\t\t\t/>\n\t\t\t<div className=\"field-type__wrap\">\n\t\t\t\t<RenderCustomComponent\n\t\t\t\t\tCustomComponent={ErrorComponent}\n\t\t\t\t\tFallback={<FieldError path={path} showError={showError} />}\n\t\t\t\t/>\n\t\t\t\t<ReactSelect\n\t\t\t\t\toptions={groupedOptions}\n\t\t\t\t\tvalue={selected}\n\t\t\t\t\tisMulti\n\t\t\t\t\tisSortable\n\t\t\t\t\tisCreatable={allowCustom}\n\t\t\t\t\tisClearable={!readOnly}\n\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\tisLoading={state.status === 'loading'}\n\t\t\t\t\tshowError={showError}\n\t\t\t\t\tplaceholder={state.status === 'loading' ? t(keys.endpointOptionsLoading) : undefined}\n\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\tfilterOption={(option, rawInput) => {\n\t\t\t\t\t\tif (!option) {\n\t\t\t\t\t\t\treturn allowCustom && isPlausibleEmail(rawInput)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst query = rawInput.toLowerCase()\n\t\t\t\t\t\treturn `${option.label} ${option.value}`.toLowerCase().includes(query)\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<RenderCustomComponent\n\t\t\t\tCustomComponent={Description}\n\t\t\t\tFallback={\n\t\t\t\t\t<FieldDescription\n\t\t\t\t\t\tdescription={state.status === 'error' ? t(keys.endpointOptionsError) : description}\n\t\t\t\t\t\tpath={path}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t/>\n\t\t</div>\n\t)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"RecipientsSelect.js","names":["useTranslation"],"sources":["../../src/client/RecipientsSelect.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tFieldDescription,\n\tFieldError,\n\tFieldLabel,\n\tReactSelect,\n\ttype ReactSelectOption,\n\tRenderCustomComponent,\n\tuseConfig,\n\tuseDocumentInfo,\n\tuseField,\n\tuseFormFields,\n} from '@payloadcms/ui'\nimport { reduceFieldsToValues } from 'payload/shared'\nimport { type CSSProperties, useEffect, useMemo, useState } from 'react'\nimport { isPlausibleEmail } from '../actions/emailRecipients'\nimport { fieldNames, fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport { keys, type TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport {\n\tbuildEndpointOptionsUrl,\n\ttype EndpointOption,\n\tparseEndpointOptions,\n} from './endpointOptions'\nimport type { FieldRow } from './synthesizeClientField'\nimport { toStaticLabel } from './toStaticLabel'\n\nexport type RecipientsSelectProps = {\n\tpath?: string\n\tfield?: { label?: unknown; required?: boolean; admin?: { description?: unknown; width?: string } }\n\tlabel?: unknown\n\treadOnly?: boolean\n\t/** Endpoint subpath supplying preset address options (e.g. `'departments'`); omit for none. */\n\tendpoint?: string\n\t/** Allow free-typed emails (default true). */\n\tallowCustom?: boolean\n\t/** Offer the form's own fields as recipient tokens (default true). */\n\tfieldTokens?: boolean\n\t/** Field types eligible as tokens (default `['email']`). */\n\ttokenFieldTypes?: string[]\n\t/** Registered recipient sources, offered as their own option group (labels resolved for the admin locale). */\n\tsources?: { value: string; label: string | Record<string, string> }[]\n\tdescriptionKey?: TranslationKey\n}\n\n/** A source label is a plain string or a per-locale record; resolve it for the admin UI language. */\nconst resolveSourceLabel = (label: string | Record<string, string>, language: string): string =>\n\ttypeof label === 'string' ? label : (label[language] ?? label.en ?? Object.values(label)[0] ?? '')\n\ntype FetchState =\n\t| { status: 'error' | 'idle' | 'loading' }\n\t| { status: 'loaded'; options: EndpointOption[] }\n\nconst tokenOptionsFromData = (\n\tdata: Record<string, unknown>,\n\ttypes?: string[]\n): ReactSelectOption[] => {\n\tconst rows = Array.isArray(data.fields) ? (data.fields as unknown[]) : []\n\tconst labels = new Map<string, string>()\n\tfor (const row of rows) {\n\t\tif (!row || typeof row !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tconst { name, label } = row as FieldRow\n\t\tif (typeof name === 'string' && typeof label === 'string' && label.length > 0) {\n\t\t\tlabels.set(name.trim(), label)\n\t\t}\n\t}\n\tconst names =\n\t\ttypes && types.length > 0 ? fieldNamesOfType(data.fields, types) : fieldNames(data.fields)\n\treturn names.map((name) => ({ label: labels.get(name) ?? name, value: `{{${name}}}` }))\n}\n\n/**\n * A creatable, multi-value recipient field: pick a department (preset options from `endpoint`), pick a\n * form-field token (`{{field}}`, offered when `fieldTokens`), or type any email and press Enter\n * (`allowCustom`). Renders as native Payload badges (drag-reorderable), stores a `string[]`, honors\n * `admin.width` and the standard field props. Unknown or legacy stored values stay selectable.\n */\nexport const RecipientsSelect = (props: RecipientsSelectProps) => {\n\tconst allowCustom = props.allowCustom !== false\n\tconst fieldTokens = props.fieldTokens !== false\n\tconst {\n\t\tcustomComponents: { Description, Error: ErrorComponent, Label } = {},\n\t\tdisabled,\n\t\tpath,\n\t\tsetValue,\n\t\tshowError,\n\t\tvalue,\n\t} = useField<string[] | string>({ path: props.path })\n\tconst { t, i18n } = useTranslation()\n\tconst sourceOptions: ReactSelectOption[] = useMemo(\n\t\t() =>\n\t\t\t(props.sources ?? []).map((source) => ({\n\t\t\t\tvalue: source.value,\n\t\t\t\tlabel: resolveSourceLabel(source.label, i18n.language),\n\t\t\t})),\n\t\t[props.sources, i18n.language]\n\t)\n\tconst label = toStaticLabel(props.field?.label ?? props.label)\n\tconst description = props.descriptionKey\n\t\t? t(props.descriptionKey)\n\t\t: toStaticLabel(props.field?.admin?.description)\n\tconst { id, collectionSlug } = useDocumentInfo()\n\tconst { config } = useConfig()\n\tconst apiRoute = config.routes.api\n\tconst readOnly = props.readOnly === true || disabled === true\n\tconst [state, setState] = useState<FetchState>({ status: 'idle' })\n\n\tuseEffect(() => {\n\t\tif (!props.endpoint || id == null || !collectionSlug) {\n\t\t\treturn\n\t\t}\n\t\tconst controller = new AbortController()\n\t\tconst url = buildEndpointOptionsUrl({ apiRoute, collectionSlug, id, endpoint: props.endpoint })\n\t\tsetState({ status: 'loading' })\n\t\tfetch(url, {\n\t\t\tcredentials: 'include',\n\t\t\theaders: { Accept: 'application/json' },\n\t\t\tsignal: controller.signal,\n\t\t})\n\t\t\t.then(async (response) => {\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Options request failed with ${response.status}`)\n\t\t\t\t}\n\t\t\t\tsetState({ status: 'loaded', options: parseEndpointOptions(await response.json()) })\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\tif (!controller.signal.aborted) {\n\t\t\t\t\tsetState({ status: 'error' })\n\t\t\t\t}\n\t\t\t})\n\t\treturn () => controller.abort()\n\t}, [apiRoute, collectionSlug, id, props.endpoint])\n\n\tconst presetOptions: ReactSelectOption[] = state.status === 'loaded' ? state.options : []\n\n\tconst tokenJson = useFormFields(([fields]) =>\n\t\tfieldTokens\n\t\t\t? JSON.stringify(\n\t\t\t\t\ttokenOptionsFromData(reduceFieldsToValues(fields, true), props.tokenFieldTypes)\n\t\t\t\t)\n\t\t\t: '[]'\n\t)\n\tconst tokenOptions = useMemo(() => JSON.parse(tokenJson) as ReactSelectOption[], [tokenJson])\n\n\tconst stored: string[] = Array.isArray(value)\n\t\t? value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n\t\t: typeof value === 'string' && value.length > 0\n\t\t\t? [value]\n\t\t\t: []\n\n\tconst byValue = useMemo(() => {\n\t\tconst map = new Map<string, ReactSelectOption>()\n\t\tfor (const option of [...presetOptions, ...tokenOptions, ...sourceOptions]) {\n\t\t\tmap.set(option.value as string, option)\n\t\t}\n\t\treturn map\n\t}, [presetOptions, tokenOptions, sourceOptions])\n\n\tconst selected = stored.map((entry) => byValue.get(entry) ?? { label: entry, value: entry })\n\n\tconst groupedOptions = useMemo(() => {\n\t\tconst groups: { label: string; options: ReactSelectOption[] }[] = []\n\t\tif (presetOptions.length > 0) {\n\t\t\tgroups.push({ label: t(keys.recipientsGroupDepartments), options: presetOptions })\n\t\t}\n\t\tif (tokenOptions.length > 0) {\n\t\t\tgroups.push({ label: t(keys.recipientsGroupFields), options: tokenOptions })\n\t\t}\n\t\tif (sourceOptions.length > 0) {\n\t\t\tgroups.push({ label: t(keys.recipientsGroupSources), options: sourceOptions })\n\t\t}\n\t\treturn groups\n\t}, [presetOptions, tokenOptions, sourceOptions, t])\n\n\tconst handleChange = (selection: ReactSelectOption | ReactSelectOption[] | null) => {\n\t\tif (readOnly) {\n\t\t\treturn\n\t\t}\n\t\tconst chosen = Array.isArray(selection) ? selection : selection ? [selection] : []\n\t\tconst entries = chosen\n\t\t\t.map((option) => String(option.value).trim())\n\t\t\t.filter((entry) => entry.length > 0 && (isPlausibleEmail(entry) || byValue.has(entry)))\n\t\tconst deduped = new Map<string, string>()\n\t\tfor (const entry of entries) {\n\t\t\tconst key = entry.toLowerCase()\n\t\t\tif (!deduped.has(key)) {\n\t\t\t\tdeduped.set(key, entry)\n\t\t\t}\n\t\t}\n\t\tsetValue(Array.from(deduped.values()))\n\t}\n\n\tconst fieldStyle = props.field?.admin?.width\n\t\t? ({ '--field-width': props.field.admin.width } as CSSProperties)\n\t\t: undefined\n\n\treturn (\n\t\t<div\n\t\t\tclassName={['field-type', showError && 'error', readOnly && 'read-only']\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(' ')}\n\t\t\tid={path ? `field-${path.replace(/\\./g, '__')}` : undefined}\n\t\t\tstyle={{ marginBlockEnd: '1rem', ...fieldStyle }}\n\t\t>\n\t\t\t<RenderCustomComponent\n\t\t\t\tCustomComponent={Label}\n\t\t\t\tFallback={<FieldLabel label={label} path={path} required={props.field?.required} />}\n\t\t\t/>\n\t\t\t<div className=\"field-type__wrap\">\n\t\t\t\t<RenderCustomComponent\n\t\t\t\t\tCustomComponent={ErrorComponent}\n\t\t\t\t\tFallback={<FieldError path={path} showError={showError} />}\n\t\t\t\t/>\n\t\t\t\t<ReactSelect\n\t\t\t\t\toptions={groupedOptions}\n\t\t\t\t\tvalue={selected}\n\t\t\t\t\tisMulti\n\t\t\t\t\tisSortable\n\t\t\t\t\tisCreatable={allowCustom}\n\t\t\t\t\tisClearable={!readOnly}\n\t\t\t\t\tdisabled={readOnly}\n\t\t\t\t\tisLoading={state.status === 'loading'}\n\t\t\t\t\tshowError={showError}\n\t\t\t\t\tplaceholder={state.status === 'loading' ? t(keys.endpointOptionsLoading) : undefined}\n\t\t\t\t\tonChange={handleChange}\n\t\t\t\t\tfilterOption={(option, rawInput) => {\n\t\t\t\t\t\tif (!option) {\n\t\t\t\t\t\t\treturn allowCustom && isPlausibleEmail(rawInput)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst query = rawInput.toLowerCase()\n\t\t\t\t\t\treturn `${option.label} ${option.value}`.toLowerCase().includes(query)\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<RenderCustomComponent\n\t\t\t\tCustomComponent={Description}\n\t\t\t\tFallback={\n\t\t\t\t\t<FieldDescription\n\t\t\t\t\t\tdescription={state.status === 'error' ? t(keys.endpointOptionsError) : description}\n\t\t\t\t\t\tpath={path}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t/>\n\t\t</div>\n\t)\n}\n"],"mappings":"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"}
@@ -273,3 +273,285 @@
273
273
  .fb-condensed-array__actions {
274
274
  margin-block-start: 0.25rem;
275
275
  }
276
+
277
+ /* Calculation expression builder */
278
+ .fb-calc-builder {
279
+ margin-block-end: 1rem;
280
+ }
281
+ .fb-calc-builder > .field-type__wrap {
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 0.5rem;
285
+ }
286
+ .fb-visually-hidden {
287
+ position: absolute;
288
+ width: 1px;
289
+ height: 1px;
290
+ padding: 0;
291
+ margin: -1px;
292
+ overflow: hidden;
293
+ clip: rect(0, 0, 0, 0);
294
+ white-space: nowrap;
295
+ border: 0;
296
+ }
297
+ .fb-calc-builder__hint {
298
+ color: var(--theme-elevation-400);
299
+ font-size: 0.875rem;
300
+ margin: 0;
301
+ }
302
+ .fb-calc-builder__empty {
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: 0.375rem;
306
+ }
307
+ .fb-calc-builder__seed {
308
+ max-width: 16rem;
309
+ }
310
+ .fb-calc-builder__preview {
311
+ color: var(--theme-elevation-400);
312
+ font-size: 0.875rem;
313
+ margin: 0;
314
+ }
315
+ .fb-calc-builder__actions {
316
+ align-self: flex-start;
317
+ }
318
+ .fb-calc-builder__warning {
319
+ color: var(--theme-elevation-400);
320
+ font-size: 0.8125rem;
321
+ margin: 0;
322
+ }
323
+ .fb-calc-builder__kinds {
324
+ margin: 0;
325
+ margin-inline-start: 0.75rem;
326
+ padding-inline-start: 1rem;
327
+ color: var(--theme-elevation-400);
328
+ font-size: 0.8125rem;
329
+ display: flex;
330
+ flex-direction: column;
331
+ gap: 0.125rem;
332
+ }
333
+ .fb-calc-builder__kind-label {
334
+ color: var(--theme-elevation-600);
335
+ font-weight: 600;
336
+ }
337
+ .fb-calc-chain {
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: 0.5rem;
341
+ min-width: 0;
342
+ }
343
+ /* Flat three-column grid, identical at every depth: [op select | spacer, fixed 90px]
344
+ * [operand controls, flexing] [remove, fixed]. The first row reserves the op column with a
345
+ * spacer so every kind select starts at the same left margin; nothing wraps, flexing
346
+ * controls shrink and ellipsize instead. */
347
+ .fb-calc-row {
348
+ display: flex;
349
+ align-items: flex-start;
350
+ gap: 0.5rem;
351
+ }
352
+ .fb-calc-row--divided {
353
+ border-block-start: 1px solid var(--theme-elevation-100);
354
+ padding-block-start: 0.5rem;
355
+ }
356
+ .fb-calc-row__op,
357
+ .fb-calc-row__op-spacer {
358
+ flex: 0 0 90px;
359
+ min-width: 0;
360
+ }
361
+ .fb-calc-row__operand {
362
+ flex: 1;
363
+ min-width: 0;
364
+ }
365
+ /* A fixed 40px centering box pins the remove level with the first control line even when
366
+ * secondary content (weight rows, nested chains) extends the row downward. */
367
+ .fb-calc-row__remove {
368
+ flex: 0 0 auto;
369
+ align-self: flex-start;
370
+ height: 40px;
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: flex-end;
374
+ }
375
+ .fb-calc-chain__add {
376
+ align-self: flex-start;
377
+ /* Column 1 width + row gap: aligns the button with the operand column. */
378
+ margin-inline-start: calc(90px + 0.5rem);
379
+ }
380
+ .fb-calc-operand {
381
+ display: flex;
382
+ flex-direction: column;
383
+ gap: 0.5rem;
384
+ min-width: 0;
385
+ }
386
+ .fb-calc-operand--group > .fb-calc-chain {
387
+ padding-inline-start: 0.75rem;
388
+ border-inline-start: 2px solid var(--theme-elevation-100);
389
+ }
390
+ .fb-calc-operand__title {
391
+ font-size: 0.6875rem;
392
+ font-weight: 600;
393
+ text-transform: uppercase;
394
+ letter-spacing: 0.04em;
395
+ color: var(--theme-elevation-400);
396
+ }
397
+ .fb-calc-operand__header {
398
+ display: flex;
399
+ align-items: center;
400
+ gap: 0.5rem;
401
+ }
402
+ .fb-calc-hint {
403
+ color: var(--theme-elevation-400);
404
+ font-size: 0.875rem;
405
+ margin: 0;
406
+ flex: 1 1 auto;
407
+ min-width: 0;
408
+ overflow: hidden;
409
+ text-overflow: ellipsis;
410
+ white-space: nowrap;
411
+ }
412
+ /* 200px fits the longest kind label in both locales ("Gewichtetes Feld" / "Weighted field":
413
+ * ~110px at the 13px admin body size) plus the react-select control chrome (2 x base(0.6)
414
+ * padding + indicator), with room for zoomed fonts. */
415
+ .fb-calc-operand__header > .fb-calc-operand__kind {
416
+ flex: 0 0 200px;
417
+ min-width: 0;
418
+ }
419
+ .fb-calc-operand__header > .fb-calc-operand__fn {
420
+ flex: 0 0 110px;
421
+ min-width: 0;
422
+ }
423
+ .fb-calc-operand__header > .fb-calc-operand__ref,
424
+ .fb-calc-operand__header > .fb-calc-operand__weight-field {
425
+ flex: 1 1 auto;
426
+ min-width: 0;
427
+ }
428
+ .fb-calc-builder .rs__single-value {
429
+ white-space: nowrap;
430
+ overflow: hidden;
431
+ text-overflow: ellipsis;
432
+ }
433
+ .fb-calc-operand__number,
434
+ .fb-calc-operand__weight {
435
+ height: 40px;
436
+ padding: 0 15px;
437
+ font-size: 1rem;
438
+ font-family: inherit;
439
+ color: var(--theme-elevation-800);
440
+ background: var(--theme-input-bg);
441
+ border: 1px solid var(--theme-elevation-150);
442
+ border-radius: var(--style-radius-s);
443
+ transition:
444
+ border-color 100ms ease,
445
+ background-color 100ms ease,
446
+ box-shadow 100ms ease;
447
+ }
448
+ .fb-calc-operand__number:hover,
449
+ .fb-calc-operand__weight:hover {
450
+ border-color: var(--theme-elevation-250);
451
+ }
452
+ .fb-calc-operand__number:focus,
453
+ .fb-calc-operand__weight:focus {
454
+ border-color: var(--theme-elevation-400);
455
+ outline: none;
456
+ }
457
+ .fb-calc-operand__header > .fb-calc-operand__number {
458
+ flex: 1 1 auto;
459
+ min-width: 60px;
460
+ width: 0;
461
+ }
462
+ .fb-calc-fn__args {
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 0.5rem;
466
+ padding-inline-start: 0.75rem;
467
+ border-inline-start: 2px solid var(--theme-elevation-100);
468
+ }
469
+ .fb-calc-fn__arg {
470
+ display: flex;
471
+ align-items: flex-start;
472
+ gap: 0.5rem;
473
+ }
474
+ .fb-calc-fn__arg-chain {
475
+ flex: 1;
476
+ min-width: 0;
477
+ }
478
+ .fb-calc-fn__arg-remove {
479
+ flex: 0 0 auto;
480
+ align-self: flex-start;
481
+ height: 40px;
482
+ display: flex;
483
+ align-items: center;
484
+ }
485
+ .fb-calc-fn__add {
486
+ align-self: flex-start;
487
+ }
488
+ .fb-calc-operand__source {
489
+ flex: 1 1 auto;
490
+ min-width: 0;
491
+ line-height: 40px;
492
+ font-size: 0.875rem;
493
+ overflow: hidden;
494
+ text-overflow: ellipsis;
495
+ white-space: nowrap;
496
+ }
497
+ .fb-calc-operand__weight-values-row {
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 0.5rem;
501
+ }
502
+ .fb-calc-operand__weight-values-label {
503
+ flex: 0 0 90px;
504
+ min-width: 0;
505
+ line-height: 40px;
506
+ font-size: 0.875rem;
507
+ color: var(--theme-elevation-600);
508
+ white-space: nowrap;
509
+ overflow: hidden;
510
+ text-overflow: ellipsis;
511
+ }
512
+ .fb-calc-operand__weight-values-row > .fb-calc-operand__weight-values {
513
+ flex: 0 0 200px;
514
+ min-width: 0;
515
+ }
516
+ .fb-calc-operand__weights {
517
+ display: flex;
518
+ flex-direction: column;
519
+ gap: 0.375rem;
520
+ }
521
+ .fb-calc-operand__weight-row {
522
+ display: flex;
523
+ align-items: center;
524
+ gap: 0.5rem;
525
+ }
526
+ .fb-calc-operand__weight-label {
527
+ flex: 1;
528
+ min-width: 0;
529
+ font-size: 0.875rem;
530
+ overflow: hidden;
531
+ text-overflow: ellipsis;
532
+ white-space: nowrap;
533
+ }
534
+ .fb-calc-operand__weight-row > .fb-calc-operand__weight {
535
+ flex: 0 0 8rem;
536
+ }
537
+ .fb-calc-finish {
538
+ display: flex;
539
+ align-items: flex-start;
540
+ gap: 0.5rem;
541
+ }
542
+ /* The wording occupies column 1 (the 90px op column), centered on the 40px control line, so the
543
+ * select below aligns exactly with the operand column's kind selects. */
544
+ .fb-calc-finish__label {
545
+ flex: 0 0 90px;
546
+ min-width: 0;
547
+ line-height: 40px;
548
+ font-size: 0.875rem;
549
+ color: var(--theme-elevation-600);
550
+ white-space: nowrap;
551
+ overflow: hidden;
552
+ text-overflow: ellipsis;
553
+ }
554
+ .fb-calc-finish__select {
555
+ flex: 0 0 200px;
556
+ min-width: 0;
557
+ }
@@ -3,19 +3,15 @@ import { labelForKey } from "../translations/server.js";
3
3
  import { dispatchActions } from "../actions/dispatch.js";
4
4
  import { resolveEventSink } from "../events/resolveEventSink.js";
5
5
  import { isLoggedIn } from "../plugin/access.js";
6
- import { buildSpamGuard } from "../spam/spamGuard.js";
7
6
  import { FORMS_SLUG } from "./forms.js";
7
+ import { formIdOf } from "../submissions/formIdOf.js";
8
+ import { makeVoteTallyHook } from "../poll/votes/voteTallyHook.js";
9
+ import { buildSpamGuard } from "../spam/spamGuard.js";
8
10
  import { POLL_CONTEXT_KEY, votedCookieName } from "../submissions/votedCookie.js";
9
11
  import { validateSubmission } from "../submissions/validateSubmission.js";
12
+ import { verifyContext } from "../submissions/verifyContext.js";
10
13
  //#region src/collections/formSubmissions.ts
11
14
  const FORM_SUBMISSIONS_SLUG = "form-submissions";
12
- const formIdOf = (form) => {
13
- if (typeof form === "number" || typeof form === "string") return form;
14
- if (form && typeof form === "object" && "id" in form) {
15
- const id = form.id;
16
- if (typeof id === "number" || typeof id === "string") return id;
17
- }
18
- };
19
15
  /**
20
16
  * On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.
21
17
  * Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw
@@ -30,20 +26,22 @@ const makeAfterChange = (args) => async ({ doc, operation, req }) => {
30
26
  try {
31
27
  const formId = formIdOf(doc.form);
32
28
  if (formId == null) return doc;
29
+ const form = await payload.findByID({
30
+ collection: FORMS_SLUG,
31
+ id: formId,
32
+ depth: 0,
33
+ overrideAccess: true,
34
+ req
35
+ }).catch(() => null);
33
36
  await dispatchActions({
34
- actions: (await payload.findByID({
35
- collection: "forms",
36
- id: formId,
37
- depth: 0,
38
- overrideAccess: true,
39
- req
40
- }).catch(() => null))?.actions ?? null,
37
+ actions: form?.actions ?? null,
41
38
  formId,
42
39
  submissionId: doc.id,
43
40
  registry: args.actionRegistry,
44
41
  payload,
45
42
  req,
46
43
  hasRunner: args.hasRunner,
44
+ persistSubmissions: form?.persistSubmissions,
47
45
  richText: args.richText
48
46
  });
49
47
  try {
@@ -89,7 +87,7 @@ const makeVotedCookieHook = () => {
89
87
  return doc;
90
88
  };
91
89
  };
92
- const buildSubmissionsCollection = ({ registry, ruleRegistry, consentSources, consentSnapshot, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, richText, uploadSlug, spam, votedCookie = false, pollSourceRegistry, showRawFields = false, overrides }) => {
90
+ const buildSubmissionsCollection = ({ registry, ruleRegistry, calcSources, calcFunctions, consentSources, consentSnapshot, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, richText, uploadSlug, spam, votedCookie = false, pollSourceRegistry, pollVotes = false, showRawFields = false, overrides }) => {
93
91
  const defaultFields = [
94
92
  {
95
93
  name: "form",
@@ -143,6 +141,22 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, consentSources, co
143
141
  name: "meta",
144
142
  type: "json",
145
143
  admin: { hidden: !showRawFields }
144
+ },
145
+ {
146
+ name: "context",
147
+ type: "group",
148
+ label: labelForKey(keys.submissionContext),
149
+ admin: {
150
+ readOnly: true,
151
+ condition: (data) => Boolean(data?.context?.relationTo)
152
+ },
153
+ fields: [{
154
+ name: "relationTo",
155
+ type: "text"
156
+ }, {
157
+ name: "value",
158
+ type: "text"
159
+ }]
146
160
  }
147
161
  ];
148
162
  return {
@@ -173,9 +187,12 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, consentSources, co
173
187
  ...overrides?.hooks ?? {},
174
188
  beforeValidate: [
175
189
  ...spam ? [buildSpamGuard(spam)] : [],
190
+ verifyContext(),
176
191
  validateSubmission({
177
192
  registry,
178
193
  ruleRegistry,
194
+ calcSources,
195
+ calcFunctions,
179
196
  consentSources,
180
197
  consentSnapshot,
181
198
  uploadSlug,
@@ -185,6 +202,7 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, consentSources, co
185
202
  ...overrides?.hooks?.beforeValidate ?? []
186
203
  ],
187
204
  afterChange: [
205
+ ...pollVotes !== false ? [makeVoteTallyHook()] : [],
188
206
  makeAfterChange({
189
207
  actionRegistry,
190
208
  events,