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

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 (87) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/actions/builtin/confirmation.js +75 -72
  3. package/dist/actions/builtin/confirmation.js.map +1 -1
  4. package/dist/actions/builtin/emailTeam.js +31 -41
  5. package/dist/actions/builtin/emailTeam.js.map +1 -1
  6. package/dist/actions/builtin/index.d.ts +2 -1
  7. package/dist/actions/builtin/index.js +3 -3
  8. package/dist/actions/builtin/index.js.map +1 -1
  9. package/dist/actions/emailRecipients.d.ts +12 -0
  10. package/dist/actions/emailRecipients.js +72 -0
  11. package/dist/actions/emailRecipients.js.map +1 -0
  12. package/dist/client/ConsentBlockLabel.d.ts +15 -0
  13. package/dist/client/ConsentBlockLabel.js +71 -0
  14. package/dist/client/ConsentBlockLabel.js.map +1 -0
  15. package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
  16. package/dist/client/ConsentSourceRowLabel.js +21 -0
  17. package/dist/client/ConsentSourceRowLabel.js.map +1 -0
  18. package/dist/client/EndpointOptionsSelect.d.ts +1 -0
  19. package/dist/client/EndpointOptionsSelect.js +4 -1
  20. package/dist/client/EndpointOptionsSelect.js.map +1 -1
  21. package/dist/client/FieldNameSelect.d.ts +1 -0
  22. package/dist/client/FieldNameSelect.js +4 -1
  23. package/dist/client/FieldNameSelect.js.map +1 -1
  24. package/dist/client/RecipientsSelect.d.ts +31 -0
  25. package/dist/client/RecipientsSelect.js +174 -0
  26. package/dist/client/RecipientsSelect.js.map +1 -0
  27. package/dist/client/admin.css +5 -0
  28. package/dist/collections/formSubmissions.js +2 -1
  29. package/dist/collections/formSubmissions.js.map +1 -1
  30. package/dist/collections/forms.js +18 -2
  31. package/dist/collections/forms.js.map +1 -1
  32. package/dist/consent/captureConsent.d.ts +14 -5
  33. package/dist/consent/captureConsent.js +24 -7
  34. package/dist/consent/captureConsent.js.map +1 -1
  35. package/dist/consent/consentSourcesField.d.ts +4 -0
  36. package/dist/consent/consentSourcesField.js +8 -1
  37. package/dist/consent/consentSourcesField.js.map +1 -1
  38. package/dist/consent/types.d.ts +4 -0
  39. package/dist/email/departments.d.ts +13 -9
  40. package/dist/email/departments.js +7 -50
  41. package/dist/email/departments.js.map +1 -1
  42. package/dist/email/departmentsField.d.ts +4 -0
  43. package/dist/email/departmentsField.js +4 -0
  44. package/dist/email/departmentsField.js.map +1 -1
  45. package/dist/exports/client.d.ts +4 -1
  46. package/dist/exports/client.js +4 -1
  47. package/dist/exports/react.js +1 -1
  48. package/dist/exports/rsc.js +1 -1
  49. package/dist/fields/buildFieldBlocks.js +4 -4
  50. package/dist/fields/buildFieldBlocks.js.map +1 -1
  51. package/dist/fields/builtin/consent.js +6 -1
  52. package/dist/fields/builtin/consent.js.map +1 -1
  53. package/dist/fields/builtin/file.js +9 -3
  54. package/dist/fields/builtin/file.js.map +1 -1
  55. package/dist/fields/builtin/index.d.ts +1 -1
  56. package/dist/fields/builtin/index.js +3 -3
  57. package/dist/fields/builtin/index.js.map +1 -1
  58. package/dist/fields/types.d.ts +9 -2
  59. package/dist/form/toFormDocument.d.ts +20 -3
  60. package/dist/form/toFormDocument.js +16 -4
  61. package/dist/form/toFormDocument.js.map +1 -1
  62. package/dist/form/types.d.ts +5 -5
  63. package/dist/index.d.ts +14 -5
  64. package/dist/index.js +9 -6
  65. package/dist/index.js.map +1 -1
  66. package/dist/plugin/registerCollections.js +2 -1
  67. package/dist/plugin/registerCollections.js.map +1 -1
  68. package/dist/plugin/uploadsCollection.d.ts +3 -0
  69. package/dist/plugin/uploadsCollection.js +10 -1
  70. package/dist/plugin/uploadsCollection.js.map +1 -1
  71. package/dist/submissions/SubmissionAnswers.js.map +1 -1
  72. package/dist/submissions/SubmissionAnswersClient.js +33 -26
  73. package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
  74. package/dist/submissions/runSubmission.js +3 -2
  75. package/dist/submissions/runSubmission.js.map +1 -1
  76. package/dist/submissions/validateSubmission.js +2 -1
  77. package/dist/submissions/validateSubmission.js.map +1 -1
  78. package/dist/translations/de.js +4 -2
  79. package/dist/translations/de.js.map +1 -1
  80. package/dist/translations/en.js +4 -2
  81. package/dist/translations/en.js.map +1 -1
  82. package/dist/translations/keys.d.ts +4 -2
  83. package/dist/translations/keys.js +4 -2
  84. package/dist/translations/keys.js.map +1 -1
  85. package/dist/uploads/resolveFileRef.js +2 -1
  86. package/dist/uploads/resolveFileRef.js.map +1 -1
  87. package/package.json +4 -4
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
3
+ import { buildEndpointOptionsUrl, parseEndpointOptions } from "./endpointOptions.js";
4
+ import { useConfig, useDocumentInfo, useRowLabel } from "@payloadcms/ui";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { useEffect, useState } from "react";
7
+ //#region src/client/ConsentBlockLabel.tsx
8
+ /** Endpoint url -> `{ sourceId: name }`, shared across every consent block label and re-render. */
9
+ const nameCache = /* @__PURE__ */ new Map();
10
+ /**
11
+ * Block row header for a consent field. The consent field stores only the source id, so this resolves
12
+ * it to the source's name via the same `consent-sources` endpoint the Source select uses, rendering
13
+ * "NN Consent {name}". Falls back to "NN Consent" while loading, when unset, or on an unsaved form
14
+ * (no document id yet, so the endpoint cannot resolve options).
15
+ */
16
+ const ConsentBlockLabel = ({ typeLabelKey }) => {
17
+ const { data, rowNumber } = useRowLabel();
18
+ const { t } = useTranslation$1();
19
+ const { id, collectionSlug } = useDocumentInfo();
20
+ const { config } = useConfig();
21
+ const source = typeof data?.source === "string" ? data.source : "";
22
+ const [names, setNames] = useState({});
23
+ useEffect(() => {
24
+ if (id == null || !collectionSlug) return;
25
+ const url = buildEndpointOptionsUrl({
26
+ apiRoute: config.routes.api,
27
+ collectionSlug,
28
+ id,
29
+ endpoint: "consent-sources"
30
+ });
31
+ const cached = nameCache.get(url);
32
+ if (cached) {
33
+ setNames(cached);
34
+ return;
35
+ }
36
+ const controller = new AbortController();
37
+ fetch(url, {
38
+ credentials: "include",
39
+ headers: { Accept: "application/json" },
40
+ signal: controller.signal
41
+ }).then(async (response) => {
42
+ if (!response.ok) throw new Error(`Options request failed with ${response.status}`);
43
+ const map = Object.fromEntries(parseEndpointOptions(await response.json()).map((o) => [o.value, o.label]));
44
+ nameCache.set(url, map);
45
+ setNames(map);
46
+ }).catch(() => {});
47
+ return () => controller.abort();
48
+ }, [
49
+ config.routes.api,
50
+ collectionSlug,
51
+ id
52
+ ]);
53
+ const typeLabel = typeLabelKey ? t(typeLabelKey) : "";
54
+ const number = String((rowNumber ?? 0) + 1).padStart(2, "0");
55
+ const name = source ? names[source] ?? "" : "";
56
+ return /* @__PURE__ */ jsxs("span", {
57
+ style: {
58
+ alignItems: "center",
59
+ display: "inline-flex",
60
+ gap: "0.5rem"
61
+ },
62
+ children: [/* @__PURE__ */ jsx("span", {
63
+ style: { color: "var(--theme-elevation-500)" },
64
+ children: typeLabel ? `${number} ${typeLabel}` : number
65
+ }), name ? /* @__PURE__ */ jsx("span", { children: name }) : null]
66
+ });
67
+ };
68
+ //#endregion
69
+ export { ConsentBlockLabel };
70
+
71
+ //# sourceMappingURL=ConsentBlockLabel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConsentBlockLabel.js","names":["useTranslation"],"sources":["../../src/client/ConsentBlockLabel.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useDocumentInfo, useRowLabel } from '@payloadcms/ui'\nimport { useEffect, useState } from 'react'\nimport type { TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { buildEndpointOptionsUrl, parseEndpointOptions } from './endpointOptions'\n\ntype RowData = { source?: unknown }\n\n/** Endpoint url -> `{ sourceId: name }`, shared across every consent block label and re-render. */\nconst nameCache = new Map<string, Record<string, string>>()\n\n/**\n * Block row header for a consent field. The consent field stores only the source id, so this resolves\n * it to the source's name via the same `consent-sources` endpoint the Source select uses, rendering\n * \"NN Consent {name}\". Falls back to \"NN Consent\" while loading, when unset, or on an unsaved form\n * (no document id yet, so the endpoint cannot resolve options).\n */\nexport const ConsentBlockLabel = ({ typeLabelKey }: { typeLabelKey?: string }) => {\n\tconst { data, rowNumber } = useRowLabel<RowData>()\n\tconst { t } = useTranslation()\n\tconst { id, collectionSlug } = useDocumentInfo()\n\tconst { config } = useConfig()\n\tconst source = typeof data?.source === 'string' ? data.source : ''\n\tconst [names, setNames] = useState<Record<string, string>>({})\n\n\tuseEffect(() => {\n\t\tif (id == null || !collectionSlug) {\n\t\t\treturn\n\t\t}\n\t\tconst url = buildEndpointOptionsUrl({\n\t\t\tapiRoute: config.routes.api,\n\t\t\tcollectionSlug,\n\t\t\tid,\n\t\t\tendpoint: 'consent-sources',\n\t\t})\n\t\tconst cached = nameCache.get(url)\n\t\tif (cached) {\n\t\t\tsetNames(cached)\n\t\t\treturn\n\t\t}\n\t\tconst controller = new AbortController()\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\tconst map = Object.fromEntries(\n\t\t\t\t\tparseEndpointOptions(await response.json()).map((o) => [o.value, o.label])\n\t\t\t\t)\n\t\t\t\tnameCache.set(url, map)\n\t\t\t\tsetNames(map)\n\t\t\t})\n\t\t\t.catch(() => {})\n\t\treturn () => controller.abort()\n\t}, [config.routes.api, collectionSlug, id])\n\n\tconst typeLabel = typeLabelKey ? t(typeLabelKey as TranslationKey) : ''\n\tconst number = String((rowNumber ?? 0) + 1).padStart(2, '0')\n\tconst name = source ? (names[source] ?? '') : ''\n\treturn (\n\t\t<span style={{ alignItems: 'center', display: 'inline-flex', gap: '0.5rem' }}>\n\t\t\t<span style={{ color: 'var(--theme-elevation-500)' }}>\n\t\t\t\t{typeLabel ? `${number} ${typeLabel}` : number}\n\t\t\t</span>\n\t\t\t{name ? <span>{name}</span> : null}\n\t\t</span>\n\t)\n}\n"],"mappings":";;;;;;;;AAWA,MAAM,4BAAY,IAAI,IAAoC;;;;;;;AAQ1D,MAAa,qBAAqB,EAAE,mBAA8C;CACjF,MAAM,EAAE,MAAM,cAAc,YAAqB;CACjD,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,EAAE,IAAI,mBAAmB,gBAAgB;CAC/C,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,SAAS,OAAO,MAAM,WAAW,WAAW,KAAK,SAAS;CAChE,MAAM,CAAC,OAAO,YAAY,SAAiC,CAAC,CAAC;CAE7D,gBAAgB;EACf,IAAI,MAAM,QAAQ,CAAC,gBAClB;EAED,MAAM,MAAM,wBAAwB;GACnC,UAAU,OAAO,OAAO;GACxB;GACA;GACA,UAAU;EACX,CAAC;EACD,MAAM,SAAS,UAAU,IAAI,GAAG;EAChC,IAAI,QAAQ;GACX,SAAS,MAAM;GACf;EACD;EACA,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,KAAK;GACV,aAAa;GACb,SAAS,EAAE,QAAQ,mBAAmB;GACtC,QAAQ,WAAW;EACpB,CAAC,EACC,KAAK,OAAO,aAAa;GACzB,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,+BAA+B,SAAS,QAAQ;GAEjE,MAAM,MAAM,OAAO,YAClB,qBAAqB,MAAM,SAAS,KAAK,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAC1E;GACA,UAAU,IAAI,KAAK,GAAG;GACtB,SAAS,GAAG;EACb,CAAC,EACA,YAAY,CAAC,CAAC;EAChB,aAAa,WAAW,MAAM;CAC/B,GAAG;EAAC,OAAO,OAAO;EAAK;EAAgB;CAAE,CAAC;CAE1C,MAAM,YAAY,eAAe,EAAE,YAA8B,IAAI;CACrE,MAAM,SAAS,QAAQ,aAAa,KAAK,CAAC,EAAE,SAAS,GAAG,GAAG;CAC3D,MAAM,OAAO,SAAU,MAAM,WAAW,KAAM;CAC9C,OACC,qBAAC,QAAD;EAAM,OAAO;GAAE,YAAY;GAAU,SAAS;GAAe,KAAK;EAAS;YAA3E,CACC,oBAAC,QAAD;GAAM,OAAO,EAAE,OAAO,6BAA6B;aACjD,YAAY,GAAG,OAAO,GAAG,cAAc;EACnC,CAAA,GACL,OAAO,oBAAC,QAAD,EAAA,UAAO,KAAW,CAAA,IAAI,IACzB;;AAER"}
@@ -0,0 +1,9 @@
1
+ //#region src/client/ConsentSourceRowLabel.d.ts
2
+ /**
3
+ * Row header for a consent source array row: shows the source `name` (e.g. "Privacy Policy") instead
4
+ * of Payload's default "Consent source NN". Falls back to the singular label + number while unnamed.
5
+ */
6
+ declare const ConsentSourceRowLabel: () => import("react/jsx-runtime").JSX.Element;
7
+ //#endregion
8
+ export { ConsentSourceRowLabel };
9
+ //# sourceMappingURL=ConsentSourceRowLabel.d.ts.map
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { keys } from "../translations/keys.js";
3
+ import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
4
+ import { useRowLabel } from "@payloadcms/ui";
5
+ import { jsx } from "react/jsx-runtime";
6
+ //#region src/client/ConsentSourceRowLabel.tsx
7
+ /**
8
+ * Row header for a consent source array row: shows the source `name` (e.g. "Privacy Policy") instead
9
+ * of Payload's default "Consent source NN". Falls back to the singular label + number while unnamed.
10
+ */
11
+ const ConsentSourceRowLabel = () => {
12
+ const { data, rowNumber } = useRowLabel();
13
+ const { t } = useTranslation$1();
14
+ const name = typeof data?.name === "string" && data.name.trim().length > 0 ? data.name.trim() : "";
15
+ const number = String((rowNumber ?? 0) + 1).padStart(2, "0");
16
+ return /* @__PURE__ */ jsx("span", { children: name || `${t(keys.consentSourceSingular)} ${number}` });
17
+ };
18
+ //#endregion
19
+ export { ConsentSourceRowLabel };
20
+
21
+ //# sourceMappingURL=ConsentSourceRowLabel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConsentSourceRowLabel.js","names":["useTranslation"],"sources":["../../src/client/ConsentSourceRowLabel.tsx"],"sourcesContent":["'use client'\n\nimport { useRowLabel } from '@payloadcms/ui'\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\n\ntype RowData = { name?: unknown }\n\n/**\n * Row header for a consent source array row: shows the source `name` (e.g. \"Privacy Policy\") instead\n * of Payload's default \"Consent source NN\". Falls back to the singular label + number while unnamed.\n */\nexport const ConsentSourceRowLabel = () => {\n\tconst { data, rowNumber } = useRowLabel<RowData>()\n\tconst { t } = useTranslation()\n\tconst name = typeof data?.name === 'string' && data.name.trim().length > 0 ? data.name.trim() : ''\n\tconst number = String((rowNumber ?? 0) + 1).padStart(2, '0')\n\treturn <span>{name || `${t(keys.consentSourceSingular)} ${number}`}</span>\n}\n"],"mappings":";;;;;;;;;;AAYA,MAAa,8BAA8B;CAC1C,MAAM,EAAE,MAAM,cAAc,YAAqB;CACjD,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,OAAO,OAAO,MAAM,SAAS,YAAY,KAAK,KAAK,KAAK,EAAE,SAAS,IAAI,KAAK,KAAK,KAAK,IAAI;CAChG,MAAM,SAAS,QAAQ,aAAa,KAAK,CAAC,EAAE,SAAS,GAAG,GAAG;CAC3D,OAAO,oBAAC,QAAD,EAAA,UAAO,QAAQ,GAAG,EAAE,KAAK,qBAAqB,EAAE,GAAG,SAAe,CAAA;AAC1E"}
@@ -8,6 +8,7 @@ type EndpointOptionsSelectProps = {
8
8
  label?: unknown;
9
9
  admin?: {
10
10
  description?: unknown;
11
+ width?: string;
11
12
  };
12
13
  };
