@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.
- package/CHANGELOG.md +29 -0
- package/dist/actions/builtin/confirmation.js +75 -72
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailTeam.js +31 -41
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/index.d.ts +2 -1
- package/dist/actions/builtin/index.js +3 -3
- package/dist/actions/builtin/index.js.map +1 -1
- package/dist/actions/emailRecipients.d.ts +12 -0
- package/dist/actions/emailRecipients.js +72 -0
- package/dist/actions/emailRecipients.js.map +1 -0
- package/dist/client/ConsentBlockLabel.d.ts +15 -0
- package/dist/client/ConsentBlockLabel.js +71 -0
- package/dist/client/ConsentBlockLabel.js.map +1 -0
- package/dist/client/ConsentSourceRowLabel.d.ts +9 -0
- package/dist/client/ConsentSourceRowLabel.js +21 -0
- package/dist/client/ConsentSourceRowLabel.js.map +1 -0
- package/dist/client/EndpointOptionsSelect.d.ts +1 -0
- package/dist/client/EndpointOptionsSelect.js +4 -1
- package/dist/client/EndpointOptionsSelect.js.map +1 -1
- package/dist/client/FieldNameSelect.d.ts +1 -0
- package/dist/client/FieldNameSelect.js +4 -1
- package/dist/client/FieldNameSelect.js.map +1 -1
- package/dist/client/RecipientsSelect.d.ts +31 -0
- package/dist/client/RecipientsSelect.js +174 -0
- package/dist/client/RecipientsSelect.js.map +1 -0
- package/dist/client/admin.css +5 -0
- package/dist/collections/formSubmissions.js +2 -1
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +18 -2
- package/dist/collections/forms.js.map +1 -1
- package/dist/consent/captureConsent.d.ts +14 -5
- package/dist/consent/captureConsent.js +24 -7
- package/dist/consent/captureConsent.js.map +1 -1
- package/dist/consent/consentSourcesField.d.ts +4 -0
- package/dist/consent/consentSourcesField.js +8 -1
- package/dist/consent/consentSourcesField.js.map +1 -1
- package/dist/consent/types.d.ts +4 -0
- package/dist/email/departments.d.ts +13 -9
- package/dist/email/departments.js +7 -50
- package/dist/email/departments.js.map +1 -1
- package/dist/email/departmentsField.d.ts +4 -0
- package/dist/email/departmentsField.js +4 -0
- package/dist/email/departmentsField.js.map +1 -1
- package/dist/exports/client.d.ts +4 -1
- package/dist/exports/client.js +4 -1
- package/dist/exports/react.js +1 -1
- package/dist/exports/rsc.js +1 -1
- package/dist/fields/buildFieldBlocks.js +4 -4
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/consent.js +6 -1
- package/dist/fields/builtin/consent.js.map +1 -1
- package/dist/fields/builtin/file.js +9 -3
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +1 -1
- package/dist/fields/builtin/index.js +3 -3
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/types.d.ts +9 -2
- package/dist/form/toFormDocument.d.ts +20 -3
- package/dist/form/toFormDocument.js +16 -4
- package/dist/form/toFormDocument.js.map +1 -1
- package/dist/form/types.d.ts +5 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/plugin/registerCollections.js +2 -1
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/plugin/uploadsCollection.d.ts +3 -0
- package/dist/plugin/uploadsCollection.js +10 -1
- package/dist/plugin/uploadsCollection.js.map +1 -1
- package/dist/submissions/SubmissionAnswers.js.map +1 -1
- package/dist/submissions/SubmissionAnswersClient.js +33 -26
- package/dist/submissions/SubmissionAnswersClient.js.map +1 -1
- package/dist/submissions/runSubmission.js +3 -2
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/validateSubmission.js +2 -1
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/translations/de.js +4 -2
- package/dist/translations/de.js.map +1 -1
- package/dist/translations/en.js +4 -2
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +4 -2
- package/dist/translations/keys.js +4 -2
- package/dist/translations/keys.js.map +1 -1
- package/dist/uploads/resolveFileRef.js +2 -1
- package/dist/uploads/resolveFileRef.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"departments.js","names":[],"sources":["../../src/email/departments.ts"],"sourcesContent":["import type { Payload, PayloadRequest, TextField } from 'payload'\nimport { localizedIf } from '../fields/localizedIf'\nimport { keys } from '../translations/keys'\nimport { asTranslate, labelFor } from '../translations/server'\n\n/** One selectable department for the `emailTeam` action; `value` is the email address routed to. */\nexport type DepartmentOption = { label: string; value: string }\n\n/**\n * Host seam resolving the selectable departments for `emailTeam`'s `to` (plugin option\n * `email.departments`). Req-scoped, so multi-tenant hosts derive tenant scoping from `req` (host\n * header, cookie, or auth context) and return only that tenant's departments. `value` is the literal\n * string `payload.sendEmail` accepts as `to` (an email address); `label` is how the department is\n * named in the admin select. Mirrors {@link FromAddressesResolver}. {@link resolveDepartmentOptions}\n * builds this from a `departmentsField()`-bearing document with the reusable locale fallback, so a\n * host does not reimplement it.\n */\nexport type DepartmentEmailsResolver = (args: {\n\treq: PayloadRequest\n}) => Promise<DepartmentOption[]> | DepartmentOption[]\n\nconst TO_FIELD_REF = '@10x-media/form-builder/client#EndpointOptionsSelect'\n\n/**\n * Validate for the `to` field, closed over the host resolver (mirrors `validateFromField`): unset is\n * fine, otherwise the value must be one of the resolver's options for this request. A throwing\n * resolver fails closed with a translated message rather than surfacing a raw error on save.\n *\n * Payload runs this on every save, not only when `to` changed, so while the resolver is down no form\n * carrying an email-team action with a `to` set can be saved at all, including edits that never touch\n * the address. That is the deliberate trade: failing open would persist a routing target the host can\n * no longer vouch for. A resolver reaching a flaky upstream should cache or fall back internally\n * rather than throw.\n */\nexport const validateToField =\n\t(resolver: DepartmentEmailsResolver) =>\n\tasync (value: unknown, { req }: { req: PayloadRequest }): Promise<string | true> => {\n\t\tif (typeof value !== 'string' || value.length === 0) {\n\t\t\treturn true\n\t\t}\n\t\tlet options: DepartmentOption[]\n\t\ttry {\n\t\t\toptions = await resolver({ req })\n\t\t} catch {\n\t\t\treturn asTranslate(req.t)(keys.validationToUnavailable)\n\t\t}\n\t\treturn options.some((option) => option.value === value)\n\t\t\t? true\n\t\t\t: asTranslate(req.t)(keys.validationToUnknown)\n\t}\n\n/**\n * The `emailTeam` `to` select when `email.departments` is set: an `EndpointOptionsSelect` backed by\n * the forms collection's `/:id/departments` endpoint (registered only then). Localized (following\n * `localizeContent`), so each admin locale stores its own resolved department address and the\n * submission's locale selects the stored `to` at send. Mirrors `buildFromField`; the route's document\n * id goes unused server-side (the option set is request-scoped, not per-form).\n */\nexport const buildToField = (\n\tresolver: DepartmentEmailsResolver,\n\tlocalizeContent: boolean\n): TextField => ({\n\tname: 'to',\n\ttype: 'text',\n\tlabel: labelFor(keys.actionConfigTo),\n\tvalidate: validateToField(resolver),\n\t...localizedIf(localizeContent),\n\tadmin: {\n\t\tcomponents: {\n\t\t\tField: {\n\t\t\t\tpath: TO_FIELD_REF,\n\t\t\t\tclientProps: {\n\t\t\t\t\tendpoint: 'departments',\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n})\n\nexport type ResolveDepartmentsRequestArgs = {\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq: PayloadRequest\n\tresolver: DepartmentEmailsResolver\n}\n\nexport type ResolveDepartmentsRequestResult = {\n\tstatus: number\n\tbody: { options: DepartmentOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/departments` request backing the `to` select: authenticated\n * callers get the host resolver's current options for this request; anonymous callers are always\n * refused. The route id is unused (see `buildToField`). Statuses mirror `resolveFromAddressesRequest`:\n * 403 unauthenticated, 503 when the resolver throws (fail closed).\n */\nexport const resolveDepartmentsRequest = async (\n\targs: ResolveDepartmentsRequestArgs\n): Promise<ResolveDepartmentsRequestResult> => {\n\tconst { isAuthed, req, resolver } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolver({ req })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'Departments unavailable' }] } }\n\t}\n}\n\n/**\n * Resolve one localized subfield value for `reqLocale` with the current -> default -> next-available\n * chain. A non-localized subfield (fetched at `locale: 'all'` on a host without localization) is a\n * bare string, used as-is; a localized one is a `{ [locale]: value }` map, from which `reqLocale`,\n * then `defaultLocale`, then the first non-empty configured locale wins. Empty strings are skipped\n * (they fall through the chain), and undefined is returned when nothing resolves.\n */\n// biome-ignore lint/complexity/useMaxParams: the locale-fallback inputs are the minimal surface (value, reqLocale, defaultLocale, localeCodes)\nexport const pickLocaleValue = (\n\tvalue: unknown,\n\treqLocale: string | undefined,\n\tdefaultLocale: string | undefined,\n\tlocaleCodes: string[]\n): string | undefined => {\n\tif (typeof value === 'string') {\n\t\treturn value.length > 0 ? value : undefined\n\t}\n\tif (value == null || typeof value !== 'object') {\n\t\treturn undefined\n\t}\n\tconst map = value as Record<string, unknown>\n\tconst nonEmpty = (entry: unknown): string | undefined =>\n\t\ttypeof entry === 'string' && entry.length > 0 ? entry : undefined\n\tconst ordered = [reqLocale, defaultLocale, ...localeCodes].filter(\n\t\t(code): code is string => typeof code === 'string' && code.length > 0\n\t)\n\tfor (const code of ordered) {\n\t\tconst hit = nonEmpty(map[code])\n\t\tif (hit !== undefined) {\n\t\t\treturn hit\n\t\t}\n\t}\n\t// Defensive: a `locale: 'all'` fetch keys by configured locales, but a key outside them still\n\t// counts toward \"next available\" so a value is never silently dropped.\n\tfor (const key of Object.keys(map)) {\n\t\tconst hit = nonEmpty(map[key])\n\t\tif (hit !== undefined) {\n\t\t\treturn hit\n\t\t}\n\t}\n\treturn undefined\n}\n\nexport type ResolveDepartmentOptionsArgs = {\n\tpayload: Payload\n\treq: PayloadRequest\n\t/** A document fetched at `locale: 'all'`, so each localized subfield is a `{ locale: value }` map. */\n\tdoc: Record<string, unknown>\n\t/** The array field name the departments live under; defaults to `departmentEmails`. */\n\tfield?: string\n}\n\n/**\n * Build the `emailTeam` `to` options from a `departmentsField()`-bearing document (a settings global,\n * a tenants row, wherever the host placed it), fetched at `locale: 'all'`. Each row's `label` and\n * `email` are resolved for `req.locale` via the current -> default -> next-available chain (see\n * {@link pickLocaleValue}), reading `defaultLocale`/`localeCodes` from `payload.config.localization`\n * (a bare string is used as-is when localization is disabled). Rows with no resolvable email are\n * dropped; a row's label is resolved independently of its email and falls back to the email when\n * absent. This is the reusable seam a host's `email.departments` resolver returns, so tenant scoping\n * (which document is read) stays the host's call while the locale fallback stays consistent.\n */\nexport const resolveDepartmentOptions = (\n\targs: ResolveDepartmentOptionsArgs\n): DepartmentOption[] => {\n\tconst { payload, req, doc, field } = args\n\tconst rows = doc[field ?? 'departmentEmails']\n\tif (!Array.isArray(rows)) {\n\t\treturn []\n\t}\n\tconst localization = payload.config.localization\n\tconst localeCodes = localization ? localization.localeCodes : []\n\tconst defaultLocale = localization ? localization.defaultLocale : undefined\n\tconst reqLocale = typeof req.locale === 'string' ? req.locale : undefined\n\tconst options: DepartmentOption[] = []\n\tfor (const row of rows) {\n\t\tif (row == null || typeof row !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tconst entry = row as Record<string, unknown>\n\t\tconst email = pickLocaleValue(entry.email, reqLocale, defaultLocale, localeCodes)\n\t\tif (email === undefined) {\n\t\t\tcontinue\n\t\t}\n\t\tconst label = pickLocaleValue(entry.label, reqLocale, defaultLocale, localeCodes) ?? email\n\t\toptions.push({ label, value: email })\n\t}\n\treturn options\n}\n"],"mappings":";;;;AAqBA,MAAM,eAAe;;;;;;;;;;;;AAarB,MAAa,mBACX,aACD,OAAO,OAAgB,EAAE,UAA2D;CACnF,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAER,IAAI;CACJ,IAAI;EACH,UAAU,MAAM,SAAS,EAAE,IAAI,CAAC;CACjC,QAAQ;EACP,OAAO,YAAY,IAAI,CAAC,EAAE,KAAK,uBAAuB;CACvD;CACA,OAAO,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK,IACnD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,mBAAmB;AAC/C;;;;;;;;AASD,MAAa,gBACZ,UACA,qBACgB;CAChB,MAAM;CACN,MAAM;CACN,OAAO,SAAS,KAAK,cAAc;CACnC,UAAU,gBAAgB,QAAQ;CAClC,GAAG,YAAY,eAAe;CAC9B,OAAO,EACN,YAAY,EACX,OAAO;EACN,MAAM;EACN,aAAa,EACZ,UAAU,cACX;CACD,EACD,EACD;AACD;;;;;;;AAoBA,MAAa,4BAA4B,OACxC,SAC8C;CAC9C,MAAM,EAAE,UAAU,KAAK,aAAa;CACpC,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAEH,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAA,MADR,SAAS,EAAE,IAAI,CAAC,EACA;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;EAAE;CAClF;AACD;;;;;;;;AAUA,MAAa,mBACZ,OACA,WACA,eACA,gBACwB;CACxB,IAAI,OAAO,UAAU,UACpB,OAAO,MAAM,SAAS,IAAI,QAAQ,KAAA;CAEnC,IAAI,SAAS,QAAQ,OAAO,UAAU,UACrC;CAED,MAAM,MAAM;CACZ,MAAM,YAAY,UACjB,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA;CACzD,MAAM,UAAU;EAAC;EAAW;EAAe,GAAG;CAAW,EAAE,QACzD,SAAyB,OAAO,SAAS,YAAY,KAAK,SAAS,CACrE;CACA,KAAK,MAAM,QAAQ,SAAS;EAC3B,MAAM,MAAM,SAAS,IAAI,KAAK;EAC9B,IAAI,QAAQ,KAAA,GACX,OAAO;CAET;CAGA,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG,GAAG;EACnC,MAAM,MAAM,SAAS,IAAI,IAAI;EAC7B,IAAI,QAAQ,KAAA,GACX,OAAO;CAET;AAED;;;;;;;;;;;AAqBA,MAAa,4BACZ,SACwB;CACxB,MAAM,EAAE,SAAS,KAAK,KAAK,UAAU;CACrC,MAAM,OAAO,IAAI,SAAS;CAC1B,IAAI,CAAC,MAAM,QAAQ,IAAI,GACtB,OAAO,CAAC;CAET,MAAM,eAAe,QAAQ,OAAO;CACpC,MAAM,cAAc,eAAe,aAAa,cAAc,CAAC;CAC/D,MAAM,gBAAgB,eAAe,aAAa,gBAAgB,KAAA;CAClE,MAAM,YAAY,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS,KAAA;CAChE,MAAM,UAA8B,CAAC;CACrC,KAAK,MAAM,OAAO,MAAM;EACvB,IAAI,OAAO,QAAQ,OAAO,QAAQ,UACjC;EAED,MAAM,QAAQ;EACd,MAAM,QAAQ,gBAAgB,MAAM,OAAO,WAAW,eAAe,WAAW;EAChF,IAAI,UAAU,KAAA,GACb;EAED,MAAM,QAAQ,gBAAgB,MAAM,OAAO,WAAW,eAAe,WAAW,KAAK;EACrF,QAAQ,KAAK;GAAE;GAAO,OAAO;EAAM,CAAC;CACrC;CACA,OAAO;AACR"}
|
|
1
|
+
{"version":3,"file":"departments.js","names":[],"sources":["../../src/email/departments.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\n/** One selectable department for the `emailTeam` recipients; `value` is the email address routed to. */\nexport type DepartmentOption = { label: string; value: string }\n\n/**\n * Host seam resolving the selectable departments for `emailTeam`'s recipient fields (plugin option\n * `email.departments`). Req-scoped, so multi-tenant hosts derive tenant scoping from `req` (host\n * header, cookie, or auth context) and return only that tenant's departments. `value` is the literal\n * string `payload.sendEmail` accepts (an email address); `label` is how the department is named in the\n * admin select. Mirrors {@link FromAddressesResolver}. {@link resolveDepartmentOptions} builds this\n * from a `departmentsField()`-bearing document with the reusable locale fallback, so a host does not\n * reimplement it.\n *\n * Do not thread the save's `req` into a `find({ locale: 'all' })` from within a form's save\n * validation: it flips `req.locale` to `all` and corrupts the localized write of the document being\n * saved. Read the settings document without the save's `req` (already-committed data, no transaction).\n */\nexport type DepartmentEmailsResolver = (args: {\n\treq: PayloadRequest\n}) => Promise<DepartmentOption[]> | DepartmentOption[]\n\nexport type ResolveDepartmentsRequestArgs = {\n\t/** Whether the caller is authenticated (an admin/user). */\n\tisAuthed: boolean\n\treq: PayloadRequest\n\tresolver: DepartmentEmailsResolver\n}\n\nexport type ResolveDepartmentsRequestResult = {\n\tstatus: number\n\tbody: { options: DepartmentOption[] } | { errors: { message: string }[] }\n}\n\n/**\n * Authorize and resolve the `GET /:id/departments` request backing the recipient selects: authenticated\n * callers get the host resolver's current options for this request; anonymous callers are always\n * refused. The route id is unused (the option set is request-scoped, not per-form). Statuses mirror\n * `resolveFromAddressesRequest`: 403 unauthenticated, 503 when the resolver throws (fail closed).\n */\nexport const resolveDepartmentsRequest = async (\n\targs: ResolveDepartmentsRequestArgs\n): Promise<ResolveDepartmentsRequestResult> => {\n\tconst { isAuthed, req, resolver } = args\n\tif (!isAuthed) {\n\t\treturn { status: 403, body: { errors: [{ message: 'Forbidden' }] } }\n\t}\n\ttry {\n\t\tconst options = await resolver({ req })\n\t\treturn { status: 200, body: { options } }\n\t} catch {\n\t\treturn { status: 503, body: { errors: [{ message: 'Departments unavailable' }] } }\n\t}\n}\n\n/**\n * Resolve one localized subfield value for `reqLocale` with the current -> default -> next-available\n * chain. A non-localized subfield (fetched at `locale: 'all'` on a host without localization) is a\n * bare string, used as-is; a localized one is a `{ [locale]: value }` map, from which `reqLocale`,\n * then `defaultLocale`, then the first non-empty configured locale wins. Empty strings are skipped\n * (they fall through the chain), and undefined is returned when nothing resolves.\n */\n// biome-ignore lint/complexity/useMaxParams: the locale-fallback inputs are the minimal surface (value, reqLocale, defaultLocale, localeCodes)\nexport const pickLocaleValue = (\n\tvalue: unknown,\n\treqLocale: string | undefined,\n\tdefaultLocale: string | undefined,\n\tlocaleCodes: string[]\n): string | undefined => {\n\tif (typeof value === 'string') {\n\t\treturn value.length > 0 ? value : undefined\n\t}\n\tif (value == null || typeof value !== 'object') {\n\t\treturn undefined\n\t}\n\tconst map = value as Record<string, unknown>\n\tconst nonEmpty = (entry: unknown): string | undefined =>\n\t\ttypeof entry === 'string' && entry.length > 0 ? entry : undefined\n\tconst ordered = [reqLocale, defaultLocale, ...localeCodes].filter(\n\t\t(code): code is string => typeof code === 'string' && code.length > 0\n\t)\n\tfor (const code of ordered) {\n\t\tconst hit = nonEmpty(map[code])\n\t\tif (hit !== undefined) {\n\t\t\treturn hit\n\t\t}\n\t}\n\t// Defensive: a `locale: 'all'` fetch keys by configured locales, but a key outside them still\n\t// counts toward \"next available\" so a value is never silently dropped.\n\tfor (const key of Object.keys(map)) {\n\t\tconst hit = nonEmpty(map[key])\n\t\tif (hit !== undefined) {\n\t\t\treturn hit\n\t\t}\n\t}\n\treturn undefined\n}\n\nexport type ResolveDepartmentOptionsArgs = {\n\tpayload: Payload\n\treq: PayloadRequest\n\t/** A document fetched at `locale: 'all'`, so each localized subfield is a `{ locale: value }` map. */\n\tdoc: Record<string, unknown>\n\t/** The array field name the departments live under; defaults to `departmentEmails`. */\n\tfield?: string\n}\n\n/**\n * Build the `emailTeam` recipient options from a `departmentsField()`-bearing document (a settings\n * global, a tenants row, wherever the host placed it), fetched at `locale: 'all'`. Each row's `label`\n * and `email` are resolved for `req.locale` via the current -> default -> next-available chain (see\n * {@link pickLocaleValue}), reading `defaultLocale`/`localeCodes` from `payload.config.localization`\n * (a bare string is used as-is when localization is disabled). Rows with no resolvable email are\n * dropped; a row's label is resolved independently of its email and falls back to the email when\n * absent. This is the reusable seam a host's `email.departments` resolver returns, so tenant scoping\n * (which document is read) stays the host's call while the locale fallback stays consistent.\n */\nexport const resolveDepartmentOptions = (\n\targs: ResolveDepartmentOptionsArgs\n): DepartmentOption[] => {\n\tconst { payload, req, doc, field } = args\n\tconst rows = doc[field ?? 'departmentEmails']\n\tif (!Array.isArray(rows)) {\n\t\treturn []\n\t}\n\tconst localization = payload.config.localization\n\tconst localeCodes = localization ? localization.localeCodes : []\n\tconst defaultLocale = localization ? localization.defaultLocale : undefined\n\tconst reqLocale = typeof req.locale === 'string' ? req.locale : undefined\n\tconst options: DepartmentOption[] = []\n\tfor (const row of rows) {\n\t\tif (row == null || typeof row !== 'object') {\n\t\t\tcontinue\n\t\t}\n\t\tconst entry = row as Record<string, unknown>\n\t\tconst email = pickLocaleValue(entry.email, reqLocale, defaultLocale, localeCodes)\n\t\tif (email === undefined) {\n\t\t\tcontinue\n\t\t}\n\t\tconst label = pickLocaleValue(entry.label, reqLocale, defaultLocale, localeCodes) ?? email\n\t\toptions.push({ label, value: email })\n\t}\n\treturn options\n}\n"],"mappings":";;;;;;;AAwCA,MAAa,4BAA4B,OACxC,SAC8C;CAC9C,MAAM,EAAE,UAAU,KAAK,aAAa;CACpC,IAAI,CAAC,UACJ,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,YAAY,CAAC,EAAE;CAAE;CAEpE,IAAI;EAEH,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,SAAA,MADR,SAAS,EAAE,IAAI,CAAC,EACA;EAAE;CACzC,QAAQ;EACP,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;EAAE;CAClF;AACD;;;;;;;;AAUA,MAAa,mBACZ,OACA,WACA,eACA,gBACwB;CACxB,IAAI,OAAO,UAAU,UACpB,OAAO,MAAM,SAAS,IAAI,QAAQ,KAAA;CAEnC,IAAI,SAAS,QAAQ,OAAO,UAAU,UACrC;CAED,MAAM,MAAM;CACZ,MAAM,YAAY,UACjB,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA;CACzD,MAAM,UAAU;EAAC;EAAW;EAAe,GAAG;CAAW,EAAE,QACzD,SAAyB,OAAO,SAAS,YAAY,KAAK,SAAS,CACrE;CACA,KAAK,MAAM,QAAQ,SAAS;EAC3B,MAAM,MAAM,SAAS,IAAI,KAAK;EAC9B,IAAI,QAAQ,KAAA,GACX,OAAO;CAET;CAGA,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG,GAAG;EACnC,MAAM,MAAM,SAAS,IAAI,IAAI;EAC7B,IAAI,QAAQ,KAAA,GACX,OAAO;CAET;AAED;;;;;;;;;;;AAqBA,MAAa,4BACZ,SACwB;CACxB,MAAM,EAAE,SAAS,KAAK,KAAK,UAAU;CACrC,MAAM,OAAO,IAAI,SAAS;CAC1B,IAAI,CAAC,MAAM,QAAQ,IAAI,GACtB,OAAO,CAAC;CAET,MAAM,eAAe,QAAQ,OAAO;CACpC,MAAM,cAAc,eAAe,aAAa,cAAc,CAAC;CAC/D,MAAM,gBAAgB,eAAe,aAAa,gBAAgB,KAAA;CAClE,MAAM,YAAY,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS,KAAA;CAChE,MAAM,UAA8B,CAAC;CACrC,KAAK,MAAM,OAAO,MAAM;EACvB,IAAI,OAAO,QAAQ,OAAO,QAAQ,UACjC;EAED,MAAM,QAAQ;EACd,MAAM,QAAQ,gBAAgB,MAAM,OAAO,WAAW,eAAe,WAAW;EAChF,IAAI,UAAU,KAAA,GACb;EAED,MAAM,QAAQ,gBAAgB,MAAM,OAAO,WAAW,eAAe,WAAW,KAAK;EACrF,QAAQ,KAAK;GAAE;GAAO,OAAO;EAAM,CAAC;CACrC;CACA,OAAO;AACR"}
|
|
@@ -42,6 +42,10 @@ type DepartmentsFieldOptions = {
|
|
|
42
42
|
* },
|
|
43
43
|
* })
|
|
44
44
|
* ```
|
|
45
|
+
*
|
|
46
|
+
* The read above omits the save's `req` on purpose: threading it into a `locale: 'all'` read from
|
|
47
|
+
* within a form's save validation flips `req.locale` to `all` and corrupts the localized write of the
|
|
48
|
+
* document being saved. It reads already-committed data, so no transaction (`req`) is needed.
|
|
45
49
|
*/
|
|
46
50
|
declare const departmentsField: (options?: DepartmentsFieldOptions) => ArrayField;
|
|
47
51
|
//#endregion
|
|
@@ -33,6 +33,10 @@ import { labelForKey } from "../translations/server.js";
|
|
|
33
33
|
* },
|
|
34
34
|
* })
|
|
35
35
|
* ```
|
|
36
|
+
*
|
|
37
|
+
* The read above omits the save's `req` on purpose: threading it into a `locale: 'all'` read from
|
|
38
|
+
* within a form's save validation flips `req.locale` to `all` and corrupts the localized write of the
|
|
39
|
+
* document being saved. It reads already-committed data, so no transaction (`req`) is needed.
|
|
36
40
|
*/
|
|
37
41
|
const departmentsField = (options = {}) => {
|
|
38
42
|
const localize = options.localized !== false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"departmentsField.js","names":[],"sources":["../../src/email/departmentsField.ts"],"sourcesContent":["import type { ArrayField } from 'payload'\nimport { localizedIf } from '../fields/localizedIf'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\n\nexport type DepartmentsFieldOptions = {\n\t/** Field name, i.e. what the row array is stored under. Defaults to `departmentEmails`. */\n\tname?: string\n\t/** Field label. Defaults to the plugin's translated one. */\n\tlabel?: ArrayField['label']\n\t/**\n\t * Whether each row's `email` (and its `label`) carry `localized: true`, so the same field can\n\t * hold a different address per locale. Default `true`; Payload strips the flag on hosts without\n\t * `localization`, so it is safe either way.\n\t */\n\tlocalized?: boolean\n}\n\n/**\n * The department-emails array, for the host to place on any collection or global they own: a\n * settings global, a tenants collection, a departments parent, wherever the addresses belong. The\n * plugin never registers it and never guesses where it lives; the `email.departments` resolver reads\n * it back, which is also where multi-tenant scoping goes: place this on the tenant-scoped document\n * and have the resolver return only that tenant's addresses, derived from `req`.\n *\n * A row is a `label` (how the address is named when routing a submission to it) and the `email` it\n * resolves to. Because `email` is `localized` by default, a host can hold a different address per\n * locale without changing the stored shape. The array itself is not localized, so per-locale edits\n * must address existing rows by their own auto-assigned `id` rather than re-sending id-less rows.\n *\n * It renders condensed: `admin.components.Field` is {@link CondensedArray}, which lays each row out\n * flat as `[label, email]` at 50% each with no per-row collapsible, while keeping Payload's native\n * per-locale storage and row ids.\n *\n * ```ts\n * // The host's own collection or global:\n * fields: [departmentsField()]\n *\n * // The plugin, reading it back to route a submission. `resolveDepartmentOptions` reads the array\n * // (fetched at `locale: 'all'`) and resolves each row's address for the requesting locale:\n * formBuilder({\n * email: {\n * departments: async ({ req }) => {\n * const settings = await req.payload.findGlobal({ slug: 'settings', depth: 0, locale: 'all' })\n * return resolveDepartmentOptions({ payload: req.payload, req, doc: settings })\n * },\n * },\n * })\n * ```\n */\nexport const departmentsField = (options: DepartmentsFieldOptions = {}): ArrayField => {\n\tconst localize = options.localized !== false\n\n\treturn {\n\t\tname: options.name ?? 'departmentEmails',\n\t\ttype: 'array',\n\t\tlabel: options.label ?? labelForKey(keys.departmentsField),\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.departmentSingular),\n\t\t\tplural: labelForKey(keys.departmentPlural),\n\t\t},\n\t\tadmin: {\n\t\t\tdescription: labelForKey(keys.departmentsFieldDescription),\n\t\t\tcomponents: { Field: '@10x-media/form-builder/client#CondensedArray' },\n\t\t},\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'label',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.departmentLabel),\n\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'email',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.departmentEmail),\n\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t],\n\t}\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"departmentsField.js","names":[],"sources":["../../src/email/departmentsField.ts"],"sourcesContent":["import type { ArrayField } from 'payload'\nimport { localizedIf } from '../fields/localizedIf'\nimport { keys } from '../translations/keys'\nimport { labelForKey } from '../translations/server'\n\nexport type DepartmentsFieldOptions = {\n\t/** Field name, i.e. what the row array is stored under. Defaults to `departmentEmails`. */\n\tname?: string\n\t/** Field label. Defaults to the plugin's translated one. */\n\tlabel?: ArrayField['label']\n\t/**\n\t * Whether each row's `email` (and its `label`) carry `localized: true`, so the same field can\n\t * hold a different address per locale. Default `true`; Payload strips the flag on hosts without\n\t * `localization`, so it is safe either way.\n\t */\n\tlocalized?: boolean\n}\n\n/**\n * The department-emails array, for the host to place on any collection or global they own: a\n * settings global, a tenants collection, a departments parent, wherever the addresses belong. The\n * plugin never registers it and never guesses where it lives; the `email.departments` resolver reads\n * it back, which is also where multi-tenant scoping goes: place this on the tenant-scoped document\n * and have the resolver return only that tenant's addresses, derived from `req`.\n *\n * A row is a `label` (how the address is named when routing a submission to it) and the `email` it\n * resolves to. Because `email` is `localized` by default, a host can hold a different address per\n * locale without changing the stored shape. The array itself is not localized, so per-locale edits\n * must address existing rows by their own auto-assigned `id` rather than re-sending id-less rows.\n *\n * It renders condensed: `admin.components.Field` is {@link CondensedArray}, which lays each row out\n * flat as `[label, email]` at 50% each with no per-row collapsible, while keeping Payload's native\n * per-locale storage and row ids.\n *\n * ```ts\n * // The host's own collection or global:\n * fields: [departmentsField()]\n *\n * // The plugin, reading it back to route a submission. `resolveDepartmentOptions` reads the array\n * // (fetched at `locale: 'all'`) and resolves each row's address for the requesting locale:\n * formBuilder({\n * email: {\n * departments: async ({ req }) => {\n * const settings = await req.payload.findGlobal({ slug: 'settings', depth: 0, locale: 'all' })\n * return resolveDepartmentOptions({ payload: req.payload, req, doc: settings })\n * },\n * },\n * })\n * ```\n *\n * The read above omits the save's `req` on purpose: threading it into a `locale: 'all'` read from\n * within a form's save validation flips `req.locale` to `all` and corrupts the localized write of the\n * document being saved. It reads already-committed data, so no transaction (`req`) is needed.\n */\nexport const departmentsField = (options: DepartmentsFieldOptions = {}): ArrayField => {\n\tconst localize = options.localized !== false\n\n\treturn {\n\t\tname: options.name ?? 'departmentEmails',\n\t\ttype: 'array',\n\t\tlabel: options.label ?? labelForKey(keys.departmentsField),\n\t\tlabels: {\n\t\t\tsingular: labelForKey(keys.departmentSingular),\n\t\t\tplural: labelForKey(keys.departmentPlural),\n\t\t},\n\t\tadmin: {\n\t\t\tdescription: labelForKey(keys.departmentsFieldDescription),\n\t\t\tcomponents: { Field: '@10x-media/form-builder/client#CondensedArray' },\n\t\t},\n\t\tfields: [\n\t\t\t{\n\t\t\t\tname: 'label',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.departmentLabel),\n\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'email',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelForKey(keys.departmentEmail),\n\t\t\t\tadmin: { width: '50%' },\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t],\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAa,oBAAoB,UAAmC,CAAC,MAAkB;CACtF,MAAM,WAAW,QAAQ,cAAc;CAEvC,OAAO;EACN,MAAM,QAAQ,QAAQ;EACtB,MAAM;EACN,OAAO,QAAQ,SAAS,YAAY,KAAK,gBAAgB;EACzD,QAAQ;GACP,UAAU,YAAY,KAAK,kBAAkB;GAC7C,QAAQ,YAAY,KAAK,gBAAgB;EAC1C;EACA,OAAO;GACN,aAAa,YAAY,KAAK,2BAA2B;GACzD,YAAY,EAAE,OAAO,gDAAgD;EACtE;EACA,QAAQ,CACP;GACC,MAAM;GACN,MAAM;GACN,OAAO,YAAY,KAAK,eAAe;GACvC,OAAO,EAAE,OAAO,MAAM;GACtB,GAAG,YAAY,QAAQ;EACxB,GACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,YAAY,KAAK,eAAe;GACvC,OAAO,EAAE,OAAO,MAAM;GACtB,GAAG,YAAY,QAAQ;EACxB,CACD;CACD;AACD"}
|
package/dist/exports/client.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { ByteSizeField } from "../client/ByteSizeField.js";
|
|
2
2
|
import { ClosePollButton } from "../client/ClosePollButton.js";
|
|
3
3
|
import { CondensedArray } from "../client/CondensedArray.js";
|
|
4
|
+
import { ConsentBlockLabel } from "../client/ConsentBlockLabel.js";
|
|
5
|
+
import { ConsentSourceRowLabel } from "../client/ConsentSourceRowLabel.js";
|
|
4
6
|
import { EndpointOptionsSelect } from "../client/EndpointOptionsSelect.js";
|
|
5
7
|
import { FieldBlockLabel } from "../client/FieldBlockLabel.js";
|
|
6
8
|
import { FieldNameSelect } from "../client/FieldNameSelect.js";
|
|
7
9
|
import { FlowBuilder } from "../client/FlowBuilder.js";
|
|
8
10
|
import { FormConditionField } from "../client/FormConditionField.js";
|
|
11
|
+
import { RecipientsSelect } from "../client/RecipientsSelect.js";
|
|
9
12
|
import { RuleDescription } from "../client/RuleDescription.js";
|
|
10
13
|
import { FieldCountCell } from "../collections/cells/FieldCountCell.js";
|
|
11
14
|
import { FlowStepsCell } from "../collections/cells/FlowStepsCell.js";
|
|
12
|
-
export { ByteSizeField, ClosePollButton, CondensedArray, EndpointOptionsSelect, FieldBlockLabel, FieldCountCell, FieldNameSelect, FlowBuilder, FlowStepsCell, FormConditionField, RuleDescription };
|
|
15
|
+
export { ByteSizeField, ClosePollButton, CondensedArray, ConsentBlockLabel, ConsentSourceRowLabel, EndpointOptionsSelect, FieldBlockLabel, FieldCountCell, FieldNameSelect, FlowBuilder, FlowStepsCell, FormConditionField, RecipientsSelect, RuleDescription };
|
package/dist/exports/client.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
import { ByteSizeField } from "../client/ByteSizeField.js";
|
|
3
3
|
import { ClosePollButton } from "../client/ClosePollButton.js";
|
|
4
4
|
import { CondensedArray } from "../client/CondensedArray.js";
|
|
5
|
+
import { ConsentBlockLabel } from "../client/ConsentBlockLabel.js";
|
|
6
|
+
import { ConsentSourceRowLabel } from "../client/ConsentSourceRowLabel.js";
|
|
5
7
|
import { EndpointOptionsSelect } from "../client/EndpointOptionsSelect.js";
|
|
6
8
|
import { FieldBlockLabel } from "../client/FieldBlockLabel.js";
|
|
7
9
|
import { FieldNameSelect } from "../client/FieldNameSelect.js";
|
|
8
10
|
import { FlowBuilder } from "../client/FlowBuilder.js";
|
|
9
11
|
import { FormConditionField } from "../client/FormConditionField.js";
|
|
12
|
+
import { RecipientsSelect } from "../client/RecipientsSelect.js";
|
|
10
13
|
import { RuleDescription } from "../client/RuleDescription.js";
|
|
11
14
|
import { FieldCountCell } from "../collections/cells/FieldCountCell.js";
|
|
12
15
|
import { FlowStepsCell } from "../collections/cells/FlowStepsCell.js";
|
|
13
|
-
export { ByteSizeField, ClosePollButton, CondensedArray, EndpointOptionsSelect, FieldBlockLabel, FieldCountCell, FieldNameSelect, FlowBuilder, FlowStepsCell, FormConditionField, RuleDescription };
|
|
16
|
+
export { ByteSizeField, ClosePollButton, CondensedArray, ConsentBlockLabel, ConsentSourceRowLabel, EndpointOptionsSelect, FieldBlockLabel, FieldCountCell, FieldNameSelect, FlowBuilder, FlowStepsCell, FormConditionField, RecipientsSelect, RuleDescription };
|
package/dist/exports/react.js
CHANGED
|
@@ -7,8 +7,8 @@ import { fieldKey } from "../fields/fieldKey.js";
|
|
|
7
7
|
import { evaluateCalc } from "../calc/evaluate.js";
|
|
8
8
|
import { computeCalcFields } from "../calc/computeCalcFields.js";
|
|
9
9
|
import { evaluateCondition } from "../conditions/evaluate.js";
|
|
10
|
-
import { en } from "../translations/en.js";
|
|
11
10
|
import { textOfBody } from "../actions/body/textOfBody.js";
|
|
11
|
+
import { en } from "../translations/en.js";
|
|
12
12
|
import { COUNTRIES, US_STATES } from "../fields/data/regions.js";
|
|
13
13
|
import { toFormDocument } from "../form/toFormDocument.js";
|
|
14
14
|
import { valuesFromSearchParams } from "../prefill/valuesFromSearchParams.js";
|
package/dist/exports/rsc.js
CHANGED
|
@@ -2,8 +2,8 @@ import { isPollClosed } from "../form/pollState.js";
|
|
|
2
2
|
import { resolvePollOptions } from "../poll/resolvePollOptions.js";
|
|
3
3
|
import { resolvePollOutcome } from "../poll/resolvePollOutcome.js";
|
|
4
4
|
import { resolveEffectivePollOptions } from "../poll/effectivePollOptions.js";
|
|
5
|
-
import { hasVotedCookie, votedCookieName } from "../submissions/votedCookie.js";
|
|
6
5
|
import { resolveConsentStatements } from "../consent/resolveConsentStatements.js";
|
|
6
|
+
import { hasVotedCookie, votedCookieName } from "../submissions/votedCookie.js";
|
|
7
7
|
import { toFormDocument } from "../form/toFormDocument.js";
|
|
8
8
|
import { SubmissionAnswers } from "../submissions/SubmissionAnswers.js";
|
|
9
9
|
export { SubmissionAnswers, hasVotedCookie, isPollClosed, resolveConsentStatements, resolveEffectivePollOptions, resolvePollOptions, resolvePollOutcome, toFormDocument, votedCookieName };
|
|
@@ -9,8 +9,8 @@ const FIELD_BLOCK_LABEL_REF = "@10x-media/form-builder/client#FieldBlockLabel";
|
|
|
9
9
|
* Replace Payload's default `blockName` row header (which reads "Untitled" until filled) with the
|
|
10
10
|
* field's own `label`, keyed to the type label as fallback. See `FieldBlockLabel`.
|
|
11
11
|
*/
|
|
12
|
-
const blockLabelAdmin = (typeLabelKey) => ({ components: { Label: {
|
|
13
|
-
path:
|
|
12
|
+
const blockLabelAdmin = (typeLabelKey, ref = FIELD_BLOCK_LABEL_REF) => ({ components: { Label: {
|
|
13
|
+
path: ref,
|
|
14
14
|
clientProps: { typeLabelKey }
|
|
15
15
|
} } });
|
|
16
16
|
/** Whether `fields` contains a field named `name`, descending into presentational rows. */
|
|
@@ -36,7 +36,7 @@ const buildFieldBlocks = ({ registry, ruleRegistry, localize = true }) => {
|
|
|
36
36
|
singular: labelFor(definition.label),
|
|
37
37
|
plural: labelFor(definition.label)
|
|
38
38
|
},
|
|
39
|
-
admin: blockLabelAdmin(definition.label),
|
|
39
|
+
admin: blockLabelAdmin(definition.label, definition.blockLabel),
|
|
40
40
|
fields: typeConfig
|
|
41
41
|
});
|
|
42
42
|
continue;
|
|
@@ -47,7 +47,7 @@ const buildFieldBlocks = ({ registry, ruleRegistry, localize = true }) => {
|
|
|
47
47
|
singular: labelFor(definition.label),
|
|
48
48
|
plural: labelFor(definition.label)
|
|
49
49
|
},
|
|
50
|
-
admin: blockLabelAdmin(definition.label),
|
|
50
|
+
admin: blockLabelAdmin(definition.label, definition.blockLabel),
|
|
51
51
|
fields: [fieldBlockTabs({
|
|
52
52
|
conditionTypes,
|
|
53
53
|
typeConfig,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildFieldBlocks.js","names":[],"sources":["../../src/fields/buildFieldBlocks.ts"],"sourcesContent":["import type { Block, Field, TabsField } from 'payload'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport { keys } from '../translations/keys'\nimport { labelFor } from '../translations/server'\nimport { buildRuleBlocks } from '../validation/buildRuleBlocks'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport type { FieldTypeRegistry } from './registry'\nimport { fieldBlockTabs } from './sharedConfig'\n\nconst FIELD_BLOCK_LABEL_REF = '@10x-media/form-builder/client#FieldBlockLabel'\n\n/**\n * Replace Payload's default `blockName` row header (which reads \"Untitled\" until filled) with the\n * field's own `label`, keyed to the type label as fallback. See `FieldBlockLabel`.\n */\nconst blockLabelAdmin = (
|
|
1
|
+
{"version":3,"file":"buildFieldBlocks.js","names":[],"sources":["../../src/fields/buildFieldBlocks.ts"],"sourcesContent":["import type { Block, Field, TabsField } from 'payload'\nimport { buildConditionTypeMap } from '../conditions/conditionType'\nimport { keys } from '../translations/keys'\nimport { labelFor } from '../translations/server'\nimport { buildRuleBlocks } from '../validation/buildRuleBlocks'\nimport type { ValidationRuleRegistry } from '../validation/registry'\nimport type { FieldTypeRegistry } from './registry'\nimport { fieldBlockTabs } from './sharedConfig'\n\nconst FIELD_BLOCK_LABEL_REF = '@10x-media/form-builder/client#FieldBlockLabel'\n\n/**\n * Replace Payload's default `blockName` row header (which reads \"Untitled\" until filled) with the\n * field's own `label`, keyed to the type label as fallback. See `FieldBlockLabel`.\n */\nconst blockLabelAdmin = (\n\ttypeLabelKey: string,\n\tref: string = FIELD_BLOCK_LABEL_REF\n): NonNullable<Block['admin']> => ({\n\tcomponents: { Label: { path: ref, clientProps: { typeLabelKey } } },\n})\n\n/** Whether `fields` contains a field named `name`, descending into presentational rows. */\nconst hasNameField = (fields: Field[]): boolean =>\n\tfields.some((f) => {\n\t\tif ('name' in f && f.name === 'name') {\n\t\t\treturn true\n\t\t}\n\t\treturn f.type === 'row' && hasNameField(f.fields)\n\t})\n\n/** The tab whose fields hold the shared basics and type config, found by the `name` field it carries. */\nconst fieldTabOf = (block: Block): Field[] | undefined => {\n\tconst tabsField = block.fields.find((f): f is TabsField => f.type === 'tabs')\n\treturn tabsField?.tabs.find((tab) => hasNameField(tab.fields))?.fields\n}\n\n/**\n * One add-field block per registered type: tabs holding shared config, type config, and\n * validations. Bare definitions (e.g. `message`) skip all of that: their block is the\n * definition's `config` alone, no name/shared basics/tabs. `localize` controls whether the\n * shared content fields carry `localized: true`; per-type config fields carry their own flag\n * from the registry definitions.\n */\ntype BuildFieldBlocksArgs = {\n\tregistry: FieldTypeRegistry\n\truleRegistry: ValidationRuleRegistry\n\tlocalize?: boolean\n}\n\nexport const buildFieldBlocks = ({\n\tregistry,\n\truleRegistry,\n\tlocalize = true,\n}: BuildFieldBlocksArgs): Block[] => {\n\tconst conditionTypes = buildConditionTypeMap(registry)\n\tconst blocks: Block[] = []\n\tconst bareSlugs = new Set<string>()\n\tfor (const definition of registry.values()) {\n\t\tconst typeConfig: Field[] = definition.config ?? []\n\n\t\tif (definition.bare === true) {\n\t\t\tbareSlugs.add(definition.type)\n\t\t\tblocks.push({\n\t\t\t\tslug: definition.type,\n\t\t\t\tlabels: { singular: labelFor(definition.label), plural: labelFor(definition.label) },\n\t\t\t\tadmin: blockLabelAdmin(definition.label, definition.blockLabel),\n\t\t\t\tfields: typeConfig,\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\tblocks.push({\n\t\t\tslug: definition.type,\n\t\t\tlabels: { singular: labelFor(definition.label), plural: labelFor(definition.label) },\n\t\t\tadmin: blockLabelAdmin(definition.label, definition.blockLabel),\n\t\t\tfields: [\n\t\t\t\tfieldBlockTabs({\n\t\t\t\t\tconditionTypes,\n\t\t\t\t\ttypeConfig,\n\t\t\t\t\tvalidations: {\n\t\t\t\t\t\tname: 'validations',\n\t\t\t\t\t\ttype: 'blocks',\n\t\t\t\t\t\tlabel: labelFor(keys.validationsLabel),\n\t\t\t\t\t\tblocks: buildRuleBlocks(ruleRegistry, definition.type),\n\t\t\t\t\t},\n\t\t\t\t\tlocalize,\n\t\t\t\t\tomitShared: definition.omitShared,\n\t\t\t\t}),\n\t\t\t],\n\t\t})\n\t}\n\n\t// Second pass: inject subFields into the repeater block using all non-repeater blocks.\n\t// Done after the main loop so every sibling block is already built.\n\t// Repeater-in-repeater is not supported in v1; the repeater block is excluded from subFields.\n\t// Bare blocks are excluded too: repeater row plumbing (values, composite error paths) is name-keyed.\n\tconst repeaterBlock = blocks.find((b) => b.slug === 'repeater')\n\tif (repeaterBlock) {\n\t\tconst subFieldsField: Field = {\n\t\t\tname: 'subFields',\n\t\t\ttype: 'blocks',\n\t\t\tlabel: labelFor(keys.configSubFields),\n\t\t\tblocks: blocks.filter((b) => b.slug !== 'repeater' && !bareSlugs.has(b.slug)),\n\t\t}\n\t\tconst fieldTabFields = fieldTabOf(repeaterBlock)\n\t\t;(fieldTabFields ?? (repeaterBlock.fields as Field[])).push(subFieldsField)\n\t}\n\n\treturn blocks\n}\n"],"mappings":";;;;;;AASA,MAAM,wBAAwB;;;;;AAM9B,MAAM,mBACL,cACA,MAAc,2BACoB,EAClC,YAAY,EAAE,OAAO;CAAE,MAAM;CAAK,aAAa,EAAE,aAAa;AAAE,EAAE,EACnE;;AAGA,MAAM,gBAAgB,WACrB,OAAO,MAAM,MAAM;CAClB,IAAI,UAAU,KAAK,EAAE,SAAS,QAC7B,OAAO;CAER,OAAO,EAAE,SAAS,SAAS,aAAa,EAAE,MAAM;AACjD,CAAC;;AAGF,MAAM,cAAc,UAAsC;CAEzD,OADkB,MAAM,OAAO,MAAM,MAAsB,EAAE,SAAS,MACvD,GAAG,KAAK,MAAM,QAAQ,aAAa,IAAI,MAAM,CAAC,GAAG;AACjE;AAeA,MAAa,oBAAoB,EAChC,UACA,cACA,WAAW,WACyB;CACpC,MAAM,iBAAiB,sBAAsB,QAAQ;CACrD,MAAM,SAAkB,CAAC;CACzB,MAAM,4BAAY,IAAI,IAAY;CAClC,KAAK,MAAM,cAAc,SAAS,OAAO,GAAG;EAC3C,MAAM,aAAsB,WAAW,UAAU,CAAC;EAElD,IAAI,WAAW,SAAS,MAAM;GAC7B,UAAU,IAAI,WAAW,IAAI;GAC7B,OAAO,KAAK;IACX,MAAM,WAAW;IACjB,QAAQ;KAAE,UAAU,SAAS,WAAW,KAAK;KAAG,QAAQ,SAAS,WAAW,KAAK;IAAE;IACnF,OAAO,gBAAgB,WAAW,OAAO,WAAW,UAAU;IAC9D,QAAQ;GACT,CAAC;GACD;EACD;EAEA,OAAO,KAAK;GACX,MAAM,WAAW;GACjB,QAAQ;IAAE,UAAU,SAAS,WAAW,KAAK;IAAG,QAAQ,SAAS,WAAW,KAAK;GAAE;GACnF,OAAO,gBAAgB,WAAW,OAAO,WAAW,UAAU;GAC9D,QAAQ,CACP,eAAe;IACd;IACA;IACA,aAAa;KACZ,MAAM;KACN,MAAM;KACN,OAAO,SAAS,KAAK,gBAAgB;KACrC,QAAQ,gBAAgB,cAAc,WAAW,IAAI;IACtD;IACA;IACA,YAAY,WAAW;GACxB,CAAC,CACF;EACD,CAAC;CACF;CAMA,MAAM,gBAAgB,OAAO,MAAM,MAAM,EAAE,SAAS,UAAU;CAC9D,IAAI,eAAe;EAClB,MAAM,iBAAwB;GAC7B,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,eAAe;GACpC,QAAQ,OAAO,QAAQ,MAAM,EAAE,SAAS,cAAc,CAAC,UAAU,IAAI,EAAE,IAAI,CAAC;EAC7E;EAEC,CADsB,WAAW,aACnB,KAAM,cAAc,QAAoB,KAAK,cAAc;CAC3E;CAEA,OAAO;AACR"}
|
|
@@ -19,7 +19,12 @@ const consentField = defineFormField({
|
|
|
19
19
|
type: "consent",
|
|
20
20
|
label: keys.fieldTypeConsent,
|
|
21
21
|
value: "boolean",
|
|
22
|
-
omitShared: [
|
|
22
|
+
omitShared: [
|
|
23
|
+
"label",
|
|
24
|
+
"placeholder",
|
|
25
|
+
"description"
|
|
26
|
+
],
|
|
27
|
+
blockLabel: "@10x-media/form-builder/client#ConsentBlockLabel",
|
|
23
28
|
config: [{
|
|
24
29
|
name: "source",
|
|
25
30
|
type: "text",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consent.js","names":[],"sources":["../../../src/fields/builtin/consent.ts"],"sourcesContent":["import { keys } from '../../translations/keys'\nimport { labelForKey } from '../../translations/server'\nimport { defineFormField } from '../defineFormField'\n\ntype ConsentConfig = { source?: string; required?: boolean }\n\nconst SOURCE_FIELD_REF = '@10x-media/form-builder/client#EndpointOptionsSelect'\n\n/**\n * Consent is a reference, so the field is a reference: the author picks a source and nothing else.\n * The statement, the policy page, and its version all live with the source (see\n * `consentSourcesField`), so there is no wording to copy per form, no URL to paste, and no version\n * to type; correcting a statement corrects every form pointing at it. `label` and `placeholder` are\n * dropped for the same reason: the statement is the visible text and the checkbox's accessible name.\n *\n * Registered only when the plugin's `consent.sources` resolver is set, since a consent field with\n * no source to reference has nothing to say and nothing to prove.\n *\n * The intrinsic validator owns `required`: the engine's shared required guard only fires on an\n * empty value, and `false` (the coerced state of both an unchecked box and an absent answer) is a\n * present one, so without this a required consent would accept an explicit refusal.\n */\nexport const consentField = defineFormField<'boolean', ConsentConfig>({\n\ttype: 'consent',\n\tlabel: keys.fieldTypeConsent,\n\tvalue: 'boolean',\n\tomitShared: ['label', 'placeholder'],\n\tconfig: [\n\t\t{\n\t\t\tname: 'source',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelForKey(keys.consentConfigSource),\n\t\t\tadmin: {\n\t\t\t\tcomponents: {\n\t\t\t\t\tField: {\n\t\t\t\t\t\tpath: SOURCE_FIELD_REF,\n\t\t\t\t\t\tclientProps: {\n\t\t\t\t\t\t\tendpoint: 'consent-sources',\n\t\t\t\t\t\t\tdescriptionKey: keys.consentConfigSourceDescription,\n\t\t\t\t\t\t\tisClearable: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t],\n\tvalidate: ({ value, config, t }) =>\n\t\tconfig.required === true && value !== true ? t(keys.validationRequired) : true,\n\tformat: ({ value, t }) => t(value === true ? keys.formatYes : keys.formatNo),\n})\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,MAAa,eAAe,gBAA0C;CACrE,MAAM;CACN,OAAO,KAAK;CACZ,OAAO;CACP,YAAY,
|
|
1
|
+
{"version":3,"file":"consent.js","names":[],"sources":["../../../src/fields/builtin/consent.ts"],"sourcesContent":["import { keys } from '../../translations/keys'\nimport { labelForKey } from '../../translations/server'\nimport { defineFormField } from '../defineFormField'\n\ntype ConsentConfig = { source?: string; required?: boolean }\n\nconst SOURCE_FIELD_REF = '@10x-media/form-builder/client#EndpointOptionsSelect'\n\n/**\n * Consent is a reference, so the field is a reference: the author picks a source and nothing else.\n * The statement, the policy page, and its version all live with the source (see\n * `consentSourcesField`), so there is no wording to copy per form, no URL to paste, and no version\n * to type; correcting a statement corrects every form pointing at it. `label` and `placeholder` are\n * dropped for the same reason: the statement is the visible text and the checkbox's accessible name.\n *\n * Registered only when the plugin's `consent.sources` resolver is set, since a consent field with\n * no source to reference has nothing to say and nothing to prove.\n *\n * The intrinsic validator owns `required`: the engine's shared required guard only fires on an\n * empty value, and `false` (the coerced state of both an unchecked box and an absent answer) is a\n * present one, so without this a required consent would accept an explicit refusal.\n */\nexport const consentField = defineFormField<'boolean', ConsentConfig>({\n\ttype: 'consent',\n\tlabel: keys.fieldTypeConsent,\n\tvalue: 'boolean',\n\tomitShared: ['label', 'placeholder', 'description'],\n\tblockLabel: '@10x-media/form-builder/client#ConsentBlockLabel',\n\tconfig: [\n\t\t{\n\t\t\tname: 'source',\n\t\t\ttype: 'text',\n\t\t\tlabel: labelForKey(keys.consentConfigSource),\n\t\t\tadmin: {\n\t\t\t\tcomponents: {\n\t\t\t\t\tField: {\n\t\t\t\t\t\tpath: SOURCE_FIELD_REF,\n\t\t\t\t\t\tclientProps: {\n\t\t\t\t\t\t\tendpoint: 'consent-sources',\n\t\t\t\t\t\t\tdescriptionKey: keys.consentConfigSourceDescription,\n\t\t\t\t\t\t\tisClearable: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t],\n\tvalidate: ({ value, config, t }) =>\n\t\tconfig.required === true && value !== true ? t(keys.validationRequired) : true,\n\tformat: ({ value, t }) => t(value === true ? keys.formatYes : keys.formatNo),\n})\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,MAAa,eAAe,gBAA0C;CACrE,MAAM;CACN,OAAO,KAAK;CACZ,OAAO;CACP,YAAY;EAAC;EAAS;EAAe;CAAa;CAClD,YAAY;CACZ,QAAQ,CACP;EACC,MAAM;EACN,MAAM;EACN,OAAO,YAAY,KAAK,mBAAmB;EAC3C,OAAO,EACN,YAAY,EACX,OAAO;GACN,MAAM;GACN,aAAa;IACZ,UAAU;IACV,gBAAgB,KAAK;IACrB,aAAa;GACd;EACD,EACD,EACD;CACD,CACD;CACA,WAAW,EAAE,OAAO,QAAQ,QAC3B,OAAO,aAAa,QAAQ,UAAU,OAAO,EAAE,KAAK,kBAAkB,IAAI;CAC3E,SAAS,EAAE,OAAO,QAAQ,EAAE,UAAU,OAAO,KAAK,YAAY,KAAK,QAAQ;AAC5E,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { keys } from "../../translations/keys.js";
|
|
2
2
|
import { labelFor } from "../../translations/server.js";
|
|
3
3
|
import { defineFormField } from "../defineFormField.js";
|
|
4
|
+
import { mimeAllowed } from "../../uploads/resolveFileRef.js";
|
|
4
5
|
//#region src/fields/builtin/file.ts
|
|
5
6
|
const isFileRef = (value) => typeof value === "object" && value !== null && "filename" in value;
|
|
6
7
|
const MAX_SIZE_REF = "@10x-media/form-builder/client#ByteSizeField";
|
|
@@ -83,8 +84,12 @@ const fileMimeTypeOptions = [
|
|
|
83
84
|
* so the client renderer knows where to POST; the server never trusts it at submit time (plugin config
|
|
84
85
|
* stays the source of truth). Named `uploadsCollection` because `collection` is a mongoose-reserved
|
|
85
86
|
* schema pathname.
|
|
87
|
+
*
|
|
88
|
+
* `allowedMimeTypes` (the host upload collection's `upload.mimeTypes`, threaded from the plugin
|
|
89
|
+
* factory) constrains the picker's MIME options to what the collection actually accepts; omitted
|
|
90
|
+
* keeps the full list. Admin-time only: the collection and `captureFileRef` remain authoritative.
|
|
86
91
|
*/
|
|
87
|
-
const
|
|
92
|
+
const buildFileField = (allowedMimeTypes) => defineFormField({
|
|
88
93
|
type: "file",
|
|
89
94
|
label: keys.fieldTypeFile,
|
|
90
95
|
value: "file",
|
|
@@ -99,7 +104,7 @@ const fileField = defineFormField({
|
|
|
99
104
|
name: "mimeTypes",
|
|
100
105
|
type: "select",
|
|
101
106
|
hasMany: true,
|
|
102
|
-
options: fileMimeTypeOptions,
|
|
107
|
+
options: fileMimeTypeOptions.filter((option) => mimeAllowed(option.value, allowedMimeTypes)),
|
|
103
108
|
label: labelFor(keys.fileConfigMimeTypes)
|
|
104
109
|
},
|
|
105
110
|
{
|
|
@@ -115,7 +120,8 @@ const fileField = defineFormField({
|
|
|
115
120
|
],
|
|
116
121
|
format: ({ value }) => isFileRef(value) ? value.filename : ""
|
|
117
122
|
});
|
|
123
|
+
buildFileField();
|
|
118
124
|
//#endregion
|
|
119
|
-
export {
|
|
125
|
+
export { buildFileField, fileMimeTypeOptions };
|
|
120
126
|
|
|
121
127
|
//# sourceMappingURL=file.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.js","names":[],"sources":["../../../src/fields/builtin/file.ts"],"sourcesContent":["import { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport type { FileRef } from '../../uploads/types'\nimport { defineFormField } from '../defineFormField'\n\ntype FileConfig = {\n\tuploadsCollection?: string\n\tmimeTypes?: string[]\n\tmaxSize?: number\n}\n\nconst isFileRef = (value: unknown): value is FileRef =>\n\ttypeof value === 'object' && value !== null && 'filename' in value\n\nconst MAX_SIZE_REF = '@10x-media/form-builder/client#ByteSizeField'\n\n/**\n * Curated MIME type choices for the file field's `mimeTypes` select. Values are raw MIME strings or\n * trailing-`*` prefix wildcards, exactly the patterns `resolveFileRef` matches server-side; `type/*`\n * values double as the file input's `accept` attribute (browsers ignore other prefix patterns, which\n * is fine since the server stays authoritative). Every value must be 63 bytes or less: Payload's\n * db-postgres adapter stores select options as a pg enum, and pg enum labels cap at 63 bytes.\n */\nexport const fileMimeTypeOptions = [\n\t{ label: 'Any image', value: 'image/*' },\n\t{ label: 'PNG image', value: 'image/png' },\n\t{ label: 'JPEG image', value: 'image/jpeg' },\n\t{ label: 'WebP image', value: 'image/webp' },\n\t{ label: 'GIF image', value: 'image/gif' },\n\t{ label: 'SVG image', value: 'image/svg+xml' },\n\t{ label: 'PDF', value: 'application/pdf' },\n\t{ label: 'Plain text', value: 'text/plain' },\n\t{ label: 'CSV', value: 'text/csv' },\n\t{ label: 'ZIP archive', value: 'application/zip' },\n\t{ label: 'Word document (.doc)', value: 'application/msword' },\n\t{ label: 'Excel spreadsheet (.xls)', value: 'application/vnd.ms-excel' },\n\t{\n\t\tlabel: 'Office documents (docx, xlsx, pptx)',\n\t\tvalue: 'application/vnd.openxmlformats-officedocument.*',\n\t},\n\t{ label: 'Any audio', value: 'audio/*' },\n\t{ label: 'Any video', value: 'video/*' },\n]\n\n/**\n * The `file` field type. Its stored value is a server-captured {@link FileRef}; the client only ever submits\n * the upload id, and `runSubmission` re-reads filename/mimeType/filesize from the upload doc at the trust\n * boundary (see `captureFileRef`). The intrinsic validator is intentionally a no-op: MIME/size/existence are\n * enforced authoritatively server-side against the stored doc, never against client metadata. `mimeTypes`\n * and `maxSize` persist on the block so the capture can enforce them. `uploadsCollection` is not\n * author-editable: a forms hook stamps the plugin-configured uploads collection slug onto it on every save\n * so the client renderer knows where to POST; the server never trusts it at submit time (plugin config\n * stays the source of truth). Named `uploadsCollection` because `collection` is a mongoose-reserved\n * schema pathname.\n */\nexport const
|
|
1
|
+
{"version":3,"file":"file.js","names":[],"sources":["../../../src/fields/builtin/file.ts"],"sourcesContent":["import { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { mimeAllowed } from '../../uploads/resolveFileRef'\nimport type { FileRef } from '../../uploads/types'\nimport { defineFormField } from '../defineFormField'\n\ntype FileConfig = {\n\tuploadsCollection?: string\n\tmimeTypes?: string[]\n\tmaxSize?: number\n}\n\nconst isFileRef = (value: unknown): value is FileRef =>\n\ttypeof value === 'object' && value !== null && 'filename' in value\n\nconst MAX_SIZE_REF = '@10x-media/form-builder/client#ByteSizeField'\n\n/**\n * Curated MIME type choices for the file field's `mimeTypes` select. Values are raw MIME strings or\n * trailing-`*` prefix wildcards, exactly the patterns `resolveFileRef` matches server-side; `type/*`\n * values double as the file input's `accept` attribute (browsers ignore other prefix patterns, which\n * is fine since the server stays authoritative). Every value must be 63 bytes or less: Payload's\n * db-postgres adapter stores select options as a pg enum, and pg enum labels cap at 63 bytes.\n */\nexport const fileMimeTypeOptions = [\n\t{ label: 'Any image', value: 'image/*' },\n\t{ label: 'PNG image', value: 'image/png' },\n\t{ label: 'JPEG image', value: 'image/jpeg' },\n\t{ label: 'WebP image', value: 'image/webp' },\n\t{ label: 'GIF image', value: 'image/gif' },\n\t{ label: 'SVG image', value: 'image/svg+xml' },\n\t{ label: 'PDF', value: 'application/pdf' },\n\t{ label: 'Plain text', value: 'text/plain' },\n\t{ label: 'CSV', value: 'text/csv' },\n\t{ label: 'ZIP archive', value: 'application/zip' },\n\t{ label: 'Word document (.doc)', value: 'application/msword' },\n\t{ label: 'Excel spreadsheet (.xls)', value: 'application/vnd.ms-excel' },\n\t{\n\t\tlabel: 'Office documents (docx, xlsx, pptx)',\n\t\tvalue: 'application/vnd.openxmlformats-officedocument.*',\n\t},\n\t{ label: 'Any audio', value: 'audio/*' },\n\t{ label: 'Any video', value: 'video/*' },\n]\n\n/**\n * The `file` field type. Its stored value is a server-captured {@link FileRef}; the client only ever submits\n * the upload id, and `runSubmission` re-reads filename/mimeType/filesize from the upload doc at the trust\n * boundary (see `captureFileRef`). The intrinsic validator is intentionally a no-op: MIME/size/existence are\n * enforced authoritatively server-side against the stored doc, never against client metadata. `mimeTypes`\n * and `maxSize` persist on the block so the capture can enforce them. `uploadsCollection` is not\n * author-editable: a forms hook stamps the plugin-configured uploads collection slug onto it on every save\n * so the client renderer knows where to POST; the server never trusts it at submit time (plugin config\n * stays the source of truth). Named `uploadsCollection` because `collection` is a mongoose-reserved\n * schema pathname.\n *\n * `allowedMimeTypes` (the host upload collection's `upload.mimeTypes`, threaded from the plugin\n * factory) constrains the picker's MIME options to what the collection actually accepts; omitted\n * keeps the full list. Admin-time only: the collection and `captureFileRef` remain authoritative.\n */\nexport const buildFileField = (allowedMimeTypes?: string[]) =>\n\tdefineFormField<'file', FileConfig>({\n\t\ttype: 'file',\n\t\tlabel: keys.fieldTypeFile,\n\t\tvalue: 'file',\n\t\tomitShared: ['placeholder'],\n\t\tconfig: [\n\t\t\t{ name: 'uploadsCollection', type: 'text', admin: { hidden: true } },\n\t\t\t{\n\t\t\t\tname: 'mimeTypes',\n\t\t\t\ttype: 'select',\n\t\t\t\thasMany: true,\n\t\t\t\toptions: fileMimeTypeOptions.filter((option) =>\n\t\t\t\t\tmimeAllowed(option.value, allowedMimeTypes)\n\t\t\t\t),\n\t\t\t\tlabel: labelFor(keys.fileConfigMimeTypes),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'maxSize',\n\t\t\t\ttype: 'number',\n\t\t\t\tmin: 0,\n\t\t\t\tlabel: labelFor(keys.fileConfigMaxSize),\n\t\t\t\tadmin: {\n\t\t\t\t\tcomponents: {\n\t\t\t\t\t\tField: {\n\t\t\t\t\t\t\tpath: MAX_SIZE_REF,\n\t\t\t\t\t\t\tclientProps: { descriptionKey: keys.fileConfigMaxSizeDescription },\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t\tformat: ({ value }) => (isFileRef(value) ? value.filename : ''),\n\t})\n\n/** The file field with the full MIME option list, for consumers spreading a default definition. */\nexport const fileField = buildFileField()\n"],"mappings":";;;;;AAYA,MAAM,aAAa,UAClB,OAAO,UAAU,YAAY,UAAU,QAAQ,cAAc;AAE9D,MAAM,eAAe;;;;;;;;AASrB,MAAa,sBAAsB;CAClC;EAAE,OAAO;EAAa,OAAO;CAAU;CACvC;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAc,OAAO;CAAa;CAC3C;EAAE,OAAO;EAAc,OAAO;CAAa;CAC3C;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAa,OAAO;CAAgB;CAC7C;EAAE,OAAO;EAAO,OAAO;CAAkB;CACzC;EAAE,OAAO;EAAc,OAAO;CAAa;CAC3C;EAAE,OAAO;EAAO,OAAO;CAAW;CAClC;EAAE,OAAO;EAAe,OAAO;CAAkB;CACjD;EAAE,OAAO;EAAwB,OAAO;CAAqB;CAC7D;EAAE,OAAO;EAA4B,OAAO;CAA2B;CACvE;EACC,OAAO;EACP,OAAO;CACR;CACA;EAAE,OAAO;EAAa,OAAO;CAAU;CACvC;EAAE,OAAO;EAAa,OAAO;CAAU;AACxC;;;;;;;;;;;;;;;;AAiBA,MAAa,kBAAkB,qBAC9B,gBAAoC;CACnC,MAAM;CACN,OAAO,KAAK;CACZ,OAAO;CACP,YAAY,CAAC,aAAa;CAC1B,QAAQ;EACP;GAAE,MAAM;GAAqB,MAAM;GAAQ,OAAO,EAAE,QAAQ,KAAK;EAAE;EACnE;GACC,MAAM;GACN,MAAM;GACN,SAAS;GACT,SAAS,oBAAoB,QAAQ,WACpC,YAAY,OAAO,OAAO,gBAAgB,CAC3C;GACA,OAAO,SAAS,KAAK,mBAAmB;EACzC;EACA;GACC,MAAM;GACN,MAAM;GACN,KAAK;GACL,OAAO,SAAS,KAAK,iBAAiB;GACtC,OAAO,EACN,YAAY,EACX,OAAO;IACN,MAAM;IACN,aAAa,EAAE,gBAAgB,KAAK,6BAA6B;GAClE,EACD,EACD;EACD;CACD;CACA,SAAS,EAAE,YAAa,UAAU,KAAK,IAAI,MAAM,WAAW;AAC7D,CAAC;AAGuB,eAAe"}
|
|
@@ -10,7 +10,7 @@ import { RichTextField } from "payload";
|
|
|
10
10
|
* value/config generics; the registry stores them erased (config is re-narrowed per matched type at
|
|
11
11
|
* execution), hence one cast per built-in, no `any`.
|
|
12
12
|
*/
|
|
13
|
-
declare const buildDefaultFieldDefinitions: (localize: boolean, editor?: RichTextField["editor"]) => AnyFormFieldDefinition[];
|
|
13
|
+
declare const buildDefaultFieldDefinitions: (localize: boolean, editor?: RichTextField["editor"], uploadMimeTypes?: string[]) => AnyFormFieldDefinition[];
|
|
14
14
|
declare const defaultFieldDefinitions: AnyFormFieldDefinition[];
|
|
15
15
|
/**
|
|
16
16
|
* `defaultFieldDefinitions` indexed by `type`, so a single built-in can be spread and tweaked
|
|
@@ -3,7 +3,7 @@ import { checkboxField } from "./checkbox.js";
|
|
|
3
3
|
import { consentField } from "./consent.js";
|
|
4
4
|
import { dateField } from "./date.js";
|
|
5
5
|
import { emailField } from "./email.js";
|
|
6
|
-
import {
|
|
6
|
+
import { buildFileField } from "./file.js";
|
|
7
7
|
import { buildMessageField } from "./message.js";
|
|
8
8
|
import { numberField } from "./number.js";
|
|
9
9
|
import { buildRepeaterField } from "./repeater.js";
|
|
@@ -19,7 +19,7 @@ import { textareaField } from "./textarea.js";
|
|
|
19
19
|
* value/config generics; the registry stores them erased (config is re-narrowed per matched type at
|
|
20
20
|
* execution), hence one cast per built-in, no `any`.
|
|
21
21
|
*/
|
|
22
|
-
const buildDefaultFieldDefinitions = (localize, editor) => [
|
|
22
|
+
const buildDefaultFieldDefinitions = (localize, editor, uploadMimeTypes) => [
|
|
23
23
|
textField,
|
|
24
24
|
textareaField,
|
|
25
25
|
emailField,
|
|
@@ -28,7 +28,7 @@ const buildDefaultFieldDefinitions = (localize, editor) => [
|
|
|
28
28
|
checkboxField,
|
|
29
29
|
calculationField,
|
|
30
30
|
consentField,
|
|
31
|
-
|
|
31
|
+
buildFileField(uploadMimeTypes),
|
|
32
32
|
buildRepeaterField(localize),
|
|
33
33
|
dateField,
|
|
34
34
|
buildMessageField(localize, editor)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/fields/builtin/index.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { AnyFormFieldDefinition } from '../types'\nimport { calculationField } from './calculation'\nimport { checkboxField } from './checkbox'\nimport { consentField } from './consent'\nimport { dateField } from './date'\nimport { emailField } from './email'\nimport {
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/fields/builtin/index.ts"],"sourcesContent":["import type { RichTextField } from 'payload'\nimport type { AnyFormFieldDefinition } from '../types'\nimport { calculationField } from './calculation'\nimport { checkboxField } from './checkbox'\nimport { consentField } from './consent'\nimport { dateField } from './date'\nimport { emailField } from './email'\nimport { buildFileField } from './file'\nimport { buildMessageField } from './message'\nimport { numberField } from './number'\nimport { buildRepeaterField } from './repeater'\nimport { buildSelectField } from './select'\nimport { textField } from './text'\nimport { textareaField } from './textarea'\n\n/**\n * Built-in field definitions with content-bearing config fields (select option labels, repeater add\n * label) carrying `localized: true` when `localize` is true. `editor`, when given, overrides the\n * richText editor on the message content field (from the plugin's `richText.editor` option). Field\n * types without content config are shared static definitions. Definitions are authored with precise\n * value/config generics; the registry stores them erased (config is re-narrowed per matched type at\n * execution), hence one cast per built-in, no `any`.\n */\nexport const buildDefaultFieldDefinitions = (\n\tlocalize: boolean,\n\teditor?: RichTextField['editor'],\n\tuploadMimeTypes?: string[]\n): AnyFormFieldDefinition[] => [\n\ttextField as AnyFormFieldDefinition,\n\ttextareaField as AnyFormFieldDefinition,\n\temailField as AnyFormFieldDefinition,\n\tnumberField as AnyFormFieldDefinition,\n\tbuildSelectField(localize) as AnyFormFieldDefinition,\n\tcheckboxField as AnyFormFieldDefinition,\n\tcalculationField as AnyFormFieldDefinition,\n\tconsentField as AnyFormFieldDefinition,\n\tbuildFileField(uploadMimeTypes) as AnyFormFieldDefinition,\n\tbuildRepeaterField(localize) as AnyFormFieldDefinition,\n\tdateField as AnyFormFieldDefinition,\n\tbuildMessageField(localize, editor) as AnyFormFieldDefinition,\n]\n\nexport const defaultFieldDefinitions: AnyFormFieldDefinition[] = buildDefaultFieldDefinitions(true)\n\n/**\n * `defaultFieldDefinitions` indexed by `type`, so a single built-in can be spread and tweaked\n * without hand-rolling a `.find()`, e.g.\n * `fields: { text: { ...defaultFieldDefinitionsByType.text, validate: myValidate } }`. The\n * `fields` plugin option replaces a built-in of the same `type`; `false` removes it, `true` keeps it.\n * Prebuilt with content localization on; with `localizeContent: false`, derive overrides from\n * `buildDefaultFieldDefinitions(false)` instead so the spread does not reintroduce `localized`.\n */\nexport const defaultFieldDefinitionsByType: Record<string, AnyFormFieldDefinition> =\n\tObject.fromEntries(defaultFieldDefinitions.map((definition) => [definition.type, definition]))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,gCACZ,UACA,QACA,oBAC8B;CAC9B;CACA;CACA;CACA;CACA,iBAAiB,QAAQ;CACzB;CACA;CACA;CACA,eAAe,eAAe;CAC9B,mBAAmB,QAAQ;CAC3B;CACA,kBAAkB,UAAU,MAAM;AACnC;AAEA,MAAa,0BAAoD,6BAA6B,IAAI;;;;;;;;;AAUlG,MAAa,gCACZ,OAAO,YAAY,wBAAwB,KAAK,eAAe,CAAC,WAAW,MAAM,UAAU,CAAC,CAAC"}
|
package/dist/fields/types.d.ts
CHANGED
|
@@ -72,7 +72,13 @@ type FormFieldDefinition<K extends FormFieldValueKind = FormFieldValueKind, TCon
|
|
|
72
72
|
config?: Field[];
|
|
73
73
|
validate?: FormFieldValidate<K, TConfig>;
|
|
74
74
|
format?: FormFieldFormat<K, TConfig>;
|
|
75
|
-
Field?: string;
|
|
75
|
+
Field?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Import-map ref for a custom block row Label, replacing the default `FieldBlockLabel` for this
|
|
78
|
+
* type's add-field block. Receives the same `{ typeLabelKey }` clientProps. The `consent` type
|
|
79
|
+
* uses this to resolve and show the referenced source's name.
|
|
80
|
+
*/
|
|
81
|
+
blockLabel?: string; /** Optional Standard Schema validator (Zod/Valibot/etc.), run by the engine after the intrinsic validator. */
|
|
76
82
|
schema?: StandardSchemaV1;
|
|
77
83
|
icon?: string;
|
|
78
84
|
group?: string; /** How this field appears in the condition builder. Defaults from `value` (see `defaultConditionType`). */
|
|
@@ -130,7 +136,8 @@ type AnyFormFieldDefinition = {
|
|
|
130
136
|
config?: Field[];
|
|
131
137
|
validate?: AnyFormFieldValidate;
|
|
132
138
|
format?: AnyFormFieldFormat;
|
|
133
|
-
Field?: string;
|
|
139
|
+
Field?: string; /** See `FormFieldDefinition.blockLabel`: import-map ref for a custom block row Label. */
|
|
140
|
+
blockLabel?: string;
|
|
134
141
|
schema?: StandardSchemaV1;
|
|
135
142
|
icon?: string;
|
|
136
143
|
group?: string; /** How this field appears in the condition builder. Defaults from `value` (see `defaultConditionType`). */
|
|
@@ -16,6 +16,17 @@ type ToFormDocumentOptions = {
|
|
|
16
16
|
* consent fields render with no statement at all.
|
|
17
17
|
*/
|
|
18
18
|
consentStatements?: ConsentStatements;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve an internal redirect `reference` to a same-site URL. Called only when
|
|
21
|
+
* `response.type === 'redirect'`, a `reference` is present, and no `redirect.url` is set. Keep it
|
|
22
|
+
* sync: operate on the already-loaded (populated) reference, since resolving a Doc to a URL is host
|
|
23
|
+
* routing knowledge the plugin cannot supply. The result is written to `redirect.url`, so `<Form>`
|
|
24
|
+
* navigates with no client change.
|
|
25
|
+
*/
|
|
26
|
+
resolveRedirect?: (reference: {
|
|
27
|
+
relationTo: string;
|
|
28
|
+
value: number | string | object;
|
|
29
|
+
}) => string | null | undefined;
|
|
19
30
|
};
|
|
20
31
|
/**
|
|
21
32
|
* Narrows a Payload-generated form document (from `getPayload().findByID()` or `fetch`) to
|
|
@@ -26,8 +37,8 @@ type ToFormDocumentOptions = {
|
|
|
26
37
|
* - `flow` is stored as opaque JSON; typed as `FormFlow | undefined`
|
|
27
38
|
* - `response` may be null; coerced to `undefined`, otherwise passed through wholesale, which
|
|
28
39
|
* includes `response.redirect.reference` (present only when the plugin's `redirectRelationships`
|
|
29
|
-
* option is set):
|
|
30
|
-
*
|
|
40
|
+
* option is set): `value` may be a bare id (`depth: 0`) or a populated document, and the host
|
|
41
|
+
* resolves it to a URL either by writing `redirect.url` or by passing `options.resolveRedirect`
|
|
31
42
|
* - the button labels (`submitLabel`/`prevLabel`/`nextLabel`) live at the document root now (there
|
|
32
43
|
* is no `buttons` group); the non-empty ones are reassembled into `FormDocument.buttons`
|
|
33
44
|
* - `title` may be null; coerced to `undefined`
|
|
@@ -61,7 +72,7 @@ declare function toFormDocument(form: {
|
|
|
61
72
|
url?: string | null;
|
|
62
73
|
reference?: {
|
|
63
74
|
relationTo?: string | null;
|
|
64
|
-
value?: number | string | null;
|
|
75
|
+
value?: number | string | object | null;
|
|
65
76
|
} | null;
|
|
66
77
|
} | null;
|
|
67
78
|
} | null;
|
|
@@ -79,6 +90,12 @@ declare function toFormDocument(form: {
|
|
|
79
90
|
winningValues?: (string | null)[] | null;
|
|
80
91
|
} | null;
|
|
81
92
|
} | null;
|
|
93
|
+
/**
|
|
94
|
+
* Consent statements resolved onto the doc by the plugin's forms `afterRead` hook (when
|
|
95
|
+
* `consent.sources` is set), so consent renders on any fetch path (modal/client, not just RSC).
|
|
96
|
+
* An explicit `options.consentStatements` takes precedence when both are present.
|
|
97
|
+
*/
|
|
98
|
+
consentStatements?: ConsentStatements;
|
|
82
99
|
}, options?: ToFormDocumentOptions): FormDocument;
|
|
83
100
|
//#endregion
|
|
84
101
|
export { ToFormDocumentOptions, toFormDocument };
|
|
@@ -26,8 +26,8 @@ const buttonSettingsOf = (form) => {
|
|
|
26
26
|
* - `flow` is stored as opaque JSON; typed as `FormFlow | undefined`
|
|
27
27
|
* - `response` may be null; coerced to `undefined`, otherwise passed through wholesale, which
|
|
28
28
|
* includes `response.redirect.reference` (present only when the plugin's `redirectRelationships`
|
|
29
|
-
* option is set):
|
|
30
|
-
*
|
|
29
|
+
* option is set): `value` may be a bare id (`depth: 0`) or a populated document, and the host
|
|
30
|
+
* resolves it to a URL either by writing `redirect.url` or by passing `options.resolveRedirect`
|
|
31
31
|
* - the button labels (`submitLabel`/`prevLabel`/`nextLabel`) live at the document root now (there
|
|
32
32
|
* is no `buttons` group); the non-empty ones are reassembled into `FormDocument.buttons`
|
|
33
33
|
* - `title` may be null; coerced to `undefined`
|
|
@@ -56,12 +56,24 @@ function toFormDocument(form, options) {
|
|
|
56
56
|
} : void 0;
|
|
57
57
|
let fields = form.fields ?? [];
|
|
58
58
|
if (options?.pollOptions) fields = applyPollOptions(fields, form.poll?.resultsField, options.pollOptions);
|
|
59
|
-
|
|
59
|
+
const consentStatements = options?.consentStatements ?? form.consentStatements;
|
|
60
|
+
if (consentStatements) fields = applyConsentStatements(fields, consentStatements);
|
|
61
|
+
let response = form.response ?? void 0;
|
|
62
|
+
if (options?.resolveRedirect && response?.type === "redirect" && response.redirect?.reference && !response.redirect.url) {
|
|
63
|
+
const url = options.resolveRedirect(response.redirect.reference);
|
|
64
|
+
if (url) response = {
|
|
65
|
+
...response,
|
|
66
|
+
redirect: {
|
|
67
|
+
...response.redirect,
|
|
68
|
+
url
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
60
72
|
return {
|
|
61
73
|
id: form.id,
|
|
62
74
|
fields,
|
|
63
75
|
flow: form.flow,
|
|
64
|
-
response
|
|
76
|
+
response,
|
|
65
77
|
buttons: buttonSettingsOf(form),
|
|
66
78
|
title: form.title ?? void 0,
|
|
67
79
|
multistep: form.multistep === true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toFormDocument.js","names":[],"sources":["../../src/form/toFormDocument.ts"],"sourcesContent":["import { applyConsentStatements } from '../consent/applyConsentStatements'\nimport type { ConsentStatements } from '../consent/resolveConsentStatements'\nimport type { FormFlow } from '../flow/types'\nimport { applyPollOptions } from '../poll/applyPollOptions'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport type { FormFieldInstance } from '../submissions/types'\nimport type { FormButtonSettings, FormDocument, FormResponseSettings } from './types'\n\nexport type ToFormDocumentOptions = {\n\t/**\n\t * Source-resolved poll options (from `resolvePollOptions`), injected as the options of the\n\t * field instance named by the form's `poll.resultsField` so the client renders the host's\n\t * current choices instead of hand-authored ones.\n\t */\n\tpollOptions?: PollOption[]\n\t/**\n\t * Source-resolved consent statements (from `resolveConsentStatements`), injected into the\n\t * consent field instances they name. A form stores only a source id, so without these its\n\t * consent fields render with no statement at all.\n\t */\n\tconsentStatements?: ConsentStatements\n}\n\n/**\n * Reassemble the client button labels from the document's top-level `submitLabel`/`prevLabel`/\n * `nextLabel` fields, keeping only non-empty strings (mirroring the client's own `storedLabel`\n * fallback). Returns `undefined` when the author set no label, so the client uses its defaults.\n */\nconst buttonSettingsOf = (form: {\n\tsubmitLabel?: string | null\n\tprevLabel?: string | null\n\tnextLabel?: string | null\n}): FormButtonSettings | undefined => {\n\tconst label = (value: unknown): string | undefined =>\n\t\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\tconst settings: FormButtonSettings = {}\n\tconst submitLabel = label(form.submitLabel)\n\tconst prevLabel = label(form.prevLabel)\n\tconst nextLabel = label(form.nextLabel)\n\tif (submitLabel !== undefined) settings.submitLabel = submitLabel\n\tif (prevLabel !== undefined) settings.prevLabel = prevLabel\n\tif (nextLabel !== undefined) settings.nextLabel = nextLabel\n\treturn Object.keys(settings).length > 0 ? settings : undefined\n}\n\n/**\n * Narrows a Payload-generated form document (from `getPayload().findByID()` or `fetch`) to\n * `FormDocument` without an unsafe `as` cast. Handles the small structural mismatches between\n * the auto-generated collection types and what `<Form>` expects:\n * - `fields` may be a typed blocks-union array or null; normalized to `FormFieldInstance[]`\n * (nameless bare rows, e.g. message blocks, pass through with their row `id` intact)\n * - `flow` is stored as opaque JSON; typed as `FormFlow | undefined`\n * - `response` may be null; coerced to `undefined`, otherwise passed through wholesale, which\n * includes `response.redirect.reference` (present only when the plugin's `redirectRelationships`\n * option is set):
|
|
1
|
+
{"version":3,"file":"toFormDocument.js","names":[],"sources":["../../src/form/toFormDocument.ts"],"sourcesContent":["import { applyConsentStatements } from '../consent/applyConsentStatements'\nimport type { ConsentStatements } from '../consent/resolveConsentStatements'\nimport type { FormFlow } from '../flow/types'\nimport { applyPollOptions } from '../poll/applyPollOptions'\nimport type { PollOption } from '../poll/definePollOptionSource'\nimport type { FormFieldInstance } from '../submissions/types'\nimport type { FormButtonSettings, FormDocument, FormResponseSettings } from './types'\n\nexport type ToFormDocumentOptions = {\n\t/**\n\t * Source-resolved poll options (from `resolvePollOptions`), injected as the options of the\n\t * field instance named by the form's `poll.resultsField` so the client renders the host's\n\t * current choices instead of hand-authored ones.\n\t */\n\tpollOptions?: PollOption[]\n\t/**\n\t * Source-resolved consent statements (from `resolveConsentStatements`), injected into the\n\t * consent field instances they name. A form stores only a source id, so without these its\n\t * consent fields render with no statement at all.\n\t */\n\tconsentStatements?: ConsentStatements\n\t/**\n\t * Resolve an internal redirect `reference` to a same-site URL. Called only when\n\t * `response.type === 'redirect'`, a `reference` is present, and no `redirect.url` is set. Keep it\n\t * sync: operate on the already-loaded (populated) reference, since resolving a Doc to a URL is host\n\t * routing knowledge the plugin cannot supply. The result is written to `redirect.url`, so `<Form>`\n\t * navigates with no client change.\n\t */\n\tresolveRedirect?: (reference: {\n\t\trelationTo: string\n\t\tvalue: number | string | object\n\t}) => string | null | undefined\n}\n\n/**\n * Reassemble the client button labels from the document's top-level `submitLabel`/`prevLabel`/\n * `nextLabel` fields, keeping only non-empty strings (mirroring the client's own `storedLabel`\n * fallback). Returns `undefined` when the author set no label, so the client uses its defaults.\n */\nconst buttonSettingsOf = (form: {\n\tsubmitLabel?: string | null\n\tprevLabel?: string | null\n\tnextLabel?: string | null\n}): FormButtonSettings | undefined => {\n\tconst label = (value: unknown): string | undefined =>\n\t\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\tconst settings: FormButtonSettings = {}\n\tconst submitLabel = label(form.submitLabel)\n\tconst prevLabel = label(form.prevLabel)\n\tconst nextLabel = label(form.nextLabel)\n\tif (submitLabel !== undefined) settings.submitLabel = submitLabel\n\tif (prevLabel !== undefined) settings.prevLabel = prevLabel\n\tif (nextLabel !== undefined) settings.nextLabel = nextLabel\n\treturn Object.keys(settings).length > 0 ? settings : undefined\n}\n\n/**\n * Narrows a Payload-generated form document (from `getPayload().findByID()` or `fetch`) to\n * `FormDocument` without an unsafe `as` cast. Handles the small structural mismatches between\n * the auto-generated collection types and what `<Form>` expects:\n * - `fields` may be a typed blocks-union array or null; normalized to `FormFieldInstance[]`\n * (nameless bare rows, e.g. message blocks, pass through with their row `id` intact)\n * - `flow` is stored as opaque JSON; typed as `FormFlow | undefined`\n * - `response` may be null; coerced to `undefined`, otherwise passed through wholesale, which\n * includes `response.redirect.reference` (present only when the plugin's `redirectRelationships`\n * option is set): `value` may be a bare id (`depth: 0`) or a populated document, and the host\n * resolves it to a URL either by writing `redirect.url` or by passing `options.resolveRedirect`\n * - the button labels (`submitLabel`/`prevLabel`/`nextLabel`) live at the document root now (there\n * is no `buttons` group); the non-empty ones are reassembled into `FormDocument.buttons`\n * - `title` may be null; coerced to `undefined`\n * - `multistep`/`pollEnabled` are the two top-level form-type flags, coerced to strict booleans\n * - `poll` may be null; coerced to `undefined` (`resultsField`, `optionSource`, and\n * `sourceConfig` are dropped: server-side only; `outcome` passes through `winningValues` only)\n *\n * Unknown keys survive on `response` but not on `poll`, which is deliberate rather than incidental.\n * `response` is a visitor-facing group a host is meant to extend (`overrides.forms.fields`) and read\n * back off `FormDocument` in custom chrome, so it is cast wholesale. `poll` is plugin-owned config\n * that is split between\n * client-safe lifecycle state and server-only members, so it is rebuilt from an allowlist: a\n * passthrough would ship whatever a host put in `sourceConfig` (domain config, ids, credentials)\n * to every anonymous visitor. Adding a client-visible poll member means naming it here.\n *\n * Pure and framework-agnostic (no 'use client'): safe to call from a Server Component or any\n * other server-side code before handing the result to the client `<Form>`.\n */\nexport function toFormDocument(\n\tform: {\n\t\tid: number | string\n\t\tfields?: { blockType: string; name?: string; [key: string]: unknown }[] | null\n\t\tflow?: unknown\n\t\tresponse?: {\n\t\t\ttype?: string | null\n\t\t\tmessage?: unknown\n\t\t\tredirect?: {\n\t\t\t\turl?: string | null\n\t\t\t\treference?: { relationTo?: string | null; value?: number | string | object | null } | null\n\t\t\t} | null\n\t\t} | null\n\t\tsubmitLabel?: string | null\n\t\tprevLabel?: string | null\n\t\tnextLabel?: string | null\n\t\ttitle?: string | null\n\t\tmultistep?: boolean | null\n\t\tpollEnabled?: boolean | null\n\t\tpoll?: {\n\t\t\tresultsField?: string | null\n\t\t\tresultsVisibility?: string | null\n\t\t\tclosesAt?: string | null\n\t\t\toutcome?: { winningValues?: (string | null)[] | null } | null\n\t\t} | null\n\t\t/**\n\t\t * Consent statements resolved onto the doc by the plugin's forms `afterRead` hook (when\n\t\t * `consent.sources` is set), so consent renders on any fetch path (modal/client, not just RSC).\n\t\t * An explicit `options.consentStatements` takes precedence when both are present.\n\t\t */\n\t\tconsentStatements?: ConsentStatements\n\t},\n\toptions?: ToFormDocumentOptions\n): FormDocument {\n\tconst rawWinningValues = form.poll?.outcome?.winningValues\n\tconst winningValues = Array.isArray(rawWinningValues)\n\t\t? rawWinningValues.filter(\n\t\t\t\t(value): value is string => typeof value === 'string' && value.length > 0\n\t\t\t)\n\t\t: []\n\tconst poll = form.poll\n\t\t? {\n\t\t\t\tresultsVisibility: (form.poll.resultsVisibility ?? undefined) as\n\t\t\t\t\t| 'afterVote'\n\t\t\t\t\t| 'afterClose'\n\t\t\t\t\t| undefined,\n\t\t\t\tclosesAt: form.poll.closesAt ?? undefined,\n\t\t\t\t...(winningValues.length > 0 ? { outcome: { winningValues } } : {}),\n\t\t\t}\n\t\t: undefined\n\tlet fields = (form.fields ?? []) as FormFieldInstance[]\n\tif (options?.pollOptions) {\n\t\tfields = applyPollOptions(fields, form.poll?.resultsField, options.pollOptions)\n\t}\n\tconst consentStatements = options?.consentStatements ?? form.consentStatements\n\tif (consentStatements) {\n\t\tfields = applyConsentStatements(fields, consentStatements)\n\t}\n\tlet response = (form.response as FormResponseSettings | null | undefined) ?? undefined\n\t// A host that passes `resolveRedirect` gets internal references turned into `redirect.url` (all\n\t// `<Form>` navigates by), but only when the author set no explicit url. Non-mutating: clone the group.\n\tif (\n\t\toptions?.resolveRedirect &&\n\t\tresponse?.type === 'redirect' &&\n\t\tresponse.redirect?.reference &&\n\t\t!response.redirect.url\n\t) {\n\t\tconst url = options.resolveRedirect(response.redirect.reference)\n\t\tif (url) {\n\t\t\tresponse = { ...response, redirect: { ...response.redirect, url } }\n\t\t}\n\t}\n\treturn {\n\t\tid: form.id,\n\t\tfields,\n\t\tflow: form.flow as FormFlow | undefined,\n\t\tresponse,\n\t\tbuttons: buttonSettingsOf(form),\n\t\ttitle: form.title ?? undefined,\n\t\tmultistep: form.multistep === true,\n\t\tpollEnabled: form.pollEnabled === true,\n\t\tpoll,\n\t}\n}\n"],"mappings":";;;;;;;;AAuCA,MAAM,oBAAoB,SAIY;CACrC,MAAM,SAAS,UACd,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA;CACzD,MAAM,WAA+B,CAAC;CACtC,MAAM,cAAc,MAAM,KAAK,WAAW;CAC1C,MAAM,YAAY,MAAM,KAAK,SAAS;CACtC,MAAM,YAAY,MAAM,KAAK,SAAS;CACtC,IAAI,gBAAgB,KAAA,GAAW,SAAS,cAAc;CACtD,IAAI,cAAc,KAAA,GAAW,SAAS,YAAY;CAClD,IAAI,cAAc,KAAA,GAAW,SAAS,YAAY;CAClD,OAAO,OAAO,KAAK,QAAQ,EAAE,SAAS,IAAI,WAAW,KAAA;AACtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,eACf,MA+BA,SACe;CACf,MAAM,mBAAmB,KAAK,MAAM,SAAS;CAC7C,MAAM,gBAAgB,MAAM,QAAQ,gBAAgB,IACjD,iBAAiB,QAChB,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CACzE,IACC,CAAC;CACJ,MAAM,OAAO,KAAK,OACf;EACA,mBAAoB,KAAK,KAAK,qBAAqB,KAAA;EAInD,UAAU,KAAK,KAAK,YAAY,KAAA;EAChC,GAAI,cAAc,SAAS,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,CAAC;CAClE,IACC,KAAA;CACH,IAAI,SAAU,KAAK,UAAU,CAAC;CAC9B,IAAI,SAAS,aACZ,SAAS,iBAAiB,QAAQ,KAAK,MAAM,cAAc,QAAQ,WAAW;CAE/E,MAAM,oBAAoB,SAAS,qBAAqB,KAAK;CAC7D,IAAI,mBACH,SAAS,uBAAuB,QAAQ,iBAAiB;CAE1D,IAAI,WAAY,KAAK,YAAwD,KAAA;CAG7E,IACC,SAAS,mBACT,UAAU,SAAS,cACnB,SAAS,UAAU,aACnB,CAAC,SAAS,SAAS,KAClB;EACD,MAAM,MAAM,QAAQ,gBAAgB,SAAS,SAAS,SAAS;EAC/D,IAAI,KACH,WAAW;GAAE,GAAG;GAAU,UAAU;IAAE,GAAG,SAAS;IAAU;GAAI;EAAE;CAEpE;CACA,OAAO;EACN,IAAI,KAAK;EACT;EACA,MAAM,KAAK;EACX;EACA,SAAS,iBAAiB,IAAI;EAC9B,OAAO,KAAK,SAAS,KAAA;EACrB,WAAW,KAAK,cAAc;EAC9B,aAAa,KAAK,gBAAgB;EAClC;CACD;AACD"}
|
package/dist/form/types.d.ts
CHANGED
|
@@ -10,14 +10,14 @@ type FormResponseSettings = {
|
|
|
10
10
|
url?: string | null;
|
|
11
11
|
/**
|
|
12
12
|
* Present when the plugin's `redirectRelationships` option is set and the author picked an
|
|
13
|
-
* internal document.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* internal document. At `depth: 0` this is Payload's raw `{ relationTo, value }` id pair; at
|
|
14
|
+
* `depth > 0` (or via the generated `Form` type) `value` is a populated document, which is why
|
|
15
|
+
* it widens to `object`. Resolve it to a URL host-side (write `redirect.url`) or by passing
|
|
16
|
+
* `resolveRedirect` to `toFormDocument`, since the routing is the host's knowledge.
|
|
17
17
|
*/
|
|
18
18
|
reference?: {
|
|
19
19
|
relationTo: string;
|
|
20
|
-
value: number | string;
|
|
20
|
+
value: number | string | object;
|
|
21
21
|
} | null;
|
|
22
22
|
} | null;
|
|
23
23
|
};
|