Merge pull request 'feat(admin): use the item description's markdown editor for email templates (#131)' (#133) from feature/131-md-editor-for-emails into main
Linting / lint (push) Successful in 1m42s
SonarQube Analysis / sonarqube (push) Failing after 16m10s

Reviewed-on: #133
This commit was merged in pull request #133.
This commit is contained in:
2026-08-23 07:21:29 -05:00
+27 -7
View File
@@ -8,6 +8,10 @@ import Col from 'antd/es/col';
import Alert from 'antd/es/alert'; import Alert from 'antd/es/alert';
import Typography from 'antd/es/typography'; import Typography from 'antd/es/typography';
import message from 'antd/es/message'; import message from 'antd/es/message';
import MDEditor from '@uiw/react-md-editor';
import '@uiw/react-md-editor/markdown-editor.css';
import '@uiw/react-markdown-preview/markdown.css';
import { useThemeMode } from '../theme/ThemeContext';
import { import {
EmailTemplate, EmailTemplate,
saveEmailTemplate, saveEmailTemplate,
@@ -27,6 +31,7 @@ type Props = Readonly<{
const PREVIEW_DEBOUNCE_MS = 400; const PREVIEW_DEBOUNCE_MS = 400;
export default function EmailTemplateEditor({ template, onChanged }: Props) { export default function EmailTemplateEditor({ template, onChanged }: Props) {
const { mode } = useThemeMode();
// Falls back to the default so the editor starts from the real copy rather // Falls back to the default so the editor starts from the real copy rather
// than an empty box. `subject`/`body` being null means "never customised", // than an empty box. `subject`/`body` being null means "never customised",
// which is why the badge on the tab can say so. // which is why the badge on the tab can say so.
@@ -111,13 +116,28 @@ export default function EmailTemplateEditor({ template, onChanged }: Props) {
placeholder="Subject" placeholder="Subject"
style={{ marginBottom: 8 }} style={{ marginBottom: 8 }}
/> />
<Input.TextArea {/* The same editor the item description uses, so an admin who has
aria-label={`${template.label} body`} learned one toolbar has learned both. data-color-mode is what makes
value={body} it follow the admin's dark mode, exactly as on the item form.
onChange={(e) => setBody(e.target.value)}
autoSize={{ minRows: 10, maxRows: 24 }} preview="edit" rather than the item form's "live": MDEditor's own
style={{ fontFamily: 'monospace', marginBottom: 12 }} preview would render the markdown with a different renderer, showing
/> a literal {{resetUrl}} and no consent footer. The pane on the right
is the server's rendering of the actual email, and two previews
disagreeing about the thing being edited is worse than one. */}
<div data-color-mode={mode} style={{ marginBottom: 12 }}>
<MDEditor
value={body}
onChange={(val) => setBody(val || '')}
height={320}
preview="edit"
// Input.TextArea carried this attribute itself. MDEditor owns the
// textarea, so the label has to be passed through — without it every
// email template test loses its handle on the field, and a screen
// reader loses the only thing naming it.
textareaProps={{ 'aria-label': `${template.label} body` }}
/>
</div>
<Space> <Space>
<Button type="primary" loading={saving} onClick={handleSave}> <Button type="primary" loading={saving} onClick={handleSave}>