import { useState, useRef, useCallback } from "react"; import { Popover, ActionIcon, Text, UnstyledButton } from "@mantine/core"; import { IconPaperclip, IconUpload, IconFile, IconX, } from "@tabler/icons-react"; import { IBaseProperty } from "@/ee/base/types/base.types"; import cellClasses from "@/ee/base/styles/cells.module.css"; import { uploadFile } from "@/features/page/services/page-service"; import { getFileUrl } from "@/lib/config"; export type FileValue = { id: string; fileName: string; mimeType?: string; fileSize?: number; url?: string; }; function buildFileUrl(file: Pick): string { return file.url ?? `/api/files/${file.id}/${encodeURIComponent(file.fileName)}`; } type CellFileProps = { value: unknown; property: IBaseProperty; rowId: string; isEditing: boolean; readOnly?: boolean; onCommit: (value: unknown) => void; onCancel: () => void; }; function formatFileSize(bytes?: number): string { if (!bytes) return ""; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function parseFiles(value: unknown): FileValue[] { if (!Array.isArray(value)) return []; return value.filter( (f): f is FileValue => f && typeof f === "object" && "id" in f && "fileName" in f, ); } export function CellFile({ value, property, isEditing, readOnly, onCommit, onCancel, }: CellFileProps) { const files = parseFiles(value); const fileInputRef = useRef(null); const [uploading, setUploading] = useState(false); const handleRemove = useCallback( (fileId: string) => { if (readOnly) return; const updated = files.filter((f) => f.id !== fileId); onCommit(updated.length > 0 ? updated : null); }, [readOnly, files, onCommit], ); const handleUpload = useCallback( async (fileList: FileList | null) => { if (!fileList || fileList.length === 0) return; setUploading(true); const newFiles: FileValue[] = [...files]; // Reuse the page-attachment upload pipeline: the base's pageId is passed // to the standard /files/upload endpoint, which enforces the same edit // access check as any other page attachment. for (const file of Array.from(fileList)) { try { const attachment = await uploadFile(file, property.pageId); newFiles.push({ id: attachment.id, fileName: attachment.fileName, mimeType: attachment.mimeType, fileSize: attachment.fileSize, url: `/api/files/${attachment.id}/${encodeURIComponent(attachment.fileName)}`, }); } catch (err) { console.error("File upload failed:", err); } } setUploading(false); onCommit(newFiles.length > 0 ? newFiles : null); }, [files, property.pageId, onCommit], ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); onCancel(); } }, [onCancel], ); const MAX_VISIBLE = 2; if (isEditing) { return ( { if (!o) onCancel(); }} onClose={onCancel} position="bottom-start" width={280} trapFocus closeOnClickOutside closeOnEscape hideDetached={false} >
{!readOnly && files.length === 0 && !uploading && ( No files attached )} {files.map((file) => (
{file.fileName} {file.fileSize != null && ( {formatFileSize(file.fileSize)} )} {!readOnly && ( handleRemove(file.id)} > )}
))} {!readOnly && ( <> { handleUpload(e.target.files); e.target.value = ""; }} /> fileInputRef.current?.click()} disabled={uploading} className={cellClasses.fileUploadBtn} style={{ color: uploading ? "var(--mantine-color-gray-5)" : "var(--mantine-color-blue-6)", }} > {uploading ? "Uploading..." : "Add file"} )}
); } if (files.length === 0) { return ; } return ; } function FileList({ files, maxVisible, }: { files: FileValue[]; maxVisible: number; }) { const visible = files.slice(0, maxVisible); const overflow = files.length - maxVisible; return (
{visible.map((file) => ( {file.fileName} ))} {overflow > 0 && ( +{overflow} )}
); }