diff --git a/frontend/src/__tests__/mail/MailSettings.validation.test.tsx b/frontend/src/__tests__/mail/MailSettings.validation.test.tsx new file mode 100644 index 0000000..21e5e44 --- /dev/null +++ b/frontend/src/__tests__/mail/MailSettings.validation.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; + +vi.mock('@/api/mail', () => ({ + fetchAccounts: vi.fn().mockResolvedValue([]), + createAccount: vi.fn().mockResolvedValue({ id: 'a1', email: 'test@example.com', display_name: '', imap_host: '', imap_port: 993, smtp_host: '', smtp_port: 587, is_shared: false, is_active: true }), + deleteAccount: vi.fn(), + testConnection: vi.fn(), + triggerSync: vi.fn(), + updateAccount: vi.fn(), + fetchFolders: vi.fn().mockResolvedValue([]), +})); + +vi.mock('@/components/ui/Toast', () => ({ + useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }), +})); + +vi.mock('@/components/mail/SignatureManager', () => ({ SignatureManager: () => null })); +vi.mock('@/components/mail/RuleEditor', () => ({ RuleEditor: () => null })); +vi.mock('@/components/mail/LabelManager', () => ({ LabelManager: () => null })); +vi.mock('@/components/mail/VacationResponder', () => ({ VacationResponder: () => null })); +vi.mock('@/components/mail/PgpSettings', () => ({ PgpSettings: () => null })); + +import { MailSettingsPage } from '@/pages/MailSettings'; +import { createAccount } from '@/api/mail'; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('MailSettings account form validation (RHF + Zod)', () => { + it('shows validation error when email is empty', async () => { + render(); + await waitFor(() => { + expect(screen.getByTestId('mail-settings-page')).toBeInTheDocument(); + }, { timeout: 3000 }); + // Click add account button + const addBtn = screen.getByTestId('add-account-btn'); + fireEvent.click(addBtn); + await waitFor(() => { + expect(screen.getByTestId('add-account-form')).toBeInTheDocument(); + }); + // Submit empty form + const form = screen.getByTestId('add-account-form').querySelector('form'); + expect(form).toBeTruthy(); + fireEvent.submit(form!); + await waitFor(() => { + const errors = screen.getAllByText(/erforderlich|required/i); + expect(errors.length).toBeGreaterThan(0); + }); + }); + + it('calls createAccount when form is valid', async () => { + render(); + await waitFor(() => { + expect(screen.getByTestId('mail-settings-page')).toBeInTheDocument(); + }, { timeout: 3000 }); + const addBtn = screen.getByTestId('add-account-btn'); + fireEvent.click(addBtn); + await waitFor(() => { + expect(screen.getByTestId('add-account-form')).toBeInTheDocument(); + }); + const form = screen.getByTestId('add-account-form').querySelector('form'); + const inputs = form!.querySelectorAll('input'); + // Fill email, imap_host, smtp_host, password + fireEvent.change(inputs[0], { target: { value: 'test@example.com' } }); // email + fireEvent.change(inputs[3], { target: { value: 'imap.example.com' } }); // imap_host + fireEvent.change(inputs[5], { target: { value: 'smtp.example.com' } }); // smtp_host + fireEvent.change(inputs[7], { target: { value: 'password123' } }); // password + fireEvent.submit(form!); + await waitFor(() => { + expect(createAccount).toHaveBeenCalledWith(expect.objectContaining({ email: 'test@example.com', imap_host: 'imap.example.com', smtp_host: 'smtp.example.com' })); + }, { timeout: 3000 }); + }); +}); diff --git a/frontend/src/components/mail/LabelManager.tsx b/frontend/src/components/mail/LabelManager.tsx index 04d2aa6..c3bf95d 100644 --- a/frontend/src/components/mail/LabelManager.tsx +++ b/frontend/src/components/mail/LabelManager.tsx @@ -4,6 +4,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Card } from '@/components/ui/Card'; @@ -30,11 +33,23 @@ export function LabelManager() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showForm, setShowForm] = useState(false); - const [name, setName] = useState(''); - const [color, setColor] = useState(PRESET_COLORS[0]); const [saving, setSaving] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); + // ── Label form (RHF + Zod) ── + const labelSchema = z.object({ + name: z.string().min(1, 'required'), + color: z.string().optional().default(PRESET_COLORS[0]), + }); + type LabelFormData = z.infer; + + const { register: registerLabel, handleSubmit: handleSubmitLabel, reset: resetLabel, watch: watchLabel, setValue: setLabelValue, formState: { errors: labelErrors } } = useForm({ + resolver: zodResolver(labelSchema), + defaultValues: { name: '', color: PRESET_COLORS[0] }, + }); + + const colorValue = watchLabel('color'); + const load = useCallback(() => { setLoading(true); fetchLabels() @@ -50,22 +65,20 @@ export function LabelManager() { useEffect(() => { load(); }, [load]); - const handleSave = useCallback(async () => { - if (!name.trim()) return; + const handleSave = useCallback(async (data: LabelFormData) => { setSaving(true); try { - const label = await createLabel({ name, color }); + const label = await createLabel({ name: data.name, color: data.color }); setLabels((prev) => [...prev, label]); toast.success(t('mail.labelCreated')); setShowForm(false); - setName(''); - setColor(PRESET_COLORS[0]); + resetLabel({ name: '', color: PRESET_COLORS[0] }); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } - }, [name, color, toast, t]); + }, [toast, t, resetLabel]); const handleDelete = useCallback(async () => { if (!deleteTarget) return; @@ -104,11 +117,11 @@ export function LabelManager() { {showForm && ( - + setName(e.target.value)} + {...registerLabel('name')} + error={labelErrors.name?.message === 'required' ? t('validation.required') : undefined} placeholder={t('mail.labelName')} required /> @@ -119,8 +132,8 @@ export function LabelManager() { setColor(c)} - className={`w-8 h-8 rounded-full ${color === c ? 'ring-2 ring-offset-2 ring-primary-500' : ''}`} + onClick={() => setLabelValue('color', c)} + className={`w-8 h-8 rounded-full ${colorValue === c ? 'ring-2 ring-offset-2 ring-primary-500' : ''}`} style={{ backgroundColor: c }} aria-label={c} /> @@ -128,10 +141,10 @@ export function LabelManager() { - {t('common.save')} - setShowForm(false)}>{t('common.cancel')} + {t('common.save')} + setShowForm(false)}>{t('common.cancel')} - + )} diff --git a/frontend/src/components/mail/RuleEditor.tsx b/frontend/src/components/mail/RuleEditor.tsx index a540193..efd1ad6 100644 --- a/frontend/src/components/mail/RuleEditor.tsx +++ b/frontend/src/components/mail/RuleEditor.tsx @@ -4,6 +4,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; @@ -47,13 +50,23 @@ export function RuleEditor({ accountId }: { accountId: string }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showForm, setShowForm] = useState(false); - const [name, setName] = useState(''); - const [priority, setPriority] = useState(1); const [conditions, setConditions] = useState([{ type: 'from_contains', value: '' }]); const [actions, setActions] = useState([{ type: 'mark_as_read', value: '' }]); const [saving, setSaving] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); + // ── Rule form (RHF + Zod) ── + const ruleSchema = z.object({ + name: z.string().min(1, 'required'), + priority: z.coerce.number().int().min(1, 'invalidNumber').default(1), + }); + type RuleFormData = z.infer; + + const { register: registerRule, handleSubmit: handleSubmitRule, reset: resetRule, formState: { errors: ruleErrors } } = useForm({ + resolver: zodResolver(ruleSchema), + defaultValues: { name: '', priority: 1 }, + }); + const load = useCallback(() => { setLoading(true); fetchRules() @@ -93,14 +106,13 @@ export function RuleEditor({ accountId }: { accountId: string }) { setActions((prev) => prev.filter((_, i) => i !== index)); }; - const handleSave = useCallback(async () => { - if (!name.trim()) return; + const handleSave = useCallback(async (data: RuleFormData) => { setSaving(true); try { const rule = await createRule({ - name, + name: data.name, account_id: accountId, - priority, + priority: data.priority, is_active: true, conditions, actions, @@ -108,8 +120,7 @@ export function RuleEditor({ accountId }: { accountId: string }) { setRules((prev) => [...prev, rule].sort((a, b) => a.priority - b.priority)); toast.success(t('mail.ruleCreated')); setShowForm(false); - setName(''); - setPriority(1); + resetRule({ name: '', priority: 1 }); setConditions([{ type: 'from_contains', value: '' }]); setActions([{ type: 'mark_as_read', value: '' }]); } catch (err) { @@ -117,7 +128,7 @@ export function RuleEditor({ accountId }: { accountId: string }) { } finally { setSaving(false); } - }, [name, accountId, priority, conditions, actions, toast, t]); + }, [accountId, conditions, actions, toast, t, resetRule]); const handleDelete = useCallback(async () => { if (!deleteTarget) return; @@ -156,19 +167,19 @@ export function RuleEditor({ accountId }: { accountId: string }) { {showForm && ( - + setName(e.target.value)} + {...registerRule('name')} + error={ruleErrors.name?.message === 'required' ? t('validation.required') : undefined} placeholder={t('mail.ruleName')} required /> setPriority(Number(e.target.value))} + {...registerRule('priority')} + error={ruleErrors.priority?.message === 'invalidNumber' ? t('validation.invalidNumber', 'Invalid number') : undefined} /> {/* Conditions */} @@ -226,10 +237,10 @@ export function RuleEditor({ accountId }: { accountId: string }) { - {t('common.save')} - setShowForm(false)}>{t('common.cancel')} + {t('common.save')} + setShowForm(false)}>{t('common.cancel')} - + )} diff --git a/frontend/src/components/mail/SignatureManager.tsx b/frontend/src/components/mail/SignatureManager.tsx index b3ae2b1..4beb214 100644 --- a/frontend/src/components/mail/SignatureManager.tsx +++ b/frontend/src/components/mail/SignatureManager.tsx @@ -5,6 +5,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Card } from '@/components/ui/Card'; @@ -41,12 +44,24 @@ export function SignatureManager() { const [error, setError] = useState(null); const [editing, setEditing] = useState(null); const [showForm, setShowForm] = useState(false); - const [name, setName] = useState(''); - const [bodyHtml, setBodyHtml] = useState(''); - const [isDefault, setIsDefault] = useState(false); const [saving, setSaving] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); + // ── Signature form (RHF + Zod) ── + const sigSchema = z.object({ + name: z.string().min(1, 'required'), + body_html: z.string().default(''), + is_default: z.boolean().default(false), + }); + type SigFormData = z.infer; + + const { register: registerSig, handleSubmit: handleSubmitSig, reset: resetSig, watch: watchSig, setValue: setSigValue, formState: { errors: sigErrors } } = useForm({ + resolver: zodResolver(sigSchema), + defaultValues: { name: '', body_html: '', is_default: false }, + }); + + const bodyHtmlValue = watchSig('body_html'); + const load = useCallback(() => { setLoading(true); fetchSignatures() @@ -64,30 +79,25 @@ export function SignatureManager() { const handleNew = () => { setEditing(null); - setName(''); - setBodyHtml(''); - setIsDefault(false); + resetSig({ name: '', body_html: '', is_default: false }); setShowForm(true); }; const handleEdit = (sig: MailSignature) => { setEditing(sig); - setName(sig.name); - setBodyHtml(sig.body_html); - setIsDefault(sig.is_default); + resetSig({ name: sig.name, body_html: sig.body_html, is_default: sig.is_default }); setShowForm(true); }; - const handleSave = useCallback(async () => { - if (!name.trim()) return; + const handleSave = useCallback(async (data: SigFormData) => { setSaving(true); try { if (editing) { - const updated = await updateSignature(editing.id, { name, body_html: bodyHtml, is_default: isDefault }); + const updated = await updateSignature(editing.id, { name: data.name, body_html: data.body_html, is_default: data.is_default }); setSignatures((prev) => prev.map((s) => (s.id === editing.id ? updated : s))); toast.success(t('mail.signatureUpdated')); } else { - const created = await createSignature({ name, body_html: bodyHtml, is_default: isDefault }); + const created = await createSignature({ name: data.name, body_html: data.body_html, is_default: data.is_default }); setSignatures((prev) => [...prev, created]); toast.success(t('mail.signatureCreated')); } @@ -97,7 +107,7 @@ export function SignatureManager() { } finally { setSaving(false); } - }, [editing, name, bodyHtml, isDefault, toast, t]); + }, [editing, toast, t]); const handleDelete = useCallback(async () => { if (!deleteTarget) return; @@ -136,11 +146,11 @@ export function SignatureManager() { {showForm && ( - + setName(e.target.value)} + {...registerSig('name')} + error={sigErrors.name?.message === 'required' ? t('validation.required') : undefined} placeholder={t('mail.signatureName')} required /> @@ -152,7 +162,7 @@ export function SignatureManager() { setBodyHtml((prev) => `${prev}${v.token}`)} + onClick={() => setSigValue('body_html', `${bodyHtmlValue}${v.token}`)} className="inline-flex items-center px-2 py-0.5 text-xs rounded border border-secondary-300 bg-secondary-50 hover:bg-secondary-100 text-secondary-700" title={v.description} > @@ -161,20 +171,20 @@ export function SignatureManager() { ))} setSigValue('body_html', html)} placeholder="Mit freundlichen Grüßen,{{user.name}}" /> - setIsDefault(e.target.checked)} className="rounded" /> + {t('mail.defaultSignature')} - {t('common.save')} - setShowForm(false)}>{t('common.cancel')} + {t('common.save')} + setShowForm(false)}>{t('common.cancel')} - + )} diff --git a/frontend/src/components/mail/VacationResponder.tsx b/frontend/src/components/mail/VacationResponder.tsx index 25e0e04..27f80b7 100644 --- a/frontend/src/components/mail/VacationResponder.tsx +++ b/frontend/src/components/mail/VacationResponder.tsx @@ -1,53 +1,90 @@ /** * Vacation responder — toggle + date range + auto-reply text. + * Form validation: React Hook Form + Zod. */ -import React, { useState, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Card } from '@/components/ui/Card'; import { useToast } from '@/components/ui/Toast'; import { configureVacation, type VacationPayload } from '@/api/mail'; +// ── Zod Schema ── +const vacationSchema = z.object({ + enabled: z.boolean().default(false), + start_date: z.string().optional().default(''), + end_date: z.string().optional().default(''), + subject: z.string().optional().default(''), + body: z.string().optional().default(''), +}).superRefine((data, ctx) => { + if (data.enabled && data.start_date && data.end_date) { + const start = new Date(data.start_date); + const end = new Date(data.end_date); + if (!Number.isNaN(start.getTime()) && !Number.isNaN(end.getTime())) { + if (end < start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'endBeforeStart', + path: ['end_date'], + }); + } + } + } +}); + +type VacationFormData = z.infer; + export function VacationResponder({ accountId }: { accountId: string }) { const { t } = useTranslation(); const toast = useToast(); - const [enabled, setEnabled] = useState(false); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); - const [subject, setSubject] = useState(''); - const [body, setBody] = useState(''); - const [saving, setSaving] = useState(false); - const handleSave = useCallback(async () => { - setSaving(true); + const { + register, + handleSubmit, + watch, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(vacationSchema), + defaultValues: { enabled: false, start_date: '', end_date: '', subject: '', body: '' }, + }); + + const enabled = watch('enabled'); + + const onSubmit = useCallback(async (data: VacationFormData) => { try { const payload: VacationPayload = { - enabled, - start_date: startDate || null, - end_date: endDate || null, - subject, - body, + enabled: data.enabled, + start_date: data.start_date || null, + end_date: data.end_date || null, + subject: data.subject, + body: data.body, }; await configureVacation(payload); toast.success(t('mail.vacationSaved')); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); - } finally { - setSaving(false); } - }, [enabled, startDate, endDate, subject, body, toast, t]); + }, [toast, t]); + + const errorMsg = (key: string | undefined) => { + if (!key) return undefined; + if (key === 'endBeforeStart') return t('calendar.endBeforeStart', 'End must be after start'); + return key; + }; return ( - + setEnabled(e.target.checked)} + {...register('enabled')} className="w-5 h-5 rounded border-secondary-300 text-primary-600 focus:ring-primary-500" data-testid="vacation-toggle" /> @@ -60,27 +97,24 @@ export function VacationResponder({ accountId }: { accountId: string }) { setStartDate(e.target.value)} + {...register('start_date')} /> setEndDate(e.target.value)} + {...register('end_date')} + error={errorMsg(errors.end_date?.message)} /> setSubject(e.target.value)} + {...register('subject')} placeholder={t('mail.vacationSubjectPlaceholder')} /> {t('mail.vacationBody')} setBody(e.target.value)} + {...register('body')} className="w-full min-h-32 border border-secondary-300 rounded-md p-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder={t('mail.vacationBodyPlaceholder')} data-testid="vacation-body" @@ -89,11 +123,11 @@ export function VacationResponder({ accountId }: { accountId: string }) { )} - + {t('common.save')} - + ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/MailSettings.tsx b/frontend/src/pages/MailSettings.tsx index 3f4d618..048d74b 100644 --- a/frontend/src/pages/MailSettings.tsx +++ b/frontend/src/pages/MailSettings.tsx @@ -5,6 +5,9 @@ import React, { useState, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; @@ -37,16 +40,24 @@ export function MailSettingsPage() { const [loading, setLoading] = useState(true); const [selectedAccountId, setSelectedAccountId] = useState(''); const [showAddAccount, setShowAddAccount] = useState(false); - const [newAccount, setNewAccount] = useState({ - email: '', - display_name: '', - imap_host: '', - imap_port: 993, - smtp_host: '', - smtp_port: 587, - username: '', - password: '', - is_shared: false, + + // ── Account create form (RHF + Zod) ── + const accountSchema = z.object({ + email: z.string().min(1, 'required').email('invalidEmail'), + display_name: z.string().optional().default(''), + username: z.string().optional().default(''), + imap_host: z.string().min(1, 'required'), + imap_port: z.coerce.number().int().min(1, 'invalidNumber').max(65535, 'invalidNumber').default(993), + smtp_host: z.string().min(1, 'required'), + smtp_port: z.coerce.number().int().min(1, 'invalidNumber').max(65535, 'invalidNumber').default(587), + password: z.string().min(1, 'required'), + is_shared: z.boolean().default(false), + }); + type AccountFormData = z.infer; + + const { register: registerAccount, handleSubmit: handleSubmitAccount, reset: resetAccount, formState: { errors: accountErrors, isSubmitting: accountSubmitting } } = useForm({ + resolver: zodResolver(accountSchema), + defaultValues: { email: '', display_name: '', username: '', imap_host: '', imap_port: 993, smtp_host: '', smtp_port: 587, password: '', is_shared: false }, }); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(null); @@ -75,31 +86,17 @@ export function MailSettingsPage() { useEffect(() => { loadAccounts(); }, [loadAccounts]); - const handleCreateAccount = useCallback(async () => { - if (!newAccount.email.trim() || !newAccount.password.trim()) return; - setSaving(true); + const handleCreateAccount = useCallback(async (data: AccountFormData) => { try { - const acc = await createAccount(newAccount); + const acc = await createAccount(data); setAccounts((prev) => [...prev, acc]); toast.success(t('mail.accountCreated')); setShowAddAccount(false); - setNewAccount({ - email: '', - display_name: '', - imap_host: '', - imap_port: 993, - smtp_host: '', - smtp_port: 587, - username: '', - password: '', - is_shared: false, - }); + resetAccount({ email: '', display_name: '', username: '', imap_host: '', imap_port: 993, smtp_host: '', smtp_port: 587, password: '', is_shared: false }); } catch (err: any) { toast.error(err?.message || err?.detail || 'Save failed'); - } finally { - setSaving(false); } - }, [newAccount, toast, t]); + }, [toast, t, resetAccount]); const handleTestConnection = useCallback(async (accountId: string) => { setTesting(accountId); @@ -218,75 +215,72 @@ export function MailSettingsPage() { {showAddAccount && ( - + setNewAccount((prev) => ({ ...prev, email: e.target.value }))} + {...registerAccount('email')} + error={accountErrors.email?.message === 'required' ? t('validation.required') : accountErrors.email?.message === 'invalidEmail' ? t('validation.email') : undefined} placeholder="user@example.com" required /> setNewAccount((prev) => ({ ...prev, display_name: e.target.value }))} + {...registerAccount('display_name')} placeholder="John Doe" /> setNewAccount((prev) => ({ ...prev, username: e.target.value }))} + {...registerAccount('username')} placeholder="Leer lassen für E-Mail-Adresse" /> setNewAccount((prev) => ({ ...prev, imap_host: e.target.value }))} + {...registerAccount('imap_host')} + error={accountErrors.imap_host?.message === 'required' ? t('validation.required') : undefined} placeholder="imap.example.com" /> setNewAccount((prev) => ({ ...prev, imap_port: Number(e.target.value) }))} + {...registerAccount('imap_port')} + error={accountErrors.imap_port?.message === 'invalidNumber' ? t('validation.invalidNumber', 'Invalid number') : undefined} /> setNewAccount((prev) => ({ ...prev, smtp_host: e.target.value }))} + {...registerAccount('smtp_host')} + error={accountErrors.smtp_host?.message === 'required' ? t('validation.required') : undefined} placeholder="smtp.example.com" /> setNewAccount((prev) => ({ ...prev, smtp_port: Number(e.target.value) }))} + {...registerAccount('smtp_port')} + error={accountErrors.smtp_port?.message === 'invalidNumber' ? t('validation.invalidNumber', 'Invalid number') : undefined} /> setNewAccount((prev) => ({ ...prev, password: e.target.value }))} + {...registerAccount('password')} + error={accountErrors.password?.message === 'required' ? t('validation.required') : undefined} required /> setNewAccount((prev) => ({ ...prev, is_shared: e.target.checked }))} + {...registerAccount('is_shared')} className="rounded" /> Geteiltes Postfach (für alle Tenant-Benutzer sichtbar) - {t('common.save')} - setShowAddAccount(false)}>{t('common.cancel')} + {t('common.save')} + setShowAddAccount(false)}>{t('common.cancel')} - + )}
Mit freundlichen Grüßen,{{user.name}}