feat(admin): Anmeldehinweise der Willkommensmail in der Vorlage bearbeitbar
Neue Felder loginHintDirectory/loginHintLocal (Platzhalter, Pflicht, max. 1000), Migration 20260930170000 (nullable, leer = Standard aus @tessera/shared). Knopf Passwort festlegen und Gueltigkeitshinweis bleiben fest; local-no-link wird nie erzeugt und bleibt fest. Vorschau springt beim Bearbeiten auf die passende Kontoart. Lokal im Browser nachgewiesen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,8 @@ const custom = {
|
||||
subject: 'Hallo {{vorname}}',
|
||||
heading: 'Willkommen, {{name}}!',
|
||||
intro: 'Eigene Einleitung',
|
||||
loginHintDirectory: 'Eigener Verzeichnis-Hinweis',
|
||||
loginHintLocal: 'Eigener Lokal-Hinweis',
|
||||
closing: 'Ihr IT-Team',
|
||||
};
|
||||
|
||||
@@ -147,7 +149,76 @@ describe('WelcomeMailTemplateEditor', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('Speichern schickt die vier Texte und meldet Erfolg; Status zeigt die eigene Vorlage', async () => {
|
||||
it('Anmeldehinweise: beide Felder nach dem Einleitungstext, mit Standardtext bzw. eigener Vorlage vorbelegt', async () => {
|
||||
await renderLoaded();
|
||||
const dir = screen.getByLabelText(
|
||||
'Anmeldehinweis für Verzeichniskonten',
|
||||
) as HTMLTextAreaElement;
|
||||
const local = screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement;
|
||||
expect(dir.value).toBe(
|
||||
'Melden Sie sich mit Ihrem Benutzernamen und Ihrem gewohnten Windows-Passwort an.',
|
||||
);
|
||||
expect(local.value).toBe(
|
||||
'Bevor Sie sich zum ersten Mal anmelden, legen Sie bitte Ihr persönliches Passwort fest.',
|
||||
);
|
||||
// Reihenfolge: Einleitung → Verzeichnis → lokal → Abschluss
|
||||
const intro = screen.getByLabelText('Einleitungstext');
|
||||
const closing = screen.getByLabelText('Abschlusstext');
|
||||
const follows = (a: Element, b: Element) =>
|
||||
Boolean(a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(follows(intro, dir)).toBe(true);
|
||||
expect(follows(dir, local)).toBe(true);
|
||||
expect(follows(local, closing)).toBe(true);
|
||||
expect(screen.getByText(/direkt vor dem Knopf „Passwort festlegen“/)).toBeTruthy();
|
||||
|
||||
cleanup();
|
||||
api.fetchWelcomeMailTemplate.mockResolvedValue(state(true));
|
||||
await renderLoaded();
|
||||
expect(
|
||||
(screen.getByLabelText('Anmeldehinweis für Verzeichniskonten') as HTMLTextAreaElement).value,
|
||||
).toBe(custom.loginHintDirectory);
|
||||
expect(
|
||||
(screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement).value,
|
||||
).toBe(custom.loginHintLocal);
|
||||
});
|
||||
|
||||
it('Anmeldehinweis: Pflichtfeld, Platzhalter-Klick fuegt ein, Fokus schaltet die Vorschau auf die Kontoart', async () => {
|
||||
await renderLoaded();
|
||||
const local = screen.getByLabelText('Anmeldehinweis für lokale Konten') as HTMLTextAreaElement;
|
||||
fireEvent.change(local, { target: { value: 'Hallo ' } });
|
||||
local.focus();
|
||||
local.setSelectionRange(6, 6);
|
||||
fireEvent.click(screen.getByRole('button', { name: '{{vorname}} einfügen' }));
|
||||
expect(local.value).toBe('Hallo {{vorname}}');
|
||||
expect(screen.getByRole('button', { name: 'Lokales Konto' }).getAttribute('aria-pressed')).toBe(
|
||||
'true',
|
||||
);
|
||||
|
||||
fireEvent.change(local, { target: { value: ' ' } });
|
||||
expect(await screen.findByText('Bitte füllen Sie dieses Feld aus.')).toBeTruthy();
|
||||
expect((screen.getByRole('button', { name: 'Speichern' }) as HTMLButtonElement).disabled).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('Speichern schickt alle sechs Texte inkl. Anmeldehinweise', async () => {
|
||||
const user = userEvent.setup();
|
||||
await renderLoaded();
|
||||
fireEvent.change(screen.getByLabelText('Anmeldehinweis für Verzeichniskonten'), {
|
||||
target: { value: 'Mit dem Firmenkennwort anmelden, {{vorname}}.' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('Anmeldehinweis für lokale Konten'), {
|
||||
target: { value: 'Erst Passwort setzen.' },
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: 'Speichern' }));
|
||||
expect(api.saveWelcomeMailTemplate).toHaveBeenCalledWith({
|
||||
...defaults,
|
||||
loginHintDirectory: 'Mit dem Firmenkennwort anmelden, {{vorname}}.',
|
||||
loginHintLocal: 'Erst Passwort setzen.',
|
||||
});
|
||||
});
|
||||
|
||||
it('Speichern schickt die Texte und meldet Erfolg; Status zeigt die eigene Vorlage', async () => {
|
||||
const user = userEvent.setup();
|
||||
const subject = await renderLoaded();
|
||||
fireEvent.change(subject, { target: { value: 'Neu für {{firma}}' } });
|
||||
|
||||
@@ -24,13 +24,41 @@ type Field = keyof WelcomeMailTexts;
|
||||
type PreviewAccount = 'directory' | 'local';
|
||||
type Feedback = { type: 'success' | 'error'; message: string } | null;
|
||||
|
||||
const FIELDS: Field[] = ['subject', 'heading', 'intro', 'closing'];
|
||||
const FIELDS: Field[] = [
|
||||
'subject',
|
||||
'heading',
|
||||
'intro',
|
||||
'loginHintDirectory',
|
||||
'loginHintLocal',
|
||||
'closing',
|
||||
];
|
||||
const MULTILINE: Record<Field, boolean> = {
|
||||
subject: false,
|
||||
heading: false,
|
||||
intro: true,
|
||||
loginHintDirectory: true,
|
||||
loginHintLocal: true,
|
||||
closing: true,
|
||||
};
|
||||
/** Pflichtfelder (dieselben wie im DTO der API). */
|
||||
const REQUIRED: ReadonlySet<Field> = new Set([
|
||||
'subject',
|
||||
'heading',
|
||||
'loginHintDirectory',
|
||||
'loginHintLocal',
|
||||
]);
|
||||
/** Zeilen der mehrzeiligen Felder. */
|
||||
const ROWS: Partial<Record<Field, number>> = {
|
||||
intro: 6,
|
||||
loginHintDirectory: 3,
|
||||
loginHintLocal: 3,
|
||||
closing: 5,
|
||||
};
|
||||
/** Welche Kontoart die Vorschau zeigt, wenn ein Anmeldehinweis bearbeitet wird. */
|
||||
const HINT_ACCOUNT: Partial<Record<Field, PreviewAccount>> = {
|
||||
loginHintDirectory: 'directory',
|
||||
loginHintLocal: 'local',
|
||||
};
|
||||
|
||||
/** Verzoegerung der Live-Vorschau nach der letzten Eingabe. */
|
||||
const PREVIEW_DELAY_MS = 400;
|
||||
@@ -38,11 +66,13 @@ const PREVIEW_DELAY_MS = 400;
|
||||
/**
|
||||
* Administrator → Willkommensmail: eigene Vorlage je Mandant.
|
||||
*
|
||||
* Vier Textfelder (Betreff, Ueberschrift, Einleitung, Abschluss), eine
|
||||
* Sechs Textfelder (Betreff, Ueberschrift, Einleitung, Anmeldehinweis fuer
|
||||
* Verzeichniskonten, Anmeldehinweis fuer lokale Konten, Abschluss), eine
|
||||
* Hinweisbox mit den Platzhaltern (Klick fuegt an der Cursorposition des
|
||||
* zuletzt benutzten Feldes ein), eine Live-Vorschau (von der API gerendert,
|
||||
* also dieselbe Mail wie im Versand) mit Umschalter Verzeichniskonto/lokales
|
||||
* Konto, "Testmail an mich senden" und "Auf Standard zuruecksetzen".
|
||||
* Konto (springt beim Bearbeiten eines Anmeldehinweises auf die passende
|
||||
* Kontoart), "Testmail an mich senden" und "Auf Standard zuruecksetzen".
|
||||
*
|
||||
* Unbekannte Platzhalter benennt das Formular schon beim Tippen; die API
|
||||
* lehnt sie beim Speichern ohnehin mit 400 ab (Meldung wird angezeigt).
|
||||
@@ -81,7 +111,7 @@ export function WelcomeMailTemplateEditor() {
|
||||
// ── Pruefung im Formular (dieselben Regeln wie die API) ───────────────
|
||||
const fieldErrors = (field: Field, value: string): string[] => {
|
||||
const errors: string[] = [];
|
||||
if ((field === 'subject' || field === 'heading') && !value.trim()) {
|
||||
if (REQUIRED.has(field) && !value.trim()) {
|
||||
errors.push(t('errors.required'));
|
||||
}
|
||||
if (value.length > WELCOME_MAIL_LIMITS[field]) {
|
||||
@@ -266,6 +296,8 @@ export function WelcomeMailTemplateEditor() {
|
||||
'aria-describedby': describedBy,
|
||||
onFocus: () => {
|
||||
lastFocused.current = field;
|
||||
const hintAccount = HINT_ACCOUNT[field];
|
||||
if (hintAccount) setAccount(hintAccount);
|
||||
},
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
@@ -283,7 +315,7 @@ export function WelcomeMailTemplateEditor() {
|
||||
ref={(el) => {
|
||||
fieldRefs.current[field] = el;
|
||||
}}
|
||||
rows={field === 'intro' ? 6 : 5}
|
||||
rows={ROWS[field] ?? 5}
|
||||
className={`${inputClass} py-2`}
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user