fix: файлы в поле отображаются горизонтально с переносом по ширине

- FileFieldRow: список файлов в одну строку, перенос на следующую только при нехватке места
- 'Добавить файл' идёт в той же строке после файлов
- Добавлена лёгкая подложка bg-muted/40 вокруг каждого файла для визуального разделения
This commit is contained in:
2026-07-14 16:22:03 +03:00
parent 507c36f7fa
commit 751fd79727

View File

@@ -533,72 +533,74 @@ function FileFieldRow({
<Upload className="w-3.5 h-3.5" /> <Upload className="w-3.5 h-3.5" />
</button> </button>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0 flex flex-wrap items-center gap-x-2 gap-y-0.5">
<strong>{field.name}:</strong>{' '} <strong>{field.name}:</strong>{' '}
{field.maxFileCount != null && ( {field.maxFileCount != null && (
<span className="text-xs text-muted-foreground ml-1">({files.length}/{field.maxFileCount})</span> <span className="text-xs text-muted-foreground">({files.length}/{field.maxFileCount})</span>
)} )}
{isUploading ? ( {isUploading ? (
<span className="text-muted-foreground text-sm">Загрузка...</span> <span className="text-muted-foreground text-sm">Загрузка...</span>
) : files.length > 0 ? ( ) : files.length > 0 ? (
<div className="flex flex-col gap-0.5 mt-0.5"> <>
{files.map((fileInfo, idx) => { <div className="inline-flex flex-wrap items-center gap-x-2 gap-y-0.5">
const vType = detectViewerType(fileInfo.url, fileInfo.name); {files.map((fileInfo, idx) => {
const canPreview = vType === 'pdf' || vType === 'word' || vType === 'excel' || vType === 'powerpoint' || vType === 'video' || vType === 'audio' || vType === 'image'; const vType = detectViewerType(fileInfo.url, fileInfo.name);
return ( const canPreview = vType === 'pdf' || vType === 'word' || vType === 'excel' || vType === 'powerpoint' || vType === 'video' || vType === 'audio' || vType === 'image';
<span key={idx} className="inline-flex items-center gap-1 group/file"> return (
{canPreview ? ( <span key={idx} className="inline-flex items-center gap-1 group/file bg-muted/40 hover:bg-muted/60 rounded px-1.5 py-0.5">
{canPreview ? (
<button
type="button"
onClick={() => setViewerFile({ url: fileInfo.url, name: fileInfo.name })}
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
data-testid={`file-link-${field.code}-${idx}`}
>
<FileText className="w-3 h-3" />
{fileInfo.name}
{fileInfo.size !== undefined && (
<span className="text-muted-foreground text-xs">({formatFileSize(fileInfo.size)})</span>
)}
</button>
) : (
<a
href={addFileToken(fileInfo.url)}
onClick={(e) => { e.preventDefault(); openFilePresigned(fileInfo.url); }}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
data-testid={`file-link-${field.code}-${idx}`}
>
<FileText className="w-3 h-3" />
{fileInfo.name}
{fileInfo.size !== undefined && (
<span className="text-muted-foreground text-xs">({formatFileSize(fileInfo.size)})</span>
)}
<Download className="w-3 h-3 text-muted-foreground" />
</a>
)}
<button <button
type="button" onClick={() => handleRemoveFile(idx)}
onClick={() => setViewerFile({ url: fileInfo.url, name: fileInfo.name })} className="opacity-0 group-hover/file:opacity-100 transition-opacity text-muted-foreground hover:text-destructive"
className="inline-flex items-center gap-1 text-sm text-primary hover:underline" title="Удалить файл"
data-testid={`file-link-${field.code}-${idx}`} data-testid={`file-clear-${field.code}-${idx}`}
> >
<FileText className="w-3 h-3" /> <X className="w-3 h-3" />
{fileInfo.name}
{fileInfo.size !== undefined && (
<span className="text-muted-foreground text-xs ml-1">({formatFileSize(fileInfo.size)})</span>
)}
</button> </button>
) : ( </span>
<a );
href={addFileToken(fileInfo.url)} })}
onClick={(e) => { e.preventDefault(); openFilePresigned(fileInfo.url); }} </div>
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
data-testid={`file-link-${field.code}-${idx}`}
>
<FileText className="w-3 h-3" />
{fileInfo.name}
{fileInfo.size !== undefined && (
<span className="text-muted-foreground text-xs ml-1">({formatFileSize(fileInfo.size)})</span>
)}
<Download className="w-3 h-3 text-muted-foreground" />
</a>
)}
<button
onClick={() => handleRemoveFile(idx)}
className="opacity-0 group-hover/file:opacity-100 transition-opacity ml-1 text-muted-foreground hover:text-destructive"
title="Удалить файл"
data-testid={`file-clear-${field.code}-${idx}`}
>
<X className="w-3 h-3" />
</button>
</span>
);
})}
{(field.maxFileCount == null || files.length < field.maxFileCount) && ( {(field.maxFileCount == null || files.length < field.maxFileCount) && (
<button <button
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={isUploading} disabled={isUploading}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-0.5 w-fit disabled:opacity-30" className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground disabled:opacity-30"
> >
<Upload className="w-3 h-3" /> <Upload className="w-3 h-3" />
Добавить файл Добавить файл
</button> </button>
)} )}
</div> </>
) : ( ) : (
<span className="text-muted-foreground text-sm" data-testid={`file-empty-${field.code}`}>Нет файла</span> <span className="text-muted-foreground text-sm" data-testid={`file-empty-${field.code}`}>Нет файла</span>
)} )}