Workspaces in Workspaces all around
This commit is contained in:
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user