Files
leocrm/frontend/src/pages/DmsTrash.tsx
T
Agent Zero abbe7a18fc fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner
- P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var
- P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup
- P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns
- P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs)
- P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import
- P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default
- P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed
- P2: 28 frontend TODOs (hardcoded constants, deprecated notification API)
- P3: dead code, duplicates, deprecated imports, private attr, __import__ inline
- P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n)
- ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix)
- F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String)
- Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
2026-08-16 01:17:18 +02:00

157 lines
4.7 KiB
TypeScript

// TODO: P3-F29 — Implement DMS trash endpoint
/**
* DMS trash page.
* Shows soft-deleted files with restore button per file.
*/
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { formatDateShort } from '@/utils/date';
import { Table, TableColumn } from '@/components/ui/Table';
import { EmptyState } from '@/components/ui/EmptyState';
import { Skeleton } from '@/components/ui/Skeleton';
import { useToast } from '@/components/ui/Toast';
import { restoreFile, type DmsFile } from '@/api/dms';
import { Trash2 } from 'lucide-react';
export function DmsTrashPage() {
const { t } = useTranslation();
const toast = useToast();
const [trashFiles, setTrashFiles] = useState<DmsFile[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [restoringId, setRestoringId] = useState<string | null>(null);
// In a real implementation we would fetch from a trash endpoint
// For now we use an empty state as the API doesn't have a dedicated trash list
useEffect(() => {
setLoading(true);
// The DMS API doesn't have a GET /trash endpoint in the spec
// We would need to filter deleted_at !== null from file listing
// Using empty list for now as trash listing is not in the API spec
setTrashFiles([]);
setLoading(false);
}, []);
const handleRestore = useCallback(async (fileId: string) => {
setRestoringId(fileId);
try {
await restoreFile(fileId);
toast.success(t('dms.restored'));
setTrashFiles((prev) => prev.filter((f) => f.id !== fileId));
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
toast.error(msg);
}
setRestoringId(null);
}, [toast, t]);
const columns: TableColumn<DmsFile>[] = [
{
key: 'name',
header: t('dms.name'),
sortable: true,
render: (file) => (
<span className="font-medium text-secondary-900">{file.name}</span>
),
},
{
key: 'size',
header: t('dms.fileSize'),
sortable: true,
accessor: (file) => file.size_bytes ?? file.size ?? 0,
render: (file) => {
const sz = file.size_bytes ?? file.size ?? 0;
if (sz < 1024) return `${sz} B`;
if (sz < 1024 * 1024) return `${(sz / 1024).toFixed(1)} KB`;
return `${(sz / (1024 * 1024)).toFixed(1)} MB`;
},
},
{
key: 'mime_type',
header: t('dms.fileType'),
render: (file) => file.mime_type.split('/')[1]?.toUpperCase() || '—',
},
{
key: 'deleted_at',
header: t('dms.fileModified'),
sortable: true,
accessor: (file) => file.deleted_at || '',
render: (file) => file.deleted_at
? (formatDateShort(file.deleted_at) || '—')
: '—',
},
{
key: 'actions',
header: t('dms.actions'),
render: (file) => (
<Button
variant="secondary"
size="sm"
onClick={() => handleRestore(file.id)}
isLoading={restoringId === file.id}
>
{t('dms.restore')}
</Button>
),
},
];
if (loading) {
return (
<div className="p-6 max-w-7xl mx-auto" data-testid="dms-trash-page">
<Skeleton className="h-8 w-64 mb-6" />
<Skeleton className="h-64" />
</div>
);
}
if (error) {
return (
<div className="p-6 max-w-7xl mx-auto" data-testid="dms-trash-page">
<EmptyState
title={t('dms.error')}
description={error}
action={<Button onClick={() => window.location.reload()}>{t('dms.cancel')}</Button>}
/>
</div>
);
}
return (
<div className="p-6 max-w-7xl mx-auto" data-testid="dms-trash-page">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Link to="/dms" className="text-sm text-primary-600 hover:text-primary-700">
{t('dms.title')}
</Link>
<h1 className="text-2xl font-bold text-secondary-900">{t('dms.trash')}</h1>
</div>
</div>
{trashFiles.length === 0 ? (
<Card>
<EmptyState
title={t('dms.trashEmpty')}
icon={
<Trash2 className="w-12 h-12" aria-hidden="true" strokeWidth={1.5} />
}
/>
</Card>
) : (
<Card data-testid="trash-file-table">
<Table
columns={columns}
data={trashFiles}
rowKey={(file) => file.id}
emptyMessage={t('dms.trashEmpty')}
/>
</Card>
)}
</div>
);
}