feat(frontend): edit the customer emails from Admin → Settings (#92)
SonarQube Analysis / sonarqube (pull_request) Failing after 13m7s
SonarQube Analysis / sonarqube (pull_request) Failing after 13m7s
A card per email under the existing settings screen: subject, body, the placeholders it understands, and which of them it cannot lose. Each card starts from the copy that is actually in use — the stored version if there is one, the built-in default otherwise — rather than an empty box, so editing means changing words rather than writing the email from scratch. A badge distinguishes customised from default, which is why the API reports an unedited template as null rather than as its default text: the two are different states and the screen has to be able to tell them apart. Restore default is offered only when there is something to restore, so it is never a button that looks like it did something and did not. It removes the stored rows rather than writing the defaults into them, which is what keeps the badge honest afterwards. The server's refusal is shown verbatim. When a body drops a placeholder it needs, the message names which one, and that message is the entire value of the validation — replacing it with a generic failure would leave an admin guessing at which of five templates and which of three placeholders they broke. A textarea rather than the markdown editor already used for item descriptions. That editor is a heavy dependency to load into the settings screen for five short bodies, and its preview would render markdown as the browser shows it rather than as the email renderer will — a preview that quietly disagrees with the output is worse than none. Worth revisiting if the copy gets longer. Two things the end-to-end spec found rather than assumed. The refusal assertion first matched three elements, because the placeholder appears as the required marker, as an available tag, and inside the error — it now asserts the whole sentence. And the four tests raced each other: the suite runs fully parallel and they all edit one shared stored template, so one asserted a template was unset while another had just saved it. That describe block now runs serially, which is the honest fix for tests that mutate shared server state rather than making the assertions vaguer. Verified: 99 end-to-end tests passing on a freshly created container, up from 95, with the whole suite run rather than the new spec alone — precisely because these tests write state other suites read. Build clean, lint unchanged at 27 warnings. Refs #92 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,12 +6,15 @@ import Typography from 'antd/es/typography';
|
||||
import message from 'antd/es/message';
|
||||
import Card from 'antd/es/card';
|
||||
import { fetchAdminSettings, updateAdminSettings } from './adminSettingsApi';
|
||||
import { EmailTemplate, fetchEmailTemplates } from './emailTemplatesApi';
|
||||
import EmailTemplateCard from './EmailTemplateCard';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Settings() {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [templates, setTemplates] = useState<EmailTemplate[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAdminSettings()
|
||||
@@ -23,6 +26,22 @@ export default function Settings() {
|
||||
.finally(() => setLoading(false));
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEmailTemplates()
|
||||
.then(setTemplates)
|
||||
// Reported rather than swallowed: an empty list would otherwise read as
|
||||
// "there are no templates" instead of "they could not be loaded".
|
||||
.catch(() => message.error('Could not load the email templates'));
|
||||
}, []);
|
||||
|
||||
// Replaces the one that changed so the Customised badge and the Restore
|
||||
// button reflect what the server now holds, without refetching the rest.
|
||||
function handleTemplateChanged(updated: EmailTemplate) {
|
||||
setTemplates(current =>
|
||||
current.map(t => (t.key === updated.key ? { ...t, subject: updated.subject, body: updated.body } : t))
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const values = await form.validateFields();
|
||||
await updateAdminSettings(values);
|
||||
@@ -30,6 +49,7 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 720 }}>
|
||||
<Card style={{ maxWidth: 480 }}>
|
||||
<Title level={4}>Cart Settings</Title>
|
||||
<Text type="secondary">
|
||||
@@ -42,5 +62,16 @@ export default function Settings() {
|
||||
<Button type="primary" onClick={handleSave} loading={loading}>Save</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Title level={4} style={{ marginTop: 32 }}>Customer emails</Title>
|
||||
<Text type="secondary">
|
||||
The wording customers receive. Leave one alone and it sends the built-in copy.
|
||||
</Text>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
{templates.map(template => (
|
||||
<EmailTemplateCard key={template.key} template={template} onChanged={handleTemplateChanged} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user