revert(frontend): Frontend auf letzten funktionierenden Stand 5680179 zurückgesetzt — i18n-Massen-Batch brach Dashboard-Shell in Produktion; Render-Loop-Fix und DSGVO-Antrags-UI liegen sicher in Historie für kontrollierten Wiedereinspiel

This commit is contained in:
Agent Zero
2026-08-27 09:14:21 +02:00
parent bea479bfad
commit 66c11d3d64
119 changed files with 925 additions and 1477 deletions
@@ -303,7 +303,7 @@ export function ComposeModal({
label={t('mail.to')}
{...register('to')}
error={errorMsg(errors.to?.message)}
placeholder={t('composeModal.recipientexamplecom')}
placeholder="recipient@example.com"
required
data-testid="compose-to"
/>
@@ -322,13 +322,13 @@ export function ComposeModal({
label={t('mail.cc')}
{...register('cc')}
error={errorMsg(errors.cc?.message)}
placeholder={t('composeModal.ccexamplecom')}
placeholder="cc@example.com"
/>
<Input
label={t('mail.bcc')}
{...register('bcc')}
error={errorMsg(errors.bcc?.message)}
placeholder={t('composeModal.bccexamplecom')}
placeholder="bcc@example.com"
/>
</div>
)}
@@ -376,7 +376,7 @@ export function ComposeModal({
>
{t('mail.addAttachment')}
</Button>
<span className="text-xs text-secondary-400">{t('composeModal.max25mbperfile')}</span>
<span className="text-xs text-secondary-400">Max 25 MB per file</span>
</div>
{attachments.length > 0 && (
<ul className="mt-2 space-y-1">
@@ -304,7 +304,7 @@ export function MailComposeForm({
label={t('mail.to')}
{...register('to')}
error={errorMsg(errors.to?.message)}
placeholder={t('mailComposeForm.recipientexamplecom')}
placeholder="recipient@example.com"
required
data-testid="compose-to"
/>
@@ -323,13 +323,13 @@ export function MailComposeForm({
label={t('mail.cc')}
{...register('cc')}
error={errorMsg(errors.cc?.message)}
placeholder={t('mailComposeForm.ccexamplecom')}
placeholder="cc@example.com"
/>
<Input
label={t('mail.bcc')}
{...register('bcc')}
error={errorMsg(errors.bcc?.message)}
placeholder={t('mailComposeForm.bccexamplecom')}
placeholder="bcc@example.com"
/>
</div>
)}
@@ -377,7 +377,7 @@ export function MailComposeForm({
>
{t('mail.addAttachment')}
</Button>
<span className="text-xs text-secondary-400">{t('mailComposeForm.max25mbperfile')}</span>
<span className="text-xs text-secondary-400">Max 25 MB per file</span>
</div>
{attachments.length > 0 && (
<ul className="mt-2 space-y-1">
@@ -7,7 +7,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { Filter, Plus, X, Bookmark } from 'lucide-react';
import { useTranslation } from 'react-i18next';
// ─── Field definitions ───────────────────────────────────────────────────────
@@ -184,7 +183,6 @@ function newConditionId() {
}
export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], onSaveFilter, onLoadFilter, onDeleteFilter }: MailFilterPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -274,8 +272,8 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title={t('mailFilterPanel.filter')}
aria-label={t('mailFilterPanel.filter')}
title="Filter"
aria-label="Filter"
className={`
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
transition-colors duration-100 cursor-pointer relative
@@ -313,7 +311,7 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
<div className="flex items-center gap-2">
{activeCount > 0 && (
<button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">
{t('mailFilterPanel.allelöschen')}
Alle löschen
</button>
)}
<button onClick={() => setOpen(false)} className="p-1 rounded hover:bg-secondary-100 text-secondary-400 hover:text-secondary-600">
@@ -325,7 +323,7 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
{/* Logic toggle */}
{activeCount > 0 && (
<div className="flex items-center gap-2 px-4 py-2 border-b border-secondary-100">
<span className="text-xs text-secondary-500">{t('mailFilterPanel.bedingungenverknüpfen')}</span>
<span className="text-xs text-secondary-500">Bedingungen verknüpfen:</span>
<button onClick={toggleLogic} className={`px-2 py-0.5 text-xs font-medium rounded transition-colors ${filters.logic === 'AND' ? 'bg-primary-600 text-white' : 'bg-secondary-100 text-secondary-600 hover:bg-secondary-200'}`}>UND</button>
<button onClick={toggleLogic} className={`px-2 py-0.5 text-xs font-medium rounded transition-colors ${filters.logic === 'OR' ? 'bg-primary-600 text-white' : 'bg-secondary-100 text-secondary-600 hover:bg-secondary-200'}`}>ODER</button>
</div>
@@ -350,7 +348,7 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
{/* Saved filters */}
{(savedFilters?.length ?? 0) > 0 && (
<div className="px-4 py-2 border-b border-secondary-100">
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">{t('mailFilterPanel.gespeichertefilter')}</div>
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">Gespeicherte Filter</div>
<div className="space-y-0.5">
{savedFilters.map((sf) => (
<div key={sf.id} className="flex items-center gap-1 group">
@@ -371,7 +369,7 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
<div className="px-4 py-3 space-y-2">
{(filters.conditions?.length ?? 0) === 0 && (
<div className="text-center py-6 text-xs text-secondary-400">
{t('mailFilterPanel.keinefilteraktivklickeuntenum')}
Keine Filter aktiv. Klicke unten um eine Bedingung hinzuzufügen.
</div>
)}
{filters.conditions.map((cond, idx) => {
@@ -411,13 +409,13 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
{currentOp?.needsValue ? (
def?.type === 'select' ? (
<select value={cond.value} onChange={(e) => updateCondition(cond.id, { value: e.target.value })} className="px-2 py-1.5 text-xs border border-secondary-200 rounded bg-white cursor-pointer focus:outline-none focus:border-primary-400">
<option value="">{t('mailFilterPanel.wählen')}</option>
<option value=""> wählen </option>
{def.options?.map((opt) => (<option key={opt.value} value={opt.value}>{opt.label}</option>))}
</select>
) : def?.type === 'date' ? (
<input type="date" value={cond.value} onChange={(e) => updateCondition(cond.id, { value: e.target.value })} className="px-2 py-1.5 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400" />
) : (
<input type="text" value={cond.value} onChange={(e) => updateCondition(cond.id, { value: e.target.value })} placeholder={t('mailFilterPanel.wert')} className="w-24 px-2 py-1.5 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400" />
<input type="text" value={cond.value} onChange={(e) => updateCondition(cond.id, { value: e.target.value })} placeholder="Wert…" className="w-24 px-2 py-1.5 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400" />
)
) : (
<div className="w-24" />
@@ -436,12 +434,12 @@ export function MailFilterPanel({ filters, onFiltersChange, savedFilters = [], o
<div className="flex items-center gap-2">
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors">
<Plus className="w-3.5 h-3.5" />
{t('mailFilterPanel.bedingunghinzufügen')}
Bedingung hinzufügen
</button>
{activeCount > 0 && onSaveFilter && (
<button onClick={handleSaveFilter} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors">
<Bookmark className="w-3.5 h-3.5" />
{t('mailFilterPanel.filterspeichern')}
Filter speichern
</button>
)}
</div>
@@ -116,7 +116,6 @@ function ContextMenu({
onClose: () => void;
onEmptyFolder: (folderId: string, folderName: string) => void;
}) {
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -137,7 +136,7 @@ function ContextMenu({
onClick={() => { onEmptyFolder(state.folderId, state.folderName); onClose(); }}
className="w-full text-left px-3 py-1.5 text-sm text-red-600 hover:bg-red-50"
>
{t('mailFolderTree.ordnerleeren')}
Ordner leeren
</button>
</div>
);
@@ -7,7 +7,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { Group as GroupIcon, Plus, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type FieldType = 'text' | 'number' | 'date';
@@ -115,7 +114,6 @@ interface MailGroupPanelProps {
}
export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -170,8 +168,8 @@ export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProp
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title={t('mailGroupPanel.gruppierung')}
aria-label={t('mailGroupPanel.gruppierung')}
title="Gruppierung"
aria-label="Gruppierung"
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors duration-100 cursor-pointer relative ${open || activeCount > 0 ? 'bg-primary-100 text-primary-700' : 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'}`}
>
<GroupIcon className="w-3.5 h-3.5" strokeWidth={2} />
@@ -186,12 +184,12 @@ export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProp
<div className="flex items-center justify-between px-4 py-3 border-b border-secondary-100">
<span className="text-sm font-semibold text-secondary-800">Gruppierung</span>
<div className="flex items-center gap-2">
{activeCount > 0 && <button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">{t('mailGroupPanel.allelöschen')}</button>}
{activeCount > 0 && <button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">Alle löschen</button>}
<button onClick={() => setOpen(false)} className="p-1 rounded hover:bg-secondary-100 text-secondary-400 hover:text-secondary-600"><X className="w-3.5 h-3.5" /></button>
</div>
</div>
<div className="px-4 py-3 space-y-2">
{groupState.conditions.length === 0 && <div className="text-center py-6 text-xs text-secondary-400">{t('mailGroupPanel.keinegruppierungaktivklickeuntenum')}</div>}
{groupState.conditions.length === 0 && <div className="text-center py-6 text-xs text-secondary-400">Keine Gruppierung aktiv. Klicke unten um ein Feld hinzuzufügen.</div>}
{groupState.conditions.map((cond, idx) => (
<div key={cond.id} className="flex items-center gap-1.5">
<span className="text-[10px] font-bold text-secondary-400 w-6 text-center">{idx + 1}.</span>
@@ -205,7 +203,7 @@ export function MailGroupPanel({ groupState, onGroupChange }: MailGroupPanelProp
))}
</div>
<div className="flex items-center justify-between px-4 py-3 border-t border-secondary-100">
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"><Plus className="w-3.5 h-3.5" />{t('mailGroupPanel.feldhinzufügen')}</button>
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"><Plus className="w-3.5 h-3.5" />Feld hinzufügen</button>
<button onClick={() => setOpen(false)} className="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded transition-colors">Fertig</button>
</div>
</div>
@@ -7,7 +7,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { ArrowUpDown, Plus, X, ChevronUp, ChevronDown } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type FieldType = 'text' | 'number' | 'date';
@@ -87,7 +86,6 @@ interface MailSortPanelProps {
}
export function MailSortPanel({ sortState, onSortChange }: MailSortPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -146,8 +144,8 @@ export function MailSortPanel({ sortState, onSortChange }: MailSortPanelProps) {
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title={t('mailSortPanel.sortieren')}
aria-label={t('mailSortPanel.sortieren')}
title="Sortieren"
aria-label="Sortieren"
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors duration-100 cursor-pointer relative ${open || activeCount > 0 ? 'bg-primary-100 text-primary-700' : 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'}`}
>
<ArrowUpDown className="w-3.5 h-3.5" strokeWidth={2} />
@@ -162,12 +160,12 @@ export function MailSortPanel({ sortState, onSortChange }: MailSortPanelProps) {
<div className="flex items-center justify-between px-4 py-3 border-b border-secondary-100">
<span className="text-sm font-semibold text-secondary-800">Sortieren</span>
<div className="flex items-center gap-2">
{activeCount > 0 && <button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">{t('mailSortPanel.allelöschen')}</button>}
{activeCount > 0 && <button onClick={clearAll} className="text-xs text-secondary-500 hover:text-red-600 transition-colors">Alle löschen</button>}
<button onClick={() => setOpen(false)} className="p-1 rounded hover:bg-secondary-100 text-secondary-400 hover:text-secondary-600"><X className="w-3.5 h-3.5" /></button>
</div>
</div>
<div className="px-4 py-3 space-y-2">
{sortState.conditions.length === 0 && <div className="text-center py-6 text-xs text-secondary-400">{t('mailSortPanel.keinesortierungaktivklickeuntenum')}</div>}
{sortState.conditions.length === 0 && <div className="text-center py-6 text-xs text-secondary-400">Keine Sortierung aktiv. Klicke unten um ein Feld hinzuzufügen.</div>}
{sortState.conditions.map((cond, idx) => (
<div key={cond.id} className="flex items-center gap-1.5">
<span className="text-[10px] font-bold text-secondary-400 w-6 text-center">{idx + 1}.</span>
@@ -184,7 +182,7 @@ export function MailSortPanel({ sortState, onSortChange }: MailSortPanelProps) {
))}
</div>
<div className="flex items-center justify-between px-4 py-3 border-t border-secondary-100">
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"><Plus className="w-3.5 h-3.5" />{t('mailSortPanel.feldhinzufügen')}</button>
<button onClick={addCondition} className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"><Plus className="w-3.5 h-3.5" />Feld hinzufügen</button>
<button onClick={() => setOpen(false)} className="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded transition-colors">Fertig</button>
</div>
</div>
+5 -5
View File
@@ -108,7 +108,7 @@ export function PgpSettings() {
value={privateKey}
onChange={(e) => setPrivateKey(e.target.value)}
className="w-full min-h-32 border border-secondary-300 rounded-md p-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder={t('pgpSettings.beginpgpprivatekeyblock')}
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK-----"
data-testid="pgp-private-key"
/>
</div>
@@ -135,7 +135,7 @@ export function PgpSettings() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-secondary-900">{key.user_id}</p>
<p className="text-xs text-secondary-500">{t('pgpSettings.keyid')} {key.key_id}</p>
<p className="text-xs text-secondary-500">Key ID: {key.key_id}</p>
<p className="text-xs text-secondary-400">Fingerprint: {key.fingerprint}</p>
</div>
<span className="text-xs text-secondary-400">{key.is_private ? t('mail.privateKeyLabel') : t('mail.publicKey')}</span>
@@ -152,7 +152,7 @@ export function PgpSettings() {
label={t('mail.contactId')}
value={contactId}
onChange={(e) => setContactId(e.target.value)}
placeholder={t('pgpSettings.contactuuid')}
placeholder="contact-uuid"
/>
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('mail.publicKey')}</label>
@@ -160,7 +160,7 @@ export function PgpSettings() {
value={contactPublicKey}
onChange={(e) => setContactPublicKey(e.target.value)}
className="w-full min-h-24 border border-secondary-300 rounded-md p-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder={t('pgpSettings.beginpgppublickeyblock')}
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----"
data-testid="contact-public-key"
/>
</div>
@@ -175,7 +175,7 @@ export function PgpSettings() {
{contactKeys.map((ck) => (
<li key={ck.contact_id} className="p-3 rounded-md border border-secondary-200">
<p className="text-sm font-medium text-secondary-900">{ck.contact_name}</p>
<p className="text-xs text-secondary-500">{t('pgpSettings.keyid')} {ck.key_id}</p>
<p className="text-xs text-secondary-500">Key ID: {ck.key_id}</p>
<p className="text-xs text-secondary-400">Fingerprint: {ck.fingerprint}</p>
</li>
))}
@@ -195,13 +195,13 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
<div className="w-px h-6 bg-secondary-200 mx-1" />
{/* Color */}
<label className="p-1.5 rounded hover:bg-secondary-100 cursor-pointer" title={t('richTextEditor.textcolor')}>
<label className="p-1.5 rounded hover:bg-secondary-100 cursor-pointer" title="Text color">
<Heading className="w-4 h-4" strokeWidth={2} />
<input
type="color"
className="sr-only"
onChange={(e) => editor.chain().focus().setColor(e.target.value).run()}
title={t('richTextEditor.textcolor')}
title="Text color"
/>
</label>
@@ -175,7 +175,7 @@ export function SignatureManager() {
<RichTextEditor
content={bodyHtmlValue}
onChange={(html: string) => setSigValue('body_html', html)}
placeholder={t('signatureManager.pmitfreundlichengrüßenbruser')}
placeholder="<p>Mit freundlichen Grüßen,<br>{{user.name}}</p>"
/>
</div>
<label className="flex items-center gap-2 text-sm text-secondary-700">