Phase 0.2: migrate remaining SVGs to lucide-react icons
This commit is contained in:
@@ -8,6 +8,7 @@ import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFile } from '@/api/dms';
|
||||
import { getFileIcon, formatFileSize } from './FileExplorer';
|
||||
import { Download, Eye, FileText, Share2, Trash2, X } from 'lucide-react';
|
||||
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '-';
|
||||
@@ -37,9 +38,7 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
if (!file) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-6 text-center" data-testid="file-details-empty">
|
||||
<svg className="w-12 h-12 text-secondary-300 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<FileText className="w-12 h-12 text-secondary-300 mb-3" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="text-sm font-medium text-secondary-600">{t('dms.noFileSelected')}</p>
|
||||
<p className="mt-1 text-xs text-secondary-400">{t('dms.noFileSelectedHint')}</p>
|
||||
</div>
|
||||
@@ -60,9 +59,7 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
className="p-1 rounded text-secondary-400 hover:text-secondary-600 hover:bg-secondary-100 min-h-touch min-w-touch"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -82,9 +79,7 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<svg className={clsx('w-24 h-24', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx('w-24 h-24', icon.color)} aria-hidden="true" strokeWidth={1} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -97,19 +92,14 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
onClick={() => onPreview(file)}
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-primary-50 text-primary-700 hover:bg-primary-100 min-h-touch"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('dms.preview')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onShare(file)}
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-secondary-50 text-secondary-700 hover:bg-secondary-100 min-h-touch"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.7 10.7l6.6-3.4M8.7 13.3l6.6 3.4M18 12a3 3 0 11-6 0 3 3 0 016 0zM9 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('dms.share')}
|
||||
</button>
|
||||
<a
|
||||
@@ -117,18 +107,14 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
download={file.name}
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-secondary-50 text-secondary-700 hover:bg-secondary-100 min-h-touch"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<Download className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('dms.download')}
|
||||
</a>
|
||||
<button
|
||||
onClick={() => onDelete(file)}
|
||||
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-danger-50 text-danger-700 hover:bg-danger-100 min-h-touch"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('dms.delete')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,27 +7,28 @@ import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFile } from '@/api/dms';
|
||||
import { Archive, ChevronDown, ChevronUp, Eye, FileText, Image, Loader2, Share2, Trash2 } from 'lucide-react';
|
||||
|
||||
export function getFileIcon(mimeType: string): { path: string; color: string } {
|
||||
export function getFileIcon(mimeType: string): { icon: React.ElementType; color: string } {
|
||||
if (mimeType === 'application/pdf') {
|
||||
return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-danger-500' };
|
||||
return { icon: FileText, color: 'text-danger-500' };
|
||||
}
|
||||
if (mimeType.startsWith('image/')) {
|
||||
return { path: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-accent-500' };
|
||||
return { icon: Image, color: 'text-accent-500' };
|
||||
}
|
||||
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) {
|
||||
return { path: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-success-500' };
|
||||
return { icon: FileText, color: 'text-success-500' };
|
||||
}
|
||||
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) {
|
||||
return { path: 'M7 8h10M7 16h10M7 12h6m-7 8h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-warning-500' };
|
||||
return { icon: FileText, color: 'text-warning-500' };
|
||||
}
|
||||
if (mimeType.startsWith('text/') || mimeType.includes('document') || mimeType.includes('word')) {
|
||||
return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-primary-500' };
|
||||
return { icon: FileText, color: 'text-primary-500' };
|
||||
}
|
||||
if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) {
|
||||
return { path: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4', color: 'text-secondary-500' };
|
||||
return { icon: Archive, color: 'text-secondary-500' };
|
||||
}
|
||||
return { path: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z', color: 'text-secondary-400' };
|
||||
return { icon: FileText, color: 'text-secondary-400' };
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number): string {
|
||||
@@ -101,9 +102,7 @@ function SortHeader({
|
||||
>
|
||||
{label}
|
||||
{isActive && (
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={sortOrder === 'asc' ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7'} />
|
||||
</svg>
|
||||
sortOrder === 'asc' ? <ChevronUp className="w-3 h-3" aria-hidden="true" strokeWidth={2} /> : <ChevronDown className="w-3 h-3" aria-hidden="true" strokeWidth={2} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
@@ -130,10 +129,7 @@ function ActionButtons({
|
||||
aria-label={t('dms.preview')}
|
||||
title={t('dms.preview')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFileShare(file); }}
|
||||
@@ -141,9 +137,7 @@ function ActionButtons({
|
||||
aria-label={t('dms.share')}
|
||||
title={t('dms.share')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.7 10.7l6.6-3.4M8.7 13.3l6.6 3.4M18 12a3 3 0 11-6 0 3 3 0 016 0zM9 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFileDelete(file); }}
|
||||
@@ -151,9 +145,7 @@ function ActionButtons({
|
||||
aria-label={t('dms.delete')}
|
||||
title={t('dms.delete')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -198,10 +190,7 @@ export function FileExplorer({
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12" data-testid="file-explorer-loading">
|
||||
<svg className="animate-spin h-6 w-6 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-6 w-6 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-secondary-500 text-sm">{t('dms.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -210,9 +199,7 @@ export function FileExplorer({
|
||||
if (files.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12" data-testid="file-explorer-empty">
|
||||
<svg className="mx-auto h-12 w-12 text-secondary-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="mt-2 text-sm text-secondary-500">{t('dms.noFiles')}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -254,9 +241,7 @@ export function FileExplorer({
|
||||
className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500 flex-shrink-0"
|
||||
aria-label={t('dms.bulkSelect')}
|
||||
/>
|
||||
<svg className={clsx('w-5 h-5 flex-shrink-0', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx('w-5 h-5 flex-shrink-0', icon.color)} aria-hidden="true" strokeWidth={1.5} />
|
||||
<span className="text-sm font-medium text-secondary-900 truncate flex-1" title={file.name}>{file.name}</span>
|
||||
<span className="text-xs text-secondary-500 flex-shrink-0">{formatFileSize(getFileSize(file))}</span>
|
||||
<span className="text-xs text-secondary-400 flex-shrink-0 hidden sm:inline">{formatDate(file.updated_at || file.created_at)}</span>
|
||||
@@ -327,9 +312,7 @@ export function FileExplorer({
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className={clsx('w-5 h-5 flex-shrink-0', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx('w-5 h-5 flex-shrink-0', icon.color)} aria-hidden="true" strokeWidth={1.5} />
|
||||
<span className="font-medium text-secondary-900 truncate" title={file.name}>{file.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -415,9 +398,7 @@ export function FileExplorer({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<svg className={clsx(iconSize, icon.color, isSmall ? 'mb-0.5' : 'mb-1')} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx(iconSize, icon.color, isSmall ? 'mb-0.5' : 'mb-1')} aria-hidden="true" strokeWidth={1.5} />
|
||||
)}
|
||||
<p className={clsx('font-medium text-secondary-900 truncate w-full', isSmall ? 'text-[10px]' : 'text-xs')} title={file.name}>{file.name}</p>
|
||||
{!isSmall && (
|
||||
|
||||
@@ -7,35 +7,9 @@ import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFile } from '@/api/dms';
|
||||
import { Eye, FileText, Share2, Trash2 } from 'lucide-react';
|
||||
|
||||
function getFileIcon(mimeType: string): { path: string; color: string } {
|
||||
if (mimeType === 'application/pdf') {
|
||||
return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-danger-500' };
|
||||
}
|
||||
if (mimeType.startsWith('image/')) {
|
||||
return { path: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-accent-500' };
|
||||
}
|
||||
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) {
|
||||
return { path: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-success-500' };
|
||||
}
|
||||
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) {
|
||||
return { path: 'M7 8h10M7 16h10M7 12h6m-7 8h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-warning-500' };
|
||||
}
|
||||
if (mimeType.startsWith('text/') || mimeType.includes('document') || mimeType.includes('word')) {
|
||||
return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-primary-500' };
|
||||
}
|
||||
if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) {
|
||||
return { path: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4', color: 'text-secondary-500' };
|
||||
}
|
||||
return { path: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z', color: 'text-secondary-400' };
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
import { getFileIcon, formatFileSize } from "./FileExplorer";
|
||||
|
||||
export interface FileGridProps {
|
||||
files: DmsFile[];
|
||||
@@ -81,9 +55,7 @@ export function FileGrid({
|
||||
if (files.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12" data-testid="file-grid-empty">
|
||||
<svg className="mx-auto h-12 w-12 text-secondary-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="mt-2 text-sm text-secondary-500">{t('dms.noFiles')}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -114,9 +86,7 @@ export function FileGrid({
|
||||
className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
|
||||
aria-label={t('dms.bulkSelect')}
|
||||
/>
|
||||
<svg className={clsx('w-10 h-10', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx('w-10 h-10', icon.color)} aria-hidden="true" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm font-medium text-secondary-900 truncate" title={file.name}>{file.name}</p>
|
||||
@@ -131,10 +101,7 @@ export function FileGrid({
|
||||
aria-label={t('dms.preview')}
|
||||
title={t('dms.preview')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
<Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFileShare(file); }}
|
||||
@@ -142,9 +109,7 @@ export function FileGrid({
|
||||
aria-label={t('dms.share')}
|
||||
title={t('dms.share')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.7 10.7l6.6-3.4M8.7 13.3l6.6 3.4M18 12a3 3 0 11-6 0 3 3 0 016 0zM9 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFileDelete(file); }}
|
||||
@@ -152,9 +117,7 @@ export function FileGrid({
|
||||
aria-label={t('dms.delete')}
|
||||
title={t('dms.delete')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Modal } from '@/components/ui/Modal';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { getFilePreviewUrl, type DmsFile } from '@/api/dms';
|
||||
import { FileText } from 'lucide-react';
|
||||
|
||||
export interface FilePreviewModalProps {
|
||||
open: boolean;
|
||||
@@ -81,9 +82,7 @@ export function FilePreviewModal({ open, file, onClose }: FilePreviewModalProps)
|
||||
|
||||
{!isPdf && !isImage && (
|
||||
<div className="text-center py-12" data-testid="no-preview-available">
|
||||
<svg className="mx-auto h-12 w-12 text-secondary-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="mt-2 text-sm text-secondary-500">{t('dms.preview')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFolder } from '@/api/dms';
|
||||
import { ChevronDown, Folder, Menu } from 'lucide-react';
|
||||
|
||||
interface FolderTreeItemProps {
|
||||
folder: DmsFolder;
|
||||
@@ -52,15 +53,11 @@ function FolderTreeItem({ folder, level, selectedFolderId, onSelect }: FolderTre
|
||||
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
|
||||
aria-label={expanded ? t('dms.folders') : t('dms.folders')}
|
||||
>
|
||||
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
||||
</svg>
|
||||
<ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
{!hasChildren && <span className="w-4 flex-shrink-0" aria-hidden="true" />}
|
||||
<svg className="w-4 h-4 text-warning-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<Folder className="w-4 h-4 text-warning-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="truncate">{folder.name}</span>
|
||||
{typeof folder.file_count === 'number' && folder.file_count > 0 && (
|
||||
<span className="ml-auto text-xs text-secondary-400">{folder.file_count}</span>
|
||||
@@ -125,9 +122,7 @@ export function FolderTree({ folders, selectedFolderId, onSelect, loading = fals
|
||||
role="button"
|
||||
aria-label={t('dms.allFiles')}
|
||||
>
|
||||
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<Menu className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
<span>{t('dms.allFiles')}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -12,6 +12,7 @@ import React, { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFolder, DmsFile } from '@/api/dms';
|
||||
import { ChevronDown, FileText, Folder, Home, Menu, Users } from 'lucide-react';
|
||||
|
||||
interface SourceTreeFolderItemProps {
|
||||
folder: DmsFolder;
|
||||
@@ -109,15 +110,11 @@ function SourceTreeFolderItem({
|
||||
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
|
||||
aria-label={expanded ? t('dms.folders') : t('dms.folders')}
|
||||
>
|
||||
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
||||
</svg>
|
||||
<ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
{!hasChildren && <span className="w-4 flex-shrink-0" aria-hidden="true" />}
|
||||
<svg className={clsx('w-4 h-4 flex-shrink-0', isDropTarget ? 'text-primary-500' : 'text-warning-500')} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<Folder className={clsx('w-4 h-4 flex-shrink-0', isDropTarget ? 'text-primary-500' : 'text-warning-500')} aria-hidden="true" strokeWidth={2} />
|
||||
<span className="truncate">{folder.name}</span>
|
||||
{typeof folder.file_count === 'number' && folder.file_count > 0 && (
|
||||
<span className="ml-auto text-xs text-secondary-400">{folder.file_count}</span>
|
||||
@@ -187,9 +184,7 @@ function SourceSection({ title, icon, selected, onClick, children, defaultExpand
|
||||
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
|
||||
aria-label={expanded ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
||||
</svg>
|
||||
<ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
{icon}
|
||||
<span className="truncate">{title}</span>
|
||||
@@ -298,9 +293,7 @@ export function SourceTree({
|
||||
role="button"
|
||||
aria-label={t('dms.allFiles')}
|
||||
>
|
||||
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<Menu className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
<span>{t('dms.allFiles')}</span>
|
||||
</div>
|
||||
|
||||
@@ -315,9 +308,7 @@ export function SourceTree({
|
||||
}
|
||||
}}
|
||||
icon={
|
||||
<svg className="w-4 h-4 text-primary-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<Folder className="w-4 h-4 text-primary-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
}
|
||||
>
|
||||
{folders.length === 0 ? (
|
||||
@@ -346,9 +337,7 @@ export function SourceTree({
|
||||
onSelectFolder(null);
|
||||
}}
|
||||
icon={
|
||||
<svg className="w-4 h-4 text-accent-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
<Users className="w-4 h-4 text-accent-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
}
|
||||
>
|
||||
{sharedFiles.length === 0 ? (
|
||||
@@ -367,9 +356,7 @@ export function SourceTree({
|
||||
role="button"
|
||||
aria-label={file.name}
|
||||
>
|
||||
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<FileText className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="truncate">{file.name}</span>
|
||||
</div>
|
||||
</li>
|
||||
@@ -386,9 +373,7 @@ export function SourceTree({
|
||||
onSelectFolder(null);
|
||||
}}
|
||||
icon={
|
||||
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12H3l9-9 9 9h-2M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7M9 21v-6h6v6" />
|
||||
</svg>
|
||||
<Home className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
}
|
||||
defaultExpanded={false}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { uploadFile } from '@/api/dms';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { Upload } from 'lucide-react';
|
||||
|
||||
export interface UploadDropzoneProps {
|
||||
folderId: string | null;
|
||||
@@ -124,9 +125,7 @@ export function UploadDropzone({ folderId, onUploaded }: UploadDropzoneProps) {
|
||||
className="hidden"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<svg className="mx-auto h-10 w-10 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<Upload className="mx-auto h-10 w-10 text-secondary-400" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="mt-2 text-sm text-secondary-600">{t('dms.uploadDropHere')}</p>
|
||||
</div>
|
||||
{uploads.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user