Files
leocrm/frontend/src/pages/DmsTrash.tsx
T
Agent Zero e9b8936091 fix: prevent [object Object] rendering in 25 frontend components
Replace direct {error} JSX rendering with typeof check + .message fallback.
When error is an object (not a string), React showed [object Object].
Now renders error.message or fallback string.
2026-08-21 11:36:55 +02:00

157 lines
4.8 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={typeof error === "string" ? error : (error as any)?.message || "Ein Fehler ist aufgetreten"}
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>
);
}