From e5a5529e210c564939cc26e68776123117e2f152 Mon Sep 17 00:00:00 2001 From: Krao Hasanee Date: Wed, 13 May 2026 13:08:46 -0400 Subject: [PATCH] Move New Folder inline next to Sync Folders button Clicking '+ New Folder' expands an inline input in the action bar. Removed the separate form below the toolbar. Co-Authored-By: Claude Sonnet 4.6 --- src/pages/team/FileSharing.jsx | 368 +++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 src/pages/team/FileSharing.jsx diff --git a/src/pages/team/FileSharing.jsx b/src/pages/team/FileSharing.jsx new file mode 100644 index 0000000..3843246 --- /dev/null +++ b/src/pages/team/FileSharing.jsx @@ -0,0 +1,368 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import Layout from '../../components/Layout'; +import LoadingButton from '../../components/LoadingButton'; +import { supabase } from '../../lib/supabase'; +import { useAuth } from '../../context/AuthContext'; +import { syncSeafileFolders } from '../../lib/seafileFolders'; + +function formatBytes(bytes) { + const value = Number(bytes || 0); + if (!value) return '—'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1); + return `${(value / (1024 ** index)).toFixed(index === 0 ? 0 : 1)} ${units[index]}`; +} + +function formatDate(timestamp) { + if (!timestamp) return '—'; + const millis = Number(timestamp) * 1000; + if (!Number.isFinite(millis)) return '—'; + return new Date(millis).toLocaleDateString(); +} + +function pathParts(path) { + return String(path || '/').split('/').filter(Boolean); +} + +function pathTo(index, parts) { + return `/${parts.slice(0, index + 1).join('/')}`; +} + +export default function FileSharing() { + const { currentUser } = useAuth(); + const [currentPath, setCurrentPath] = useState('/'); + const [entries, setEntries] = useState([]); + const [usageBytes, setUsageBytes] = useState(0); + const [configured, setConfigured] = useState(true); + const [parentPath, setParentPath] = useState('/'); + const [loading, setLoading] = useState(true); + const [working, setWorking] = useState(''); + const [dragging, setDragging] = useState(false); + const [error, setError] = useState(''); + const [folderName, setFolderName] = useState(''); + const [showFolderInput, setShowFolderInput] = useState(false); + const fileInputRef = useRef(null); + + const breadcrumbs = useMemo(() => pathParts(currentPath), [currentPath]); + + const apiFetch = async (url, options = {}) => { + const { data: { session } } = await supabase.auth.getSession(); + if (!session?.access_token) throw new Error('Your session expired. Please sign in again.'); + + const response = await fetch(url, { + ...options, + headers: { + Authorization: `Bearer ${session.access_token}`, + ...(options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }), + ...(options.headers || {}), + }, + }); + + const data = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(data.error || 'File sharing request failed.'); + return data; + }; + + const loadFiles = async (path = currentPath, options = {}) => { + const { invalidateUsage = false } = options; + setLoading(true); + setError(''); + try { + const params = new URLSearchParams({ + action: 'list', + path, + }); + if (invalidateUsage) params.set('invalidateUsage', '1'); + + const data = await apiFetch(`/api/seafile?${params.toString()}`); + setConfigured(data.configured !== false); + setEntries(data.entries || []); + setUsageBytes(Number(data.usageBytes || 0)); + setCurrentPath(data.path || '/'); + setParentPath(data.parentPath || '/'); + if (data.configured === false) setError(data.error || 'Seafile is not configured yet.'); + } catch (err) { + setError(err.message); + setUsageBytes(0); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadFiles('/'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSyncFolders = async () => { + setWorking('sync'); + setError(''); + try { + await syncSeafileFolders(); + await loadFiles(currentPath, { invalidateUsage: true }); + } catch (err) { + setError(err.message); + } finally { + setWorking(''); + } + }; + + const openFolder = (entry) => { + if (entry.type === 'dir') loadFiles(entry.path); + }; + + const downloadFile = async (entry) => { + setWorking(`download:${entry.path}`); + setError(''); + try { + const data = await apiFetch(`/api/seafile?action=download&path=${encodeURIComponent(entry.path)}`); + if (data.url) window.open(data.url, '_blank'); + } catch (err) { + setError(err.message); + } finally { + setWorking(''); + } + }; + + const deleteEntry = async (entry) => { + const kind = entry.type === 'dir' ? 'folder' : 'file'; + if (!window.confirm(`Delete "${entry.name}" ${kind}? This cannot be undone.`)) return; + + setWorking(`delete:${entry.path}`); + setError(''); + try { + await apiFetch(`/api/seafile?action=delete&path=${encodeURIComponent(entry.path)}&type=${encodeURIComponent(entry.type)}`, { + method: 'DELETE', + }); + await loadFiles(currentPath); + } catch (err) { + setError(err.message); + } finally { + setWorking(''); + } + }; + + const createFolder = async (e) => { + e.preventDefault(); + if (!folderName.trim()) return; + + setWorking('mkdir'); + setError(''); + try { + await apiFetch('/api/seafile?action=mkdir', { + method: 'POST', + body: JSON.stringify({ path: currentPath, name: folderName }), + }); + setFolderName(''); + setShowFolderInput(false); + await loadFiles(currentPath, { invalidateUsage: true }); + } catch (err) { + setError(err.message); + } finally { + setWorking(''); + } + }; + + const uploadFiles = async (files) => { + const selected = Array.from(files || []); + if (!selected.length) return; + + setWorking('upload'); + setError(''); + try { + const data = await apiFetch('/api/seafile?action=upload-link', { + method: 'POST', + body: JSON.stringify({ path: currentPath }), + }); + + if (!data.uploadLink || !data.parentDir) throw new Error('Seafile did not return an upload link.'); + + const formData = new FormData(); + selected.forEach(file => formData.append('file', file)); + formData.append('parent_dir', data.parentDir); + formData.append('replace', '0'); + + const uploadResponse = await fetch(`${data.uploadLink}${data.uploadLink.includes('?') ? '&' : '?'}ret-json=1`, { + method: 'POST', + body: formData, + }); + + if (!uploadResponse.ok) { + const text = await uploadResponse.text(); + throw new Error(text || 'Upload failed.'); + } + + await loadFiles(currentPath); + } catch (err) { + setError(err.message); + } finally { + setWorking(''); + if (fileInputRef.current) fileInputRef.current.value = ''; + setDragging(false); + } + }; + + const handleDragEnter = (e) => { + e.preventDefault(); + if (!configured || loading || working) return; + setDragging(true); + }; + + const handleDragOver = (e) => { + e.preventDefault(); + if (!configured || loading || working) return; + e.dataTransfer.dropEffect = 'copy'; + setDragging(true); + }; + + const handleDragLeave = (e) => { + e.preventDefault(); + if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false); + }; + + const handleDrop = (e) => { + e.preventDefault(); + setDragging(false); + if (!configured || loading || working) return; + uploadFiles(e.dataTransfer.files); + }; + + return ( + +
+
+
File Sharing
+
Shared Seafile workspace for team members and subcontractors.
+
+
+ Used in this location: {formatBytes(usageBytes)} +
+
+ +
+ {dragging && ( +
+
+
+
Drop files to upload
+
Files will be added to the current folder.
+
+
+ )} + +
+
+ + {breadcrumbs.map((part, index) => ( + + ))} +
+ +
+ {currentUser?.role === 'team' && ( + + Sync Folders + + )} + {showFolderInput ? ( +
+ setFolderName(e.target.value)} + placeholder="Folder name" + autoFocus + disabled={!configured || loading || Boolean(working)} + style={{ fontSize: 13, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-primary)', width: 160 }} + /> + + Create + + +
+ ) : ( + + )} + loadFiles(currentPath)}> + ⟳ Refresh + + uploadFiles(e.target.files)} + /> + fileInputRef.current?.click()}> + ↑ Upload + +
+
+ + {error &&
{error}
} + +
+ {currentPath !== '/' && ( + + )} + +
+ + Name + Size + Modified + +
+ + {loading ? ( +
Loading files...
+ ) : entries.length === 0 ? ( +
+

No files here yet

+

Upload files or create a folder to start this workspace.

+
+ ) : entries.map(entry => ( +
+ {entry.type === 'dir' ? '▣' : '□'} + {entry.type === 'dir' ? ( + + ) : ( + {entry.name} + )} + {formatBytes(entry.aggregateSize ?? entry.size)} + {formatDate(entry.mtime)} + + {entry.type === 'file' && ( + downloadFile(entry)}> + Download + + )} + deleteEntry(entry)}> + Delete + + +
+ ))} +
+
+
+ ); +}