@10x-media/form-builder 0.1.0-beta.2 → 0.1.0-beta.4
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 +60 -0
- package/README.md +4 -4
- package/dist/actions/body/converters.d.ts +20 -0
- package/dist/actions/body/converters.js +57 -0
- package/dist/actions/body/converters.js.map +1 -0
- package/dist/actions/body/escapeHtml.d.ts +6 -0
- package/dist/actions/body/escapeHtml.js +14 -0
- package/dist/actions/body/escapeHtml.js.map +1 -0
- package/dist/actions/body/serializeBody.d.ts +50 -0
- package/dist/actions/body/serializeBody.js +78 -0
- package/dist/actions/body/serializeBody.js.map +1 -0
- package/dist/actions/body/serializeSlate.js +31 -0
- package/dist/actions/body/serializeSlate.js.map +1 -0
- package/dist/actions/body/wildcards.d.ts +10 -0
- package/dist/actions/body/wildcards.js +33 -0
- package/dist/actions/body/wildcards.js.map +1 -0
- package/dist/actions/builtin/confirmation.js +40 -15
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailTeam.js +19 -12
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/index.d.ts +13 -1
- package/dist/actions/builtin/index.js +18 -7
- package/dist/actions/builtin/index.js.map +1 -1
- package/dist/actions/builtin/signedWebhook.js +6 -2
- package/dist/actions/builtin/signedWebhook.js.map +1 -1
- package/dist/actions/defineAction.d.ts +2 -1
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/dispatch.js +2 -1
- package/dist/actions/dispatch.js.map +1 -1
- package/dist/actions/runActions.js +10 -1
- package/dist/actions/runActions.js.map +1 -1
- package/dist/actions/task.js +7 -5
- package/dist/actions/task.js.map +1 -1
- package/dist/aggregation/resolveResultsRequest.d.ts +22 -6
- package/dist/aggregation/resolveResultsRequest.js +18 -7
- package/dist/aggregation/resolveResultsRequest.js.map +1 -1
- package/dist/calc/normalizeCalc.js +5 -17
- package/dist/calc/normalizeCalc.js.map +1 -1
- package/dist/calc/types.d.ts +8 -2
- package/dist/calc/types.js +22 -0
- package/dist/calc/types.js.map +1 -0
- package/dist/client/ByteSizeField.d.ts +21 -0
- package/dist/client/ByteSizeField.js +56 -0
- package/dist/client/ByteSizeField.js.map +1 -0
- package/dist/client/ConditionBuilder.js +2 -1
- package/dist/client/ConditionBuilder.js.map +1 -1
- package/dist/client/FieldNameSelect.d.ts +20 -0
- package/dist/client/FieldNameSelect.js +58 -0
- package/dist/client/FieldNameSelect.js.map +1 -0
- package/dist/client/FlowBuilder.d.ts +0 -1
- package/dist/client/FlowBuilder.js +102 -59
- package/dist/client/FlowBuilder.js.map +1 -1
- package/dist/client/FormConditionField.js +1 -4
- package/dist/client/FormConditionField.js.map +1 -1
- package/dist/client/admin.css +234 -0
- package/dist/client/flowAuthoring.js +35 -0
- package/dist/client/flowAuthoring.js.map +1 -0
- package/dist/client/synthesizeClientField.js +6 -2
- package/dist/client/synthesizeClientField.js.map +1 -1
- package/dist/client/toStaticLabel.js +13 -0
- package/dist/client/toStaticLabel.js.map +1 -0
- package/dist/collections/formSubmissions.js +49 -9
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +251 -88
- package/dist/collections/forms.js.map +1 -1
- package/dist/conditions/conditionType.js +9 -2
- package/dist/conditions/conditionType.js.map +1 -1
- package/dist/conditions/normalizeConditions.js +2 -1
- package/dist/conditions/normalizeConditions.js.map +1 -1
- package/dist/consent/builtin/index.d.ts +11 -1
- package/dist/consent/builtin/index.js +15 -5
- package/dist/consent/builtin/index.js.map +1 -1
- package/dist/consent/builtin/static.js +7 -3
- package/dist/consent/builtin/static.js.map +1 -1
- package/dist/exports/client.d.ts +3 -1
- package/dist/exports/client.js +3 -1
- package/dist/exports/react.d.ts +15 -6
- package/dist/exports/react.js +10 -2
- package/dist/exports/rsc.d.ts +6 -1
- package/dist/exports/rsc.js +6 -1
- package/dist/exports/types.d.ts +4 -4
- package/dist/fields/buildFieldBlocks.js +20 -15
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/calculation.js +115 -2
- package/dist/fields/builtin/calculation.js.map +1 -1
- package/dist/fields/builtin/consent.js +8 -4
- package/dist/fields/builtin/consent.js.map +1 -1
- package/dist/fields/builtin/date.js +29 -0
- package/dist/fields/builtin/date.js.map +1 -0
- package/dist/fields/builtin/file.d.ts +15 -0
- package/dist/fields/builtin/file.js +87 -7
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +24 -0
- package/dist/fields/builtin/index.js +29 -8
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/builtin/message.js +26 -0
- package/dist/fields/builtin/message.js.map +1 -0
- package/dist/fields/builtin/repeater.js +7 -3
- package/dist/fields/builtin/repeater.js.map +1 -1
- package/dist/fields/builtin/select.js +7 -3
- package/dist/fields/builtin/select.js.map +1 -1
- package/dist/fields/fieldNamesOfType.js +25 -0
- package/dist/fields/fieldNamesOfType.js.map +1 -0
- package/dist/fields/localizedIf.d.ts +13 -0
- package/dist/fields/localizedIf.js +12 -0
- package/dist/fields/localizedIf.js.map +1 -0
- package/dist/fields/sharedConfig.js +82 -57
- package/dist/fields/sharedConfig.js.map +1 -1
- package/dist/fields/types.d.ts +7 -2
- package/dist/flow/normalizeFlow.js +7 -1
- package/dist/flow/normalizeFlow.js.map +1 -1
- package/dist/flow/stepLabel.js +15 -0
- package/dist/flow/stepLabel.js.map +1 -0
- package/dist/form/pollState.d.ts +22 -0
- package/dist/form/pollState.js +17 -0
- package/dist/form/pollState.js.map +1 -0
- package/dist/form/toFormDocument.d.ts +60 -0
- package/dist/form/toFormDocument.js +39 -0
- package/dist/form/toFormDocument.js.map +1 -0
- package/dist/form/types.d.ts +48 -0
- package/dist/index.d.ts +81 -13
- package/dist/index.js +36 -15
- package/dist/index.js.map +1 -1
- package/dist/plugin/registerCollections.js +17 -37
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/plugin/uploadsCollection.d.ts +11 -0
- package/dist/plugin/uploadsCollection.js +44 -0
- package/dist/plugin/uploadsCollection.js.map +1 -0
- package/dist/poll/applyPollOptions.js +17 -0
- package/dist/poll/applyPollOptions.js.map +1 -0
- package/dist/poll/buildPollOptionSourceFields.js +53 -0
- package/dist/poll/buildPollOptionSourceFields.js.map +1 -0
- package/dist/poll/definePollOptionSource.d.ts +35 -0
- package/dist/poll/definePollOptionSource.js +6 -0
- package/dist/poll/definePollOptionSource.js.map +1 -0
- package/dist/poll/registry.d.ts +16 -0
- package/dist/poll/registry.js +16 -0
- package/dist/poll/registry.js.map +1 -0
- package/dist/poll/resolvePollOptions.d.ts +28 -0
- package/dist/poll/resolvePollOptions.js +64 -0
- package/dist/poll/resolvePollOptions.js.map +1 -0
- package/dist/poll/resolvePollOutcome.d.ts +21 -0
- package/dist/poll/resolvePollOutcome.js +36 -0
- package/dist/poll/resolvePollOutcome.js.map +1 -0
- package/dist/prefill/valuesFromSearchParams.js +1 -1
- package/dist/prefill/valuesFromSearchParams.js.map +1 -1
- package/dist/presentations/types.d.ts +3 -2
- package/dist/react/Form.d.ts +13 -31
- package/dist/react/Form.js +98 -85
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.d.ts +35 -1
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/FormControls.d.ts +55 -0
- package/dist/react/FormControls.js +56 -0
- package/dist/react/FormControls.js.map +1 -0
- package/dist/react/FormResults.d.ts +6 -3
- package/dist/react/FormResults.js +37 -29
- package/dist/react/FormResults.js.map +1 -1
- package/dist/react/FormSteps.d.ts +17 -0
- package/dist/react/FormSteps.js +35 -0
- package/dist/react/FormSteps.js.map +1 -0
- package/dist/react/Poll.d.ts +17 -6
- package/dist/react/Poll.js +66 -18
- package/dist/react/Poll.js.map +1 -1
- package/dist/react/captcha/HcaptchaCaptcha.d.ts +28 -0
- package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
- package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/RecaptchaCaptcha.d.ts +41 -0
- package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
- package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/TurnstileCaptcha.d.ts +28 -0
- package/dist/react/captcha/TurnstileCaptcha.js +53 -0
- package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
- package/dist/react/captcha/types.d.ts +8 -0
- package/dist/react/captcha/useCaptchaScript.js +45 -0
- package/dist/react/captcha/useCaptchaScript.js.map +1 -0
- package/dist/react/captcha/vendors.js +8 -0
- package/dist/react/captcha/vendors.js.map +1 -0
- package/dist/react/fetchResults.d.ts +1 -1
- package/dist/react/fetchResults.js +1 -1
- package/dist/react/fetchResults.js.map +1 -1
- package/dist/react/primitives/Input.d.ts +1 -1
- package/dist/react/primitives/Input.js.map +1 -1
- package/dist/react/renderers/calculation.js +1 -1
- package/dist/react/renderers/checkbox.js +1 -1
- package/dist/react/renderers/consent.js +1 -1
- package/dist/react/renderers/date.js +37 -0
- package/dist/react/renderers/date.js.map +1 -0
- package/dist/react/renderers/email.js +1 -1
- package/dist/react/renderers/file.js +25 -8
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/index.js +4 -0
- package/dist/react/renderers/index.js.map +1 -1
- package/dist/react/renderers/message.js +35 -0
- package/dist/react/renderers/message.js.map +1 -0
- package/dist/react/renderers/number.js +1 -1
- package/dist/react/renderers/repeater.js +1 -1
- package/dist/react/renderers/select.js +1 -1
- package/dist/react/renderers/text.js +1 -1
- package/dist/react/renderers/textarea.js +1 -1
- package/dist/react/state.d.ts +25 -1
- package/dist/react/state.js +14 -1
- package/dist/react/state.js.map +1 -1
- package/dist/react/uploadFile.d.ts +2 -2
- package/dist/react/uploadFile.js +1 -1
- package/dist/react/uploadFile.js.map +1 -1
- package/dist/spam/captcha.d.ts +3 -2
- package/dist/spam/captcha.js +3 -2
- package/dist/spam/captcha.js.map +1 -1
- package/dist/spam/clientIp.js +9 -0
- package/dist/spam/clientIp.js.map +1 -0
- package/dist/spam/identify.js +3 -6
- package/dist/spam/identify.js.map +1 -1
- package/dist/spam/providers/hcaptcha.d.ts +17 -0
- package/dist/spam/providers/hcaptcha.js +18 -0
- package/dist/spam/providers/hcaptcha.js.map +1 -0
- package/dist/spam/providers/recaptcha.d.ts +23 -0
- package/dist/spam/providers/recaptcha.js +23 -0
- package/dist/spam/providers/recaptcha.js.map +1 -0
- package/dist/spam/providers/siteverify.js +34 -0
- package/dist/spam/providers/siteverify.js.map +1 -0
- package/dist/spam/providers/turnstile.d.ts +17 -0
- package/dist/spam/providers/turnstile.js +18 -0
- package/dist/spam/providers/turnstile.js.map +1 -0
- package/dist/spam/spamGuard.js +2 -5
- package/dist/spam/spamGuard.js.map +1 -1
- package/dist/spam/types.d.ts +2 -2
- package/dist/submissions/runSubmission.js +70 -33
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/validateSubmission.js +41 -7
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/submissions/votedCookie.d.ts +13 -0
- package/dist/submissions/votedCookie.js +27 -0
- package/dist/submissions/votedCookie.js.map +1 -0
- package/dist/translations/de.js +219 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +71 -10
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/index.d.ts +2 -1
- package/dist/translations/index.js +6 -2
- package/dist/translations/index.js.map +1 -1
- package/dist/translations/keys.d.ts +69 -8
- package/dist/translations/keys.js +70 -9
- package/dist/translations/keys.js.map +1 -1
- package/dist/uploads/formatBytes.d.ts +9 -0
- package/dist/uploads/formatBytes.js +25 -0
- package/dist/uploads/formatBytes.js.map +1 -0
- package/dist/uploads/resolveFileRef.js +1 -1
- package/dist/uploads/resolveFileRef.js.map +1 -1
- package/dist/uploads/types.d.ts +1 -2
- package/dist/validation/builtin/index.d.ts +14 -0
- package/dist/validation/builtin/index.js +12 -1
- package/dist/validation/builtin/index.js.map +1 -1
- package/dist/validation/builtin/maxDate.js +23 -0
- package/dist/validation/builtin/maxDate.js.map +1 -0
- package/dist/validation/builtin/minDate.js +23 -0
- package/dist/validation/builtin/minDate.js.map +1 -0
- package/dist/validation/builtin/validateDateBound.js +18 -0
- package/dist/validation/builtin/validateDateBound.js.map +1 -0
- package/dist/validation/validateUrl.js +22 -0
- package/dist/validation/validateUrl.js.map +1 -0
- package/package.json +4 -2
- package/styles/form-builder.css +10 -190
- package/dist/collections/uploads.d.ts +0 -30
- package/dist/collections/uploads.js +0 -55
- package/dist/collections/uploads.js.map +0 -1
- package/dist/presentations/registry.d.ts +0 -15
- package/dist/presentations/registry.js +0 -15
- package/dist/presentations/registry.js.map +0 -1
- package/dist/react/toFormDocument.d.ts +0 -24
- package/dist/react/toFormDocument.js +0 -22
- package/dist/react/toFormDocument.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# @10x-media/form-builder
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.4
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Feedback round: flow authoring, localized content, response/display settings, bring-your-own uploads, poll lifecycle, composable form chrome, and a German locale.
|
|
8
|
+
|
|
9
|
+
**Breaking:**
|
|
10
|
+
|
|
11
|
+
- **Uploads are bring-your-own and off by default.** The plugin no longer registers a `form-uploads` collection. `uploads` is now `false` (default; the `file` field type is removed from the registry entirely) or `{ collection: 'slug' }` pointing at an upload-enabled collection the app owns; the plugin validates it at boot, appends a hidden `owner` field, and prepends the spam upload hooks. The file field's `relationTo` config is gone (the server stamps the configured slug onto each `file` block as a hidden `uploadsCollection`), and `overrides.uploads` no longer exists.
|
|
12
|
+
- **Stored presentation is gone.** The `defaultPresentation` field on `forms` and the plugin-level `presentations` option are removed. Presentation is decided at render time via the `presentation`/`presentations` props on `<Form>`; the React presentation system (page/inline/modal/drawer, custom `FormPresentation`) is unchanged.
|
|
13
|
+
- **Poll config moved into a `poll` group.** The top-level `showResults`/`resultsField` fields on `forms` are replaced by `poll.enabled`, `poll.resultsField`, plus the new `poll.resultsVisibility` and `poll.closesAt`. The results endpoint now gates on `poll.enabled` and visibility.
|
|
14
|
+
- **Content fields are localized by default.** Author-facing content (field labels, placeholders, descriptions, option labels, consent statements, action subjects/bodies, response/display content) carries `localized: true`; on hosts with Payload `localization` configured the stored data shape changes to per-locale objects. Hosts without localization are unaffected (Payload strips the flag). Opt out with `localizeContent: false`; when opting out, derive registry overrides from `buildDefaultFieldDefinitions(false)` / `buildDefaultActionDefinitions(false)` / `buildDefaultConsentSources(false)` instead of spreading the prebuilt defaults, which carry the localized flags.
|
|
15
|
+
- **`<Poll hasVoted>` ORs with the localStorage guard.** `hasVoted: true` marks the visitor as voted regardless of localStorage; it previously could suppress the localStorage read. `false`/omitted falls back to localStorage as before.
|
|
16
|
+
- **Form chrome markup changed.** The default buttons now carry stable classes (`fb-form__back`, `fb-form__next`, `fb-form__submit`) and always render inside a `.fb-form__controls` wrapper (previously classless buttons, wrapped only on multi-step forms). CSS or tests targeting the old bare buttons need the new hooks.
|
|
17
|
+
|
|
18
|
+
**Features:**
|
|
19
|
+
|
|
20
|
+
- Flow authoring without ids: steps are named by title (ids auto-generated, never shown), field assignment is exclusive with an unassigned-fields list, and save-time normalization dedupes fields across steps.
|
|
21
|
+
- `message` field type: display-only rich text between fields, never validated or stored, condition-targetable, recall-token aware.
|
|
22
|
+
- Response settings (`response` group + tab): success `message` rich text, `redirect` URL (fires on the custom-`children` path too), and `submitLabel` with prop-over-document-over-default precedence.
|
|
23
|
+
- Display settings (`display` group + tab): opt-in form title (`showTitle`/`title`) and rich text `intro` rendered above the fields, recall-token aware.
|
|
24
|
+
- Field config layout: the Field tab lays basics out in rows (name/label, width/placeholder), `width` is required with a `full` default, and `required` sits last.
|
|
25
|
+
- Poll lifecycle: `closesAt` close date enforced at submission time, `resultsVisibility` (`afterVote`/`afterClose`) gating for anonymous results, a `results.access` seam for multi-tenant scoping, and `poll: { votedCookie: true }` for an httpOnly SSR voted marker read via `hasVotedCookie`.
|
|
26
|
+
- Poll option sources: `definePollOptionSource` + the `poll.sources` registry resolve a poll's choices from host domain data at render (`resolvePollOptions` + `toFormDocument(doc, { pollOptions })`) and at submission (resolved values are the only accepted answers, fail closed).
|
|
27
|
+
- Poll outcome: `resolvePollOutcome` records a final `winningValue` (admin read-only, server-only write path); `<Poll>` renders the final state and `<FormResults winningValue>` highlights the winner.
|
|
28
|
+
- `richText.editor` overrides the editor on both action body fields (e.g. a minimal toolbar for email authors); a custom `richText.serialize` now receives the submitted `form` and `req` for per-tenant lookups or handing off to react-email.
|
|
29
|
+
- Composable chrome: `<FormControls>` (the default buttons with all `render*`/className overrides) and `<FormSteps>` (step progress with `aria-current`) are exported for custom `children` layouts, and `useFormContext` exposes the controller context including resolved `labels`, the active `t`, and `effectiveValues`.
|
|
30
|
+
- Server-safe `toFormDocument`: no `'use client'` directive, exported from the root and the `/rsc` subpath (which also gains `resolvePollOptions`, `resolvePollOutcome`, `hasVotedCookie`, `votedCookieName`, and `isPollClosed`), so Server Components narrow the document without pulling in the React client layer.
|
|
31
|
+
- German (`de`) admin and renderer strings ship built in alongside English.
|
|
32
|
+
- The shadcn registry now covers every built-in field type (adds date, consent, calculation, and repeater renderers); the default `textarea` type label is now "Textarea".
|
|
33
|
+
- Repeaters with `minRows` start pre-seeded with that many empty rows instead of appearing blank until validation; an explicit `initialValues` entry for the field (including `[]`) still wins over the seed.
|
|
34
|
+
|
|
35
|
+
**Fixes:**
|
|
36
|
+
|
|
37
|
+
- File fields nested inside a repeater row now get the same server-side upload enforcement as top-level file fields: each captured value is re-derived into a `FileRef` from the stored upload doc (owner, MIME type, and size checked against the sub-field config) instead of storing the raw client-submitted id, and the submission fails closed on a forged id or a missing uploads collection.
|
|
38
|
+
- Upload enforcement is now driven by a field's value kind rather than its block type, so a custom field type registered with `value: 'file'` is captured and enforced (owner/MIME/size) at both the top level and inside repeaters, not just the built-in `file` type.
|
|
39
|
+
|
|
40
|
+
**Notes:**
|
|
41
|
+
|
|
42
|
+
- The voted cookie is deliberately not `Secure`: it is a UX-only marker (skip re-showing an answered form), never an integrity or auth signal, and staying non-Secure keeps plain-HTTP local development working.
|
|
43
|
+
- No migrations are shipped for the removed/moved `forms` columns (`defaultPresentation`, `showResults`, top-level `resultsField`); pre-1.0 with no known production consumers, write your own if you carry data.
|
|
44
|
+
|
|
45
|
+
## 0.1.0-beta.3
|
|
46
|
+
|
|
47
|
+
### Minor Changes
|
|
48
|
+
|
|
49
|
+
- Bundled captcha adapters and widget components. `turnstileProvider`, `recaptchaProvider` (v2 + v3 with `minScore`), and `hcaptchaProvider` verify tokens server-side with fail-closed semantics (network errors, timeouts, and non-2xx responses reject the submission). Matching headless `TurnstileCaptcha`, `RecaptchaCaptcha`, and `HcaptchaCaptcha` components on the `/react` export load each vendor script once, report tokens through `onToken` for `<Form captchaToken>`, clear on expiry or error, and expose `reset` (plus on-demand `execute` refresh for reCAPTCHA v3) via a ref handle.
|
|
50
|
+
|
|
51
|
+
- Feedback round: `date` field type (native date input, real-calendar validation) with `minDate`/`maxDate` rules; rich text notification bodies for `emailTeam`/`confirmation` using the project's configured editor, serialized with escaping, sanitized links, `{{ name|fallback }}`/`{{*}}`/`{{*:table}}` tokens, a plugin `richText { converters, serialize }` option for custom nodes and non-HTML channels, and `renderBody` on action run args; the confirmation recipient is a select over the form's email fields, validated server-side on save; `signedWebhook` validates its URL as absolute http(s) and describes its config fields; calculation expressions get an admin description, Monaco JSON-schema autocomplete, and save-time validation of the expression tree; the file field's `mimeTypes` is a curated select and `maxSize` live-previews as a human-readable size, with a client-side size pre-check and hint line on the renderer; flow transitions resolve against effective values so calculation fields can drive branching; the forms document (Fields/Flow/Actions) and every field block (Field/Validation/Advanced) use tabbed admin layouts, and admin components ship their own CSS (no `styles.css` import needed for the admin). New exports: `serializeBody`, `defaultBodyConverters`, `sanitizeUrl`, `escapeHtml`, `renderAllValues`, `renderAllValuesTable`, `fileMimeTypeOptions`, `formatBytes`, `defaultFieldDefinitionsByType`, `defaultValidationRulesByType`, and the `ByteSizeField`/`FieldNameSelect` client components.
|
|
52
|
+
|
|
53
|
+
**Soft compat:** existing plain-string action bodies keep working through the send path (interpolated exactly as before), though the admin editor shows them as empty rich text until re-entered. File fields keep enforcing stored free-text MIME values, but values outside the curated list need re-selecting the next time that field is edited; submitted data is untouched.
|
|
54
|
+
|
|
55
|
+
**Postgres migration note:** on `@payloadcms/db-postgres`, two `forms` column types change: action `body` goes `varchar` to `jsonb` (pre-existing string bodies are not valid JSON, so cast with `USING to_jsonb(body)` when writing the migration), and `mimeTypes` moves from a text array to an enum-backed select. Mongo needs no migration. The project config must also set an `editor` (rich text bodies inherit it; Payload throws `MissingEditorProp` without one).
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- The registry (shadcn-style) file field now matches the built-in file renderer: a client-side max-size pre-check rejects oversized files before uploading, and an accepted-types/max-size hint line renders under the input.
|
|
60
|
+
|
|
61
|
+
`minDate`/`maxDate` rule bounds are now validated as real `YYYY-MM-DD` calendar dates in the admin UI, so a malformed bound (e.g. `abc` or `2024-02-30`) is rejected at config time instead of silently breaking the rule's comparisons.
|
|
62
|
+
|
|
3
63
|
## 0.1.0-beta.2
|
|
4
64
|
|
|
5
65
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-pl
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Fields**: text, textarea, email, number, select, checkbox, file, consent, calculation, and repeater (dynamic row lists with per-row sub-field validation), authored as Payload blocks; custom types via `defineFormField` are never second-class.
|
|
11
|
+
- **Fields**: text, textarea, email, number, date, select, checkbox, file, consent, calculation, and repeater (dynamic row lists with per-row sub-field validation), authored as Payload blocks; custom types via `defineFormField` are never second-class.
|
|
12
12
|
- **Validation**: declarative per-field rules, custom messages and severities, cross-field and async server-only rules, a Standard Schema escape hatch (zod, valibot, ...), one server-authoritative engine.
|
|
13
13
|
- **Conditional logic**: `visibleWhen` / `validateWhen` in Payload's `Where` shape, one isomorphic engine on client and server.
|
|
14
14
|
- **Multi-step flows** with conditional branching, authored in the admin flow builder.
|
|
@@ -17,10 +17,10 @@ Part of the [@10x-media Payload plugins](https://github.com/10x-media/payload-pl
|
|
|
17
17
|
- **Recall and prefill**: pipe answers into labels and messages; URL prefill; hidden context fields.
|
|
18
18
|
- **Calculations**: a safe no-eval expression engine for totals and quiz scores, recomputed on the server.
|
|
19
19
|
- **Post-submit actions**: email, confirmation, signed webhook, and custom actions, queued via Payload jobs with a bounded inline fallback.
|
|
20
|
-
- **Consent** with proof by reference and policy-version capture; **file uploads** with server-enforced MIME/size; **polls** with a gated public results endpoint.
|
|
21
|
-
- **Spam protection** on by default: honeypot, per-identity rate limiting,
|
|
20
|
+
- **Consent** with proof by reference and policy-version capture; **file uploads** into your own upload collection with server-enforced MIME/size; **polls** with a gated public results endpoint.
|
|
21
|
+
- **Spam protection** on by default: honeypot, per-identity rate limiting, bundled captcha adapters (Turnstile, reCAPTCHA, hCaptcha), upload-ownership scoping, privacy-first metadata.
|
|
22
22
|
- **Accessibility** verified by automated axe checks; **typed translations** via `@10x-media/form-builder/i18n`.
|
|
23
|
-
- **Collection overrides**: extend `forms
|
|
23
|
+
- **Collection overrides**: extend `forms` and `form-submissions` with extra fields, hooks, and access rules using an explicit spread API that guarantees plugin-critical hooks always run.
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/actions/body/converters.d.ts
|
|
2
|
+
/** Text pipelines a converter can apply to author-typed content. */
|
|
3
|
+
type BodyRender = {
|
|
4
|
+
/** Full text pipeline: HTML-escape, then expand `{{*}}`/`{{*:table}}` and recall tokens with escaped values. */text: (raw: string) => string; /** Recall-token interpolation only, unescaped; for URL-like values that get escaped separately. */
|
|
5
|
+
interpolate: (raw: string) => string;
|
|
6
|
+
};
|
|
7
|
+
type BodyConverterArgs = {
|
|
8
|
+
node: Record<string, unknown>; /** The node's already-serialized children. */
|
|
9
|
+
children: string;
|
|
10
|
+
render: BodyRender;
|
|
11
|
+
};
|
|
12
|
+
/** Serializes one Lexical node type to an HTML string. Keyed by `node.type` in the registry. */
|
|
13
|
+
type BodyConverter = (args: BodyConverterArgs) => string;
|
|
14
|
+
/** Allow http(s)/mailto/tel and scheme-less (relative) URLs; anything else becomes `#`. */
|
|
15
|
+
declare const sanitizeUrl: (url: string) => string;
|
|
16
|
+
/** Default Lexical-to-HTML converter registry; consumers spread overrides per node type. */
|
|
17
|
+
declare const defaultBodyConverters: Record<string, BodyConverter>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { BodyConverter, BodyConverterArgs, BodyRender, defaultBodyConverters, sanitizeUrl };
|
|
20
|
+
//# sourceMappingURL=converters.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { escapeHtml } from "./escapeHtml.js";
|
|
2
|
+
//#region src/actions/body/converters.ts
|
|
3
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
4
|
+
const SAFE_SCHEME = /^(https?|mailto|tel):/i;
|
|
5
|
+
/** Allow http(s)/mailto/tel and scheme-less (relative) URLs; anything else becomes `#`. */
|
|
6
|
+
const sanitizeUrl = (url) => {
|
|
7
|
+
const probe = url.replace(/\s/g, "");
|
|
8
|
+
if (probe === "") return "#";
|
|
9
|
+
if (SCHEME.test(probe)) return SAFE_SCHEME.test(probe) ? url.trim() : "#";
|
|
10
|
+
return url.trim();
|
|
11
|
+
};
|
|
12
|
+
const BOLD = 1;
|
|
13
|
+
const ITALIC = 2;
|
|
14
|
+
const STRIKETHROUGH = 4;
|
|
15
|
+
const UNDERLINE = 8;
|
|
16
|
+
const CODE = 16;
|
|
17
|
+
const text = ({ node, render }) => {
|
|
18
|
+
let html = render.text(typeof node.text === "string" ? node.text : "");
|
|
19
|
+
const format = typeof node.format === "number" ? node.format : 0;
|
|
20
|
+
if (format & CODE) html = `<code>${html}</code>`;
|
|
21
|
+
if (format & BOLD) html = `<strong>${html}</strong>`;
|
|
22
|
+
if (format & ITALIC) html = `<em>${html}</em>`;
|
|
23
|
+
if (format & STRIKETHROUGH) html = `<s>${html}</s>`;
|
|
24
|
+
if (format & UNDERLINE) html = `<u>${html}</u>`;
|
|
25
|
+
return html;
|
|
26
|
+
};
|
|
27
|
+
const link = ({ node, children, render }) => {
|
|
28
|
+
const fields = node.fields ?? {};
|
|
29
|
+
const raw = typeof fields.url === "string" ? fields.url : typeof node.url === "string" ? node.url : "";
|
|
30
|
+
return `<a href="${escapeHtml(sanitizeUrl(render.interpolate(raw)))}"${fields.newTab === true ? " rel=\"noopener noreferrer\" target=\"_blank\"" : ""}>${children}</a>`;
|
|
31
|
+
};
|
|
32
|
+
const HEADING_TAG = /^h[1-6]$/;
|
|
33
|
+
const heading = ({ node, children }) => {
|
|
34
|
+
const tag = typeof node.tag === "string" && HEADING_TAG.test(node.tag) ? node.tag : "h2";
|
|
35
|
+
return `<${tag}>${children}</${tag}>`;
|
|
36
|
+
};
|
|
37
|
+
const list = ({ node, children }) => {
|
|
38
|
+
const tag = node.tag === "ol" || node.listType === "number" ? "ol" : "ul";
|
|
39
|
+
return `<${tag}>${children}</${tag}>`;
|
|
40
|
+
};
|
|
41
|
+
/** Default Lexical-to-HTML converter registry; consumers spread overrides per node type. */
|
|
42
|
+
const defaultBodyConverters = {
|
|
43
|
+
autolink: link,
|
|
44
|
+
heading,
|
|
45
|
+
horizontalrule: () => "<hr />",
|
|
46
|
+
linebreak: () => "<br />",
|
|
47
|
+
link,
|
|
48
|
+
list,
|
|
49
|
+
listitem: ({ children }) => `<li>${children}</li>`,
|
|
50
|
+
paragraph: ({ children }) => `<p>${children}</p>`,
|
|
51
|
+
quote: ({ children }) => `<blockquote>${children}</blockquote>`,
|
|
52
|
+
text
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { defaultBodyConverters, sanitizeUrl };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=converters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"converters.js","names":[],"sources":["../../../src/actions/body/converters.ts"],"sourcesContent":["import { escapeHtml } from './escapeHtml'\n\n/** Text pipelines a converter can apply to author-typed content. */\nexport type BodyRender = {\n\t/** Full text pipeline: HTML-escape, then expand `{{*}}`/`{{*:table}}` and recall tokens with escaped values. */\n\ttext: (raw: string) => string\n\t/** Recall-token interpolation only, unescaped; for URL-like values that get escaped separately. */\n\tinterpolate: (raw: string) => string\n}\n\nexport type BodyConverterArgs = {\n\tnode: Record<string, unknown>\n\t/** The node's already-serialized children. */\n\tchildren: string\n\trender: BodyRender\n}\n\n/** Serializes one Lexical node type to an HTML string. Keyed by `node.type` in the registry. */\nexport type BodyConverter = (args: BodyConverterArgs) => string\n\nconst SCHEME = /^[a-z][a-z0-9+.-]*:/i\nconst SAFE_SCHEME = /^(https?|mailto|tel):/i\n\n/** Allow http(s)/mailto/tel and scheme-less (relative) URLs; anything else becomes `#`. */\nexport const sanitizeUrl = (url: string): string => {\n\tconst probe = url.replace(/\\s/g, '')\n\tif (probe === '') {\n\t\treturn '#'\n\t}\n\tif (SCHEME.test(probe)) {\n\t\treturn SAFE_SCHEME.test(probe) ? url.trim() : '#'\n\t}\n\treturn url.trim()\n}\n\nconst BOLD = 1\nconst ITALIC = 2\nconst STRIKETHROUGH = 4\nconst UNDERLINE = 8\nconst CODE = 16\n\nconst text: BodyConverter = ({ node, render }) => {\n\tlet html = render.text(typeof node.text === 'string' ? node.text : '')\n\tconst format = typeof node.format === 'number' ? node.format : 0\n\tif (format & CODE) {\n\t\thtml = `<code>${html}</code>`\n\t}\n\tif (format & BOLD) {\n\t\thtml = `<strong>${html}</strong>`\n\t}\n\tif (format & ITALIC) {\n\t\thtml = `<em>${html}</em>`\n\t}\n\tif (format & STRIKETHROUGH) {\n\t\thtml = `<s>${html}</s>`\n\t}\n\tif (format & UNDERLINE) {\n\t\thtml = `<u>${html}</u>`\n\t}\n\treturn html\n}\n\nconst link: BodyConverter = ({ node, children, render }) => {\n\tconst fields = (node.fields ?? {}) as Record<string, unknown>\n\tconst raw =\n\t\ttypeof fields.url === 'string' ? fields.url : typeof node.url === 'string' ? node.url : ''\n\tconst href = escapeHtml(sanitizeUrl(render.interpolate(raw)))\n\tconst newTab = fields.newTab === true\n\treturn `<a href=\"${href}\"${newTab ? ' rel=\"noopener noreferrer\" target=\"_blank\"' : ''}>${children}</a>`\n}\n\nconst HEADING_TAG = /^h[1-6]$/\n\nconst heading: BodyConverter = ({ node, children }) => {\n\tconst tag = typeof node.tag === 'string' && HEADING_TAG.test(node.tag) ? node.tag : 'h2'\n\treturn `<${tag}>${children}</${tag}>`\n}\n\nconst list: BodyConverter = ({ node, children }) => {\n\tconst tag = node.tag === 'ol' || node.listType === 'number' ? 'ol' : 'ul'\n\treturn `<${tag}>${children}</${tag}>`\n}\n\n/** Default Lexical-to-HTML converter registry; consumers spread overrides per node type. */\nexport const defaultBodyConverters: Record<string, BodyConverter> = {\n\tautolink: link,\n\theading,\n\thorizontalrule: () => '<hr />',\n\tlinebreak: () => '<br />',\n\tlink,\n\tlist,\n\tlistitem: ({ children }) => `<li>${children}</li>`,\n\tparagraph: ({ children }) => `<p>${children}</p>`,\n\tquote: ({ children }) => `<blockquote>${children}</blockquote>`,\n\ttext,\n}\n"],"mappings":";;AAoBA,MAAM,SAAS;AACf,MAAM,cAAc;;AAGpB,MAAa,eAAe,QAAwB;CACnD,MAAM,QAAQ,IAAI,QAAQ,OAAO,EAAE;CACnC,IAAI,UAAU,IACb,OAAO;CAER,IAAI,OAAO,KAAK,KAAK,GACpB,OAAO,YAAY,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI;CAE/C,OAAO,IAAI,KAAK;AACjB;AAEA,MAAM,OAAO;AACb,MAAM,SAAS;AACf,MAAM,gBAAgB;AACtB,MAAM,YAAY;AAClB,MAAM,OAAO;AAEb,MAAM,QAAuB,EAAE,MAAM,aAAa;CACjD,IAAI,OAAO,OAAO,KAAK,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,EAAE;CACrE,MAAM,SAAS,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS;CAC/D,IAAI,SAAS,MACZ,OAAO,SAAS,KAAK;CAEtB,IAAI,SAAS,MACZ,OAAO,WAAW,KAAK;CAExB,IAAI,SAAS,QACZ,OAAO,OAAO,KAAK;CAEpB,IAAI,SAAS,eACZ,OAAO,MAAM,KAAK;CAEnB,IAAI,SAAS,WACZ,OAAO,MAAM,KAAK;CAEnB,OAAO;AACR;AAEA,MAAM,QAAuB,EAAE,MAAM,UAAU,aAAa;CAC3D,MAAM,SAAU,KAAK,UAAU,CAAC;CAChC,MAAM,MACL,OAAO,OAAO,QAAQ,WAAW,OAAO,MAAM,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;CAGzF,OAAO,YAFM,WAAW,YAAY,OAAO,YAAY,GAAG,CAAC,CAErC,EAAE,GADT,OAAO,WAAW,OACG,mDAA+C,GAAG,GAAG,SAAS;AACnG;AAEA,MAAM,cAAc;AAEpB,MAAM,WAA0B,EAAE,MAAM,eAAe;CACtD,MAAM,MAAM,OAAO,KAAK,QAAQ,YAAY,YAAY,KAAK,KAAK,GAAG,IAAI,KAAK,MAAM;CACpF,OAAO,IAAI,IAAI,GAAG,SAAS,IAAI,IAAI;AACpC;AAEA,MAAM,QAAuB,EAAE,MAAM,eAAe;CACnD,MAAM,MAAM,KAAK,QAAQ,QAAQ,KAAK,aAAa,WAAW,OAAO;CACrE,OAAO,IAAI,IAAI,GAAG,SAAS,IAAI,IAAI;AACpC;;AAGA,MAAa,wBAAuD;CACnE,UAAU;CACV;CACA,sBAAsB;CACtB,iBAAiB;CACjB;CACA;CACA,WAAW,EAAE,eAAe,OAAO,SAAS;CAC5C,YAAY,EAAE,eAAe,MAAM,SAAS;CAC5C,QAAQ,EAAE,eAAe,eAAe,SAAS;CACjD;AACD"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/actions/body/escapeHtml.d.ts
|
|
2
|
+
/** Escape `&`, `<`, `>`, `"`, `'` for safe embedding in HTML text or attribute values. */
|
|
3
|
+
declare const escapeHtml: (value: string) => string;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { escapeHtml };
|
|
6
|
+
//# sourceMappingURL=escapeHtml.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/actions/body/escapeHtml.ts
|
|
2
|
+
const REPLACEMENTS = {
|
|
3
|
+
"&": "&",
|
|
4
|
+
"<": "<",
|
|
5
|
+
">": ">",
|
|
6
|
+
"\"": """,
|
|
7
|
+
"'": "'"
|
|
8
|
+
};
|
|
9
|
+
/** Escape `&`, `<`, `>`, `"`, `'` for safe embedding in HTML text or attribute values. */
|
|
10
|
+
const escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => REPLACEMENTS[char] ?? char);
|
|
11
|
+
//#endregion
|
|
12
|
+
export { escapeHtml };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=escapeHtml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escapeHtml.js","names":[],"sources":["../../../src/actions/body/escapeHtml.ts"],"sourcesContent":["const REPLACEMENTS: Record<string, string> = {\n\t'&': '&',\n\t'<': '<',\n\t'>': '>',\n\t'\"': '"',\n\t\"'\": ''',\n}\n\n/** Escape `&`, `<`, `>`, `\"`, `'` for safe embedding in HTML text or attribute values. */\nexport const escapeHtml = (value: string): string =>\n\tvalue.replace(/[&<>\"']/g, (char) => REPLACEMENTS[char] ?? char)\n"],"mappings":";AAAA,MAAM,eAAuC;CAC5C,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAK;CACL,KAAK;AACN;;AAGA,MAAa,cAAc,UAC1B,MAAM,QAAQ,aAAa,SAAS,aAAa,SAAS,IAAI"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { SubmissionDescriptor, SubmissionValue } from "../../submissions/types.js";
|
|
2
|
+
import { BodyConverter } from "./converters.js";
|
|
3
|
+
import { PayloadRequest, RichTextField } from "payload";
|
|
4
|
+
|
|
5
|
+
//#region src/actions/body/serializeBody.d.ts
|
|
6
|
+
/** Minimal form identity threaded alongside a rendered body (e.g. per-tenant template lookups). */
|
|
7
|
+
type SerializeBodyForm = {
|
|
8
|
+
id: number | string;
|
|
9
|
+
title?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Submission data plus optional converter overrides available while serializing a body. */
|
|
12
|
+
type BodyContext = {
|
|
13
|
+
values: SubmissionValue[];
|
|
14
|
+
descriptors: SubmissionDescriptor[];
|
|
15
|
+
converters?: Record<string, BodyConverter>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Args a custom `richText.serialize` replacement receives per rendered body. Always populated by
|
|
19
|
+
* `makeRenderBody` (the action-body pipeline): `form` and `req` enable per-tenant template
|
|
20
|
+
* lookups or handing the body off to a renderer like react-email.
|
|
21
|
+
*/
|
|
22
|
+
type SerializeBodyArgs = {
|
|
23
|
+
body: unknown;
|
|
24
|
+
values: SubmissionValue[];
|
|
25
|
+
descriptors: SubmissionDescriptor[];
|
|
26
|
+
form: SerializeBodyForm;
|
|
27
|
+
req?: PayloadRequest;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Customizes how action bodies are authored and rendered. `converters` spread over the default
|
|
31
|
+
* Lexical node converters; `serialize` replaces the whole pipeline (for non-HTML channels like
|
|
32
|
+
* chat or plain text, or to hand the body plus the submitted `form`/`req` off to a renderer like
|
|
33
|
+
* react-email). `editor` overrides the Lexical/richText editor on both the emailTeam and
|
|
34
|
+
* confirmation action body fields (see `buildDefaultActionDefinitions`).
|
|
35
|
+
*/
|
|
36
|
+
type RichTextBodyOption = {
|
|
37
|
+
converters?: Record<string, BodyConverter>;
|
|
38
|
+
serialize?: (args: SerializeBodyArgs) => Promise<string> | string;
|
|
39
|
+
editor?: RichTextField['editor'];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is
|
|
43
|
+
* (pre-richText behavior, no escaping); a Lexical state walks the converter registry; a Slate
|
|
44
|
+
* array uses the minimal legacy serializer; anything else yields `''`. Rendered text is
|
|
45
|
+
* HTML-escaped and supports `{{ name|fallback }}`, `{{*}}`, and `{{*:table}}` tokens.
|
|
46
|
+
*/
|
|
47
|
+
declare const serializeBody: (body: unknown, ctx: BodyContext) => string;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { BodyContext, RichTextBodyOption, SerializeBodyArgs, serializeBody };
|
|
50
|
+
//# sourceMappingURL=serializeBody.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { interpolate } from "../../recall/interpolate.js";
|
|
2
|
+
import { escapeHtml } from "./escapeHtml.js";
|
|
3
|
+
import { defaultBodyConverters } from "./converters.js";
|
|
4
|
+
import { serializeSlate } from "./serializeSlate.js";
|
|
5
|
+
import { renderAllValues, renderAllValuesTable } from "./wildcards.js";
|
|
6
|
+
//#region src/actions/body/serializeBody.ts
|
|
7
|
+
/** Recall resolver over submission values: field name to stringified value, `''` when absent. */
|
|
8
|
+
const resolverFor = (values) => (name) => {
|
|
9
|
+
const entry = values.find((value) => value.field === name);
|
|
10
|
+
return entry == null ? "" : String(entry.value ?? "");
|
|
11
|
+
};
|
|
12
|
+
const renderFor = (ctx) => {
|
|
13
|
+
const resolve = resolverFor(ctx.values);
|
|
14
|
+
const htmlResolve = (name) => {
|
|
15
|
+
if (name === "*") return renderAllValues(ctx.values, ctx.descriptors);
|
|
16
|
+
if (name === "*:table") return renderAllValuesTable(ctx.values, ctx.descriptors);
|
|
17
|
+
return escapeHtml(resolve(name));
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
text: (raw) => interpolate(escapeHtml(raw), htmlResolve),
|
|
21
|
+
interpolate: (raw) => interpolate(raw, resolve)
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const serializeNodes = (nodes, converters, render) => nodes.map((node) => {
|
|
25
|
+
if (node == null || typeof node !== "object") return "";
|
|
26
|
+
const lexicalNode = node;
|
|
27
|
+
const children = Array.isArray(lexicalNode.children) ? serializeNodes(lexicalNode.children, converters, render) : "";
|
|
28
|
+
const converter = typeof lexicalNode.type === "string" ? converters[lexicalNode.type] : void 0;
|
|
29
|
+
return converter ? converter({
|
|
30
|
+
node: lexicalNode,
|
|
31
|
+
children,
|
|
32
|
+
render
|
|
33
|
+
}) : children;
|
|
34
|
+
}).join("");
|
|
35
|
+
const lexicalRootOf = (body) => {
|
|
36
|
+
if (body == null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
37
|
+
const root = body.root;
|
|
38
|
+
return root != null && typeof root === "object" ? root : null;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is
|
|
42
|
+
* (pre-richText behavior, no escaping); a Lexical state walks the converter registry; a Slate
|
|
43
|
+
* array uses the minimal legacy serializer; anything else yields `''`. Rendered text is
|
|
44
|
+
* HTML-escaped and supports `{{ name|fallback }}`, `{{*}}`, and `{{*:table}}` tokens.
|
|
45
|
+
*/
|
|
46
|
+
const serializeBody = (body, ctx) => {
|
|
47
|
+
if (typeof body === "string") return interpolate(body, resolverFor(ctx.values));
|
|
48
|
+
const render = renderFor(ctx);
|
|
49
|
+
if (Array.isArray(body)) return serializeSlate(body, render);
|
|
50
|
+
const root = lexicalRootOf(body);
|
|
51
|
+
if (root) {
|
|
52
|
+
const converters = {
|
|
53
|
+
...defaultBodyConverters,
|
|
54
|
+
...ctx.converters ?? {}
|
|
55
|
+
};
|
|
56
|
+
return serializeNodes(Array.isArray(root.children) ? root.children : [], converters, render);
|
|
57
|
+
}
|
|
58
|
+
return "";
|
|
59
|
+
};
|
|
60
|
+
/** Build the `renderBody` passed to actions, honoring a plugin-level `richText` customization. */
|
|
61
|
+
const makeRenderBody = (args) => async (body) => {
|
|
62
|
+
if (args.richText?.serialize) return await args.richText.serialize({
|
|
63
|
+
body,
|
|
64
|
+
values: args.values,
|
|
65
|
+
descriptors: args.descriptors,
|
|
66
|
+
form: args.form,
|
|
67
|
+
req: args.req
|
|
68
|
+
});
|
|
69
|
+
return serializeBody(body, {
|
|
70
|
+
values: args.values,
|
|
71
|
+
descriptors: args.descriptors,
|
|
72
|
+
converters: args.richText?.converters
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { makeRenderBody, resolverFor, serializeBody };
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=serializeBody.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializeBody.js","names":[],"sources":["../../../src/actions/body/serializeBody.ts"],"sourcesContent":["import type { PayloadRequest, RichTextField } from 'payload'\nimport { interpolate } from '../../recall/interpolate'\nimport type { SubmissionDescriptor, SubmissionValue } from '../../submissions/types'\nimport type { BodyConverter, BodyRender } from './converters'\nimport { defaultBodyConverters } from './converters'\nimport { escapeHtml } from './escapeHtml'\nimport { serializeSlate } from './serializeSlate'\nimport { renderAllValues, renderAllValuesTable } from './wildcards'\n\n/** Minimal form identity threaded alongside a rendered body (e.g. per-tenant template lookups). */\ntype SerializeBodyForm = { id: number | string; title?: string }\n\n/** Submission data plus optional converter overrides available while serializing a body. */\nexport type BodyContext = {\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tconverters?: Record<string, BodyConverter>\n}\n\n/**\n * Args a custom `richText.serialize` replacement receives per rendered body. Always populated by\n * `makeRenderBody` (the action-body pipeline): `form` and `req` enable per-tenant template\n * lookups or handing the body off to a renderer like react-email.\n */\nexport type SerializeBodyArgs = {\n\tbody: unknown\n\tvalues: SubmissionValue[]\n\tdescriptors: SubmissionDescriptor[]\n\tform: SerializeBodyForm\n\treq?: PayloadRequest\n}\n\n/**\n * Customizes how action bodies are authored and rendered. `converters` spread over the default\n * Lexical node converters; `serialize` replaces the whole pipeline (for non-HTML channels like\n * chat or plain text, or to hand the body plus the submitted `form`/`req` off to a renderer like\n * react-email). `editor` overrides the Lexical/richText editor on both the emailTeam and\n * confirmation action body fields (see `buildDefaultActionDefinitions`).\n */\nexport type RichTextBodyOption = {\n\tconverters?: Record<string, BodyConverter>\n\tserialize?: (args: SerializeBodyArgs) => Promise<string> | string\n\teditor?: RichTextField['editor']\n}\n\n/** Recall resolver over submission values: field name to stringified value, `''` when absent. */\nexport const resolverFor =\n\t(values: SubmissionValue[]) =>\n\t(name: string): string => {\n\t\tconst entry = values.find((value) => value.field === name)\n\t\treturn entry == null ? '' : String(entry.value ?? '')\n\t}\n\nconst renderFor = (ctx: BodyContext): BodyRender => {\n\tconst resolve = resolverFor(ctx.values)\n\tconst htmlResolve = (name: string): string => {\n\t\tif (name === '*') {\n\t\t\treturn renderAllValues(ctx.values, ctx.descriptors)\n\t\t}\n\t\tif (name === '*:table') {\n\t\t\treturn renderAllValuesTable(ctx.values, ctx.descriptors)\n\t\t}\n\t\treturn escapeHtml(resolve(name))\n\t}\n\treturn {\n\t\ttext: (raw) => interpolate(escapeHtml(raw), htmlResolve),\n\t\tinterpolate: (raw) => interpolate(raw, resolve),\n\t}\n}\n\nconst serializeNodes = (\n\tnodes: unknown[],\n\tconverters: Record<string, BodyConverter>,\n\trender: BodyRender\n): string =>\n\tnodes\n\t\t.map((node) => {\n\t\t\tif (node == null || typeof node !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst lexicalNode = node as Record<string, unknown>\n\t\t\tconst children = Array.isArray(lexicalNode.children)\n\t\t\t\t? serializeNodes(lexicalNode.children, converters, render)\n\t\t\t\t: ''\n\t\t\tconst converter =\n\t\t\t\ttypeof lexicalNode.type === 'string' ? converters[lexicalNode.type] : undefined\n\t\t\treturn converter ? converter({ node: lexicalNode, children, render }) : children\n\t\t})\n\t\t.join('')\n\nconst lexicalRootOf = (body: unknown): Record<string, unknown> | null => {\n\tif (body == null || typeof body !== 'object' || Array.isArray(body)) {\n\t\treturn null\n\t}\n\tconst root = (body as { root?: unknown }).root\n\treturn root != null && typeof root === 'object' ? (root as Record<string, unknown>) : null\n}\n\n/**\n * Serialize an action's `body` config into HTML. A legacy string body is interpolated as-is\n * (pre-richText behavior, no escaping); a Lexical state walks the converter registry; a Slate\n * array uses the minimal legacy serializer; anything else yields `''`. Rendered text is\n * HTML-escaped and supports `{{ name|fallback }}`, `{{*}}`, and `{{*:table}}` tokens.\n */\nexport const serializeBody = (body: unknown, ctx: BodyContext): string => {\n\tif (typeof body === 'string') {\n\t\treturn interpolate(body, resolverFor(ctx.values))\n\t}\n\tconst render = renderFor(ctx)\n\tif (Array.isArray(body)) {\n\t\treturn serializeSlate(body, render)\n\t}\n\tconst root = lexicalRootOf(body)\n\tif (root) {\n\t\tconst converters = { ...defaultBodyConverters, ...(ctx.converters ?? {}) }\n\t\treturn serializeNodes(Array.isArray(root.children) ? root.children : [], converters, render)\n\t}\n\treturn ''\n}\n\n/** Build the `renderBody` passed to actions, honoring a plugin-level `richText` customization. */\nexport const makeRenderBody =\n\t(args: {\n\t\tvalues: SubmissionValue[]\n\t\tdescriptors: SubmissionDescriptor[]\n\t\tform: SerializeBodyForm\n\t\treq?: PayloadRequest\n\t\trichText?: RichTextBodyOption\n\t}) =>\n\tasync (body: unknown): Promise<string> => {\n\t\tif (args.richText?.serialize) {\n\t\t\treturn await args.richText.serialize({\n\t\t\t\tbody,\n\t\t\t\tvalues: args.values,\n\t\t\t\tdescriptors: args.descriptors,\n\t\t\t\tform: args.form,\n\t\t\t\treq: args.req,\n\t\t\t})\n\t\t}\n\t\treturn serializeBody(body, {\n\t\t\tvalues: args.values,\n\t\t\tdescriptors: args.descriptors,\n\t\t\tconverters: args.richText?.converters,\n\t\t})\n\t}\n"],"mappings":";;;;;;;AA8CA,MAAa,eACX,YACA,SAAyB;CACzB,MAAM,QAAQ,OAAO,MAAM,UAAU,MAAM,UAAU,IAAI;CACzD,OAAO,SAAS,OAAO,KAAK,OAAO,MAAM,SAAS,EAAE;AACrD;AAED,MAAM,aAAa,QAAiC;CACnD,MAAM,UAAU,YAAY,IAAI,MAAM;CACtC,MAAM,eAAe,SAAyB;EAC7C,IAAI,SAAS,KACZ,OAAO,gBAAgB,IAAI,QAAQ,IAAI,WAAW;EAEnD,IAAI,SAAS,WACZ,OAAO,qBAAqB,IAAI,QAAQ,IAAI,WAAW;EAExD,OAAO,WAAW,QAAQ,IAAI,CAAC;CAChC;CACA,OAAO;EACN,OAAO,QAAQ,YAAY,WAAW,GAAG,GAAG,WAAW;EACvD,cAAc,QAAQ,YAAY,KAAK,OAAO;CAC/C;AACD;AAEA,MAAM,kBACL,OACA,YACA,WAEA,MACE,KAAK,SAAS;CACd,IAAI,QAAQ,QAAQ,OAAO,SAAS,UACnC,OAAO;CAER,MAAM,cAAc;CACpB,MAAM,WAAW,MAAM,QAAQ,YAAY,QAAQ,IAChD,eAAe,YAAY,UAAU,YAAY,MAAM,IACvD;CACH,MAAM,YACL,OAAO,YAAY,SAAS,WAAW,WAAW,YAAY,QAAQ,KAAA;CACvE,OAAO,YAAY,UAAU;EAAE,MAAM;EAAa;EAAU;CAAO,CAAC,IAAI;AACzE,CAAC,EACA,KAAK,EAAE;AAEV,MAAM,iBAAiB,SAAkD;CACxE,IAAI,QAAQ,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GACjE,OAAO;CAER,MAAM,OAAQ,KAA4B;CAC1C,OAAO,QAAQ,QAAQ,OAAO,SAAS,WAAY,OAAmC;AACvF;;;;;;;AAQA,MAAa,iBAAiB,MAAe,QAA6B;CACzE,IAAI,OAAO,SAAS,UACnB,OAAO,YAAY,MAAM,YAAY,IAAI,MAAM,CAAC;CAEjD,MAAM,SAAS,UAAU,GAAG;CAC5B,IAAI,MAAM,QAAQ,IAAI,GACrB,OAAO,eAAe,MAAM,MAAM;CAEnC,MAAM,OAAO,cAAc,IAAI;CAC/B,IAAI,MAAM;EACT,MAAM,aAAa;GAAE,GAAG;GAAuB,GAAI,IAAI,cAAc,CAAC;EAAG;EACzE,OAAO,eAAe,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC,GAAG,YAAY,MAAM;CAC5F;CACA,OAAO;AACR;;AAGA,MAAa,kBACX,SAOD,OAAO,SAAmC;CACzC,IAAI,KAAK,UAAU,WAClB,OAAO,MAAM,KAAK,SAAS,UAAU;EACpC;EACA,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,MAAM,KAAK;EACX,KAAK,KAAK;CACX,CAAC;CAEF,OAAO,cAAc,MAAM;EAC1B,QAAQ,KAAK;EACb,aAAa,KAAK;EAClB,YAAY,KAAK,UAAU;CAC5B,CAAC;AACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { escapeHtml } from "./escapeHtml.js";
|
|
2
|
+
import { sanitizeUrl } from "./converters.js";
|
|
3
|
+
//#region src/actions/body/serializeSlate.ts
|
|
4
|
+
const leaf = (node, render) => {
|
|
5
|
+
let html = render.text(typeof node.text === "string" ? node.text : "");
|
|
6
|
+
if (node.code === true) html = `<code>${html}</code>`;
|
|
7
|
+
if (node.bold === true) html = `<strong>${html}</strong>`;
|
|
8
|
+
if (node.italic === true) html = `<em>${html}</em>`;
|
|
9
|
+
if (node.underline === true) html = `<u>${html}</u>`;
|
|
10
|
+
return html;
|
|
11
|
+
};
|
|
12
|
+
const serializeNode = (node, render) => {
|
|
13
|
+
if (node == null || typeof node !== "object") return "";
|
|
14
|
+
const slateNode = node;
|
|
15
|
+
if (typeof slateNode.text === "string") return leaf(slateNode, render);
|
|
16
|
+
const children = Array.isArray(slateNode.children) ? slateNode.children.map((child) => serializeNode(child, render)).join("") : "";
|
|
17
|
+
if (slateNode.type === "link") {
|
|
18
|
+
const raw = typeof slateNode.url === "string" ? slateNode.url : "";
|
|
19
|
+
return `<a href="${escapeHtml(sanitizeUrl(render.interpolate(raw)))}">${children}</a>`;
|
|
20
|
+
}
|
|
21
|
+
return `<p>${children}</p>`;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Minimal legacy Slate serializer: paragraph-ish elements, bold/italic/underline/code marks, links.
|
|
25
|
+
* Strikethrough is intentionally unsupported for legacy Slate bodies.
|
|
26
|
+
*/
|
|
27
|
+
const serializeSlate = (nodes, render) => nodes.map((node) => serializeNode(node, render)).join("");
|
|
28
|
+
//#endregion
|
|
29
|
+
export { serializeSlate };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=serializeSlate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializeSlate.js","names":[],"sources":["../../../src/actions/body/serializeSlate.ts"],"sourcesContent":["import type { BodyRender } from './converters'\nimport { sanitizeUrl } from './converters'\nimport { escapeHtml } from './escapeHtml'\n\ntype SlateNode = Record<string, unknown>\n\nconst leaf = (node: SlateNode, render: BodyRender): string => {\n\tlet html = render.text(typeof node.text === 'string' ? node.text : '')\n\tif (node.code === true) {\n\t\thtml = `<code>${html}</code>`\n\t}\n\tif (node.bold === true) {\n\t\thtml = `<strong>${html}</strong>`\n\t}\n\tif (node.italic === true) {\n\t\thtml = `<em>${html}</em>`\n\t}\n\tif (node.underline === true) {\n\t\thtml = `<u>${html}</u>`\n\t}\n\treturn html\n}\n\nconst serializeNode = (node: unknown, render: BodyRender): string => {\n\tif (node == null || typeof node !== 'object') {\n\t\treturn ''\n\t}\n\tconst slateNode = node as SlateNode\n\tif (typeof slateNode.text === 'string') {\n\t\treturn leaf(slateNode, render)\n\t}\n\tconst children = Array.isArray(slateNode.children)\n\t\t? slateNode.children.map((child) => serializeNode(child, render)).join('')\n\t\t: ''\n\tif (slateNode.type === 'link') {\n\t\tconst raw = typeof slateNode.url === 'string' ? slateNode.url : ''\n\t\tconst href = escapeHtml(sanitizeUrl(render.interpolate(raw)))\n\t\treturn `<a href=\"${href}\">${children}</a>`\n\t}\n\treturn `<p>${children}</p>`\n}\n\n/**\n * Minimal legacy Slate serializer: paragraph-ish elements, bold/italic/underline/code marks, links.\n * Strikethrough is intentionally unsupported for legacy Slate bodies.\n */\nexport const serializeSlate = (nodes: unknown[], render: BodyRender): string =>\n\tnodes.map((node) => serializeNode(node, render)).join('')\n"],"mappings":";;;AAMA,MAAM,QAAQ,MAAiB,WAA+B;CAC7D,IAAI,OAAO,OAAO,KAAK,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,EAAE;CACrE,IAAI,KAAK,SAAS,MACjB,OAAO,SAAS,KAAK;CAEtB,IAAI,KAAK,SAAS,MACjB,OAAO,WAAW,KAAK;CAExB,IAAI,KAAK,WAAW,MACnB,OAAO,OAAO,KAAK;CAEpB,IAAI,KAAK,cAAc,MACtB,OAAO,MAAM,KAAK;CAEnB,OAAO;AACR;AAEA,MAAM,iBAAiB,MAAe,WAA+B;CACpE,IAAI,QAAQ,QAAQ,OAAO,SAAS,UACnC,OAAO;CAER,MAAM,YAAY;CAClB,IAAI,OAAO,UAAU,SAAS,UAC7B,OAAO,KAAK,WAAW,MAAM;CAE9B,MAAM,WAAW,MAAM,QAAQ,UAAU,QAAQ,IAC9C,UAAU,SAAS,KAAK,UAAU,cAAc,OAAO,MAAM,CAAC,EAAE,KAAK,EAAE,IACvE;CACH,IAAI,UAAU,SAAS,QAAQ;EAC9B,MAAM,MAAM,OAAO,UAAU,QAAQ,WAAW,UAAU,MAAM;EAEhE,OAAO,YADM,WAAW,YAAY,OAAO,YAAY,GAAG,CAAC,CACrC,EAAE,IAAI,SAAS;CACtC;CACA,OAAO,MAAM,SAAS;AACvB;;;;;AAMA,MAAa,kBAAkB,OAAkB,WAChD,MAAM,KAAK,SAAS,cAAc,MAAM,MAAM,CAAC,EAAE,KAAK,EAAE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SubmissionDescriptor, SubmissionValue } from "../../submissions/types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/actions/body/wildcards.d.ts
|
|
4
|
+
/** All answers as HTML-escaped `Label: value` lines joined with `<br />`. */
|
|
5
|
+
declare const renderAllValues: (values: SubmissionValue[], descriptors: SubmissionDescriptor[]) => string;
|
|
6
|
+
/** All answers as a simple HTML-escaped two-column table. */
|
|
7
|
+
declare const renderAllValuesTable: (values: SubmissionValue[], descriptors: SubmissionDescriptor[]) => string;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { renderAllValues, renderAllValuesTable };
|
|
10
|
+
//# sourceMappingURL=wildcards.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { escapeHtml } from "./escapeHtml.js";
|
|
2
|
+
//#region src/actions/body/wildcards.ts
|
|
3
|
+
const displayValue = (value, descriptor) => {
|
|
4
|
+
if (value == null) return "";
|
|
5
|
+
if (Array.isArray(value)) return value.map((entry) => displayValue(entry, descriptor)).join(", ");
|
|
6
|
+
const raw = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
7
|
+
return descriptor?.optionLabels?.[raw] ?? raw;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Descriptors are the submit-time snapshot of real form fields, so when present they gate which
|
|
11
|
+
* values render (internal keys like the honeypot never get a descriptor). Legacy rows without
|
|
12
|
+
* descriptors fall back to rendering every value under its field name.
|
|
13
|
+
*/
|
|
14
|
+
const rowsOf = (values, descriptors) => {
|
|
15
|
+
const byField = new Map(descriptors.map((descriptor) => [descriptor.field, descriptor]));
|
|
16
|
+
return (byField.size > 0 ? values.filter((value) => byField.has(value.field)) : values).map((entry) => {
|
|
17
|
+
const descriptor = byField.get(entry.field);
|
|
18
|
+
return {
|
|
19
|
+
label: descriptor?.label ?? entry.field,
|
|
20
|
+
value: displayValue(entry.value, descriptor)
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
/** All answers as HTML-escaped `Label: value` lines joined with `<br />`. */
|
|
25
|
+
const renderAllValues = (values, descriptors) => rowsOf(values, descriptors).map((row) => `${escapeHtml(row.label)}: ${escapeHtml(row.value)}`).join("<br />");
|
|
26
|
+
/** All answers as a simple HTML-escaped two-column table. */
|
|
27
|
+
const renderAllValuesTable = (values, descriptors) => {
|
|
28
|
+
return `<table><tbody>${rowsOf(values, descriptors).map((row) => `<tr><td>${escapeHtml(row.label)}</td><td>${escapeHtml(row.value)}</td></tr>`).join("")}</tbody></table>`;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { renderAllValues, renderAllValuesTable };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=wildcards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wildcards.js","names":[],"sources":["../../../src/actions/body/wildcards.ts"],"sourcesContent":["import type { SubmissionDescriptor, SubmissionValue } from '../../submissions/types'\nimport { escapeHtml } from './escapeHtml'\n\ntype Row = { label: string; value: string }\n\nconst displayValue = (value: unknown, descriptor?: SubmissionDescriptor): string => {\n\tif (value == null) {\n\t\treturn ''\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn value.map((entry) => displayValue(entry, descriptor)).join(', ')\n\t}\n\tconst raw = typeof value === 'object' ? JSON.stringify(value) : String(value)\n\treturn descriptor?.optionLabels?.[raw] ?? raw\n}\n\n/**\n * Descriptors are the submit-time snapshot of real form fields, so when present they gate which\n * values render (internal keys like the honeypot never get a descriptor). Legacy rows without\n * descriptors fall back to rendering every value under its field name.\n */\nconst rowsOf = (values: SubmissionValue[], descriptors: SubmissionDescriptor[]): Row[] => {\n\tconst byField = new Map(descriptors.map((descriptor) => [descriptor.field, descriptor]))\n\tconst included = byField.size > 0 ? values.filter((value) => byField.has(value.field)) : values\n\treturn included.map((entry) => {\n\t\tconst descriptor = byField.get(entry.field)\n\t\treturn { label: descriptor?.label ?? entry.field, value: displayValue(entry.value, descriptor) }\n\t})\n}\n\n/** All answers as HTML-escaped `Label: value` lines joined with `<br />`. */\nexport const renderAllValues = (\n\tvalues: SubmissionValue[],\n\tdescriptors: SubmissionDescriptor[]\n): string =>\n\trowsOf(values, descriptors)\n\t\t.map((row) => `${escapeHtml(row.label)}: ${escapeHtml(row.value)}`)\n\t\t.join('<br />')\n\n/** All answers as a simple HTML-escaped two-column table. */\nexport const renderAllValuesTable = (\n\tvalues: SubmissionValue[],\n\tdescriptors: SubmissionDescriptor[]\n): string => {\n\tconst rows = rowsOf(values, descriptors)\n\t\t.map((row) => `<tr><td>${escapeHtml(row.label)}</td><td>${escapeHtml(row.value)}</td></tr>`)\n\t\t.join('')\n\treturn `<table><tbody>${rows}</tbody></table>`\n}\n"],"mappings":";;AAKA,MAAM,gBAAgB,OAAgB,eAA8C;CACnF,IAAI,SAAS,MACZ,OAAO;CAER,IAAI,MAAM,QAAQ,KAAK,GACtB,OAAO,MAAM,KAAK,UAAU,aAAa,OAAO,UAAU,CAAC,EAAE,KAAK,IAAI;CAEvE,MAAM,MAAM,OAAO,UAAU,WAAW,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK;CAC5E,OAAO,YAAY,eAAe,QAAQ;AAC3C;;;;;;AAOA,MAAM,UAAU,QAA2B,gBAA+C;CACzF,MAAM,UAAU,IAAI,IAAI,YAAY,KAAK,eAAe,CAAC,WAAW,OAAO,UAAU,CAAC,CAAC;CAEvF,QADiB,QAAQ,OAAO,IAAI,OAAO,QAAQ,UAAU,QAAQ,IAAI,MAAM,KAAK,CAAC,IAAI,QACzE,KAAK,UAAU;EAC9B,MAAM,aAAa,QAAQ,IAAI,MAAM,KAAK;EAC1C,OAAO;GAAE,OAAO,YAAY,SAAS,MAAM;GAAO,OAAO,aAAa,MAAM,OAAO,UAAU;EAAE;CAChG,CAAC;AACF;;AAGA,MAAa,mBACZ,QACA,gBAEA,OAAO,QAAQ,WAAW,EACxB,KAAK,QAAQ,GAAG,WAAW,IAAI,KAAK,EAAE,IAAI,WAAW,IAAI,KAAK,GAAG,EACjE,KAAK,QAAQ;;AAGhB,MAAa,wBACZ,QACA,gBACY;CAIZ,OAAO,iBAHM,OAAO,QAAQ,WAAW,EACrC,KAAK,QAAQ,WAAW,WAAW,IAAI,KAAK,EAAE,WAAW,WAAW,IAAI,KAAK,EAAE,WAAW,EAC1F,KAAK,EACoB,EAAE;AAC9B"}
|
|
@@ -1,41 +1,65 @@
|
|
|
1
|
+
import { fieldNamesOfType } from "../../fields/fieldNamesOfType.js";
|
|
2
|
+
import { localizedIf } from "../../fields/localizedIf.js";
|
|
1
3
|
import { interpolate } from "../../recall/interpolate.js";
|
|
2
4
|
import { keys } from "../../translations/keys.js";
|
|
3
|
-
import { labelFor } from "../../translations/server.js";
|
|
5
|
+
import { asTranslate, labelFor } from "../../translations/server.js";
|
|
6
|
+
import { resolverFor } from "../body/serializeBody.js";
|
|
4
7
|
import { defineAction } from "../defineAction.js";
|
|
5
8
|
//#region src/actions/builtin/confirmation.ts
|
|
6
|
-
const
|
|
9
|
+
const TO_FIELD_REF = "@10x-media/form-builder/client#FieldNameSelect";
|
|
10
|
+
/**
|
|
11
|
+
* Validates the confirmation action's `toField`: unset is fine, otherwise it must name an
|
|
12
|
+
* existing `email`-type field on the form. Exported for unit testing.
|
|
13
|
+
*/
|
|
14
|
+
const validateToField = (value, { data, req }) => {
|
|
15
|
+
if (typeof value !== "string" || value.length === 0) return true;
|
|
16
|
+
return fieldNamesOfType(data && typeof data === "object" ? data.fields : void 0, ["email"]).includes(value) ? true : asTranslate(req.t)(keys.validationEmailFieldUnknown);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `subject` and `body` are email content and follow `localize`; `toField` is an identifier and never does.
|
|
20
|
+
* `editor` overrides the body field's Lexical/richText editor (from the plugin's `richText.editor` option).
|
|
21
|
+
*/
|
|
22
|
+
const buildConfirmation = (localize, editor) => defineAction({
|
|
7
23
|
type: "confirmation",
|
|
8
24
|
label: keys.actionConfirmation,
|
|
9
25
|
config: [
|
|
10
26
|
{
|
|
11
27
|
name: "toField",
|
|
12
28
|
type: "text",
|
|
13
|
-
label: labelFor(keys.actionConfigToField)
|
|
29
|
+
label: labelFor(keys.actionConfigToField),
|
|
30
|
+
admin: {
|
|
31
|
+
description: labelFor(keys.actionConfigToFieldDescription),
|
|
32
|
+
components: { Field: {
|
|
33
|
+
path: TO_FIELD_REF,
|
|
34
|
+
clientProps: { types: ["email"] }
|
|
35
|
+
} }
|
|
36
|
+
},
|
|
37
|
+
validate: validateToField
|
|
14
38
|
},
|
|
15
39
|
{
|
|
16
40
|
name: "subject",
|
|
17
41
|
type: "text",
|
|
18
|
-
label: labelFor(keys.actionConfigSubject)
|
|
42
|
+
label: labelFor(keys.actionConfigSubject),
|
|
43
|
+
...localizedIf(localize)
|
|
19
44
|
},
|
|
20
45
|
{
|
|
21
46
|
name: "body",
|
|
22
|
-
type: "
|
|
23
|
-
label: labelFor(keys.actionConfigBody)
|
|
47
|
+
type: "richText",
|
|
48
|
+
label: labelFor(keys.actionConfigBody),
|
|
49
|
+
admin: { description: labelFor(keys.actionConfigBodyDescription) },
|
|
50
|
+
...localizedIf(localize),
|
|
51
|
+
...editor ? { editor } : {}
|
|
24
52
|
}
|
|
25
53
|
],
|
|
26
54
|
run: async (args) => {
|
|
27
|
-
const { config, values, payload } = args;
|
|
55
|
+
const { config, values, payload, renderBody } = args;
|
|
28
56
|
if (!config.toField) return;
|
|
29
|
-
const
|
|
30
|
-
const to =
|
|
57
|
+
const resolve = resolverFor(values);
|
|
58
|
+
const to = resolve(config.toField);
|
|
31
59
|
if (!to) return;
|
|
32
60
|
if (typeof payload.sendEmail !== "function") throw new Error("confirmation: no email adapter configured");
|
|
33
|
-
const resolve = (name) => {
|
|
34
|
-
const found = values.find((v) => v.field === name);
|
|
35
|
-
return found == null ? "" : String(found.value ?? "");
|
|
36
|
-
};
|
|
37
61
|
const subject = interpolate(config.subject ?? "", resolve);
|
|
38
|
-
const html =
|
|
62
|
+
const html = await renderBody(config.body);
|
|
39
63
|
await payload.sendEmail({
|
|
40
64
|
to,
|
|
41
65
|
subject,
|
|
@@ -43,7 +67,8 @@ const confirmation = defineAction({
|
|
|
43
67
|
});
|
|
44
68
|
}
|
|
45
69
|
});
|
|
70
|
+
buildConfirmation(true);
|
|
46
71
|
//#endregion
|
|
47
|
-
export {
|
|
72
|
+
export { buildConfirmation };
|
|
48
73
|
|
|
49
74
|
//# sourceMappingURL=confirmation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"confirmation.js","names":[],"sources":["../../../src/actions/builtin/confirmation.ts"],"sourcesContent":["import { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { labelFor } from '../../translations/server'\nimport { defineAction } from '../defineAction'\n\ntype ConfirmationConfig = { toField?: string; subject?: string; body?: string }\n\nexport const
|
|
1
|
+
{"version":3,"file":"confirmation.js","names":[],"sources":["../../../src/actions/builtin/confirmation.ts"],"sourcesContent":["import type { PayloadRequest, RichTextField } from 'payload'\nimport { fieldNamesOfType } from '../../fields/fieldNamesOfType'\nimport { localizedIf } from '../../fields/localizedIf'\nimport { interpolate } from '../../recall/interpolate'\nimport { keys } from '../../translations/keys'\nimport { asTranslate, labelFor } from '../../translations/server'\nimport { resolverFor } from '../body/serializeBody'\nimport { defineAction } from '../defineAction'\n\ntype ConfirmationConfig = { toField?: string; subject?: string; body?: unknown }\n\nconst TO_FIELD_REF = '@10x-media/form-builder/client#FieldNameSelect'\n\n/**\n * Validates the confirmation action's `toField`: unset is fine, otherwise it must name an\n * existing `email`-type field on the form. Exported for unit testing.\n */\nexport const validateToField = (\n\tvalue: unknown,\n\t{ data, req }: { data?: unknown; req: PayloadRequest }\n): string | true => {\n\tif (typeof value !== 'string' || value.length === 0) {\n\t\treturn true\n\t}\n\tconst fields =\n\t\tdata && typeof data === 'object' ? (data as Record<string, unknown>).fields : undefined\n\treturn fieldNamesOfType(fields, ['email']).includes(value)\n\t\t? true\n\t\t: asTranslate(req.t)(keys.validationEmailFieldUnknown)\n}\n\n/**\n * `subject` and `body` are email content and follow `localize`; `toField` is an identifier and never does.\n * `editor` overrides the body field's Lexical/richText editor (from the plugin's `richText.editor` option).\n */\nexport const buildConfirmation = (localize: boolean, editor?: RichTextField['editor']) =>\n\tdefineAction<ConfirmationConfig>({\n\t\ttype: 'confirmation',\n\t\tlabel: keys.actionConfirmation,\n\t\tconfig: [\n\t\t\t{\n\t\t\t\tname: 'toField',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigToField),\n\t\t\t\tadmin: {\n\t\t\t\t\tdescription: labelFor(keys.actionConfigToFieldDescription),\n\t\t\t\t\tcomponents: { Field: { path: TO_FIELD_REF, clientProps: { types: ['email'] } } },\n\t\t\t\t},\n\t\t\t\tvalidate: validateToField,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'subject',\n\t\t\t\ttype: 'text',\n\t\t\t\tlabel: labelFor(keys.actionConfigSubject),\n\t\t\t\t...localizedIf(localize),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: 'body',\n\t\t\t\ttype: 'richText',\n\t\t\t\tlabel: labelFor(keys.actionConfigBody),\n\t\t\t\tadmin: { description: labelFor(keys.actionConfigBodyDescription) },\n\t\t\t\t...localizedIf(localize),\n\t\t\t\t...(editor ? { editor } : {}),\n\t\t\t},\n\t\t],\n\t\trun: async (args) => {\n\t\t\tconst { config, values, payload, renderBody } = args\n\n\t\t\tif (!config.toField) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst resolve = resolverFor(values)\n\t\t\tconst to = resolve(config.toField)\n\n\t\t\tif (!to) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (typeof payload.sendEmail !== 'function') {\n\t\t\t\tthrow new Error('confirmation: no email adapter configured')\n\t\t\t}\n\n\t\t\tconst subject = interpolate(config.subject ?? '', resolve)\n\t\t\tconst html = await renderBody(config.body)\n\n\t\t\tawait payload.sendEmail({ to, subject, html })\n\t\t},\n\t})\n\nexport const confirmation = buildConfirmation(true)\n"],"mappings":";;;;;;;;AAWA,MAAM,eAAe;;;;;AAMrB,MAAa,mBACZ,OACA,EAAE,MAAM,UACW;CACnB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GACjD,OAAO;CAIR,OAAO,iBADN,QAAQ,OAAO,SAAS,WAAY,KAAiC,SAAS,KAAA,GAC/C,CAAC,OAAO,CAAC,EAAE,SAAS,KAAK,IACtD,OACA,YAAY,IAAI,CAAC,EAAE,KAAK,2BAA2B;AACvD;;;;;AAMA,MAAa,qBAAqB,UAAmB,WACpD,aAAiC;CAChC,MAAM;CACN,OAAO,KAAK;CACZ,QAAQ;EACP;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,mBAAmB;GACxC,OAAO;IACN,aAAa,SAAS,KAAK,8BAA8B;IACzD,YAAY,EAAE,OAAO;KAAE,MAAM;KAAc,aAAa,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,EAAE;GAChF;GACA,UAAU;EACX;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,mBAAmB;GACxC,GAAG,YAAY,QAAQ;EACxB;EACA;GACC,MAAM;GACN,MAAM;GACN,OAAO,SAAS,KAAK,gBAAgB;GACrC,OAAO,EAAE,aAAa,SAAS,KAAK,2BAA2B,EAAE;GACjE,GAAG,YAAY,QAAQ;GACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC5B;CACD;CACA,KAAK,OAAO,SAAS;EACpB,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;EAEhD,IAAI,CAAC,OAAO,SACX;EAGD,MAAM,UAAU,YAAY,MAAM;EAClC,MAAM,KAAK,QAAQ,OAAO,OAAO;EAEjC,IAAI,CAAC,IACJ;EAGD,IAAI,OAAO,QAAQ,cAAc,YAChC,MAAM,IAAI,MAAM,2CAA2C;EAG5D,MAAM,UAAU,YAAY,OAAO,WAAW,IAAI,OAAO;EACzD,MAAM,OAAO,MAAM,WAAW,OAAO,IAAI;EAEzC,MAAM,QAAQ,UAAU;GAAE;GAAI;GAAS;EAAK,CAAC;CAC9C;AACD,CAAC;AAE0B,kBAAkB,IAAI"}
|