fix(email): derive preview sample data from each template's variables
The preview modal's hardcoded sampleData had drifted from the templates'
declared variables arrays: it carried `password` and `expiration_date` and no
`host_name` at all, so {{host_name}}, {{gallery_password}} and {{expiry_date}}
rendered as literal placeholders in the gallery_created preview while
event_name/event_date/gallery_link substituted fine.
Derive the key set from the template's own `variables` instead, so nothing can
be missing again. editedTemplate already carries the array at the call site,
so no plumbing was needed. A small module-level lookup keeps sensible shapes
for the ~11 variables where shape matters (dates look like dates, links like
URLs), with a readable [name] fallback for anything uncurated -- curating all
~60 distinct variable names across the ~32 template seeds would just recreate
the drift trap.
Preview-only; real sent mail was never affected.
Refs testplan REPORT.md #17 (Part 3, J.04).
This commit is contained in:
@@ -68,6 +68,39 @@ const CORE_SUBCATEGORY_ORDER: readonly string[] = [
|
|||||||
'system',
|
'system',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Realistic stand-ins for the variables whose *shape* matters in a
|
||||||
|
* preview — a date has to read like a date, a link like a link. This is
|
||||||
|
* deliberately not a full list of every variable every template declares;
|
||||||
|
* `buildPreviewSampleData` below covers the rest.
|
||||||
|
*/
|
||||||
|
const PREVIEW_SAMPLE_VALUES: Record<string, string> = {
|
||||||
|
event_name: 'John & Jane Wedding',
|
||||||
|
event_date: 'December 25, 2024',
|
||||||
|
expiry_date: 'January 25, 2025',
|
||||||
|
gallery_link: 'https://photos.example.com/gallery/john-jane-wedding',
|
||||||
|
gallery_password: '••••••••',
|
||||||
|
password: '••••••••',
|
||||||
|
host_name: 'Jane Doe',
|
||||||
|
host_email: '[email protected]',
|
||||||
|
admin_email: '[email protected]',
|
||||||
|
days_remaining: '30',
|
||||||
|
welcome_message: 'Thank you for celebrating our special day with us!',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the preview payload from the template's OWN declared `variables`,
|
||||||
|
* so the two can no longer drift apart. The previous hand-maintained key
|
||||||
|
* list had gone stale and left {{host_name}}, {{gallery_password}} and
|
||||||
|
* {{expiry_date}} rendering as raw tokens in the gallery_created preview.
|
||||||
|
* Variables without a curated value get a readable stand-in rather than an
|
||||||
|
* unsubstituted {{token}}.
|
||||||
|
*/
|
||||||
|
export const buildPreviewSampleData = (variables: string[] = []): Record<string, string> =>
|
||||||
|
Object.fromEntries(
|
||||||
|
variables.map((name) => [name, PREVIEW_SAMPLE_VALUES[name] ?? `[${name}]`])
|
||||||
|
);
|
||||||
|
|
||||||
const defaultTemplateKeys = [
|
const defaultTemplateKeys = [
|
||||||
{
|
{
|
||||||
key: 'gallery_created',
|
key: 'gallery_created',
|
||||||
@@ -395,18 +428,8 @@ export const EmailConfigPage: React.FC = () => {
|
|||||||
const handlePreviewTemplate = async () => {
|
const handlePreviewTemplate = async () => {
|
||||||
if (!selectedTemplateKey || !editedTemplate) return;
|
if (!selectedTemplateKey || !editedTemplate) return;
|
||||||
|
|
||||||
// Generate sample data based on the template
|
// Sample data is derived from the template's declared variables
|
||||||
const sampleData: Record<string, string> = {
|
const sampleData = buildPreviewSampleData(editedTemplate.variables);
|
||||||
event_name: 'John & Jane Wedding',
|
|
||||||
event_date: 'December 25, 2024',
|
|
||||||
password: '••••••••',
|
|
||||||
gallery_link: 'https://photos.example.com/gallery/john-jane-wedding',
|
|
||||||
expiration_date: 'January 25, 2025',
|
|
||||||
welcome_message: 'Thank you for celebrating our special day with us!',
|
|
||||||
days_remaining: '30',
|
|
||||||
admin_email: '[email protected]',
|
|
||||||
host_email: '[email protected]'
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const preview = await emailService.previewTemplate(selectedTemplateKey, sampleData, editingLang);
|
const preview = await emailService.previewTemplate(selectedTemplateKey, sampleData, editingLang);
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* The template Preview modal substituted a hand-maintained sample-data object
|
||||||
|
* whose keys had drifted from the templates' declared `variables` (QA J.04):
|
||||||
|
* {{host_name}}, {{gallery_password}} and {{expiry_date}} rendered as literal
|
||||||
|
* tokens because the object still carried `password` / `expiration_date` and
|
||||||
|
* no `host_name` at all. The payload is now derived from `variables`, so the
|
||||||
|
* property that matters is coverage, not the contents of any one value.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
import { buildPreviewSampleData } from '../EmailConfigPage';
|
||||||
|
|
||||||
|
describe('buildPreviewSampleData', () => {
|
||||||
|
const galleryCreatedVariables = [
|
||||||
|
'host_name',
|
||||||
|
'event_name',
|
||||||
|
'event_date',
|
||||||
|
'gallery_link',
|
||||||
|
'gallery_password',
|
||||||
|
'expiry_date',
|
||||||
|
];
|
||||||
|
|
||||||
|
it('supplies a value for every variable the template declares', () => {
|
||||||
|
const sample = buildPreviewSampleData(galleryCreatedVariables);
|
||||||
|
|
||||||
|
expect(Object.keys(sample).sort()).toEqual([...galleryCreatedVariables].sort());
|
||||||
|
for (const name of galleryCreatedVariables) {
|
||||||
|
expect(sample[name]).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never leaves a variable to render as a raw {{token}}', () => {
|
||||||
|
const sample = buildPreviewSampleData(['customer_name', 'invoice_number', 'total_amount']);
|
||||||
|
|
||||||
|
for (const value of Object.values(sample)) {
|
||||||
|
expect(value).not.toMatch(/\{\{|\}\}/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps date- and link-shaped variables looking like dates and links', () => {
|
||||||
|
const sample = buildPreviewSampleData(galleryCreatedVariables);
|
||||||
|
|
||||||
|
expect(sample.event_date).toMatch(/\d{4}/);
|
||||||
|
expect(sample.expiry_date).toMatch(/\d{4}/);
|
||||||
|
expect(sample.gallery_link).toMatch(/^https?:\/\//);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to a readable stand-in for variables it does not know', () => {
|
||||||
|
expect(buildPreviewSampleData(['storno_number'])).toEqual({ storno_number: '[storno_number]' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a template with no declared variables', () => {
|
||||||
|
expect(buildPreviewSampleData()).toEqual({});
|
||||||
|
expect(buildPreviewSampleData([])).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user