deleting folder logic
This commit is contained in:
parent
aa97a6771e
commit
9a5a0e13d3
@ -13,6 +13,7 @@ import {
|
||||
createFile,
|
||||
deleteFile,
|
||||
generateCode,
|
||||
getFolder,
|
||||
getProjectSize,
|
||||
getSandboxFiles,
|
||||
renameFile,
|
||||
@ -130,6 +131,11 @@ io.on("connection", async (socket) => {
|
||||
callback(file.data)
|
||||
})
|
||||
|
||||
socket.on("getFolder", async (folderId: string, callback) => {
|
||||
const files = await getFolder(folderId)
|
||||
callback(files)
|
||||
})
|
||||
|
||||
// todo: send diffs + debounce for efficiency
|
||||
socket.on("saveFile", async (fileId: string, body: string) => {
|
||||
try {
|
||||
@ -264,6 +270,33 @@ io.on("connection", async (socket) => {
|
||||
}
|
||||
})
|
||||
|
||||
socket.on("renameFolder", async (folderId: string, newName: string) => {
|
||||
// todo
|
||||
})
|
||||
|
||||
socket.on("deleteFolder", async (folderId: string, callback) => {
|
||||
const files = await getFolder(folderId)
|
||||
|
||||
console.log("deleting folder", folderId, files)
|
||||
|
||||
await Promise.all(files.map(async (file) => {
|
||||
fs.unlink(path.join(dirName, file), function (err) {
|
||||
if (err) throw err
|
||||
})
|
||||
|
||||
sandboxFiles.fileData = sandboxFiles.fileData.filter(
|
||||
(f) => f.id !== file
|
||||
)
|
||||
|
||||
await deleteFile(file)
|
||||
}))
|
||||
|
||||
const newFiles = await getSandboxFiles(data.sandboxId)
|
||||
|
||||
callback(newFiles.files)
|
||||
|
||||
})
|
||||
|
||||
socket.on("createTerminal", (id: string, callback) => {
|
||||
console.log("creating terminal", id)
|
||||
if (terminals[id] || Object.keys(terminals).length >= 4) {
|
||||
@ -339,6 +372,7 @@ io.on("connection", async (socket) => {
|
||||
instructions: string,
|
||||
callback
|
||||
) => {
|
||||
// Log code generation credit in DB
|
||||
const fetchPromise = fetch(`https://database.ishaan1013.workers.dev/api/sandbox/generate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -349,6 +383,7 @@ io.on("connection", async (socket) => {
|
||||
}),
|
||||
})
|
||||
|
||||
// Generate code from cloudflare workers AI
|
||||
const generateCodePromise = generateCode({
|
||||
fileName,
|
||||
code,
|
||||
|
@ -1,23 +1,28 @@
|
||||
import { RateLimiterMemory } from "rate-limiter-flexible"
|
||||
|
||||
export const saveFileRL = new RateLimiterMemory({
|
||||
points: 3,
|
||||
points: 2,
|
||||
duration: 1,
|
||||
})
|
||||
|
||||
export const MAX_BODY_SIZE = 5 * 1024 * 1024
|
||||
|
||||
export const createFileRL = new RateLimiterMemory({
|
||||
points: 3,
|
||||
duration: 1,
|
||||
points: 1,
|
||||
duration: 2,
|
||||
})
|
||||
|
||||
export const renameFileRL = new RateLimiterMemory({
|
||||
points: 3,
|
||||
duration: 1,
|
||||
points: 1,
|
||||
duration: 2,
|
||||
})
|
||||
|
||||
export const deleteFileRL = new RateLimiterMemory({
|
||||
points: 3,
|
||||
duration: 1,
|
||||
points: 1,
|
||||
duration: 2,
|
||||
})
|
||||
|
||||
export const deleteFolderRL = new RateLimiterMemory({
|
||||
points: 1,
|
||||
duration: 2,
|
||||
})
|
@ -10,16 +10,25 @@ import {
|
||||
} from "./types"
|
||||
|
||||
export const getSandboxFiles = async (id: string) => {
|
||||
const sandboxRes = await fetch(
|
||||
const res = await fetch(
|
||||
`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`
|
||||
)
|
||||
const sandboxData: R2Files = await sandboxRes.json()
|
||||
const data: R2Files = await res.json()
|
||||
|
||||
const paths = sandboxData.objects.map((obj) => obj.key)
|
||||
const paths = data.objects.map((obj) => obj.key)
|
||||
const processedFiles = await processFiles(paths, id)
|
||||
return processedFiles
|
||||
}
|
||||
|
||||
export const getFolder = async (folderId: string) => {
|
||||
const res = await fetch(
|
||||
`https://storage.ishaan1013.workers.dev/api?folderId=${folderId}`
|
||||
)
|
||||
const data: R2Files = await res.json()
|
||||
|
||||
return data.objects.map((obj) => obj.key)
|
||||
}
|
||||
|
||||
const processFiles = async (paths: string[], id: string) => {
|
||||
const root: TFolder = { id: "/", type: "folder", name: "/", children: [] }
|
||||
const fileData: TFileData[] = []
|
||||
|
@ -36,11 +36,15 @@ export default {
|
||||
if (method === 'GET') {
|
||||
const params = url.searchParams;
|
||||
const sandboxId = params.get('sandboxId');
|
||||
const folderId = params.get('folderId');
|
||||
const fileId = params.get('fileId');
|
||||
|
||||
if (sandboxId) {
|
||||
const res = await env.R2.list({ prefix: `projects/${sandboxId}` });
|
||||
return new Response(JSON.stringify(res), { status: 200 });
|
||||
} else if (folderId) {
|
||||
const res = await env.R2.list({ prefix: folderId });
|
||||
return new Response(JSON.stringify(res), { status: 200 });
|
||||
} else if (fileId) {
|
||||
const obj = await env.R2.get(fileId);
|
||||
if (obj === null) {
|
||||
|
@ -58,6 +58,7 @@ export default function CodeEditor({
|
||||
const [tabs, setTabs] = useState<TTab[]>([]);
|
||||
const [activeFileId, setActiveFileId] = useState<string>("");
|
||||
const [activeFileContent, setActiveFileContent] = useState("");
|
||||
const [deletingFolderId, setDeletingFolderId] = useState("");
|
||||
|
||||
// Editor state
|
||||
const [editorLanguage, setEditorLanguage] = useState("plaintext");
|
||||
@ -435,14 +436,16 @@ export default function CodeEditor({
|
||||
};
|
||||
|
||||
// Close tab and remove from tabs
|
||||
const closeTab = (tab: TFile) => {
|
||||
const closeTab = (id: string) => {
|
||||
const numTabs = tabs.length;
|
||||
const index = tabs.findIndex((t) => t.id === tab.id);
|
||||
const index = tabs.findIndex((t) => t.id === id);
|
||||
|
||||
console.log("closing tab", id, index);
|
||||
|
||||
if (index === -1) return;
|
||||
|
||||
const nextId =
|
||||
activeFileId === tab.id
|
||||
activeFileId === id
|
||||
? numTabs === 1
|
||||
? null
|
||||
: index < numTabs - 1
|
||||
@ -450,7 +453,7 @@ export default function CodeEditor({
|
||||
: tabs[index - 1].id
|
||||
: activeFileId;
|
||||
|
||||
setTabs((prev) => prev.filter((t) => t.id !== tab.id));
|
||||
setTabs((prev) => prev.filter((t) => t.id !== id));
|
||||
|
||||
if (!nextId) {
|
||||
setActiveFileId("");
|
||||
@ -462,6 +465,36 @@ export default function CodeEditor({
|
||||
}
|
||||
};
|
||||
|
||||
const closeTabs = (ids: string[]) => {
|
||||
const numTabs = tabs.length;
|
||||
|
||||
if (numTabs === 0) return;
|
||||
|
||||
const allIndexes = ids.map((id) => tabs.findIndex((t) => t.id === id));
|
||||
|
||||
const indexes = allIndexes.filter((index) => index !== -1);
|
||||
if (indexes.length === 0) return;
|
||||
|
||||
console.log("closing tabs", ids, indexes);
|
||||
|
||||
const activeIndex = tabs.findIndex((t) => t.id === activeFileId);
|
||||
|
||||
const newTabs = tabs.filter((t) => !ids.includes(t.id));
|
||||
setTabs(newTabs);
|
||||
|
||||
if (indexes.length === numTabs) {
|
||||
setActiveFileId("");
|
||||
} else {
|
||||
const nextTab =
|
||||
newTabs.length > activeIndex
|
||||
? newTabs[activeIndex]
|
||||
: newTabs[newTabs.length - 1];
|
||||
if (nextTab) {
|
||||
selectFile(nextTab);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRename = (
|
||||
id: string,
|
||||
newName: string,
|
||||
@ -486,13 +519,25 @@ export default function CodeEditor({
|
||||
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
|
||||
setFiles(response);
|
||||
});
|
||||
closeTab(file);
|
||||
closeTab(file.id);
|
||||
};
|
||||
|
||||
const handleDeleteFolder = (folder: TFolder) => {
|
||||
// socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
||||
// setFiles(response)
|
||||
// })
|
||||
setDeletingFolderId(folder.id);
|
||||
console.log("deleting folder", folder.id);
|
||||
|
||||
socket.emit("getFolder", folder.id, (response: string[]) =>
|
||||
closeTabs(response)
|
||||
);
|
||||
|
||||
socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
|
||||
setFiles(response);
|
||||
setDeletingFolderId("");
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setDeletingFolderId("");
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// On disabled access for shared users, show un-interactable loading placeholder + info modal
|
||||
@ -581,6 +626,7 @@ export default function CodeEditor({
|
||||
// setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
|
||||
}
|
||||
}}
|
||||
deletingFolderId={deletingFolderId}
|
||||
// AI Copilot Toggle
|
||||
ai={ai}
|
||||
setAi={setAi}
|
||||
@ -604,7 +650,7 @@ export default function CodeEditor({
|
||||
onClick={(e) => {
|
||||
selectFile(tab);
|
||||
}}
|
||||
onClose={() => closeTab(tab)}
|
||||
onClose={() => closeTab(tab.id)}
|
||||
>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
|
@ -20,6 +20,7 @@ export default function SidebarFile({
|
||||
handleRename,
|
||||
handleDeleteFile,
|
||||
movingId,
|
||||
deletingFolderId,
|
||||
}: {
|
||||
data: TFile;
|
||||
selectFile: (file: TTab) => void;
|
||||
@ -31,8 +32,11 @@ export default function SidebarFile({
|
||||
) => boolean;
|
||||
handleDeleteFile: (file: TFile) => void;
|
||||
movingId: string;
|
||||
deletingFolderId: string;
|
||||
}) {
|
||||
const isMoving = movingId === data.id;
|
||||
const isDeleting =
|
||||
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
|
||||
|
||||
const ref = useRef(null); // for draggable
|
||||
const [dragging, setDragging] = useState(false);
|
||||
@ -40,7 +44,11 @@ export default function SidebarFile({
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [pendingDelete, setPendingDelete] = useState(false);
|
||||
const [pendingDelete, setPendingDelete] = useState(isDeleting);
|
||||
|
||||
useEffect(() => {
|
||||
setPendingDelete(isDeleting);
|
||||
}, [isDeleting]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
@ -104,8 +112,9 @@ export default function SidebarFile({
|
||||
</>
|
||||
) : pendingDelete ? (
|
||||
<>
|
||||
<Loader2 className="text-muted-foreground w-4 h-4 animate-spin mr-2" />
|
||||
<div className="text-muted-foreground">Deleting...</div>
|
||||
<div className="text-muted-foreground animate-pulse">
|
||||
Deleting...
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<form
|
||||
|
@ -11,9 +11,11 @@ import {
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { Loader2, Pencil, Trash2 } from "lucide-react";
|
||||
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
|
||||
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out
|
||||
|
||||
export default function SidebarFolder({
|
||||
data,
|
||||
selectFile,
|
||||
@ -21,6 +23,7 @@ export default function SidebarFolder({
|
||||
handleDeleteFile,
|
||||
handleDeleteFolder,
|
||||
movingId,
|
||||
deletingFolderId,
|
||||
}: {
|
||||
data: TFolder;
|
||||
selectFile: (file: TTab) => void;
|
||||
@ -33,10 +36,14 @@ export default function SidebarFolder({
|
||||
handleDeleteFile: (file: TFile) => void;
|
||||
handleDeleteFolder: (folder: TFolder) => void;
|
||||
movingId: string;
|
||||
deletingFolderId: string;
|
||||
}) {
|
||||
const ref = useRef(null); // drop target
|
||||
const [isDraggedOver, setIsDraggedOver] = useState(false);
|
||||
|
||||
const isDeleting =
|
||||
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
|
||||
@ -58,9 +65,10 @@ export default function SidebarFolder({
|
||||
// return !file;
|
||||
// },
|
||||
|
||||
// no dropping while awaiting move
|
||||
canDrop: () => {
|
||||
return !movingId;
|
||||
}, // no dropping while awaiting move
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -69,28 +77,20 @@ export default function SidebarFolder({
|
||||
? getIconForOpenFolder(data.name)
|
||||
: getIconForFolder(data.name);
|
||||
|
||||
const [editing, setEditing] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// const [editing, setEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [editing]);
|
||||
|
||||
// return (
|
||||
// <div
|
||||
// ref={ref}
|
||||
// className="w-full h-7 rounded-full"
|
||||
// style={{backgroundColor: isDraggedOver ? "red" : "blue"}}
|
||||
// >
|
||||
// </div>
|
||||
// )
|
||||
// useEffect(() => {
|
||||
// if (editing) {
|
||||
// inputRef.current?.focus();
|
||||
// }
|
||||
// }, [editing]);
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger
|
||||
ref={ref}
|
||||
disabled={isDeleting}
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
className={`${
|
||||
isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm"
|
||||
@ -103,13 +103,26 @@ export default function SidebarFolder({
|
||||
height={18}
|
||||
className="mr-2"
|
||||
/>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setEditing(false);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<div className="text-muted-foreground animate-pulse">
|
||||
Deleting...
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<form
|
||||
// onSubmit={(e) => {
|
||||
// e.preventDefault();
|
||||
// setEditing(false);
|
||||
// }}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
disabled
|
||||
className={`pointer-events-none bg-transparent transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-sm w-full`}
|
||||
defaultValue={data.name}
|
||||
/>
|
||||
{/* <input
|
||||
ref={inputRef}
|
||||
className={`bg-transparent transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-sm w-full ${
|
||||
editing ? "" : "pointer-events-none"
|
||||
@ -119,24 +132,24 @@ export default function SidebarFolder({
|
||||
onBlur={() => {
|
||||
setEditing(false);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
/> */}
|
||||
</form>
|
||||
)}
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
setEditing(true);
|
||||
}}
|
||||
disabled
|
||||
// onClick={() => {
|
||||
// setEditing(true);
|
||||
// }}
|
||||
>
|
||||
<Pencil className="w-4 h-4 mr-2" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
// disabled={pendingDelete}
|
||||
disabled={isDeleting}
|
||||
onClick={() => {
|
||||
console.log("delete");
|
||||
// setPendingDelete(true)
|
||||
// handleDeleteFile(data)
|
||||
handleDeleteFolder(data);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
@ -160,6 +173,7 @@ export default function SidebarFolder({
|
||||
handleRename={handleRename}
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
movingId={movingId}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
) : (
|
||||
<SidebarFolder
|
||||
@ -170,6 +184,7 @@ export default function SidebarFolder({
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
movingId={movingId}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
@ -26,6 +26,7 @@ export default function Sidebar({
|
||||
addNew,
|
||||
ai,
|
||||
setAi,
|
||||
deletingFolderId,
|
||||
}: {
|
||||
sandboxData: Sandbox;
|
||||
files: (TFile | TFolder)[];
|
||||
@ -43,6 +44,7 @@ export default function Sidebar({
|
||||
addNew: (name: string, type: "file" | "folder") => void;
|
||||
ai: boolean;
|
||||
setAi: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
deletingFolderId: string;
|
||||
}) {
|
||||
const ref = useRef(null); // drop target
|
||||
|
||||
@ -147,6 +149,7 @@ export default function Sidebar({
|
||||
handleRename={handleRename}
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
movingId={movingId}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
) : (
|
||||
<SidebarFolder
|
||||
@ -157,6 +160,7 @@ export default function Sidebar({
|
||||
handleDeleteFile={handleDeleteFile}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
movingId={movingId}
|
||||
deletingFolderId={deletingFolderId}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
Loading…
x
Reference in New Issue
Block a user