Workspaces in Workspaces all around

This commit is contained in:
2026-02-19 01:49:15 +00:00
parent 72bca6cd42
commit dd8ab84df5
84 changed files with 3047 additions and 749 deletions
+25 -2
View File
@@ -1,7 +1,8 @@
import { useRef, useState, useCallback } from 'react';
import { FolderPlus, Upload, FolderUp, Scissors, Copy, ClipboardPaste, Trash2, X, Check } from 'lucide-react';
import { RefreshCw, FolderPlus, Upload, FolderUp, Scissors, Copy, ClipboardPaste, Trash2, X, Check, Download } from 'lucide-react';
type ToolbarProps = {
onRefresh: () => void;
onCreateDir: (name: string) => void;
onUpload: (files: FileList) => void;
selectionCount: number;
@@ -9,11 +10,13 @@ type ToolbarProps = {
onCut: () => void;
onCopy: () => void;
onPaste: () => void;
onDownloadSelected: () => void;
onDeleteSelected: () => void;
onClearSelection: () => void;
};
export const Toolbar = ({
onRefresh,
onCreateDir,
onUpload,
selectionCount,
@@ -21,6 +24,7 @@ export const Toolbar = ({
onCut,
onCopy,
onPaste,
onDownloadSelected,
onDeleteSelected,
onClearSelection,
}: ToolbarProps) => {
@@ -69,9 +73,20 @@ export const Toolbar = ({
</>
);
const refreshBtn = (
<button
onClick={onRefresh}
title="Refresh"
className="p-1.5 rounded-md text-duck-dark/50 hover:bg-duck-dark/5 cursor-pointer transition-colors"
>
<RefreshCw className="h-4 w-4" />
</button>
);
if (selectionCount > 0) {
return (
<div className="flex items-center gap-1">
{refreshBtn}
<span className="text-sm font-medium text-duck-dark/70 mr-1">
{selectionCount}
<span className="hidden md:inline"> selected</span>
@@ -99,6 +114,13 @@ export const Toolbar = ({
<ClipboardPaste className="h-4 w-4" />
</button>
)}
<button
onClick={onDownloadSelected}
title="Download"
className="p-1.5 rounded-md text-duck-dark/50 hover:bg-duck-dark/5 cursor-pointer transition-colors"
>
<Download className="h-4 w-4" />
</button>
<button
onClick={onDeleteSelected}
title="Delete"
@@ -120,6 +142,7 @@ export const Toolbar = ({
return (
<div className="flex items-center gap-1">
{refreshBtn}
{showInput ? (
<form
onSubmit={(ev) => {
@@ -133,7 +156,7 @@ export const Toolbar = ({
value={folderName}
onChange={(ev) => setFolderName(ev.target.value)}
placeholder="Folder name"
className="h-8 w-40 text-sm rounded-md border border-duck-dark/20 bg-white/60 text-duck-dark placeholder:text-duck-dark/40 outline-none focus:border-duck-teal/50 px-2"
className="h-8 w-40 text-sm rounded-md border border-duck-dark/20 bg-background/60 text-duck-dark placeholder:text-duck-dark/40 outline-none focus:border-duck-teal/50 px-2"
onKeyDown={(ev) => {
if (ev.key === 'Escape') {
setShowInput(false);
@@ -15,6 +15,16 @@ type ListDirResponse = {
reset?: boolean;
};
const triggerBlobDownload = (blob: Blob, filename: string) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
export const useFiles = (root: string = 'home') => {
const client = useClient();
const rootParam = root !== 'home' ? `root=${encodeURIComponent(root)}` : '';
@@ -58,6 +68,26 @@ export const useFiles = (root: string = 'home') => {
gitClone: (url: string, path: string) => client.post(withRoot('/file-browser/git-clone'), { url, path }),
download: async (paths: string[]) => {
if (paths.length === 1) {
const blob = await client.getBlob(withRoot(`/file-browser/download?path=${encodeURIComponent(paths[0]!)}`));
const name = paths[0]!.split('/').pop()!;
const isDir = !name.includes('.') || blob.type === 'application/zip';
triggerBlobDownload(blob, isDir && !name.endsWith('.zip') ? `${name}.zip` : name);
} else {
const headers = getHeaders();
const url = `${config.API_URL}/${withRoot('/file-browser/download').replace(/^\//, '')}`;
const res = await fetch(url, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ paths }),
});
if (!res.ok) throw new Error('Download failed');
const blob = await res.blob();
triggerBlobDownload(blob, 'download.zip');
}
},
uploadFiles: (path: string, files: FileList | File[], onProgress?: (pct: number) => void): Promise<void> => {
const formData = new FormData();
for (const file of Array.from(files)) {