From 05e9eceefd4079b1cecd749b930d5a96c58bada2 Mon Sep 17 00:00:00 2001
From: ogp-weeloong <135598754+ogp-weeloong@users.noreply.github.com>
Date: Tue, 9 Jun 2026 06:58:22 +0000
Subject: [PATCH] [EMAIL-PREVIEW-1] PLU-386: add TFieldPreviewType and
previewType to IFieldRichText (#1652)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Problem
We want pipe owners to be able to preview their email across a variety of email clients (Outlook, Yahoo etc).
# High-level Approach
We will use the [emaillens](https://emailens.dev/) library to generate the preview. This is a front-end only change; high level approach is
1. Introduce the concept of "previewing" a rich text field by adding a `previewType` property in the `IRichTextField` type.
1. When this is specified, we will:
1. Add a new "Preview" button to the field itself (to preview live rich text content)
2. Update the "View" button in the corresponding row of the "Previous Result" table (to view formatted dataOut for that run).
2. When clicked, the front end substitutes variables in the rich text content with test / dataOut data, and loads the appropriate react component to render it
2. Add a `email` preview type.
1. This pops up a modal which supports choosing between several email clients; it calls into the emaillens library with the chosen client to generate the client's HTML
### IMPORTANT
The `emailens` library is technically meant to be run on a node backend. However, I want this to run on the frontend to reduce latency and save costs.
It turns out you can run this on the browser as long as we don't need the features [see [github](https://github.com/emailens/engine#installation)] that use node stuff:
1. `@emailens/engine/server` — imports `dns` (the `checkDeliverability` + SpamAssassin stuff).
2. `@emailens/engine/compile` — JSX/MJML/Maizzle compilation, which uses `node:vm` / `isolated-vm` / `mjml` etc.
I added a vite safeguard to prevent us from from doing so. In the event that emailens updates the code to make this impossible, we can always add a REST API to do this (non-ideal)
# This PR
- Creates the `previewType` field
- Creates the `email` value for preview type and also a new `PreviewEmailModal` for previewing emails in selected clients
# Tests
Only regression tests as this is a no-op.
- [ ] Check that I can still compose postman emails in the rich text editor
- [ ] Check that I can still view email from past executions
Full test in later PR
---
CREDITS.md | 29 +++
package-lock.json | 195 +++++++++++++++++-
packages/frontend/package.json | 2 +
.../components/EmailPreviewModal/index.tsx | 182 ++++++++++++++++
packages/frontend/vite-config-utils.ts | 31 +++
packages/frontend/vite.config.lib.ts | 3 +
packages/frontend/vite.config.ts | 5 +
packages/types/index.d.ts | 13 ++
8 files changed, 459 insertions(+), 1 deletion(-)
create mode 100644 packages/frontend/src/components/EmailPreviewModal/index.tsx
create mode 100644 packages/frontend/vite-config-utils.ts
diff --git a/CREDITS.md b/CREDITS.md
index 66b1a6741d..50ffb09e05 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -1770,6 +1770,35 @@ Permission to use, copy, modify, and/or distribute this software for any purpose
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+-------------------------------------------------------------------------------
+
+## Project
+react-error-boundary
+
+### Source
+https://github.com/bvaughn/react-error-boundary
+
+### License
+The MIT License (MIT)
+Copyright (c) 2020 Brian Vaughn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
-------------------------------------------------------------------------------
diff --git a/package-lock.json b/package-lock.json
index 684ed00b27..c263d01dbd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3663,6 +3663,55 @@
"graphql": "^15.0.0 || ^16.0.0"
}
},
+ "node_modules/@emailens/engine": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emailens/engine/-/engine-0.9.2.tgz",
+ "integrity": "sha512-VKRFnBhPlqDx9IhzdQTocNt5TC02LgIdUPg4YvNM+OXJq58W9yGRMyqxkzoWh7ayLRZrk7ufJDLJCc2ZsskhIA==",
+ "license": "MIT",
+ "dependencies": {
+ "cheerio": "^1.2.0",
+ "css-tree": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@maizzle/framework": ">=5.0.0",
+ "@react-email/components": ">=0.0.36",
+ "@react-email/render": ">=1.0.0",
+ "isolated-vm": ">=5.0.0",
+ "mjml": ">=4.0.0",
+ "quickjs-emscripten": ">=0.29.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "sucrase": "^3.35.0"
+ },
+ "peerDependenciesMeta": {
+ "@maizzle/framework": {
+ "optional": true
+ },
+ "@react-email/components": {
+ "optional": true
+ },
+ "@react-email/render": {
+ "optional": true
+ },
+ "isolated-vm": {
+ "optional": true
+ },
+ "mjml": {
+ "optional": true
+ },
+ "quickjs-emscripten": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "sucrase": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
@@ -12497,6 +12546,48 @@
"node": ">= 16"
}
},
+ "node_modules/cheerio": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
+ "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
+ "license": "MIT",
+ "dependencies": {
+ "cheerio-select": "^2.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "encoding-sniffer": "^0.2.1",
+ "htmlparser2": "^10.1.0",
+ "parse5": "^7.3.0",
+ "parse5-htmlparser2-tree-adapter": "^7.1.0",
+ "parse5-parser-stream": "^7.1.2",
+ "undici": "^7.19.0",
+ "whatwg-mimetype": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=20.18.1"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -13669,6 +13760,19 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
@@ -15136,6 +15240,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/encoding-sniffer": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
+ "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "^0.6.3",
+ "whatwg-encoding": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
+ }
+ },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -18631,6 +18748,37 @@
"node": ">=16.0.0"
}
},
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -23885,6 +24033,12 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "license": "CC0-1.0"
+ },
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
@@ -26144,6 +26298,31 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
+ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "^5.0.3",
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-parser-stream": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
+ "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
+ "license": "MIT",
+ "dependencies": {
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/parse5/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -32151,7 +32330,6 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -33251,6 +33429,19 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
@@ -33903,6 +34094,7 @@
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
+ "@emailens/engine": "0.9.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/space-grotesk": "4.5.13",
@@ -33932,6 +34124,7 @@
"luxon": "^2.3.1",
"node-html-parser": "6.1.13",
"papaparse": "5.4.1",
+ "react-error-boundary": "6.1.1",
"react-helmet": "6.1.0",
"react-hook-form": "^7.17.2",
"react-icons": "^4.12.0",
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index ec8389e437..c6c639db15 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -19,6 +19,7 @@
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
+ "@emailens/engine": "0.9.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/space-grotesk": "4.5.13",
@@ -48,6 +49,7 @@
"luxon": "^2.3.1",
"node-html-parser": "6.1.13",
"papaparse": "5.4.1",
+ "react-error-boundary": "6.1.1",
"react-helmet": "6.1.0",
"react-hook-form": "^7.17.2",
"react-icons": "^4.12.0",
diff --git a/packages/frontend/src/components/EmailPreviewModal/index.tsx b/packages/frontend/src/components/EmailPreviewModal/index.tsx
new file mode 100644
index 0000000000..e2acc1f6da
--- /dev/null
+++ b/packages/frontend/src/components/EmailPreviewModal/index.tsx
@@ -0,0 +1,182 @@
+import { useMemo, useState } from 'react'
+import { ErrorBoundary } from 'react-error-boundary'
+import {
+ Box,
+ Flex,
+ Heading,
+ Icon,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+} from '@chakra-ui/react'
+import { datadogRum } from '@datadog/browser-rum'
+import { transformForClient } from '@emailens/engine'
+
+import { BrokenPipeIcon } from '@/components/Icons'
+
+interface ClientOption {
+ id: string
+ label: string
+ subtitle: string
+}
+
+const CLIENT_OPTIONS: ClientOption[] = [
+ {
+ id: 'outlook-windows-legacy',
+ label: 'Outlook Classic',
+ subtitle: 'Microsoft Word engine',
+ },
+ {
+ id: 'gmail-web',
+ label: 'Gmail',
+ subtitle: 'Gmail Web',
+ },
+ {
+ id: 'apple-mail-macos',
+ label: 'Apple Mail',
+ subtitle: 'WebKit on macOS',
+ },
+ {
+ id: 'yahoo-mail',
+ label: 'Yahoo Mail',
+ subtitle: 'Yahoo webmail',
+ },
+]
+
+interface PreviewPaneProps {
+ html: string
+ clientId: string
+}
+
+// Renders the transformed email. Extracted so the ErrorBoundary below sits
+// above the transformForClient() call, which is what can actually throw.
+function PreviewPane({ html, clientId }: PreviewPaneProps) {
+ const transformed = useMemo(() => {
+ return transformForClient(html, clientId).html
+ }, [html, clientId])
+
+ return (
+
+ )
+}
+
+function PreviewErrorFallback() {
+ return (
+
+
+
+ An error occurred
+
+
+ Try previewing again, or contact support if this problem persists.
+
+
+ )
+}
+
+interface EmailPreviewModalProps {
+ isOpen: boolean
+ onClose: () => void
+ html: string
+}
+
+export default function EmailPreviewModal({
+ isOpen,
+ onClose,
+ html,
+}: EmailPreviewModalProps) {
+ const [selectedClientId, setSelectedClientId] = useState(
+ 'outlook-windows-legacy',
+ )
+
+ return (
+
+
+
+
+ Email preview
+
+
+
+
+
+ {CLIENT_OPTIONS.map((option) => {
+ const isSelected = option.id === selectedClientId
+ return (
+ setSelectedClientId(option.id)}
+ >
+ {option.label}
+
+ {option.subtitle}
+
+
+ )
+ })}
+
+
+ }
+ resetKeys={[html, selectedClientId]}
+ onError={(error) => {
+ datadogRum.addError(error, {
+ feature: 'email-preview',
+ clientId: selectedClientId,
+ })
+ }}
+ >
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/frontend/vite-config-utils.ts b/packages/frontend/vite-config-utils.ts
new file mode 100644
index 0000000000..92f58c0a90
--- /dev/null
+++ b/packages/frontend/vite-config-utils.ts
@@ -0,0 +1,31 @@
+// @ts-check
+import type { Rollup } from 'vite'
+
+/**
+ * Guardrail for the @emailens/engine tech-debt: the engine declares
+ * `engines.node: ">=18"` but the slice we use (transformForClient) is
+ * browser-safe. If a future version of @emailens/engine — or any other
+ * dep — starts importing a node-only module (fs, path, crypto, etc.),
+ * Vite externalizes it and emits a warning. We promote that warning to
+ * a hard error so CI catches the regression instead of silently
+ * shipping a broken bundle.
+ *
+ * Wire this up as `build.rollupOptions.onwarn` in the Vite config.
+ */
+export const failOnLeakedNodeBuiltins: Rollup.WarningHandlerWithDefault = (
+ warning,
+ defaultHandler,
+) => {
+ const msg = warning.message ?? ''
+ if (
+ msg.includes('has been externalized for browser compatibility') ||
+ warning.code === 'MISSING_NODE_BUILTINS'
+ ) {
+ throw new Error(
+ `[vite-guardrail] A node-only import leaked into the frontend bundle. ` +
+ `This guardrail exists to catch @emailens/engine (and similar libraries) ` +
+ `pulling in backend-only modules. Original warning: ${msg}`,
+ )
+ }
+ defaultHandler(warning)
+}
diff --git a/packages/frontend/vite.config.lib.ts b/packages/frontend/vite.config.lib.ts
index fc0d0318d9..91da29c0cb 100644
--- a/packages/frontend/vite.config.lib.ts
+++ b/packages/frontend/vite.config.lib.ts
@@ -5,6 +5,8 @@ import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
import viteTsconfigPaths from 'vite-tsconfig-paths'
+import { failOnLeakedNodeBuiltins } from './vite-config-utils'
+
export default defineConfig({
plugins: [
react(),
@@ -44,6 +46,7 @@ export default defineConfig({
'react-router-dom',
'zod',
],
+ onwarn: failOnLeakedNodeBuiltins,
},
},
})
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 3dc89773bf..4d4a5ed476 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -3,6 +3,8 @@ import { defineConfig } from 'vite'
import loadVersion from 'vite-plugin-package-version'
import viteTsconfigPaths from 'vite-tsconfig-paths'
+import { failOnLeakedNodeBuiltins } from './vite-config-utils'
+
// https://vitejs.dev/config/
export default defineConfig({
// loadVersion injects package.json version into import.meta.env.PACKAGE_VERSION
@@ -10,6 +12,9 @@ export default defineConfig({
build: {
// disable inline images since we don't allow them in csp
assetsInlineLimit: 0,
+ rollupOptions: {
+ onwarn: failOnLeakedNodeBuiltins,
+ },
},
server: {
open: 'http://localhost:3001',
diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index 0bc9e00218..84828316c7 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -462,6 +462,13 @@ export interface IFieldMultiRow extends IBaseField {
subFields: IField[]
}
+/**
+ * Marks a rich-text field as previewable in a kind-specific modal in
+ * FlowStepTestController. Keep this union narrow; expand only as new preview
+ * kinds (e.g. 'sms') ship.
+ */
+export type TFieldPreviewType = 'email'
+
export type TRteMenuOption =
| 'Bold'
| 'Italic'
@@ -497,6 +504,12 @@ export interface IFieldRichText extends IBaseField {
// Specifies the order and what menu options to show in the RTE
// 'Divider' is specified manually to determine when a divider should be shown
customRteMenuOptions?: TRteMenuOption[]
+
+ /**
+ * If set, FlowStepTestController renders a kind-specific "Preview" button
+ * next to "Check step" and feeds it the live form value of this field.
+ */
+ previewType?: TFieldPreviewType
}
export interface IFieldDragDrop extends IBaseField {