13
14
  label?: unknown;
@@ -74,7 +74,10 @@ const EndpointOptionsSelect = (props) => {
74
74
  };
75
75
  return /* @__PURE__ */ jsxs("div", {
76
76
  className: "field-type",
77
- style: { marginBlockEnd: "1rem" },
77
+ style: {
78
+ marginBlockEnd: "1rem",
79
+ ...props.field?.admin?.width ? { "--field-width": props.field.admin.width } : void 0
80
+ },
78
81
  children: [
79
82
  /* @__PURE__ */ jsx(FieldLabel, {
80
83
  label,
@@ -1 +1 @@
1
- {"version":3,"file":"EndpointOptionsSelect.js","names":["useTranslation"],"sources":["../../src/client/EndpointOptionsSelect.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tFieldDescription,\n\tFieldLabel,\n\tReactSelect,\n\ttype ReactSelectOption,\n\tuseConfig,\n\tuseDocumentInfo,\n\tuseField,\n} from '@payloadcms/ui'\nimport { useEffect, useState } from 'react'\nimport { keys, type TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport {\n\tbuildEndpointOptionsUrl,\n\ttype EndpointOption,\n\tparseEndpointOptions,\n} from './endpointOptions'\nimport { toStaticLabel } from './toStaticLabel'\n\n/** Standard text-field client props plus this component's `clientProps`. */\nexport type EndpointOptionsSelectProps = {\n\t/** The field path within the document (Payload-injected, not the endpoint path). */\n\tpath?: string\n\tfield?: { label?: unknown; admin?: { description?: unknown } }\n\tlabel?: unknown\n\t/**\n\t * Endpoint subpath under the current document's collection API route; `'poll-options'` fetches\n\t * `GET {routes.api}/{collectionSlug}/{id}/poll-options`. The endpoint must return\n\t * `{ options: { label, value }[] }`.\n\t */\n\tendpoint: string\n\t/**\n\t * Field description as a translation key, resolved client-side. Payload drops `admin.description`\n\t * functions from client fields, so a translated description must travel as a key; a static\n\t * `admin.description` string still renders when this is unset.\n\t */\n\tdescriptionKey?: TranslationKey\n\t/** Mirrors ReactSelect's `isClearable`; defaults to true. */\n\tisClearable?: boolean\n\t/**\n\t * Render a multi-value select bound to a `string[]`, for a `hasMany` field (e.g. a poll outcome\n\t * with tied winners). Default false: a single-value select bound to a `string`.\n\t */\n\tisMulti?: boolean\n}\n\ntype FetchState =\n\t| { status: 'error' | 'idle' | 'loading'; options?: never }\n\t| { status: 'loaded'; options: EndpointOption[] }\n\n/**\n * A select whose options load from a document-scoped plugin endpoint, for stored values whose\n * valid choices only the server knows (e.g. a poll's source-resolved options). The pattern: pass\n * `endpoint` via `clientProps`, the component reads the document id from `useDocumentInfo` and the\n * API route from `useConfig`, fetches once per document with the admin cookie, and renders\n * translated loading/error states. The stored value stays selectable even when it is missing from\n * the fetched options (or the fetch failed), so opening an old document never silently drops data;\n * unsaved documents skip the fetch since the server cannot resolve options for them yet.\n */\nexport const EndpointOptionsSelect = (props: EndpointOptionsSelectProps) => {\n\tconst isMulti = props.isMulti === true\n\tconst { path, setValue, value } = useField<string | string[]>({ path: props.path })\n\tconst label = toStaticLabel(props.field?.label ?? props.label)\n\tconst { t } = useTranslation()\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 [state, setState] = useState<FetchState>({ status: 'idle' })\n\n\tuseEffect(() => {\n\t\tif (id == null || !collectionSlug) {\n\t\t\treturn\n\t\t}\n\t\tconst controller = new AbortController()\n\t\tconst url = buildEndpointOptionsUrl({\n\t\t\tapiRoute,\n\t\t\tcollectionSlug,\n\t\t\tid,\n\t\t\tendpoint: props.endpoint,\n\t\t})\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\t// A missing fetched option (or a failed fetch) must never silently drop a stored value, so every\n\t// selected value the options don't cover is kept selectable by its own label.\n\tconst selectedValues: string[] = isMulti\n\t\t? Array.isArray(value)\n\t\t\t? value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n\t\t\t: []\n\t\t: typeof value === 'string' && value.length > 0\n\t\t\t? [value]\n\t\t\t: []\n\tconst options: ReactSelectOption[] = state.status === 'loaded' ? state.options : []\n\tconst missing = selectedValues\n\t\t.filter((selected) => !options.some((option) => option.value === selected))\n\t\t.map((selected) => ({ label: selected, value: selected }))\n\tconst allOptions = missing.length > 0 ? [...options, ...missing] : options\n\tconst selectedOptions = selectedValues\n\t\t.map((selected) => allOptions.find((option) => option.value === selected))\n\t\t.filter((option): option is ReactSelectOption => option !== undefined)\n\n\tconst handleChange = (selected: ReactSelectOption | ReactSelectOption[] | null) => {\n\t\tif (isMulti) {\n\t\t\tconst chosen = Array.isArray(selected) ? selected : selected ? [selected] : []\n\t\t\tsetValue(chosen.map((option) => option.value as string))\n\t\t\treturn\n\t\t}\n\t\tconst chosen = Array.isArray(selected) ? selected[0] : selected\n\t\tsetValue(chosen ? (chosen.value as string) : '')\n\t}\n\n\treturn (\n\t\t<div className=\"field-type\" style={{ marginBlockEnd: '1rem' }}>\n\t\t\t<FieldLabel label={label} path={path} />\n\t\t\t<ReactSelect\n\t\t\t\toptions={allOptions}\n\t\t\t\tvalue={isMulti ? selectedOptions : (selectedOptions[0] ?? undefined)}\n\t\t\t\tisMulti={isMulti}\n\t\t\t\tisClearable={props.isClearable !== false}\n\t\t\t\tisLoading={state.status === 'loading'}\n\t\t\t\tplaceholder={state.status === 'loading' ? t(keys.endpointOptionsLoading) : undefined}\n\t\t\t\tshowError={state.status === 'error'}\n\t\t\t\tonChange={handleChange}\n\t\t\t/>\n\t\t\t<FieldDescription\n\t\t\t\tdescription={state.status === 'error' ? t(keys.endpointOptionsError) : description}\n\t\t\t\tpath={path}\n\t\t\t/>\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6DA,MAAa,yBAAyB,UAAsC;CAC3E,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,EAAE,MAAM,UAAU,UAAU,SAA4B,EAAE,MAAM,MAAM,KAAK,CAAC;CAClF,MAAM,QAAQ,cAAc,MAAM,OAAO,SAAS,MAAM,KAAK;CAC7D,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,cAAc,MAAM,iBACvB,EAAE,MAAM,cAAc,IACtB,cAAc,MAAM,OAAO,OAAO,WAAW;CAChD,MAAM,EAAE,IAAI,mBAAmB,gBAAgB;CAC/C,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,WAAW,OAAO,OAAO;CAC/B,MAAM,CAAC,OAAO,YAAY,SAAqB,EAAE,QAAQ,OAAO,CAAC;CAEjE,gBAAgB;EACf,IAAI,MAAM,QAAQ,CAAC,gBAClB;EAED,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,MAAM,wBAAwB;GACnC;GACA;GACA;GACA,UAAU,MAAM;EACjB,CAAC;EACD,SAAS,EAAE,QAAQ,UAAU,CAAC;EAC9B,MAAM,KAAK;GACV,aAAa;GACb,SAAS,EAAE,QAAQ,mBAAmB;GACtC,QAAQ,WAAW;EACpB,CAAC,EACC,KAAK,OAAO,aAAa;GACzB,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,+BAA+B,SAAS,QAAQ;GAEjE,SAAS;IAAE,QAAQ;IAAU,SAAS,qBAAqB,MAAM,SAAS,KAAK,CAAC;GAAE,CAAC;EACpF,CAAC,EACA,YAAY;GACZ,IAAI,CAAC,WAAW,OAAO,SACtB,SAAS,EAAE,QAAQ,QAAQ,CAAC;EAE9B,CAAC;EACF,aAAa,WAAW,MAAM;CAC/B,GAAG;EAAC;EAAU;EAAgB;EAAI,MAAM;CAAQ,CAAC;CAIjD,MAAM,iBAA2B,UAC9B,MAAM,QAAQ,KAAK,IAClB,MAAM,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,IACtF,CAAC,IACF,OAAO,UAAU,YAAY,MAAM,SAAS,IAC3C,CAAC,KAAK,IACN,CAAC;CACL,MAAM,UAA+B,MAAM,WAAW,WAAW,MAAM,UAAU,CAAC;CAClF,MAAM,UAAU,eACd,QAAQ,aAAa,CAAC,QAAQ,MAAM,WAAW,OAAO,UAAU,QAAQ,CAAC,EACzE,KAAK,cAAc;EAAE,OAAO;EAAU,OAAO;CAAS,EAAE;CAC1D,MAAM,aAAa,QAAQ,SAAS,IAAI,CAAC,GAAG,SAAS,GAAG,OAAO,IAAI;CACnE,MAAM,kBAAkB,eACtB,KAAK,aAAa,WAAW,MAAM,WAAW,OAAO,UAAU,QAAQ,CAAC,EACxE,QAAQ,WAAwC,WAAW,KAAA,CAAS;CAEtE,MAAM,gBAAgB,aAA6D;EAClF,IAAI,SAAS;GAEZ,UADe,MAAM,QAAQ,QAAQ,IAAI,WAAW,WAAW,CAAC,QAAQ,IAAI,CAAC,GAC7D,KAAK,WAAW,OAAO,KAAe,CAAC;GACvD;EACD;EACA,MAAM,SAAS,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK;EACvD,SAAS,SAAU,OAAO,QAAmB,EAAE;CAChD;CAEA,OACC,qBAAC,OAAD;EAAK,WAAU;EAAa,OAAO,EAAE,gBAAgB,OAAO;YAA5D;GACC,oBAAC,YAAD;IAAmB;IAAa;GAAO,CAAA;GACvC,oBAAC,aAAD;IACC,SAAS;IACT,OAAO,UAAU,kBAAmB,gBAAgB,MAAM,KAAA;IACjD;IACT,aAAa,MAAM,gBAAgB;IACnC,WAAW,MAAM,WAAW;IAC5B,aAAa,MAAM,WAAW,YAAY,EAAE,KAAK,sBAAsB,IAAI,KAAA;IAC3E,WAAW,MAAM,WAAW;IAC5B,UAAU;GACV,CAAA;GACD,oBAAC,kBAAD;IACC,aAAa,MAAM,WAAW,UAAU,EAAE,KAAK,oBAAoB,IAAI;IACjE;GACN,CAAA;EACG;;AAEP"}
1
+ {"version":3,"file":"EndpointOptionsSelect.js","names":["useTranslation"],"sources":["../../src/client/EndpointOptionsSelect.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tFieldDescription,\n\tFieldLabel,\n\tReactSelect,\n\ttype ReactSelectOption,\n\tuseConfig,\n\tuseDocumentInfo,\n\tuseField,\n} from '@payloadcms/ui'\nimport { type CSSProperties, useEffect, useState } from 'react'\nimport { keys, type TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport {\n\tbuildEndpointOptionsUrl,\n\ttype EndpointOption,\n\tparseEndpointOptions,\n} from './endpointOptions'\nimport { toStaticLabel } from './toStaticLabel'\n\n/** Standard text-field client props plus this component's `clientProps`. */\nexport type EndpointOptionsSelectProps = {\n\t/** The field path within the document (Payload-injected, not the endpoint path). */\n\tpath?: string\n\tfield?: { label?: unknown; admin?: { description?: unknown; width?: string } }\n\tlabel?: unknown\n\t/**\n\t * Endpoint subpath under the current document's collection API route; `'poll-options'` fetches\n\t * `GET {routes.api}/{collectionSlug}/{id}/poll-options`. The endpoint must return\n\t * `{ options: { label, value }[] }`.\n\t */\n\tendpoint: string\n\t/**\n\t * Field description as a translation key, resolved client-side. Payload drops `admin.description`\n\t * functions from client fields, so a translated description must travel as a key; a static\n\t * `admin.description` string still renders when this is unset.\n\t */\n\tdescriptionKey?: TranslationKey\n\t/** Mirrors ReactSelect's `isClearable`; defaults to true. */\n\tisClearable?: boolean\n\t/**\n\t * Render a multi-value select bound to a `string[]`, for a `hasMany` field (e.g. a poll outcome\n\t * with tied winners). Default false: a single-value select bound to a `string`.\n\t */\n\tisMulti?: boolean\n}\n\ntype FetchState =\n\t| { status: 'error' | 'idle' | 'loading'; options?: never }\n\t| { status: 'loaded'; options: EndpointOption[] }\n\n/**\n * A select whose options load from a document-scoped plugin endpoint, for stored values whose\n * valid choices only the server knows (e.g. a poll's source-resolved options). The pattern: pass\n * `endpoint` via `clientProps`, the component reads the document id from `useDocumentInfo` and the\n * API route from `useConfig`, fetches once per document with the admin cookie, and renders\n * translated loading/error states. The stored value stays selectable even when it is missing from\n * the fetched options (or the fetch failed), so opening an old document never silently drops data;\n * unsaved documents skip the fetch since the server cannot resolve options for them yet.\n */\nexport const EndpointOptionsSelect = (props: EndpointOptionsSelectProps) => {\n\tconst isMulti = props.isMulti === true\n\tconst { path, setValue, value } = useField<string | string[]>({ path: props.path })\n\tconst label = toStaticLabel(props.field?.label ?? props.label)\n\tconst { t } = useTranslation()\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 [state, setState] = useState<FetchState>({ status: 'idle' })\n\n\tuseEffect(() => {\n\t\tif (id == null || !collectionSlug) {\n\t\t\treturn\n\t\t}\n\t\tconst controller = new AbortController()\n\t\tconst url = buildEndpointOptionsUrl({\n\t\t\tapiRoute,\n\t\t\tcollectionSlug,\n\t\t\tid,\n\t\t\tendpoint: props.endpoint,\n\t\t})\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\t// A missing fetched option (or a failed fetch) must never silently drop a stored value, so every\n\t// selected value the options don't cover is kept selectable by its own label.\n\tconst selectedValues: string[] = isMulti\n\t\t? Array.isArray(value)\n\t\t\t? value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n\t\t\t: []\n\t\t: typeof value === 'string' && value.length > 0\n\t\t\t? [value]\n\t\t\t: []\n\tconst options: ReactSelectOption[] = state.status === 'loaded' ? state.options : []\n\tconst missing = selectedValues\n\t\t.filter((selected) => !options.some((option) => option.value === selected))\n\t\t.map((selected) => ({ label: selected, value: selected }))\n\tconst allOptions = missing.length > 0 ? [...options, ...missing] : options\n\tconst selectedOptions = selectedValues\n\t\t.map((selected) => allOptions.find((option) => option.value === selected))\n\t\t.filter((option): option is ReactSelectOption => option !== undefined)\n\n\tconst handleChange = (selected: ReactSelectOption | ReactSelectOption[] | null) => {\n\t\tif (isMulti) {\n\t\t\tconst chosen = Array.isArray(selected) ? selected : selected ? [selected] : []\n\t\t\tsetValue(chosen.map((option) => option.value as string))\n\t\t\treturn\n\t\t}\n\t\tconst chosen = Array.isArray(selected) ? selected[0] : selected\n\t\tsetValue(chosen ? (chosen.value as string) : '')\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 className=\"field-type\" style={{ marginBlockEnd: '1rem', ...fieldStyle }}>\n\t\t\t<FieldLabel label={label} path={path} />\n\t\t\t<ReactSelect\n\t\t\t\toptions={allOptions}\n\t\t\t\tvalue={isMulti ? selectedOptions : (selectedOptions[0] ?? undefined)}\n\t\t\t\tisMulti={isMulti}\n\t\t\t\tisClearable={props.isClearable !== false}\n\t\t\t\tisLoading={state.status === 'loading'}\n\t\t\t\tplaceholder={state.status === 'loading' ? t(keys.endpointOptionsLoading) : undefined}\n\t\t\t\tshowError={state.status === 'error'}\n\t\t\t\tonChange={handleChange}\n\t\t\t/>\n\t\t\t<FieldDescription\n\t\t\t\tdescription={state.status === 'error' ? t(keys.endpointOptionsError) : description}\n\t\t\t\tpath={path}\n\t\t\t/>\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6DA,MAAa,yBAAyB,UAAsC;CAC3E,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,EAAE,MAAM,UAAU,UAAU,SAA4B,EAAE,MAAM,MAAM,KAAK,CAAC;CAClF,MAAM,QAAQ,cAAc,MAAM,OAAO,SAAS,MAAM,KAAK;CAC7D,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,cAAc,MAAM,iBACvB,EAAE,MAAM,cAAc,IACtB,cAAc,MAAM,OAAO,OAAO,WAAW;CAChD,MAAM,EAAE,IAAI,mBAAmB,gBAAgB;CAC/C,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,WAAW,OAAO,OAAO;CAC/B,MAAM,CAAC,OAAO,YAAY,SAAqB,EAAE,QAAQ,OAAO,CAAC;CAEjE,gBAAgB;EACf,IAAI,MAAM,QAAQ,CAAC,gBAClB;EAED,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,MAAM,wBAAwB;GACnC;GACA;GACA;GACA,UAAU,MAAM;EACjB,CAAC;EACD,SAAS,EAAE,QAAQ,UAAU,CAAC;EAC9B,MAAM,KAAK;GACV,aAAa;GACb,SAAS,EAAE,QAAQ,mBAAmB;GACtC,QAAQ,WAAW;EACpB,CAAC,EACC,KAAK,OAAO,aAAa;GACzB,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,+BAA+B,SAAS,QAAQ;GAEjE,SAAS;IAAE,QAAQ;IAAU,SAAS,qBAAqB,MAAM,SAAS,KAAK,CAAC;GAAE,CAAC;EACpF,CAAC,EACA,YAAY;GACZ,IAAI,CAAC,WAAW,OAAO,SACtB,SAAS,EAAE,QAAQ,QAAQ,CAAC;EAE9B,CAAC;EACF,aAAa,WAAW,MAAM;CAC/B,GAAG;EAAC;EAAU;EAAgB;EAAI,MAAM;CAAQ,CAAC;CAIjD,MAAM,iBAA2B,UAC9B,MAAM,QAAQ,KAAK,IAClB,MAAM,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,IACtF,CAAC,IACF,OAAO,UAAU,YAAY,MAAM,SAAS,IAC3C,CAAC,KAAK,IACN,CAAC;CACL,MAAM,UAA+B,MAAM,WAAW,WAAW,MAAM,UAAU,CAAC;CAClF,MAAM,UAAU,eACd,QAAQ,aAAa,CAAC,QAAQ,MAAM,WAAW,OAAO,UAAU,QAAQ,CAAC,EACzE,KAAK,cAAc;EAAE,OAAO;EAAU,OAAO;CAAS,EAAE;CAC1D,MAAM,aAAa,QAAQ,SAAS,IAAI,CAAC,GAAG,SAAS,GAAG,OAAO,IAAI;CACnE,MAAM,kBAAkB,eACtB,KAAK,aAAa,WAAW,MAAM,WAAW,OAAO,UAAU,QAAQ,CAAC,EACxE,QAAQ,WAAwC,WAAW,KAAA,CAAS;CAEtE,MAAM,gBAAgB,aAA6D;EAClF,IAAI,SAAS;GAEZ,UADe,MAAM,QAAQ,QAAQ,IAAI,WAAW,WAAW,CAAC,QAAQ,IAAI,CAAC,GAC7D,KAAK,WAAW,OAAO,KAAe,CAAC;GACvD;EACD;EACA,MAAM,SAAS,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK;EACvD,SAAS,SAAU,OAAO,QAAmB,EAAE;CAChD;CAMA,OACC,qBAAC,OAAD;EAAK,WAAU;EAAa,OAAO;GAAE,gBAAgB;GAAQ,GAL3C,MAAM,OAAO,OAAO,QACnC,EAAE,iBAAiB,MAAM,MAAM,MAAM,MAAM,IAC5C,KAAA;EAGyE;YAA3E;GACC,oBAAC,YAAD;IAAmB;IAAa;GAAO,CAAA;GACvC,oBAAC,aAAD;IACC,SAAS;IACT,OAAO,UAAU,kBAAmB,gBAAgB,MAAM,KAAA;IACjD;IACT,aAAa,MAAM,gBAAgB;IACnC,WAAW,MAAM,WAAW;IAC5B,aAAa,MAAM,WAAW,YAAY,EAAE,KAAK,sBAAsB,IAAI,KAAA;IAC3E,WAAW,MAAM,WAAW;IAC5B,UAAU;GACV,CAAA;GACD,oBAAC,kBAAD;IACC,aAAa,MAAM,WAAW,UAAU,EAAE,KAAK,oBAAoB,IAAI;IACjE;GACN,CAAA;EACG;;AAEP"}
@@ -8,6 +8,7 @@ type FieldNameSelectProps = {
8
8
  label?: unknown;
9
9
  admin?: {
10
10
  description?: unknown;
11
+ width?: string;
11
12
  };
12
13
  };
13
14
  label?: unknown;
@@ -44,7 +44,10 @@ const FieldNameSelect = (props) => {
44
44
  };
45
45
  return /* @__PURE__ */ jsxs("div", {
46
46
  className: "field-type",
47
- style: { marginBlockEnd: "1rem" },
47
+ style: {
48
+ marginBlockEnd: "1rem",
49
+ ...props.field?.admin?.width ? { "--field-width": props.field.admin.width } : void 0
50
+ },
48
51
  children: [
49
52
  /* @__PURE__ */ jsx(FieldLabel, {
50
53
  label,
@@ -1 +1 @@
1
- {"version":3,"file":"FieldNameSelect.js","names":["useTranslation"],"sources":["../../src/client/FieldNameSelect.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tFieldDescription,\n\tFieldLabel,\n\tReactSelect,\n\ttype ReactSelectOption,\n\tuseField,\n\tuseFormFields,\n} from '@payloadcms/ui'\nimport { reduceFieldsToValues } from 'payload/shared'\nimport { useMemo } from 'react'\nimport { fieldNames, fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport type { TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport type { FieldRow } from './synthesizeClientField'\nimport { toStaticLabel } from './toStaticLabel'\n\n/** Props: standard JSON/text field client props plus the `types` allow-list we pass via clientProps. */\nexport type FieldNameSelectProps = {\n\tpath?: string\n\tfield?: { label?: unknown; admin?: { description?: unknown } }\n\tlabel?: unknown\n\t/**\n\t * Sibling `fields` blocks whose `blockType` is in this list populate the select's options. Omit to\n\t * offer every named field regardless of type (a general field-target picker).\n\t */\n\ttypes?: string[]\n\t/**\n\t * Field description as a translation key, resolved client-side. Payload drops `admin.description`\n\t * functions from client fields, so a translated description must travel as a key; a static\n\t * `admin.description` string still renders when this is unset.\n\t */\n\tdescriptionKey?: TranslationKey\n}\n\nconst optionsFromData = (data: Record<string, unknown>, types?: string[]): 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 = types ? fieldNamesOfType(data.fields, types) : fieldNames(data.fields)\n\treturn names.map((name) => ({\n\t\tlabel: labels.get(name) ?? name,\n\t\tvalue: name,\n\t}))\n}\n\n/**\n * The `Field` mounted on a text config field (e.g. an action's `toField`) that should be authored\n * by picking from the form's own fields rather than free-typing a name. Options come from the\n * sibling `fields` blocks array, filtered to `types` when given (every named field otherwise); the\n * stored value is kept selectable even if it no longer matches any field, so switching to this\n * component never silently drops data.\n */\nexport const FieldNameSelect = (props: FieldNameSelectProps) => {\n\tconst { path, setValue, value } = useField<string>({ path: props.path })\n\tconst label = toStaticLabel(props.field?.label ?? props.label)\n\tconst { t } = useTranslation()\n\tconst description = props.descriptionKey\n\t\t? t(props.descriptionKey)\n\t\t: toStaticLabel(props.field?.admin?.description)\n\n\tconst optionsJson = useFormFields(([fields]) =>\n\t\tJSON.stringify(optionsFromData(reduceFieldsToValues(fields, true), props.types))\n\t)\n\tconst options = useMemo(() => JSON.parse(optionsJson) as ReactSelectOption[], [optionsJson])\n\n\tconst allOptions =\n\t\tvalue && !options.some((option) => option.value === value)\n\t\t\t? [...options, { label: value, value }]\n\t\t\t: options\n\n\tconst handleChange = (selected: ReactSelectOption | ReactSelectOption[] | null) => {\n\t\tconst chosen = Array.isArray(selected) ? selected[0] : selected\n\t\tsetValue(chosen ? (chosen.value as string) : '')\n\t}\n\n\treturn (\n\t\t<div className=\"field-type\" style={{ marginBlockEnd: '1rem' }}>\n\t\t\t<FieldLabel label={label} path={path} />\n\t\t\t<ReactSelect\n\t\t\t\toptions={allOptions}\n\t\t\t\tvalue={allOptions.find((option) => option.value === value) ?? undefined}\n\t\t\t\tisClearable\n\t\t\t\tonChange={handleChange}\n\t\t\t/>\n\t\t\t<FieldDescription description={description} path={path} />\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;;;;AAoCA,MAAM,mBAAmB,MAA+B,UAA0C;CACjG,MAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAuB,CAAC;CACxE,MAAM,yBAAS,IAAI,IAAoB;CACvC,KAAK,MAAM,OAAO,MAAM;EACvB,IAAI,CAAC,OAAO,OAAO,QAAQ,UAC1B;EAED,MAAM,EAAE,MAAM,UAAU;EACxB,IAAI,OAAO,SAAS,YAAY,OAAO,UAAU,YAAY,MAAM,SAAS,GAC3E,OAAO,IAAI,KAAK,KAAK,GAAG,KAAK;CAE/B;CAEA,QADc,QAAQ,iBAAiB,KAAK,QAAQ,KAAK,IAAI,WAAW,KAAK,MAAM,GACtE,KAAK,UAAU;EAC3B,OAAO,OAAO,IAAI,IAAI,KAAK;EAC3B,OAAO;CACR,EAAE;AACH;;;;;;;;AASA,MAAa,mBAAmB,UAAgC;CAC/D,MAAM,EAAE,MAAM,UAAU,UAAU,SAAiB,EAAE,MAAM,MAAM,KAAK,CAAC;CACvE,MAAM,QAAQ,cAAc,MAAM,OAAO,SAAS,MAAM,KAAK;CAC7D,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,cAAc,MAAM,iBACvB,EAAE,MAAM,cAAc,IACtB,cAAc,MAAM,OAAO,OAAO,WAAW;CAEhD,MAAM,cAAc,eAAe,CAAC,YACnC,KAAK,UAAU,gBAAgB,qBAAqB,QAAQ,IAAI,GAAG,MAAM,KAAK,CAAC,CAChF;CACA,MAAM,UAAU,cAAc,KAAK,MAAM,WAAW,GAA0B,CAAC,WAAW,CAAC;CAE3F,MAAM,aACL,SAAS,CAAC,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK,IACtD,CAAC,GAAG,SAAS;EAAE,OAAO;EAAO;CAAM,CAAC,IACpC;CAEJ,MAAM,gBAAgB,aAA6D;EAClF,MAAM,SAAS,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK;EACvD,SAAS,SAAU,OAAO,QAAmB,EAAE;CAChD;CAEA,OACC,qBAAC,OAAD;EAAK,WAAU;EAAa,OAAO,EAAE,gBAAgB,OAAO;YAA5D;GACC,oBAAC,YAAD;IAAmB;IAAa;GAAO,CAAA;GACvC,oBAAC,aAAD;IACC,SAAS;IACT,OAAO,WAAW,MAAM,WAAW,OAAO,UAAU,KAAK,KAAK,KAAA;IAC9D,aAAA;IACA,UAAU;GACV,CAAA;GACD,oBAAC,kBAAD;IAA+B;IAAmB;GAAO,CAAA;EACrD;;AAEP"}
1
+ {"version":3,"file":"FieldNameSelect.js","names":["useTranslation"],"sources":["../../src/client/FieldNameSelect.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tFieldDescription,\n\tFieldLabel,\n\tReactSelect,\n\ttype ReactSelectOption,\n\tuseField,\n\tuseFormFields,\n} from '@payloadcms/ui'\nimport { reduceFieldsToValues } from 'payload/shared'\nimport { type CSSProperties, useMemo } from 'react'\nimport { fieldNames, fieldNamesOfType } from '../fields/fieldNamesOfType'\nimport type { TranslationKey } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport type { FieldRow } from './synthesizeClientField'\nimport { toStaticLabel } from './toStaticLabel'\n\n/** Props: standard JSON/text field client props plus the `types` allow-list we pass via clientProps. */\nexport type FieldNameSelectProps = {\n\tpath?: string\n\tfield?: { label?: unknown; admin?: { description?: unknown; width?: string } }\n\tlabel?: unknown\n\t/**\n\t * Sibling `fields` blocks whose `blockType` is in this list populate the select's options. Omit to\n\t * offer every named field regardless of type (a general field-target picker).\n\t */\n\ttypes?: string[]\n\t/**\n\t * Field description as a translation key, resolved client-side. Payload drops `admin.description`\n\t * functions from client fields, so a translated description must travel as a key; a static\n\t * `admin.description` string still renders when this is unset.\n\t */\n\tdescriptionKey?: TranslationKey\n}\n\nconst optionsFromData = (data: Record<string, unknown>, types?: string[]): 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 = types ? fieldNamesOfType(data.fields, types) : fieldNames(data.fields)\n\treturn names.map((name) => ({\n\t\tlabel: labels.get(name) ?? name,\n\t\tvalue: name,\n\t}))\n}\n\n/**\n * The `Field` mounted on a text config field (e.g. an action's `toField`) that should be authored\n * by picking from the form's own fields rather than free-typing a name. Options come from the\n * sibling `fields` blocks array, filtered to `types` when given (every named field otherwise); the\n * stored value is kept selectable even if it no longer matches any field, so switching to this\n * component never silently drops data.\n */\nexport const FieldNameSelect = (props: FieldNameSelectProps) => {\n\tconst { path, setValue, value } = useField<string>({ path: props.path })\n\tconst label = toStaticLabel(props.field?.label ?? props.label)\n\tconst { t } = useTranslation()\n\tconst description = props.descriptionKey\n\t\t? t(props.descriptionKey)\n\t\t: toStaticLabel(props.field?.admin?.description)\n\n\tconst optionsJson = useFormFields(([fields]) =>\n\t\tJSON.stringify(optionsFromData(reduceFieldsToValues(fields, true), props.types))\n\t)\n\tconst options = useMemo(() => JSON.parse(optionsJson) as ReactSelectOption[], [optionsJson])\n\n\tconst allOptions =\n\t\tvalue && !options.some((option) => option.value === value)\n\t\t\t? [...options, { label: value, value }]\n\t\t\t: options\n\n\tconst handleChange = (selected: ReactSelectOption | ReactSelectOption[] | null) => {\n\t\tconst chosen = Array.isArray(selected) ? selected[0] : selected\n\t\tsetValue(chosen ? (chosen.value as string) : '')\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 className=\"field-type\" style={{ marginBlockEnd: '1rem', ...fieldStyle }}>\n\t\t\t<FieldLabel label={label} path={path} />\n\t\t\t<ReactSelect\n\t\t\t\toptions={allOptions}\n\t\t\t\tvalue={allOptions.find((option) => option.value === value) ?? undefined}\n\t\t\t\tisClearable\n\t\t\t\tonChange={handleChange}\n\t\t\t/>\n\t\t\t<FieldDescription description={description} path={path} />\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;;;;AAoCA,MAAM,mBAAmB,MAA+B,UAA0C;CACjG,MAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAuB,CAAC;CACxE,MAAM,yBAAS,IAAI,IAAoB;CACvC,KAAK,MAAM,OAAO,MAAM;EACvB,IAAI,CAAC,OAAO,OAAO,QAAQ,UAC1B;EAED,MAAM,EAAE,MAAM,UAAU;EACxB,IAAI,OAAO,SAAS,YAAY,OAAO,UAAU,YAAY,MAAM,SAAS,GAC3E,OAAO,IAAI,KAAK,KAAK,GAAG,KAAK;CAE/B;CAEA,QADc,QAAQ,iBAAiB,KAAK,QAAQ,KAAK,IAAI,WAAW,KAAK,MAAM,GACtE,KAAK,UAAU;EAC3B,OAAO,OAAO,IAAI,IAAI,KAAK;EAC3B,OAAO;CACR,EAAE;AACH;;;;;;;;AASA,MAAa,mBAAmB,UAAgC;CAC/D,MAAM,EAAE,MAAM,UAAU,UAAU,SAAiB,EAAE,MAAM,MAAM,KAAK,CAAC;CACvE,MAAM,QAAQ,cAAc,MAAM,OAAO,SAAS,MAAM,KAAK;CAC7D,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,cAAc,MAAM,iBACvB,EAAE,MAAM,cAAc,IACtB,cAAc,MAAM,OAAO,OAAO,WAAW;CAEhD,MAAM,cAAc,eAAe,CAAC,YACnC,KAAK,UAAU,gBAAgB,qBAAqB,QAAQ,IAAI,GAAG,MAAM,KAAK,CAAC,CAChF;CACA,MAAM,UAAU,cAAc,KAAK,MAAM,WAAW,GAA0B,CAAC,WAAW,CAAC;CAE3F,MAAM,aACL,SAAS,CAAC,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK,IACtD,CAAC,GAAG,SAAS;EAAE,OAAO;EAAO;CAAM,CAAC,IACpC;CAEJ,MAAM,gBAAgB,aAA6D;EAClF,MAAM,SAAS,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK;EACvD,SAAS,SAAU,OAAO,QAAmB,EAAE;CAChD;CAMA,OACC,qBAAC,OAAD;EAAK,WAAU;EAAa,OAAO;GAAE,gBAAgB;GAAQ,GAL3C,MAAM,OAAO,OAAO,QACnC,EAAE,iBAAiB,MAAM,MAAM,MAAM,MAAM,IAC5C,KAAA;EAGyE;YAA3E;GACC,oBAAC,YAAD;IAAmB;IAAa;GAAO,CAAA;GACvC,oBAAC,aAAD;IACC,SAAS;IACT,OAAO,WAAW,MAAM,WAAW,OAAO,UAAU,KAAK,KAAK,KAAA;IAC9D,aAAA;IACA,UAAU;GACV,CAAA;GACD,oBAAC,kBAAD;IAA+B;IAAmB;GAAO,CAAA;EACrD;;AAEP"}
@@ -0,0 +1,31 @@
1
+ import { TranslationKey } from "../translations/keys.js";
2
+
3
+ //#region src/client/RecipientsSelect.d.ts
4
+ type RecipientsSelectProps = {
5
+ path?: string;
6
+ field?: {
7
+ label?: unknown;
8
+ required?: boolean;
9
+ admin?: {
10
+ description?: unknown;
11
+ width?: string;
12
+ };
13
+ };
14
+ label?: unknown;
15
+ readOnly?: boolean; /** Endpoint subpath supplying preset address options (e.g. `'departments'`); omit for none. */
16
+ endpoint?: string; /** Allow free-typed emails (default true). */
17
+ allowCustom?: boolean; /** Offer the form's own fields as recipient tokens (default true). */
18
+ fieldTokens?: boolean; /** Field types eligible as tokens (default `['email']`). */
19
+ tokenFieldTypes?: string[];
20
+ descriptionKey?: TranslationKey;
21
+ };
22
+ /**
23
+ * A creatable, multi-value recipient field: pick a department (preset options from `endpoint`), pick a
24
+ * form-field token (`{{field}}`, offered when `fieldTokens`), or type any email and press Enter
25
+ * (`allowCustom`). Renders as native Payload badges (drag-reorderable), stores a `string[]`, honors
26
+ * `admin.width` and the standard field props. Unknown or legacy stored values stay selectable.
27
+ */
28
+ declare const RecipientsSelect: (props: RecipientsSelectProps) => import("react/jsx-runtime").JSX.Element;
29
+ //#endregion
30
+ export { RecipientsSelect };
31
+ //# sourceMappingURL=RecipientsSelect.d.ts.map
@@ -0,0 +1,174 @@
1
+ "use client";
2
+ import { fieldNames, fieldNamesOfType } from "../fields/fieldNamesOfType.js";
3
+ import { keys } from "../translations/keys.js";
4
+ import { isPlausibleEmail } from "../actions/emailRecipients.js";
5
+ import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
6
+ import { toStaticLabel } from "./toStaticLabel.js";
7
+ import { buildEndpointOptionsUrl, parseEndpointOptions } from "./endpointOptions.js";
8
+ import { reduceFieldsToValues } from "payload/shared";
9
+ import { FieldDescription, FieldError, FieldLabel, ReactSelect, RenderCustomComponent, useConfig, useDocumentInfo, useField, useFormFields } from "@payloadcms/ui";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { useEffect, useMemo, useState } from "react";
12
+ //#region src/client/RecipientsSelect.tsx
13
+ const tokenOptionsFromData = (data, types) => {
14
+ const rows = Array.isArray(data.fields) ? data.fields : [];
15
+ const labels = /* @__PURE__ */ new Map();
16
+ for (const row of rows) {
17
+ if (!row || typeof row !== "object") continue;
18
+ const { name, label } = row;
19
+ if (typeof name === "string" && typeof label === "string" && label.length > 0) labels.set(name.trim(), label);
20
+ }
21
+ return (types && types.length > 0 ? fieldNamesOfType(data.fields, types) : fieldNames(data.fields)).map((name) => ({
22
+ label: labels.get(name) ?? name,
23
+ value: `{{${name}}}`
24
+ }));
25
+ };
26
+ /**
27
+ * A creatable, multi-value recipient field: pick a department (preset options from `endpoint`), pick a
28
+ * form-field token (`{{field}}`, offered when `fieldTokens`), or type any email and press Enter
29
+ * (`allowCustom`). Renders as native Payload badges (drag-reorderable), stores a `string[]`, honors
30
+ * `admin.width` and the standard field props. Unknown or legacy stored values stay selectable.
31
+ */
32
+ const RecipientsSelect = (props) => {
33
+ const allowCustom = props.allowCustom !== false;
34
+ const fieldTokens = props.fieldTokens !== false;
35
+ const { customComponents: { Description, Error: ErrorComponent, Label } = {}, disabled, path, setValue, showError, value } = useField({ path: props.path });
36
+ const { t } = useTranslation$1();
37
+ const label = toStaticLabel(props.field?.label ?? props.label);
38
+ const description = props.descriptionKey ? t(props.descriptionKey) : toStaticLabel(props.field?.admin?.description);
39
+ const { id, collectionSlug } = useDocumentInfo();
40
+ const { config } = useConfig();
41
+ const apiRoute = config.routes.api;
42
+ const readOnly = props.readOnly === true || disabled === true;
43
+ const [state, setState] = useState({ status: "idle" });
44
+ useEffect(() => {
45
+ if (!props.endpoint || id == null || !collectionSlug) return;
46
+ const controller = new AbortController();
47
+ const url = buildEndpointOptionsUrl({
48
+ apiRoute,
49
+ collectionSlug,
50
+ id,
51
+ endpoint: props.endpoint
52
+ });
53
+ setState({ status: "loading" });
54
+ fetch(url, {
55
+ credentials: "include",
56
+ headers: { Accept: "application/json" },
57
+ signal: controller.signal
58
+ }).then(async (response) => {
59
+ if (!response.ok) throw new Error(`Options request failed with ${response.status}`);
60
+ setState({
61
+ status: "loaded",
62
+ options: parseEndpointOptions(await response.json())
63
+ });
64
+ }).catch(() => {
65
+ if (!controller.signal.aborted) setState({ status: "error" });
66
+ });
67
+ return () => controller.abort();
68
+ }, [
69
+ apiRoute,
70
+ collectionSlug,
71
+ id,
72
+ props.endpoint
73
+ ]);
74
+ const presetOptions = state.status === "loaded" ? state.options : [];
75
+ const tokenJson = useFormFields(([fields]) => fieldTokens ? JSON.stringify(tokenOptionsFromData(reduceFieldsToValues(fields, true), props.tokenFieldTypes)) : "[]");
76
+ const tokenOptions = useMemo(() => JSON.parse(tokenJson), [tokenJson]);
77
+ const stored = Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : typeof value === "string" && value.length > 0 ? [value] : [];
78
+ const byValue = useMemo(() => {
79
+ const map = /* @__PURE__ */ new Map();
80
+ for (const option of [...presetOptions, ...tokenOptions]) map.set(option.value, option);
81
+ return map;
82
+ }, [presetOptions, tokenOptions]);
83
+ const selected = stored.map((entry) => byValue.get(entry) ?? {
84
+ label: entry,
85
+ value: entry
86
+ });
87
+ const groupedOptions = useMemo(() => {
88
+ const groups = [];
89
+ if (presetOptions.length > 0) groups.push({
90
+ label: t(keys.recipientsGroupDepartments),
91
+ options: presetOptions
92
+ });
93
+ if (tokenOptions.length > 0) groups.push({
94
+ label: t(keys.recipientsGroupFields),
95
+ options: tokenOptions
96
+ });
97
+ return groups;
98
+ }, [
99
+ presetOptions,
100
+ tokenOptions,
101
+ t
102
+ ]);
103
+ const handleChange = (selection) => {
104
+ if (readOnly) return;
105
+ const entries = (Array.isArray(selection) ? selection : selection ? [selection] : []).map((option) => String(option.value).trim()).filter((entry) => entry.length > 0 && (isPlausibleEmail(entry) || byValue.has(entry)));
106
+ const deduped = /* @__PURE__ */ new Map();
107
+ for (const entry of entries) {
108
+ const key = entry.toLowerCase();
109
+ if (!deduped.has(key)) deduped.set(key, entry);
110
+ }
111
+ setValue(Array.from(deduped.values()));
112
+ };
113
+ const fieldStyle = props.field?.admin?.width ? { "--field-width": props.field.admin.width } : void 0;
114
+ return /* @__PURE__ */ jsxs("div", {
115
+ className: [
116
+ "field-type",
117
+ showError && "error",
118
+ readOnly && "read-only"
119
+ ].filter(Boolean).join(" "),
120
+ id: path ? `field-${path.replace(/\./g, "__")}` : void 0,
121
+ style: {
122
+ marginBlockEnd: "1rem",
123
+ ...fieldStyle
124
+ },
125
+ children: [
126
+ /* @__PURE__ */ jsx(RenderCustomComponent, {
127
+ CustomComponent: Label,
128
+ Fallback: /* @__PURE__ */ jsx(FieldLabel, {
129
+ label,
130
+ path,
131
+ required: props.field?.required
132
+ })
133
+ }),
134
+ /* @__PURE__ */ jsxs("div", {
135
+ className: "field-type__wrap",
136
+ children: [/* @__PURE__ */ jsx(RenderCustomComponent, {
137
+ CustomComponent: ErrorComponent,
138
+ Fallback: /* @__PURE__ */ jsx(FieldError, {
139
+ path,
140
+ showError
141
+ })
142
+ }), /* @__PURE__ */ jsx(ReactSelect, {
143
+ options: groupedOptions,
144
+ value: selected,
145
+ isMulti: true,
146
+ isSortable: true,
147
+ isCreatable: allowCustom,
148
+ isClearable: !readOnly,
149
+ disabled: readOnly,
150
+ isLoading: state.status === "loading",
151
+ showError,
152
+ placeholder: state.status === "loading" ? t(keys.endpointOptionsLoading) : void 0,
153
+ onChange: handleChange,
154
+ filterOption: (option, rawInput) => {
155
+ if (!option) return allowCustom && isPlausibleEmail(rawInput);
156
+ const query = rawInput.toLowerCase();
157
+ return `${option.label} ${option.value}`.toLowerCase().includes(query);
158
+ }
159
+ })]
160
+ }),
161
+ /* @__PURE__ */ jsx(RenderCustomComponent, {
162
+ CustomComponent: Description,
163
+ Fallback: /* @__PURE__ */ jsx(FieldDescription, {
164
+ description: state.status === "error" ? t(keys.endpointOptionsError) : description,
165
+ path
166
+ })
167
+ })
168
+ ]
169
+ });
170
+ };
171
+ //#endregion
172
+ export { RecipientsSelect };
173
+
174
+ //# sourceMappingURL=RecipientsSelect.js.map
@@ -0,0 +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":";;;;;;;;;;;;AAgDA,MAAM,wBACL,MACA,UACyB;CACzB,MAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAuB,CAAC;CACxE,MAAM,yBAAS,IAAI,IAAoB;CACvC,KAAK,MAAM,OAAO,MAAM;EACvB,IAAI,CAAC,OAAO,OAAO,QAAQ,UAC1B;EAED,MAAM,EAAE,MAAM,UAAU;EACxB,IAAI,OAAO,SAAS,YAAY,OAAO,UAAU,YAAY,MAAM,SAAS,GAC3E,OAAO,IAAI,KAAK,KAAK,GAAG,KAAK;CAE/B;CAGA,QADC,SAAS,MAAM,SAAS,IAAI,iBAAiB,KAAK,QAAQ,KAAK,IAAI,WAAW,KAAK,MAAM,GAC7E,KAAK,UAAU;EAAE,OAAO,OAAO,IAAI,IAAI,KAAK;EAAM,OAAO,KAAK,KAAK;CAAI,EAAE;AACvF;;;;;;;AAQA,MAAa,oBAAoB,UAAiC;CACjE,MAAM,cAAc,MAAM,gBAAgB;CAC1C,MAAM,cAAc,MAAM,gBAAgB;CAC1C,MAAM,EACL,kBAAkB,EAAE,aAAa,OAAO,gBAAgB,UAAU,CAAC,GACnE,UACA,MACA,UACA,WACA,UACG,SAA4B,EAAE,MAAM,MAAM,KAAK,CAAC;CACpD,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,QAAQ,cAAc,MAAM,OAAO,SAAS,MAAM,KAAK;CAC7D,MAAM,cAAc,MAAM,iBACvB,EAAE,MAAM,cAAc,IACtB,cAAc,MAAM,OAAO,OAAO,WAAW;CAChD,MAAM,EAAE,IAAI,mBAAmB,gBAAgB;CAC/C,MAAM,EAAE,WAAW,UAAU;CAC7B,MAAM,WAAW,OAAO,OAAO;CAC/B,MAAM,WAAW,MAAM,aAAa,QAAQ,aAAa;CACzD,MAAM,CAAC,OAAO,YAAY,SAAqB,EAAE,QAAQ,OAAO,CAAC;CAEjE,gBAAgB;EACf,IAAI,CAAC,MAAM,YAAY,MAAM,QAAQ,CAAC,gBACrC;EAED,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,MAAM,wBAAwB;GAAE;GAAU;GAAgB;GAAI,UAAU,MAAM;EAAS,CAAC;EAC9F,SAAS,EAAE,QAAQ,UAAU,CAAC;EAC9B,MAAM,KAAK;GACV,aAAa;GACb,SAAS,EAAE,QAAQ,mBAAmB;GACtC,QAAQ,WAAW;EACpB,CAAC,EACC,KAAK,OAAO,aAAa;GACzB,IAAI,CAAC,SAAS,IACb,MAAM,IAAI,MAAM,+BAA+B,SAAS,QAAQ;GAEjE,SAAS;IAAE,QAAQ;IAAU,SAAS,qBAAqB,MAAM,SAAS,KAAK,CAAC;GAAE,CAAC;EACpF,CAAC,EACA,YAAY;GACZ,IAAI,CAAC,WAAW,OAAO,SACtB,SAAS,EAAE,QAAQ,QAAQ,CAAC;EAE9B,CAAC;EACF,aAAa,WAAW,MAAM;CAC/B,GAAG;EAAC;EAAU;EAAgB;EAAI,MAAM;CAAQ,CAAC;CAEjD,MAAM,gBAAqC,MAAM,WAAW,WAAW,MAAM,UAAU,CAAC;CAExF,MAAM,YAAY,eAAe,CAAC,YACjC,cACG,KAAK,UACL,qBAAqB,qBAAqB,QAAQ,IAAI,GAAG,MAAM,eAAe,CAC/E,IACC,IACJ;CACA,MAAM,eAAe,cAAc,KAAK,MAAM,SAAS,GAA0B,CAAC,SAAS,CAAC;CAE5F,MAAM,SAAmB,MAAM,QAAQ,KAAK,IACzC,MAAM,QAAQ,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,IACtF,OAAO,UAAU,YAAY,MAAM,SAAS,IAC3C,CAAC,KAAK,IACN,CAAC;CAEL,MAAM,UAAU,cAAc;EAC7B,MAAM,sBAAM,IAAI,IAA+B;EAC/C,KAAK,MAAM,UAAU,CAAC,GAAG,eAAe,GAAG,YAAY,GACtD,IAAI,IAAI,OAAO,OAAiB,MAAM;EAEvC,OAAO;CACR,GAAG,CAAC,eAAe,YAAY,CAAC;CAEhC,MAAM,WAAW,OAAO,KAAK,UAAU,QAAQ,IAAI,KAAK,KAAK;EAAE,OAAO;EAAO,OAAO;CAAM,CAAC;CAE3F,MAAM,iBAAiB,cAAc;EACpC,MAAM,SAA4D,CAAC;EACnE,IAAI,cAAc,SAAS,GAC1B,OAAO,KAAK;GAAE,OAAO,EAAE,KAAK,0BAA0B;GAAG,SAAS;EAAc,CAAC;EAElF,IAAI,aAAa,SAAS,GACzB,OAAO,KAAK;GAAE,OAAO,EAAE,KAAK,qBAAqB;GAAG,SAAS;EAAa,CAAC;EAE5E,OAAO;CACR,GAAG;EAAC;EAAe;EAAc;CAAC,CAAC;CAEnC,MAAM,gBAAgB,cAA8D;EACnF,IAAI,UACH;EAGD,MAAM,WADS,MAAM,QAAQ,SAAS,IAAI,YAAY,YAAY,CAAC,SAAS,IAAI,CAAC,GAE/E,KAAK,WAAW,OAAO,OAAO,KAAK,EAAE,KAAK,CAAC,EAC3C,QAAQ,UAAU,MAAM,SAAS,MAAM,iBAAiB,KAAK,KAAK,QAAQ,IAAI,KAAK,EAAE;EACvF,MAAM,0BAAU,IAAI,IAAoB;EACxC,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,MAAM,MAAM,YAAY;GAC9B,IAAI,CAAC,QAAQ,IAAI,GAAG,GACnB,QAAQ,IAAI,KAAK,KAAK;EAExB;EACA,SAAS,MAAM,KAAK,QAAQ,OAAO,CAAC,CAAC;CACtC;CAEA,MAAM,aAAa,MAAM,OAAO,OAAO,QACnC,EAAE,iBAAiB,MAAM,MAAM,MAAM,MAAM,IAC5C,KAAA;CAEH,OACC,qBAAC,OAAD;EACC,WAAW;GAAC;GAAc,aAAa;GAAS,YAAY;EAAW,EACrE,OAAO,OAAO,EACd,KAAK,GAAG;EACV,IAAI,OAAO,SAAS,KAAK,QAAQ,OAAO,IAAI,MAAM,KAAA;EAClD,OAAO;GAAE,gBAAgB;GAAQ,GAAG;EAAW;YALhD;GAOC,oBAAC,uBAAD;IACC,iBAAiB;IACjB,UAAU,oBAAC,YAAD;KAAmB;KAAa;KAAM,UAAU,MAAM,OAAO;IAAW,CAAA;GAClF,CAAA;GACD,qBAAC,OAAD;IAAK,WAAU;cAAf,CACC,oBAAC,uBAAD;KACC,iBAAiB;KACjB,UAAU,oBAAC,YAAD;MAAkB;MAAiB;KAAY,CAAA;IACzD,CAAA,GACD,oBAAC,aAAD;KACC,SAAS;KACT,OAAO;KACP,SAAA;KACA,YAAA;KACA,aAAa;KACb,aAAa,CAAC;KACd,UAAU;KACV,WAAW,MAAM,WAAW;KACjB;KACX,aAAa,MAAM,WAAW,YAAY,EAAE,KAAK,sBAAsB,IAAI,KAAA;KAC3E,UAAU;KACV,eAAe,QAAQ,aAAa;MACnC,IAAI,CAAC,QACJ,OAAO,eAAe,iBAAiB,QAAQ;MAEhD,MAAM,QAAQ,SAAS,YAAY;MACnC,OAAO,GAAG,OAAO,MAAM,GAAG,OAAO,QAAQ,YAAY,EAAE,SAAS,KAAK;KACtE;IACA,CAAA,CACG;;GACL,oBAAC,uBAAD;IACC,iBAAiB;IACjB,UACC,oBAAC,kBAAD;KACC,aAAa,MAAM,WAAW,UAAU,EAAE,KAAK,oBAAoB,IAAI;KACjE;IACN,CAAA;GAEF,CAAA;EACG;;AAEP"}
@@ -264,6 +264,11 @@
264
264
  }
265
265
  .fb-condensed-array__remove {
266
266
  flex-shrink: 0;
267
+ display: flex;
268
+ align-items: center;
269
+ /* Payload input height, so with the row's flex-end the button centers on the input, not the
270
+ * taller label+input stack. */
271
+ height: 40px;
267
272
  }
268
273
  .fb-condensed-array__actions {
269
274
  margin-block-start: 0.25rem;
@@ -89,7 +89,7 @@ const makeVotedCookieHook = () => {
89
89
  return doc;
90
90
  };
91
91
  };
92
- const buildSubmissionsCollection = ({ registry, ruleRegistry, consentSources, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, richText, uploadSlug, spam, votedCookie = false, pollSourceRegistry, showRawFields = false, overrides }) => {
92
+ const buildSubmissionsCollection = ({ registry, ruleRegistry, consentSources, consentSnapshot, actionRegistry = /* @__PURE__ */ new Map(), events, hasRunner = false, richText, uploadSlug, spam, votedCookie = false, pollSourceRegistry, showRawFields = false, overrides }) => {
93
93
  const defaultFields = [
94
94
  {
95
95
  name: "form",
@@ -177,6 +177,7 @@ const buildSubmissionsCollection = ({ registry, ruleRegistry, consentSources, ac
177
177
  registry,
178
178
  ruleRegistry,
179
179
  consentSources,
180
+ consentSnapshot,
180
181
  uploadSlug,
181
182
  pollSourceRegistry
182
183
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"formSubmissions.js","names":[],"sources":["../../src/collections/formSubmissions.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionConfig, Field } from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport { dispatchActions } from '../actions/dispatch'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { ActionInstance } from '../actions/runActions'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport { resolveEventSink } from '../events/resolveEventSink'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { buildSpamGuard } from '../spam/spamGuard'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport { validateSubmission } from '../submissions/validateSubmission'\nimport { POLL_CONTEXT_KEY, votedCookieName } from '../submissions/votedCookie'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { FORMS_SLUG } from './forms'\n\nexport const FORM_SUBMISSIONS_SLUG = 'form-submissions'\n\ntype BuildSubmissionsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** The host's consent sources resolver (plugin option `consent.sources`); absent when no sources are configured. */\n\tconsentSources?: ConsentSourcesResolver\n\tactionRegistry?: ActionRegistry\n\tevents?: FormEventSink\n\t/** Whether a job runner is likely present; gates the queued vs bounded-inline dispatch path. */\n\thasRunner?: boolean\n\t/** Body serialization customization forwarded to the inline action dispatch path. */\n\trichText?: RichTextBodyOption\n\t/** The plugin-configured uploads collection slug; absent when uploads are disabled. */\n\tuploadSlug?: string\n\t/** Resolved spam config; when active, prepends the spam guard before validation. `false` disables it. */\n\tspam?: ResolvedSpamConfig | false\n\t/** Opt-in (`poll.votedCookie`): set an httpOnly `fb-voted-{formId}` cookie on poll submission creates. */\n\tvotedCookie?: boolean\n\t/** Registered poll option sources; submission validation resolves allowed values through them. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/**\n\t * When `true`, reveals the standalone `locale`, `values`, `descriptors`, `consent`, and `meta`\n\t * fields in the admin UI. Default `false`, because the `SubmissionAnswers` UI component already\n\t * represents them fully (locale and meta are folded into its Submission-details section).\n\t */\n\tshowRawFields?: boolean\n\toverrides?: CollectionOverrides\n}\n\nconst formIdOf = (form: unknown): number | string | undefined => {\n\tif (typeof form === 'number' || typeof form === 'string') {\n\t\treturn form\n\t}\n\tif (form && typeof form === 'object' && 'id' in form) {\n\t\tconst id = (form as { id: unknown }).id\n\t\tif (typeof id === 'number' || typeof id === 'string') {\n\t\t\treturn id\n\t\t}\n\t}\n\treturn undefined\n}\n\n/**\n * On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.\n * Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw\n * past the hook (a failed action or sink must never fail the submission write). Dispatch is itself bounded\n * and non-throwing; the inline fallback is awaited so the bound caps the added latency, while the queued\n * path returns immediately. The form's `actions` are null-guarded for legacy rows created before the field\n * existed.\n */\nconst makeAfterChange =\n\t(args: {\n\t\tactionRegistry: ActionRegistry\n\t\tevents?: FormEventSink\n\t\thasRunner: boolean\n\t\trichText?: RichTextBodyOption\n\t}): CollectionAfterChangeHook =>\n\tasync ({ doc, operation, req }) => {\n\t\tif (operation !== 'create' || (doc.status != null && doc.status !== 'complete')) {\n\t\t\treturn doc\n\t\t}\n\t\tconst { payload } = req\n\t\ttry {\n\t\t\tconst formId = formIdOf(doc.form)\n\t\t\tif (formId == null) {\n\t\t\t\treturn doc\n\t\t\t}\n\t\t\tconst form = await payload\n\t\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t\t.catch(() => null)\n\n\t\t\tawait dispatchActions({\n\t\t\t\tactions: (form?.actions ?? null) as ActionInstance[] | null,\n\t\t\t\tformId,\n\t\t\t\tsubmissionId: doc.id as number | string,\n\t\t\t\tregistry: args.actionRegistry,\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\thasRunner: args.hasRunner,\n\t\t\t\trichText: args.richText,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait resolveEventSink(args.events).emit({\n\t\t\t\t\ttype: 'submission.created',\n\t\t\t\t\tformId: String(formId),\n\t\t\t\t\tsubmissionId: String(doc.id),\n\t\t\t\t\tat: new Date().toISOString(),\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: submission.created sink threw: ${\n\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t}`\n\t\t\t\t)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: afterChange dispatch failed for submission ${String(doc.id)}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`\n\t\t\t)\n\t\t}\n\t\treturn doc\n\t}\n\n/**\n * Opt-in (`poll.votedCookie`) hook: after a submission to a poll-enabled form is created, appends\n * an httpOnly `fb-voted-{formId}=1` cookie via `req.responseHeaders`, Payload's supported channel\n * for hook-set response headers (its own auth strategies use it; `handleEndpoints` merges it into\n * every REST response). Reads the poll config `validateSubmission` stashed on `req.context`,\n * falling back to a form fetch when absent. httpOnly keeps the marker readable only server-side\n * (`hasVotedCookie`); the client `<Poll>` keeps its localStorage guard.\n */\nconst makeVotedCookieHook = (): CollectionAfterChangeHook => {\n\treturn async ({ doc, operation, req }) => {\n\t\tif (operation !== 'create') {\n\t\t\treturn doc\n\t\t}\n\t\tconst formId = formIdOf(doc.form)\n\t\tif (formId == null) {\n\t\t\treturn doc\n\t\t}\n\t\tconst stashed = req.context?.[POLL_CONTEXT_KEY]\n\t\tlet pollEnabled = typeof stashed === 'boolean' ? stashed : undefined\n\t\tif (pollEnabled === undefined) {\n\t\t\tconst form = await req.payload\n\t\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t\t.catch(() => null)\n\t\t\tpollEnabled = form?.pollEnabled === true\n\t\t}\n\t\tif (!pollEnabled) {\n\t\t\treturn doc\n\t\t}\n\t\treq.responseHeaders ??= new Headers()\n\t\treq.responseHeaders.append(\n\t\t\t'Set-Cookie',\n\t\t\t`${votedCookieName(formId)}=1; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax`\n\t\t)\n\t\treturn doc\n\t}\n}\n\nexport const buildSubmissionsCollection = ({\n\tregistry,\n\truleRegistry,\n\tconsentSources,\n\tactionRegistry = new Map(),\n\tevents,\n\thasRunner = false,\n\trichText,\n\tuploadSlug,\n\tspam,\n\tvotedCookie = false,\n\tpollSourceRegistry,\n\tshowRawFields = false,\n\toverrides,\n}: BuildSubmissionsCollectionArgs): CollectionConfig => {\n\tconst defaultFields: Field[] = [\n\t\t{ name: 'form', type: 'relationship', relationTo: FORMS_SLUG, required: true },\n\t\t{\n\t\t\tname: 'status',\n\t\t\ttype: 'select',\n\t\t\tdefaultValue: 'complete',\n\t\t\toptions: [\n\t\t\t\t{ label: labelForKey(keys.statusComplete), value: 'complete' },\n\t\t\t\t{ label: labelForKey(keys.statusPartial), value: 'partial' },\n\t\t\t],\n\t\t\t// Not clearable: aggregation counts `status: complete` submissions, so a cleared status\n\t\t\t// would drop the submission out of every poll result with nothing in the UI saying so.\n\t\t\tadmin: { isClearable: false },\n\t\t\t// Defense-in-depth at the REST layer: anonymous clients cannot set status via the API.\n\t\t\t// The validateSubmission hook also forces 'complete' server-side, so this covers both paths.\n\t\t\taccess: { create: isLoggedIn, update: isLoggedIn },\n\t\t},\n\t\t// answers UI appears first so it is the dominant view when opening a submission document.\n\t\t{\n\t\t\tname: 'answers',\n\t\t\ttype: 'ui',\n\t\t\tadmin: {\n\t\t\t\tcomponents: { Field: '@10x-media/form-builder/rsc#SubmissionAnswers' },\n\t\t\t},\n\t\t},\n\t\t{ name: 'locale', type: 'text', admin: { hidden: !showRawFields } },\n\t\t{ name: 'values', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'descriptors', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'consent', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'meta', type: 'json', admin: { hidden: !showRawFields } },\n\t]\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORM_SUBMISSIONS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionSubmissionSingular),\n\t\t\tplural: labelForKey(keys.collectionSubmissionPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: {\n\t\t\tgroup: 'Forms',\n\t\t\tdefaultColumns: ['form', 'status', 'locale', 'createdAt'],\n\t\t\t...(overrides?.admin ?? {}),\n\t\t},\n\t\taccess: {\n\t\t\tcreate: () => true,\n\t\t\tread: isLoggedIn,\n\t\t\tupdate: () => false,\n\t\t\t...(overrides?.access ?? {}),\n\t\t},\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// Spam guard + validateSubmission must remain first: they enforce the security invariant\n\t\t\t// that anonymous callers cannot bypass post-submit actions or supply a forged status.\n\t\t\t// Consumer beforeValidate hooks are appended after so they run on already-validated data.\n\t\t\tbeforeValidate: [\n\t\t\t\t...(spam ? [buildSpamGuard(spam)] : []),\n\t\t\t\tvalidateSubmission({\n\t\t\t\t\tregistry,\n\t\t\t\t\truleRegistry,\n\t\t\t\t\tconsentSources,\n\t\t\t\t\tuploadSlug,\n\t\t\t\t\tpollSourceRegistry,\n\t\t\t\t}),\n\t\t\t\t...(overrides?.hooks?.beforeValidate ?? []),\n\t\t\t],\n\t\t\tafterChange: [\n\t\t\t\tmakeAfterChange({ actionRegistry, events, hasRunner, richText }),\n\t\t\t\t...(votedCookie ? [makeVotedCookieHook()] : []),\n\t\t\t\t...(overrides?.hooks?.afterChange ?? []),\n\t\t\t],\n\t\t},\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":";;;;;;;;;;AAqBA,MAAa,wBAAwB;AA8BrC,MAAM,YAAY,SAA+C;CAChE,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAC/C,OAAO;CAER,IAAI,QAAQ,OAAO,SAAS,YAAY,QAAQ,MAAM;EACrD,MAAM,KAAM,KAAyB;EACrC,IAAI,OAAO,OAAO,YAAY,OAAO,OAAO,UAC3C,OAAO;CAET;AAED;;;;;;;;;AAUA,MAAM,mBACJ,SAMD,OAAO,EAAE,KAAK,WAAW,UAAU;CAClC,IAAI,cAAc,YAAa,IAAI,UAAU,QAAQ,IAAI,WAAW,YACnE,OAAO;CAER,MAAM,EAAE,YAAY;CACpB,IAAI;EACH,MAAM,SAAS,SAAS,IAAI,IAAI;EAChC,IAAI,UAAU,MACb,OAAO;EAMR,MAAM,gBAAgB;GACrB,UAAU,MALQ,QACjB,SAAS;IAAE,YAAA;IAAwB,IAAI;IAAQ,OAAO;IAAG,gBAAgB;IAAM;GAAI,CAAC,EACpF,YAAY,IAAI,IAGD,WAAW;GAC3B;GACA,cAAc,IAAI;GAClB,UAAU,KAAK;GACf;GACA;GACA,WAAW,KAAK;GAChB,UAAU,KAAK;EAChB,CAAC;EAED,IAAI;GACH,MAAM,iBAAiB,KAAK,MAAM,EAAE,KAAK;IACxC,MAAM;IACN,QAAQ,OAAO,MAAM;IACrB,cAAc,OAAO,IAAI,EAAE;IAC3B,qBAAI,IAAI,KAAK,GAAE,YAAY;GAC5B,CAAC;EACF,SAAS,OAAO;GACf,QAAQ,QAAQ,MACf,2DACC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;EACD;CACD,SAAS,OAAO;EACf,QAAQ,QAAQ,MACf,uEAAuE,OAAO,IAAI,EAAE,EAAE,IACrF,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEvD;CACD;CACA,OAAO;AACR;;;;;;;;;AAUD,MAAM,4BAAuD;CAC5D,OAAO,OAAO,EAAE,KAAK,WAAW,UAAU;EACzC,IAAI,cAAc,UACjB,OAAO;EAER,MAAM,SAAS,SAAS,IAAI,IAAI;EAChC,IAAI,UAAU,MACb,OAAO;EAER,MAAM,UAAU,IAAI,UAAU;EAC9B,IAAI,cAAc,OAAO,YAAY,YAAY,UAAU,KAAA;EAC3D,IAAI,gBAAgB,KAAA,GAInB,eAAc,MAHK,IAAI,QACrB,SAAS;GAAE,YAAY;GAAY,IAAI;GAAQ,OAAO;GAAG,gBAAgB;GAAM;EAAI,CAAC,EACpF,YAAY,IAAI,IACE,gBAAgB;EAErC,IAAI,CAAC,aACJ,OAAO;EAER,IAAI,oBAAoB,IAAI,QAAQ;EACpC,IAAI,gBAAgB,OACnB,cACA,GAAG,gBAAgB,MAAM,EAAE,qDAC5B;EACA,OAAO;CACR;AACD;AAEA,MAAa,8BAA8B,EAC1C,UACA,cACA,gBACA,iCAAiB,IAAI,IAAI,GACzB,QACA,YAAY,OACZ,UACA,YACA,MACA,cAAc,OACd,oBACA,gBAAgB,OAChB,gBACuD;CACvD,MAAM,gBAAyB;EAC9B;GAAE,MAAM;GAAQ,MAAM;GAAgB,YAAY;GAAY,UAAU;EAAK;EAC7E;GACC,MAAM;GACN,MAAM;GACN,cAAc;GACd,SAAS,CACR;IAAE,OAAO,YAAY,KAAK,cAAc;IAAG,OAAO;GAAW,GAC7D;IAAE,OAAO,YAAY,KAAK,aAAa;IAAG,OAAO;GAAU,CAC5D;GAGA,OAAO,EAAE,aAAa,MAAM;GAG5B,QAAQ;IAAE,QAAQ;IAAY,QAAQ;GAAW;EAClD;EAEA;GACC,MAAM;GACN,MAAM;GACN,OAAO,EACN,YAAY,EAAE,OAAO,gDAAgD,EACtE;EACD;EACA;GAAE,MAAM;GAAU,MAAM;GAAQ,OAAO,EAAE,QAAQ,CAAC,cAAc;EAAE;EAClE;GAAE,MAAM;GAAU,MAAM;GAAQ,OAAO,EAAE,QAAQ,CAAC,cAAc;EAAE;EAClE;GAAE,MAAM;GAAe,MAAM;GAAQ,OAAO,EAAE,QAAQ,CAAC,cAAc;EAAE;EACvE;GAAE,MAAM;GAAW,MAAM;GAAQ,OAAO,EAAE,QAAQ,CAAC,cAAc;EAAE;EACnE;GAAE,MAAM;GAAQ,MAAM;GAAQ,OAAO,EAAE,QAAQ,CAAC,cAAc;EAAE;CACjE;CAEA,OAAO;EACN,GAAI,aAAa,CAAC;EAClB,MAAM;EACN,QAAQ;GACP,UAAU,YAAY,KAAK,4BAA4B;GACvD,QAAQ,YAAY,KAAK,0BAA0B;GACnD,GAAI,WAAW,UAAU,CAAC;EAC3B;EACA,OAAO;GACN,OAAO;GACP,gBAAgB;IAAC;IAAQ;IAAU;IAAU;GAAW;GACxD,GAAI,WAAW,SAAS,CAAC;EAC1B;EACA,QAAQ;GACP,cAAc;GACd,MAAM;GACN,cAAc;GACd,GAAI,WAAW,UAAU,CAAC;EAC3B;EACA,OAAO;GACN,GAAI,WAAW,SAAS,CAAC;GAIzB,gBAAgB;IACf,GAAI,OAAO,CAAC,eAAe,IAAI,CAAC,IAAI,CAAC;IACrC,mBAAmB;KAClB;KACA;KACA;KACA;KACA;IACD,CAAC;IACD,GAAI,WAAW,OAAO,kBAAkB,CAAC;GAC1C;GACA,aAAa;IACZ,gBAAgB;KAAE;KAAgB;KAAQ;KAAW;IAAS,CAAC;IAC/D,GAAI,cAAc,CAAC,oBAAoB,CAAC,IAAI,CAAC;IAC7C,GAAI,WAAW,OAAO,eAAe,CAAC;GACvC;EACD;EACA,QAAQ,WAAW,SAAS,UAAU,OAAO,EAAE,cAAc,CAAC,IAAI;CACnE;AACD"}
1
+ {"version":3,"file":"formSubmissions.js","names":[],"sources":["../../src/collections/formSubmissions.ts"],"sourcesContent":["import type { CollectionAfterChangeHook, CollectionConfig, Field } from 'payload'\nimport type { RichTextBodyOption } from '../actions/body/serializeBody'\nimport { dispatchActions } from '../actions/dispatch'\nimport type { ActionRegistry } from '../actions/registry'\nimport type { ActionInstance } from '../actions/runActions'\nimport type { ConsentSnapshotMode } from '../consent/captureConsent'\nimport type { ConsentSourcesResolver } from '../consent/types'\nimport { resolveEventSink } from '../events/resolveEventSink'\nimport type { FormEventSink } from '../events/types'\nimport type { FieldTypeRegistry } from '../fields/registry'\nimport { isLoggedIn } from '../plugin/access'\nimport type { CollectionOverrides } from '../plugin/collectionOverrides'\nimport type { PollOptionSourceRegistry } from '../poll/registry'\nimport { buildSpamGuard } from '../spam/spamGuard'\nimport type { ResolvedSpamConfig } from '../spam/types'\nimport { validateSubmission } from '../submissions/validateSubmission'\nimport { POLL_CONTEXT_KEY, votedCookieName } from '../submissions/votedCookie'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport { FORMS_SLUG } from './forms'\n\nexport const FORM_SUBMISSIONS_SLUG = 'form-submissions'\n\ntype BuildSubmissionsCollectionArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\t/** The host's consent sources resolver (plugin option `consent.sources`); absent when no sources are configured. */\n\tconsentSources?: ConsentSourcesResolver\n\t/** What each consent proof snapshots of the agreed wording (plugin option `consent.snapshot`). */\n\tconsentSnapshot?: ConsentSnapshotMode\n\tactionRegistry?: ActionRegistry\n\tevents?: FormEventSink\n\t/** Whether a job runner is likely present; gates the queued vs bounded-inline dispatch path. */\n\thasRunner?: boolean\n\t/** Body serialization customization forwarded to the inline action dispatch path. */\n\trichText?: RichTextBodyOption\n\t/** The plugin-configured uploads collection slug; absent when uploads are disabled. */\n\tuploadSlug?: string\n\t/** Resolved spam config; when active, prepends the spam guard before validation. `false` disables it. */\n\tspam?: ResolvedSpamConfig | false\n\t/** Opt-in (`poll.votedCookie`): set an httpOnly `fb-voted-{formId}` cookie on poll submission creates. */\n\tvotedCookie?: boolean\n\t/** Registered poll option sources; submission validation resolves allowed values through them. */\n\tpollSourceRegistry?: PollOptionSourceRegistry\n\t/**\n\t * When `true`, reveals the standalone `locale`, `values`, `descriptors`, `consent`, and `meta`\n\t * fields in the admin UI. Default `false`, because the `SubmissionAnswers` UI component already\n\t * represents them fully (locale and meta are folded into its Submission-details section).\n\t */\n\tshowRawFields?: boolean\n\toverrides?: CollectionOverrides\n}\n\nconst formIdOf = (form: unknown): number | string | undefined => {\n\tif (typeof form === 'number' || typeof form === 'string') {\n\t\treturn form\n\t}\n\tif (form && typeof form === 'object' && 'id' in form) {\n\t\tconst id = (form as { id: unknown }).id\n\t\tif (typeof id === 'number' || typeof id === 'string') {\n\t\t\treturn id\n\t\t}\n\t}\n\treturn undefined\n}\n\n/**\n * On a completed submission create, dispatch the form's post-submit actions and emit `submission.created`.\n * Both are side effects on an already-written row, so the whole body is wrapped: nothing it does can throw\n * past the hook (a failed action or sink must never fail the submission write). Dispatch is itself bounded\n * and non-throwing; the inline fallback is awaited so the bound caps the added latency, while the queued\n * path returns immediately. The form's `actions` are null-guarded for legacy rows created before the field\n * existed.\n */\nconst makeAfterChange =\n\t(args: {\n\t\tactionRegistry: ActionRegistry\n\t\tevents?: FormEventSink\n\t\thasRunner: boolean\n\t\trichText?: RichTextBodyOption\n\t}): CollectionAfterChangeHook =>\n\tasync ({ doc, operation, req }) => {\n\t\tif (operation !== 'create' || (doc.status != null && doc.status !== 'complete')) {\n\t\t\treturn doc\n\t\t}\n\t\tconst { payload } = req\n\t\ttry {\n\t\t\tconst formId = formIdOf(doc.form)\n\t\t\tif (formId == null) {\n\t\t\t\treturn doc\n\t\t\t}\n\t\t\tconst form = await payload\n\t\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t\t.catch(() => null)\n\n\t\t\tawait dispatchActions({\n\t\t\t\tactions: (form?.actions ?? null) as ActionInstance[] | null,\n\t\t\t\tformId,\n\t\t\t\tsubmissionId: doc.id as number | string,\n\t\t\t\tregistry: args.actionRegistry,\n\t\t\t\tpayload,\n\t\t\t\treq,\n\t\t\t\thasRunner: args.hasRunner,\n\t\t\t\trichText: args.richText,\n\t\t\t})\n\n\t\t\ttry {\n\t\t\t\tawait resolveEventSink(args.events).emit({\n\t\t\t\t\ttype: 'submission.created',\n\t\t\t\t\tformId: String(formId),\n\t\t\t\t\tsubmissionId: String(doc.id),\n\t\t\t\t\tat: new Date().toISOString(),\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\tpayload.logger?.error(\n\t\t\t\t\t`@10x-media/form-builder: submission.created sink threw: ${\n\t\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t\t}`\n\t\t\t\t)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tpayload.logger?.error(\n\t\t\t\t`@10x-media/form-builder: afterChange dispatch failed for submission ${String(doc.id)}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`\n\t\t\t)\n\t\t}\n\t\treturn doc\n\t}\n\n/**\n * Opt-in (`poll.votedCookie`) hook: after a submission to a poll-enabled form is created, appends\n * an httpOnly `fb-voted-{formId}=1` cookie via `req.responseHeaders`, Payload's supported channel\n * for hook-set response headers (its own auth strategies use it; `handleEndpoints` merges it into\n * every REST response). Reads the poll config `validateSubmission` stashed on `req.context`,\n * falling back to a form fetch when absent. httpOnly keeps the marker readable only server-side\n * (`hasVotedCookie`); the client `<Poll>` keeps its localStorage guard.\n */\nconst makeVotedCookieHook = (): CollectionAfterChangeHook => {\n\treturn async ({ doc, operation, req }) => {\n\t\tif (operation !== 'create') {\n\t\t\treturn doc\n\t\t}\n\t\tconst formId = formIdOf(doc.form)\n\t\tif (formId == null) {\n\t\t\treturn doc\n\t\t}\n\t\tconst stashed = req.context?.[POLL_CONTEXT_KEY]\n\t\tlet pollEnabled = typeof stashed === 'boolean' ? stashed : undefined\n\t\tif (pollEnabled === undefined) {\n\t\t\tconst form = await req.payload\n\t\t\t\t.findByID({ collection: FORMS_SLUG, id: formId, depth: 0, overrideAccess: true, req })\n\t\t\t\t.catch(() => null)\n\t\t\tpollEnabled = form?.pollEnabled === true\n\t\t}\n\t\tif (!pollEnabled) {\n\t\t\treturn doc\n\t\t}\n\t\treq.responseHeaders ??= new Headers()\n\t\treq.responseHeaders.append(\n\t\t\t'Set-Cookie',\n\t\t\t`${votedCookieName(formId)}=1; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax`\n\t\t)\n\t\treturn doc\n\t}\n}\n\nexport const buildSubmissionsCollection = ({\n\tregistry,\n\truleRegistry,\n\tconsentSources,\n\tconsentSnapshot,\n\tactionRegistry = new Map(),\n\tevents,\n\thasRunner = false,\n\trichText,\n\tuploadSlug,\n\tspam,\n\tvotedCookie = false,\n\tpollSourceRegistry,\n\tshowRawFields = false,\n\toverrides,\n}: BuildSubmissionsCollectionArgs): CollectionConfig => {\n\tconst defaultFields: Field[] = [\n\t\t{ name: 'form', type: 'relationship', relationTo: FORMS_SLUG, required: true },\n\t\t{\n\t\t\tname: 'status',\n\t\t\ttype: 'select',\n\t\t\tdefaultValue: 'complete',\n\t\t\toptions: [\n\t\t\t\t{ label: labelForKey(keys.statusComplete), value: 'complete' },\n\t\t\t\t{ label: labelForKey(keys.statusPartial), value: 'partial' },\n\t\t\t],\n\t\t\t// Not clearable: aggregation counts `status: complete` submissions, so a cleared status\n\t\t\t// would drop the submission out of every poll result with nothing in the UI saying so.\n\t\t\tadmin: { isClearable: false },\n\t\t\t// Defense-in-depth at the REST layer: anonymous clients cannot set status via the API.\n\t\t\t// The validateSubmission hook also forces 'complete' server-side, so this covers both paths.\n\t\t\taccess: { create: isLoggedIn, update: isLoggedIn },\n\t\t},\n\t\t// answers UI appears first so it is the dominant view when opening a submission document.\n\t\t{\n\t\t\tname: 'answers',\n\t\t\ttype: 'ui',\n\t\t\tadmin: {\n\t\t\t\tcomponents: { Field: '@10x-media/form-builder/rsc#SubmissionAnswers' },\n\t\t\t},\n\t\t},\n\t\t{ name: 'locale', type: 'text', admin: { hidden: !showRawFields } },\n\t\t{ name: 'values', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'descriptors', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'consent', type: 'json', admin: { hidden: !showRawFields } },\n\t\t{ name: 'meta', type: 'json', admin: { hidden: !showRawFields } },\n\t]\n\n\treturn {\n\t\t...(overrides ?? {}),\n\t\tslug: FORM_SUBMISSIONS_SLUG,\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.collectionSubmissionSingular),\n\t\t\tplural: labelForKey(keys.collectionSubmissionPlural),\n\t\t\t...(overrides?.labels ?? {}),\n\t\t},\n\t\tadmin: {\n\t\t\tgroup: 'Forms',\n\t\t\tdefaultColumns: ['form', 'status', 'locale', 'createdAt'],\n\t\t\t...(overrides?.admin ?? {}),\n\t\t},\n\t\taccess: {\n\t\t\tcreate: () => true,\n\t\t\tread: isLoggedIn,\n\t\t\tupdate: () => false,\n\t\t\t...(overrides?.access ?? {}),\n\t\t},\n\t\thooks: {\n\t\t\t...(overrides?.hooks ?? {}),\n\t\t\t// Spam guard + validateSubmission must remain first: they enforce the security invariant\n\t\t\t// that anonymous callers cannot bypass post-submit actions or supply a forged status.\n\t\t\t// Consumer beforeValidate hooks are appended after so they run on already-validated data.\n\t\t\tbeforeValidate: [\n\t\t\t\t...(spam ? [buildSpamGuard(spam)] : []),\n\t\t\t\tvalidateSubmission({\n\t\t\t\t\tregistry,\n\t\t\t\t\truleRegistry,\n\t\t\t\t\tconsentSources,\n\t\t\t\t\tconsentSnapshot,\n\t\t\t\t\tuploadSlug,\n\t\t\t\t\tpollSourceRegistry,\n\t\t\t\t}),\n\t\t\t\t...(overrides?.hooks?.beforeValidate ?? []),\n\t\t\t],\n\t\t\tafterChange: [\n\t\t\t\tmakeAfterChange({ actionRegistry, events, hasRunner, richText }),\n\t\t\t\t...(votedCookie ? [makeVotedCookieHook()] : []),\n\t\t\t\t...(overrides?.hooks?.afterChange ?? []),\n\t\t\t],\n\t\t},\n\t\tfields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields,\n\t}\n}\n"],"mappings":"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"}
@@ -3,7 +3,6 @@ import { localizedIf } from "../fields/localizedIf.js";
3
3
  import { keys } from "../translations/keys.js";
4
4
  import { asTranslate, labelFor, labelForKey } from "../translations/server.js";
5
5
  import { resolveFromAddressesRequest } from "../actions/fromAddresses.js";
6
- import { resolveDepartmentsRequest } from "../email/departments.js";
7
6
  import { validateUrl } from "../validation/validateUrl.js";
8
7
  import { normalizeCalc } from "../calc/normalizeCalc.js";
9
8
  import { isLoggedIn } from "../plugin/access.js";
@@ -14,6 +13,8 @@ import { resolveFormResultsRequest } from "../aggregation/resolveResultsRequest.
14
13
  import { buildConditionTypeMap } from "../conditions/conditionType.js";
15
14
  import { normalizeFormConditions } from "../conditions/normalizeConditions.js";
16
15
  import { resolveConsentSourcesRequest } from "../consent/resolveConsentSourcesRequest.js";
16
+ import { resolveConsentStatements } from "../consent/resolveConsentStatements.js";
17
+ import { resolveDepartmentsRequest } from "../email/departments.js";
17
18
  import { buildFieldBlocks } from "../fields/buildFieldBlocks.js";
18
19
  import { normalizeFlow } from "../flow/normalizeFlow.js";
19
20
  import { END_OF_FORM } from "../flow/types.js";
@@ -411,6 +412,20 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
411
412
  ]
412
413
  }
413
414
  ];
415
+ const consentAfterRead = consentSources ? async ({ doc, req }) => {
416
+ try {
417
+ const statements = await resolveConsentStatements({
418
+ payload: req.payload,
419
+ req,
420
+ form: doc,
421
+ sources: consentSources
422
+ });
423
+ if (Object.keys(statements).length > 0) doc.consentStatements = statements;
424
+ } catch (error) {
425
+ req.payload.logger?.warn(`form-builder consent afterRead: ${String(error)}`);
426
+ }
427
+ return doc;
428
+ } : void 0;
414
429
  const defaultEndpoints = [
415
430
  {
416
431
  path: "/:id/results",
@@ -522,7 +537,8 @@ const buildFormsCollection = ({ overrides, registry, ruleRegistry, consentSource
522
537
  ...overrides?.hooks ?? {},
523
538
  beforeValidate: [beforeValidate, ...overrides?.hooks?.beforeValidate ?? []],
524
539
  beforeChange: [pollOutcomeBeforeChange, ...overrides?.hooks?.beforeChange ?? []],
525
- afterChange: [pollCloseAfterChange, ...overrides?.hooks?.afterChange ?? []]
540
+ afterChange: [pollCloseAfterChange, ...overrides?.hooks?.afterChange ?? []],
541
+ afterRead: [...consentAfterRead ? [consentAfterRead] : [], ...overrides?.hooks?.afterRead ?? []]
526
542
  },
527
543
  endpoints: [...defaultEndpoints, ...Array.isArray(overrides?.endpoints) ? overrides.endpoints : []],
528
544
  fields: overrides?.fields ? overrides.fields({ defaultFields }) : defaultFields