feat(email): global signature footer from the business profile (#1264)
The business profile already carried the operator's full issuer block —
address, phone, email, website, VAT id — but none of it reached an email.
Those columns only fed the quote/invoice PDF renderer, so every outgoing
mail footer was the fixed logo + company name + copyright line.
The signature is rendered by wrapEmailHtml and nowhere else, so no
template, no per-type send path and no queue row needed a change. Two new
columns on business_profile (migration 198) carry the toggle and one
free-text legal line; everything else is read from the address fields the
operator already maintains.
Default off, with a test pinning that the disabled path is byte-identical
to a no-profile install.
Includes three rounds of external review fixes: the plain-text MIME part
also carries the signature; string booleans ('false'/'0') no longer
invert the toggle; the status line stays silent rather than asserting
"off" while unauthorised or loading; and the preview's Text tab mirrors
the send path's htmlToText fallback.
Manual Messages replies deliberately keep no signature — they bypass the
wrapper by design — and the UI copy names that exception.
Closes #1264 (Part A)
This commit is contained in:
@@ -3855,7 +3855,10 @@
|
||||
"requiredFields": "Host, Port und Benutzername sind erforderlich.",
|
||||
"testOk": "Verbunden mit {{folder}} — {{messages}} Nachrichten, {{unseen}} ungelesen.",
|
||||
"testFailed": "Verbindung fehlgeschlagen."
|
||||
}
|
||||
},
|
||||
"signatureOn": "Fusszeilen-Signatur ist an — Ihre Geschäftsadresse wird an automatische E-Mails angehängt. Antworten, die Sie unter Nachrichten schreiben, werden unverändert gesendet.",
|
||||
"signatureOff": "Fusszeilen-Signatur ist aus — E-Mails zeigen nur Logo und Firmenname.",
|
||||
"signatureEdit": "Im Geschäftsprofil bearbeiten"
|
||||
},
|
||||
"cms": {
|
||||
"title": "CMS-Seiten",
|
||||
@@ -6113,7 +6116,19 @@
|
||||
"logoUploadedToast": "PDF-Logo hochgeladen.",
|
||||
"logoConfirmClear": "PDF-Logo entfernen? Der Renderer greift dann auf das Branding-Logo zurück, sofern eines hinterlegt ist.",
|
||||
"logoClearedToast": "PDF-Logo entfernt.",
|
||||
"bankUpdatedToast": "Bankverbindung aktualisiert."
|
||||
"bankUpdatedToast": "Bankverbindung aktualisiert.",
|
||||
"emailSignature": {
|
||||
"title": "E-Mail-Signatur",
|
||||
"subtitle": "Hängt Ihre Firmendaten an die Fusszeile jeder E-Mail an, die diese Installation versendet. Wird aus den Adress- und Kontaktfeldern oben gebildet — nichts wird doppelt erfasst.",
|
||||
"toggle": "Signatur in E-Mail-Fusszeilen anzeigen",
|
||||
"toggleHelp": "Wenn aus, behalten E-Mails die schlichte Fusszeile mit Logo und Firmenname. Gilt für jede automatische E-Mail — Galerie-Benachrichtigungen, Offerten, Rechnungen, Erinnerungen — sowie für Testversand und Vorschau. Antworten, die Sie selbst unter Nachrichten schreiben, werden unverändert gesendet und erhalten keine Signatur.",
|
||||
"extra": "Rechtliche Zeile (optional)",
|
||||
"extraPlaceholder": "Handelsregister Vaduz FL-0002.123.456-7",
|
||||
"extraHelp": "Registernummer, Haftungsausschluss oder jede Zeile ohne eigenes Feld. Reiner Text, maximal 500 Zeichen.",
|
||||
"previewTitle": "Vorschau der Fusszeile",
|
||||
"previewOff": "Signatur ist aus — E-Mails zeigen nur Logo und Firmenname.",
|
||||
"previewEmpty": "Signatur ist an, aber alle Felder oben sind leer — es wird nichts an die Fusszeile angehängt."
|
||||
}
|
||||
},
|
||||
"crmSettings": {
|
||||
"savedToast": "CRM-Einstellungen gespeichert.",
|
||||
|
||||
@@ -3368,7 +3368,10 @@
|
||||
"requiredFields": "Host, port and username are required.",
|
||||
"testOk": "Connected to {{folder}} — {{messages}} messages, {{unseen}} unread.",
|
||||
"testFailed": "Connection failed."
|
||||
}
|
||||
},
|
||||
"signatureOn": "Footer signature is on — your business address is appended to automatic emails. Replies you write in Messages are sent as typed.",
|
||||
"signatureOff": "Footer signature is off — emails show the logo and company name only.",
|
||||
"signatureEdit": "Edit in Business profile"
|
||||
},
|
||||
"cms": {
|
||||
"title": "CMS Pages",
|
||||
@@ -6112,7 +6115,19 @@
|
||||
"logoUploadedToast": "PDF logo uploaded.",
|
||||
"logoConfirmClear": "Remove the PDF logo? The renderer will fall back to the Branding logo if one is set.",
|
||||
"logoClearedToast": "PDF logo removed.",
|
||||
"bankUpdatedToast": "Bank account updated."
|
||||
"bankUpdatedToast": "Bank account updated.",
|
||||
"emailSignature": {
|
||||
"title": "Email signature",
|
||||
"subtitle": "Append your company details to the footer of every email this installation sends. Built from the address and contact fields above — nothing is duplicated.",
|
||||
"toggle": "Show signature in email footers",
|
||||
"toggleHelp": "When off, emails keep the plain logo + company name footer. Applies to every automatic email — gallery notices, quotes, invoices, reminders — plus test sends and previews. Replies you write yourself in Messages are sent as typed and do not get the signature.",
|
||||
"extra": "Legal line (optional)",
|
||||
"extraPlaceholder": "Handelsregister Vaduz FL-0002.123.456-7",
|
||||
"extraHelp": "Registration number, disclaimer or any line with no field of its own. Plain text, up to 500 characters.",
|
||||
"previewTitle": "Footer preview",
|
||||
"previewOff": "Signature is off — emails show the logo and company name only.",
|
||||
"previewEmpty": "Signature is on but every field above is blank — nothing will be added to the footer."
|
||||
}
|
||||
},
|
||||
"crmSettings": {
|
||||
"savedToast": "CRM settings saved.",
|
||||
|
||||
@@ -23,10 +23,12 @@ import { ReceivedEmailsPanel } from '../../components/admin/ReceivedEmailsPanel'
|
||||
import { IncomingMailConfigCard } from '../../components/admin/IncomingMailConfigCard';
|
||||
import { CustomerMailboxCard } from '../../components/admin/CustomerMailboxCard';
|
||||
import { Palette, RefreshCw, Info } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useModal, useMutationWithToast } from '../../hooks';
|
||||
import { emailService, type EmailConfig, type EmailTemplate, type EmailTemplateTranslation } from '../../services/email.service';
|
||||
import { settingsService } from '../../services/settings.service';
|
||||
import { businessProfileService } from '../../services/businessProfile.service';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SUPPORTED_LANGUAGES } from "../../components/common/LanguageSelector.tsx";
|
||||
import { useFeatureFlags, type FeatureKey } from '../../contexts/FeatureFlagsContext';
|
||||
@@ -210,6 +212,27 @@ export const EmailConfigPage: React.FC = () => {
|
||||
tls_reject_unauthorized: true
|
||||
});
|
||||
|
||||
// Migration 198 — whether the global footer signature is on. Read-only
|
||||
// here; the toggle itself lives on Settings → Business profile.
|
||||
//
|
||||
// This tab is reachable with `email.view`, but GET /admin/business-profile
|
||||
// requires `settings.view` / `settings.banking`. An email-only role gets a
|
||||
// 403, and reporting that as "signature is off" would be stating something
|
||||
// false about a mail they are about to send — so an unreadable profile
|
||||
// renders nothing at all rather than a guess (#1264 review).
|
||||
const { data: businessProfile, isError, isPending } = useQuery({
|
||||
queryKey: ['business-profile'],
|
||||
queryFn: () => businessProfileService.get(),
|
||||
enabled: activeTab === 'smtp',
|
||||
retry: false,
|
||||
});
|
||||
// Pending counts as unknown too. The other queries on this tab are often
|
||||
// cached and paint first, so `?? false` announced "signature is off" for
|
||||
// as long as this request was in flight — a wrong statement about a mail
|
||||
// the admin is about to send, not merely a slow one.
|
||||
const signatureUnknown = isError || isPending || !businessProfile;
|
||||
const signatureEnabled = businessProfile?.profile?.emailSignatureEnabled ?? false;
|
||||
|
||||
// Fetch SMTP config
|
||||
const { isLoading: configLoading } = useQuery({
|
||||
queryKey: ['email-config'],
|
||||
@@ -536,6 +559,24 @@ export const EmailConfigPage: React.FC = () => {
|
||||
{/* SMTP Settings Tab */}
|
||||
{activeTab === 'smtp' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* The footer signature (migration 198) is applied by the email
|
||||
wrapper to every send from this page, but it's configured on
|
||||
the Business profile — point at it from where the mail is set
|
||||
up rather than making the operator hunt for it. */}
|
||||
{!signatureUnknown && (
|
||||
<div className="lg:col-span-2 flex items-start gap-2 rounded-md border border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/60 p-3 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<span>
|
||||
{signatureEnabled
|
||||
? t('email.signatureOn', 'Footer signature is on — your business address is appended to automatic emails. Replies you write in Messages are sent as typed.')
|
||||
: t('email.signatureOff', 'Footer signature is off — emails show the logo and company name only.')}
|
||||
{' '}
|
||||
<Link to="/admin/settings?tab=businessProfile" className="underline hover:no-underline" style={{ color: 'var(--color-accent)' }}>
|
||||
{t('email.signatureEdit', 'Edit in Business profile')}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Card padding="md">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('email.smtpConfiguration')}</h2>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Plus, Trash2, Star, Pencil, Save, Clock, Copy } from 'lucide-react';
|
||||
import { Plus, Trash2, Star, Pencil, Save, Clock, Copy, Mail } from 'lucide-react';
|
||||
import {
|
||||
businessProfileService,
|
||||
type BusinessProfile,
|
||||
@@ -305,6 +305,58 @@ export const SettingsBusinessProfilePage: React.FC = () => {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Global email footer signature (migration 198, issue #1264).
|
||||
Rendered by wrapEmailHtml only, so flipping this toggle changes
|
||||
the footer of EVERY outgoing mail — transactional, preview, test
|
||||
and manual — without any per-template edit. Every value except
|
||||
the legal line below comes from the fields already on this page. */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Mail className="w-5 h-5 text-neutral-500" />
|
||||
<h3 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{t('businessProfile.emailSignature.title', 'Email signature')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
{t('businessProfile.emailSignature.subtitle',
|
||||
'Append your company details to the footer of every email this installation sends. Built from the address and contact fields above — nothing is duplicated.')}
|
||||
</p>
|
||||
|
||||
<PdfToggleRow
|
||||
label={t('businessProfile.emailSignature.toggle', 'Show signature in email footers') as string}
|
||||
description={t('businessProfile.emailSignature.toggleHelp',
|
||||
'When off, emails keep the plain logo + company name footer. Applies to every automatic email — gallery notices, quotes, invoices, reminders — plus test sends and previews. Replies you write yourself in Messages are sent as typed and do not get the signature.') as string}
|
||||
enabled={profile.emailSignatureEnabled}
|
||||
onChange={(v) => setProfile({ ...profile, emailSignatureEnabled: v })}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
{t('businessProfile.emailSignature.extra', 'Legal line (optional)')}
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
value={profile.emailSignatureExtra}
|
||||
onChange={(e) => setProfile({ ...profile, emailSignatureExtra: e.target.value })}
|
||||
placeholder={t('businessProfile.emailSignature.extraPlaceholder',
|
||||
'Handelsregister Vaduz FL-0002.123.456-7') as string}
|
||||
className="w-full rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 px-3 py-2 text-sm text-neutral-900 dark:text-neutral-100"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{t('businessProfile.emailSignature.extraHelp',
|
||||
'Registration number, disclaimer or any line with no field of its own. Plain text, up to 500 characters.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<p className="text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-2 uppercase tracking-wide">
|
||||
{t('businessProfile.emailSignature.previewTitle', 'Footer preview')}
|
||||
</p>
|
||||
<EmailSignaturePreview profile={profile} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Disclaimer banner for QR-bill / IBAN data. picpeak renders
|
||||
what the operator types — it cannot validate IBAN/BIC, QR-IID
|
||||
or scan-compatibility with any specific bank's e-banking app.
|
||||
@@ -361,6 +413,64 @@ const PdfToggleRow: React.FC<PdfToggleRowProps> = ({ label, description, enabled
|
||||
</label>
|
||||
);
|
||||
|
||||
/**
|
||||
* Read-only render of the email footer signature (migration 198).
|
||||
*
|
||||
* Deliberately mirrors `renderEmailSignature` in the backend's
|
||||
* emailProcessor: same field order, same middle-dot separators, same
|
||||
* omit-when-empty rule. It is a preview, not a second renderer — the mail
|
||||
* itself is always built server-side.
|
||||
*/
|
||||
const EmailSignaturePreview: React.FC<{ profile: BusinessProfile }> = ({ profile }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
if (!profile.emailSignatureEnabled) {
|
||||
return (
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 italic">
|
||||
{t('businessProfile.emailSignature.previewOff', 'Signature is off — emails show the logo and company name only.')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
// "LI-9494 Schaan / Liechtenstein" — the same shape the PDF issuer
|
||||
// block uses, assembled identically on the backend.
|
||||
const cc = profile.countryCode ? profile.countryCode.toUpperCase() : '';
|
||||
const pc = profile.postalCode || '';
|
||||
const left = [cc && pc ? `${cc}-${pc}` : (pc || cc), profile.city || ''].filter(Boolean).join(' ');
|
||||
const cityCountry = [left, profile.countryName || ''].filter(Boolean).join(' / ');
|
||||
|
||||
const addressLines = [profile.addressLine1, profile.addressLine2, cityCountry]
|
||||
.map((l) => (l || '').trim())
|
||||
.filter(Boolean);
|
||||
const contacts = [profile.phone, profile.mobile, profile.email, profile.website]
|
||||
.map((c) => (c || '').trim())
|
||||
.filter(Boolean);
|
||||
const vatLabel = i18n.language?.startsWith('de') ? 'USt-IdNr.' : 'VAT ID';
|
||||
|
||||
const isEmpty = !profile.companyName && !addressLines.length && !contacts.length
|
||||
&& !profile.vatId && !profile.emailSignatureExtra;
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400">
|
||||
{t('businessProfile.emailSignature.previewEmpty',
|
||||
'Signature is on but every field above is blank — nothing will be added to the footer.')}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="email-signature-preview" className="rounded-md bg-neutral-50 dark:bg-neutral-800/60 p-3 text-center text-xs text-neutral-600 dark:text-neutral-400 space-y-1">
|
||||
{profile.companyName && <p>{profile.companyName}</p>}
|
||||
{addressLines.length > 0 && <p>{addressLines.join(' \u00b7 ')}</p>}
|
||||
{contacts.length > 0 && <p>{contacts.join(' \u00b7 ')}</p>}
|
||||
{profile.vatId && <p>{vatLabel}: {profile.vatId}</p>}
|
||||
{profile.emailSignatureExtra && (
|
||||
<p className="text-[11px] whitespace-pre-line">{profile.emailSignatureExtra}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-weekday business-hours editor (migration 114). Google-style: each
|
||||
* weekday holds zero or more {start,end} blocks, so a day can be closed
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Settings → Business profile — email signature card (migration 198, #1264).
|
||||
*
|
||||
* The preview here mirrors the backend's `renderEmailSignature`, so the
|
||||
* cases worth pinning are the ones where the two could silently drift:
|
||||
* the "LI-9494 Schaan / Liechtenstein" city line, the middle-dot joins,
|
||||
* and the omit-when-empty rule. Plus the two states an operator can land
|
||||
* in and misread — signature off, and signature on with a blank profile.
|
||||
*/
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { vi } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import type { BusinessProfile } from '../../../../services/businessProfile.service';
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next');
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (k: string, fb?: unknown) => (typeof fb === 'string' ? fb : k),
|
||||
i18n: { language: 'en' },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('react-toastify', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
|
||||
const baseProfile: BusinessProfile = {
|
||||
id: 1,
|
||||
companyName: 'Müller Fotografie GmbH',
|
||||
addressLine1: 'Bahnhofstrasse 1',
|
||||
addressLine2: '',
|
||||
postalCode: '9494',
|
||||
city: 'Schaan',
|
||||
state: '',
|
||||
countryCode: 'LI',
|
||||
countryName: 'Liechtenstein',
|
||||
phone: '+41 79 123 45 67',
|
||||
mobile: '',
|
||||
email: '[email protected]',
|
||||
website: 'example.com',
|
||||
vatId: 'CHE-123.456.789',
|
||||
taxId: '',
|
||||
vatLabel: 'MwSt.',
|
||||
vatRateDefault: null,
|
||||
defaultHourlyRateMinor: null,
|
||||
defaultCurrency: 'CHF',
|
||||
defaultLocale: 'de',
|
||||
defaultQrFormat: 'none',
|
||||
footerLine: '',
|
||||
logoPath: '',
|
||||
pdfFontTtfPath: '',
|
||||
pdfFontFamily: null,
|
||||
pdfShowLogo: true,
|
||||
pdfShowCompanyName: true,
|
||||
pdfCompanyNameInline: false,
|
||||
pdfLogoHeight: 56,
|
||||
pdfFoldingMarks: 'none',
|
||||
pdfQuoteShowNetDays: false,
|
||||
pdfQuoteShowSkonto: false,
|
||||
timezone: null,
|
||||
businessHours: null,
|
||||
scheduledEmailFloorEnabled: true,
|
||||
emailSignatureEnabled: true,
|
||||
emailSignatureExtra: 'Handelsregister Vaduz',
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
updatedAt: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
let profileFixture: BusinessProfile = baseProfile;
|
||||
|
||||
vi.mock('../../../../services/businessProfile.service', () => ({
|
||||
businessProfileService: {
|
||||
get: vi.fn(async () => ({ profile: profileFixture, bankAccounts: [] })),
|
||||
update: vi.fn(async () => ({ profile: profileFixture, bankAccounts: [] })),
|
||||
listBankAccounts: vi.fn(async () => ({ bankAccounts: [] })),
|
||||
createBankAccount: vi.fn(),
|
||||
updateBankAccount: vi.fn(),
|
||||
deleteBankAccount: vi.fn(),
|
||||
uploadLogo: vi.fn(),
|
||||
clearLogo: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { SettingsBusinessProfilePage } from '../SettingsBusinessProfilePage';
|
||||
|
||||
function renderPage() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<SettingsBusinessProfilePage />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('email signature card', () => {
|
||||
beforeEach(() => {
|
||||
profileFixture = { ...baseProfile };
|
||||
});
|
||||
|
||||
it('renders the signature preview from the address fields above', async () => {
|
||||
renderPage();
|
||||
|
||||
// Scoped to the preview block — the legal line also lives in the
|
||||
// textarea above it, and the company name in the form fields.
|
||||
const preview = within(await screen.findByTestId('email-signature-preview'));
|
||||
|
||||
expect(preview.getByText('Müller Fotografie GmbH')).toBeInTheDocument();
|
||||
// City line uses the PDF issuer shape, not a raw "9494 Schaan".
|
||||
expect(preview.getByText('Bahnhofstrasse 1 · LI-9494 Schaan / Liechtenstein')).toBeInTheDocument();
|
||||
// Empty mobile is omitted rather than producing a doubled separator.
|
||||
expect(preview.getByText('+41 79 123 45 67 · [email protected] · example.com')).toBeInTheDocument();
|
||||
expect(preview.getByText('VAT ID: CHE-123.456.789')).toBeInTheDocument();
|
||||
expect(preview.getByText('Handelsregister Vaduz')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('explains the off state instead of showing a stale preview', async () => {
|
||||
profileFixture = { ...baseProfile, emailSignatureEnabled: false };
|
||||
renderPage();
|
||||
|
||||
expect(
|
||||
await screen.findByText('Signature is off — emails show the logo and company name only.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('email-signature-preview')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('warns when the signature is on but every field is blank', async () => {
|
||||
profileFixture = {
|
||||
...baseProfile,
|
||||
companyName: '', addressLine1: '', addressLine2: '', postalCode: '', city: '',
|
||||
countryCode: '', countryName: '', phone: '', mobile: '', email: '', website: '',
|
||||
vatId: '', emailSignatureExtra: '',
|
||||
};
|
||||
renderPage();
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'Signature is on but every field above is blank — nothing will be added to the footer.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggling off updates the preview without a save', async () => {
|
||||
renderPage();
|
||||
await screen.findByText('Footer preview');
|
||||
|
||||
await userEvent.click(screen.getByRole('switch', { name: /Show signature in email footers/i }));
|
||||
|
||||
expect(
|
||||
screen.getByText('Signature is off — emails show the logo and company name only.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('caps the legal line at 500 characters', async () => {
|
||||
renderPage();
|
||||
|
||||
const textarea = (await screen.findByPlaceholderText(
|
||||
'Handelsregister Vaduz FL-0002.123.456-7'
|
||||
)) as HTMLTextAreaElement;
|
||||
expect(textarea.maxLength).toBe(500);
|
||||
});
|
||||
});
|
||||
@@ -94,6 +94,16 @@ export interface BusinessProfile {
|
||||
* (migration 114). Defaults true. When off, scheduled emails send at
|
||||
* their requested instant regardless of `businessHours`. */
|
||||
scheduledEmailFloorEnabled: boolean;
|
||||
/** Global email footer signature master switch (migration 198). When
|
||||
* on, `wrapEmailHtml` appends the issuer block below — address,
|
||||
* contacts, VAT id and `emailSignatureExtra` — to the footer of EVERY
|
||||
* outgoing mail: transactional, preview, test and manual alike.
|
||||
* Defaults false so an upgraded install's footer is unchanged. */
|
||||
emailSignatureEnabled: boolean;
|
||||
/** Free-text legal line under the signature (Handelsregister /
|
||||
* registration number / disclaimer). Plain text, max 500 chars —
|
||||
* rendered escaped with newlines turned into <br>. Migration 198. */
|
||||
emailSignatureExtra: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user