deleting folder logic

This commit is contained in:
Ishaan Dey 2024-05-11 17:23:45 -07:00
parent aa97a6771e
commit 9a5a0e13d3
8 changed files with 182 additions and 55 deletions

View File

@ -13,6 +13,7 @@ import {
createFile, createFile,
deleteFile, deleteFile,
generateCode, generateCode,
getFolder,
getProjectSize, getProjectSize,
getSandboxFiles, getSandboxFiles,
renameFile, renameFile,
@ -130,6 +131,11 @@ io.on("connection", async (socket) => {
callback(file.data) callback(file.data)
}) })
socket.on("getFolder", async (folderId: string, callback) => {
const files = await getFolder(folderId)
callback(files)
})
// todo: send diffs + debounce for efficiency // todo: send diffs + debounce for efficiency
socket.on("saveFile", async (fileId: string, body: string) => { socket.on("saveFile", async (fileId: string, body: string) => {
try { 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) => { socket.on("createTerminal", (id: string, callback) => {
console.log("creating terminal", id) console.log("creating terminal", id)
if (terminals[id] || Object.keys(terminals).length >= 4) { if (terminals[id] || Object.keys(terminals).length >= 4) {
@ -339,6 +372,7 @@ io.on("connection", async (socket) => {
instructions: string, instructions: string,
callback callback
) => { ) => {
// Log code generation credit in DB
const fetchPromise = fetch(`https://database.ishaan1013.workers.dev/api/sandbox/generate`, { const fetchPromise = fetch(`https://database.ishaan1013.workers.dev/api/sandbox/generate`, {
method: "POST", method: "POST",
headers: { headers: {
@ -349,6 +383,7 @@ io.on("connection", async (socket) => {
}), }),
}) })
// Generate code from cloudflare workers AI
const generateCodePromise = generateCode({ const generateCodePromise = generateCode({
fileName, fileName,
code, code,

View File

@ -1,23 +1,28 @@
import { RateLimiterMemory } from "rate-limiter-flexible" import { RateLimiterMemory } from "rate-limiter-flexible"
export const saveFileRL = new RateLimiterMemory({ export const saveFileRL = new RateLimiterMemory({
points: 3, points: 2,
duration: 1, duration: 1,
}) })
export const MAX_BODY_SIZE = 5 * 1024 * 1024 export const MAX_BODY_SIZE = 5 * 1024 * 1024
export const createFileRL = new RateLimiterMemory({ export const createFileRL = new RateLimiterMemory({
points: 3, points: 1,
duration: 1, duration: 2,
}) })
export const renameFileRL = new RateLimiterMemory({ export const renameFileRL = new RateLimiterMemory({
points: 3, points: 1,
duration: 1, duration: 2,
}) })
export const deleteFileRL = new RateLimiterMemory({ export const deleteFileRL = new RateLimiterMemory({
points: 3, points: 1,
duration: 1, duration: 2,
})
export const deleteFolderRL = new RateLimiterMemory({
points: 1,
duration: 2,
}) })

View File

@ -10,16 +10,25 @@ import {
} from "./types" } from "./types"
export const getSandboxFiles = async (id: string) => { export const getSandboxFiles = async (id: string) => {
const sandboxRes = await fetch( const res = await fetch(
`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}` `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) const processedFiles = await processFiles(paths, id)
return processedFiles 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 processFiles = async (paths: string[], id: string) => {
const root: TFolder = { id: "/", type: "folder", name: "/", children: [] } const root: TFolder = { id: "/", type: "folder", name: "/", children: [] }
const fileData: TFileData[] = [] const fileData: TFileData[] = []

View File

@ -36,11 +36,15 @@ export default {
if (method === 'GET') { if (method === 'GET') {
const params = url.searchParams; const params = url.searchParams;
const sandboxId = params.get('sandboxId'); const sandboxId = params.get('sandboxId');
const folderId = params.get('folderId');
const fileId = params.get('fileId'); const fileId = params.get('fileId');
if (sandboxId) { if (sandboxId) {
const res = await env.R2.list({ prefix: `projects/${sandboxId}` }); const res = await env.R2.list({ prefix: `projects/${sandboxId}` });
return new Response(JSON.stringify(res), { status: 200 }); 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) { } else if (fileId) {
const obj = await env.R2.get(fileId); const obj = await env.R2.get(fileId);
if (obj === null) { if (obj === null) {

View File

@ -58,6 +58,7 @@ export default function CodeEditor({
const [tabs, setTabs] = useState<TTab[]>([]); const [tabs, setTabs] = useState<TTab[]>([]);
const [activeFileId, setActiveFileId] = useState<string>(""); const [activeFileId, setActiveFileId] = useState<string>("");
const [activeFileContent, setActiveFileContent] = useState(""); const [activeFileContent, setActiveFileContent] = useState("");
const [deletingFolderId, setDeletingFolderId] = useState("");
// Editor state // Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext"); const [editorLanguage, setEditorLanguage] = useState("plaintext");
@ -435,14 +436,16 @@ export default function CodeEditor({
}; };
// Close tab and remove from tabs // Close tab and remove from tabs
const closeTab = (tab: TFile) => { const closeTab = (id: string) => {
const numTabs = tabs.length; 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; if (index === -1) return;
const nextId = const nextId =
activeFileId === tab.id activeFileId === id
? numTabs === 1 ? numTabs === 1
? null ? null
: index < numTabs - 1 : index < numTabs - 1
@ -450,7 +453,7 @@ export default function CodeEditor({
: tabs[index - 1].id : tabs[index - 1].id
: activeFileId; : activeFileId;
setTabs((prev) => prev.filter((t) => t.id !== tab.id)); setTabs((prev) => prev.filter((t) => t.id !== id));
if (!nextId) { if (!nextId) {
setActiveFileId(""); 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 = ( const handleRename = (
id: string, id: string,
newName: string, newName: string,
@ -486,13 +519,25 @@ export default function CodeEditor({
socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { socket.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response); setFiles(response);
}); });
closeTab(file); closeTab(file.id);
}; };
const handleDeleteFolder = (folder: TFolder) => { const handleDeleteFolder = (folder: TFolder) => {
// socket.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { setDeletingFolderId(folder.id);
// setFiles(response) 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 // 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: [] }]) // setFiles(prev => [...prev, { id, name, type: "folder", children: [] }])
} }
}} }}
deletingFolderId={deletingFolderId}
// AI Copilot Toggle // AI Copilot Toggle
ai={ai} ai={ai}
setAi={setAi} setAi={setAi}
@ -604,7 +650,7 @@ export default function CodeEditor({
onClick={(e) => { onClick={(e) => {
selectFile(tab); selectFile(tab);
}} }}
onClose={() => closeTab(tab)} onClose={() => closeTab(tab.id)}
> >
{tab.name} {tab.name}
</Tab> </Tab>

View File

@ -20,6 +20,7 @@ export default function SidebarFile({
handleRename, handleRename,
handleDeleteFile, handleDeleteFile,
movingId, movingId,
deletingFolderId,
}: { }: {
data: TFile; data: TFile;
selectFile: (file: TTab) => void; selectFile: (file: TTab) => void;
@ -31,8 +32,11 @@ export default function SidebarFile({
) => boolean; ) => boolean;
handleDeleteFile: (file: TFile) => void; handleDeleteFile: (file: TFile) => void;
movingId: string; movingId: string;
deletingFolderId: string;
}) { }) {
const isMoving = movingId === data.id; const isMoving = movingId === data.id;
const isDeleting =
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
const ref = useRef(null); // for draggable const ref = useRef(null); // for draggable
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
@ -40,7 +44,11 @@ export default function SidebarFile({
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`); const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [pendingDelete, setPendingDelete] = useState(false); const [pendingDelete, setPendingDelete] = useState(isDeleting);
useEffect(() => {
setPendingDelete(isDeleting);
}, [isDeleting]);
useEffect(() => { useEffect(() => {
const el = ref.current; const el = ref.current;
@ -104,8 +112,9 @@ export default function SidebarFile({
</> </>
) : pendingDelete ? ( ) : pendingDelete ? (
<> <>
<Loader2 className="text-muted-foreground w-4 h-4 animate-spin mr-2" /> <div className="text-muted-foreground animate-pulse">
<div className="text-muted-foreground">Deleting...</div> Deleting...
</div>
</> </>
) : ( ) : (
<form <form

View File

@ -11,9 +11,11 @@ import {
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu"; } 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"; 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({ export default function SidebarFolder({
data, data,
selectFile, selectFile,
@ -21,6 +23,7 @@ export default function SidebarFolder({
handleDeleteFile, handleDeleteFile,
handleDeleteFolder, handleDeleteFolder,
movingId, movingId,
deletingFolderId,
}: { }: {
data: TFolder; data: TFolder;
selectFile: (file: TTab) => void; selectFile: (file: TTab) => void;
@ -33,10 +36,14 @@ export default function SidebarFolder({
handleDeleteFile: (file: TFile) => void; handleDeleteFile: (file: TFile) => void;
handleDeleteFolder: (folder: TFolder) => void; handleDeleteFolder: (folder: TFolder) => void;
movingId: string; movingId: string;
deletingFolderId: string;
}) { }) {
const ref = useRef(null); // drop target const ref = useRef(null); // drop target
const [isDraggedOver, setIsDraggedOver] = useState(false); const [isDraggedOver, setIsDraggedOver] = useState(false);
const isDeleting =
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId);
useEffect(() => { useEffect(() => {
const el = ref.current; const el = ref.current;
@ -58,9 +65,10 @@ export default function SidebarFolder({
// return !file; // return !file;
// }, // },
// no dropping while awaiting move
canDrop: () => { canDrop: () => {
return !movingId; return !movingId;
}, // no dropping while awaiting move },
}); });
}, []); }, []);
@ -69,28 +77,20 @@ export default function SidebarFolder({
? getIconForOpenFolder(data.name) ? getIconForOpenFolder(data.name)
: getIconForFolder(data.name); : getIconForFolder(data.name);
const [editing, setEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
// const [editing, setEditing] = useState(false);
useEffect(() => { // useEffect(() => {
if (editing) { // if (editing) {
inputRef.current?.focus(); // inputRef.current?.focus();
} // }
}, [editing]); // }, [editing]);
// return (
// <div
// ref={ref}
// className="w-full h-7 rounded-full"
// style={{backgroundColor: isDraggedOver ? "red" : "blue"}}
// >
// </div>
// )
return ( return (
<ContextMenu> <ContextMenu>
<ContextMenuTrigger <ContextMenuTrigger
ref={ref} ref={ref}
disabled={isDeleting}
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
className={`${ className={`${
isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm" isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm"
@ -103,13 +103,26 @@ export default function SidebarFolder({
height={18} height={18}
className="mr-2" className="mr-2"
/> />
<form {isDeleting ? (
onSubmit={(e) => { <>
e.preventDefault(); <div className="text-muted-foreground animate-pulse">
setEditing(false); Deleting...
}} </div>
> </>
<input ) : (
<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} 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 ${ 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" editing ? "" : "pointer-events-none"
@ -119,24 +132,24 @@ export default function SidebarFolder({
onBlur={() => { onBlur={() => {
setEditing(false); setEditing(false);
}} }}
/> /> */}
</form> </form>
)}
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <ContextMenuItem
onClick={() => { disabled
setEditing(true); // onClick={() => {
}} // setEditing(true);
// }}
> >
<Pencil className="w-4 h-4 mr-2" /> <Pencil className="w-4 h-4 mr-2" />
Rename Rename
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem <ContextMenuItem
// disabled={pendingDelete} disabled={isDeleting}
onClick={() => { onClick={() => {
console.log("delete"); handleDeleteFolder(data);
// setPendingDelete(true)
// handleDeleteFile(data)
}} }}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
@ -160,6 +173,7 @@ export default function SidebarFolder({
handleRename={handleRename} handleRename={handleRename}
handleDeleteFile={handleDeleteFile} handleDeleteFile={handleDeleteFile}
movingId={movingId} movingId={movingId}
deletingFolderId={deletingFolderId}
/> />
) : ( ) : (
<SidebarFolder <SidebarFolder
@ -170,6 +184,7 @@ export default function SidebarFolder({
handleDeleteFile={handleDeleteFile} handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder} handleDeleteFolder={handleDeleteFolder}
movingId={movingId} movingId={movingId}
deletingFolderId={deletingFolderId}
/> />
) )
)} )}

View File

@ -26,6 +26,7 @@ export default function Sidebar({
addNew, addNew,
ai, ai,
setAi, setAi,
deletingFolderId,
}: { }: {
sandboxData: Sandbox; sandboxData: Sandbox;
files: (TFile | TFolder)[]; files: (TFile | TFolder)[];
@ -43,6 +44,7 @@ export default function Sidebar({
addNew: (name: string, type: "file" | "folder") => void; addNew: (name: string, type: "file" | "folder") => void;
ai: boolean; ai: boolean;
setAi: React.Dispatch<React.SetStateAction<boolean>>; setAi: React.Dispatch<React.SetStateAction<boolean>>;
deletingFolderId: string;
}) { }) {
const ref = useRef(null); // drop target const ref = useRef(null); // drop target
@ -147,6 +149,7 @@ export default function Sidebar({
handleRename={handleRename} handleRename={handleRename}
handleDeleteFile={handleDeleteFile} handleDeleteFile={handleDeleteFile}
movingId={movingId} movingId={movingId}
deletingFolderId={deletingFolderId}
/> />
) : ( ) : (
<SidebarFolder <SidebarFolder
@ -157,6 +160,7 @@ export default function Sidebar({
handleDeleteFile={handleDeleteFile} handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder} handleDeleteFolder={handleDeleteFolder}
movingId={movingId} movingId={movingId}
deletingFolderId={deletingFolderId}
/> />
) )
)} )}