Session 2026-05-29: shared dashboard, team tasks page, requests card style, copy/paste files, loading modals, avatar cache bust
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+81
-34
@@ -161,6 +161,7 @@ export default function FileSharing() {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(null);
|
||||
const [downloadingSelection, setDownloadingSelection] = useState(false);
|
||||
const [downloadProgress, setDownloadProgress] = useState(null);
|
||||
const [ctxMenu, setCtxMenu] = useState(null);
|
||||
const [clipboard, setClipboard] = useState(null);
|
||||
const [hoveredNavPath, setHoveredNavPath] = useState(null);
|
||||
@@ -420,18 +421,24 @@ export default function FileSharing() {
|
||||
localStorage.setItem(pinsStorageKey, JSON.stringify(next));
|
||||
}
|
||||
|
||||
function triggerDownload(url, filename) {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function handleDownload(entry) {
|
||||
const nextPath = entry.path ?? (path === '/' ? `/${entry.name}` : `${path}/${entry.name}`);
|
||||
try {
|
||||
const blob = await (await fbqProxy('download', nextPath)).blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = entry.name;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
setError('Download failed.');
|
||||
triggerDownload(URL.createObjectURL(blob), entry.name);
|
||||
} catch (err) {
|
||||
setError(err?.message || 'Download failed.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,6 +468,7 @@ export default function FileSharing() {
|
||||
async function handleDownloadSelected() {
|
||||
if (!selected.size) return;
|
||||
setDownloadingSelection(true);
|
||||
setDownloadProgress(0);
|
||||
setError('');
|
||||
try {
|
||||
const selectedEntries = [...selected]
|
||||
@@ -475,53 +483,42 @@ export default function FileSharing() {
|
||||
|
||||
if (!isDir) {
|
||||
const blob = await (await fbqProxy('download', targetPath)).blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = name;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setDownloadProgress(100);
|
||||
triggerDownload(URL.createObjectURL(blob), name);
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
setDownloadProgress(50);
|
||||
await addPathToZip(zip, targetPath, name);
|
||||
const blob = await zip.generateAsync({ type: 'blob' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${name}.zip`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setDownloadProgress(100);
|
||||
triggerDownload(URL.createObjectURL(await zip.generateAsync({ type: 'blob' })), `${name}.zip`);
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
for (const entry of selectedEntries) {
|
||||
for (let i = 0; i < selectedEntries.length; i++) {
|
||||
const entry = selectedEntries[i];
|
||||
const name = entry.name ?? entry.filename ?? '';
|
||||
const targetPath = entry.path ?? (path === '/' ? `/${name}` : `${path}/${name}`);
|
||||
const isDir = entry.type === 'directory' || entry.isDir;
|
||||
if (isDir) {
|
||||
await addPathToZip(zip, targetPath, name);
|
||||
continue;
|
||||
} else {
|
||||
const blob = await (await fbqProxy('download', targetPath)).blob();
|
||||
zip.file(name, blob);
|
||||
}
|
||||
const blob = await (await fbqProxy('download', targetPath)).blob();
|
||||
zip.file(name, blob);
|
||||
setDownloadProgress(Math.round(((i + 1) / selectedEntries.length) * 100));
|
||||
}
|
||||
|
||||
const blob = await zip.generateAsync({ type: 'blob' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
const folderName = parsePath(path).at(-1) || 'files';
|
||||
link.href = url;
|
||||
link.download = `${folderName}-selection.zip`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
setError('Download failed.');
|
||||
triggerDownload(URL.createObjectURL(await zip.generateAsync({ type: 'blob' })), `${folderName}-selection.zip`);
|
||||
} catch (err) {
|
||||
setError(err?.message || 'Download failed.');
|
||||
} finally {
|
||||
setDownloadingSelection(false);
|
||||
setDownloadProgress(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -938,6 +935,56 @@ export default function FileSharing() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(loading || uploading || downloadingSelection) && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 1200,
|
||||
background: 'rgba(0,0,0,0.58)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
WebkitBackdropFilter: 'blur(6px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--card-bg)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 8,
|
||||
padding: '18px 21px',
|
||||
backdropFilter: 'blur(12px)',
|
||||
WebkitBackdropFilter: 'blur(12px)',
|
||||
width: 'min(320px, 100%)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 14 }}>
|
||||
{uploading ? 'Uploading' : downloadingSelection ? 'Downloading' : 'Loading'}
|
||||
</div>
|
||||
{(() => {
|
||||
const pct = uploading ? uploadProgress : downloadingSelection ? downloadProgress : null;
|
||||
const hasProgress = (uploading || downloadingSelection) && pct !== null;
|
||||
return (
|
||||
<>
|
||||
<div style={{ height: 4, borderRadius: 2, background: 'var(--border)', overflow: 'hidden', marginBottom: hasProgress ? 8 : 0 }}>
|
||||
<div
|
||||
className={hasProgress ? undefined : 'file-browser-progress-bar indeterminate'}
|
||||
style={{ height: '100%', borderRadius: 2, background: 'var(--accent)', width: hasProgress ? `${pct}%` : undefined, transition: 'width 0.2s ease' }}
|
||||
/>
|
||||
</div>
|
||||
{hasProgress && (
|
||||
<div style={{ fontSize: 12, color: 'var(--text-secondary)', textAlign: 'right' }}>{pct}%</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ctxMenu && (() => {
|
||||
const { x, y, entry, childPath } = ctxMenu;
|
||||
const isDir = entry ? (entry.type === 'directory' || entry.isDir) : false;
|
||||
|
||||
Reference in New Issue
Block a user