diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index f9450b4..89a33c7 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -156,6 +156,29 @@ io.on("connection", async (socket) => { } }) + socket.on("moveFile", async (fileId: string, folderId: string, callback) => { + const file = sandboxFiles.fileData.find((f) => f.id === fileId) + if (!file) return + + const parts = fileId.split("/") + const newFileId = folderId + "/" + parts.pop() + + fs.rename( + path.join(dirName, fileId), + path.join(dirName, newFileId), + function (err) { + if (err) throw err + } + ) + + file.id = newFileId + + await renameFile(fileId, newFileId, file.data) + const newFiles = await getSandboxFiles(data.sandboxId) + + callback(newFiles.files) + }) + socket.on("createFile", async (name: string, callback) => { try { diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index dbb1328..41ec0b0 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -562,12 +562,14 @@ export default function CodeEditor({ {/* Main editor components */} { if (type === "file") { setFiles((prev) => [ diff --git a/frontend/components/editor/sidebar/file.tsx b/frontend/components/editor/sidebar/file.tsx index 1f4db2e..6b00b86 100644 --- a/frontend/components/editor/sidebar/file.tsx +++ b/frontend/components/editor/sidebar/file.tsx @@ -12,11 +12,14 @@ import { } from "@/components/ui/context-menu"; import { Loader2, Pencil, Trash2 } from "lucide-react"; +import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; + export default function SidebarFile({ data, selectFile, handleRename, handleDeleteFile, + movingId, }: { data: TFile; selectFile: (file: TTab) => void; @@ -27,12 +30,30 @@ export default function SidebarFile({ type: "file" | "folder" ) => boolean; handleDeleteFile: (file: TFile) => void; + movingId: string; }) { + const isMoving = movingId === data.id; + + const ref = useRef(null); // for draggable + const [dragging, setDragging] = useState(false); + + const inputRef = useRef(null); const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`); const [editing, setEditing] = useState(false); - const inputRef = useRef(null); const [pendingDelete, setPendingDelete] = useState(false); + useEffect(() => { + const el = ref.current; + + if (el) + return draggable({ + element: el, + onDragStart: () => setDragging(true), + onDrop: () => setDragging(false), + getInitialData: () => ({ id: data.id }), + }); + }, []); + useEffect(() => { if (editing) { setTimeout(() => inputRef.current?.focus(), 0); @@ -55,14 +76,18 @@ export default function SidebarFile({ return ( { - if (!editing && !pendingDelete) selectFile({ ...data, saved: true }); + if (!editing && !pendingDelete && !isMoving) + selectFile({ ...data, saved: true }); }} // onDoubleClick={() => { // setEditing(true) // }} - className="data-[state=open]:bg-secondary/50 w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" + className={`${ + dragging ? "opacity-50 hover:!bg-background" : "" + } data-[state=open]:bg-secondary/50 w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring`} > setImgSrc("/icons/default_file.svg")} /> - {pendingDelete ? ( + {isMoving ? ( + <> + +
{data.name}
+ + ) : pendingDelete ? ( <>
Deleting...
diff --git a/frontend/components/editor/sidebar/folder.tsx b/frontend/components/editor/sidebar/folder.tsx index 6a1d148..1be188f 100644 --- a/frontend/components/editor/sidebar/folder.tsx +++ b/frontend/components/editor/sidebar/folder.tsx @@ -12,6 +12,7 @@ import { ContextMenuTrigger, } from "@/components/ui/context-menu"; import { Pencil, Trash2 } from "lucide-react"; +import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; export default function SidebarFolder({ data, @@ -19,6 +20,7 @@ export default function SidebarFolder({ handleRename, handleDeleteFile, handleDeleteFolder, + movingId, }: { data: TFolder; selectFile: (file: TTab) => void; @@ -30,7 +32,38 @@ export default function SidebarFolder({ ) => boolean; handleDeleteFile: (file: TFile) => void; handleDeleteFolder: (folder: TFolder) => void; + movingId: string; }) { + const ref = useRef(null); // drop target + const [isDraggedOver, setIsDraggedOver] = useState(false); + + useEffect(() => { + const el = ref.current; + + if (el) + return dropTargetForElements({ + element: el, + onDragEnter: () => setIsDraggedOver(true), + onDragLeave: () => setIsDraggedOver(false), + onDrop: () => setIsDraggedOver(false), + getData: () => ({ id: data.id }), + + // Commented out to avoid propagating drop event downwards + // Todo: Make this logic more elegant, the current implementation is just checking at the end in index.tsx + + // canDrop: ({ source }) => { + // const file = data.children.find( + // (child) => child.id === source.data.id + // ); + // return !file; + // }, + + canDrop: () => { + return !movingId; + }, // no dropping while awaiting move + }); + }, []); + const [isOpen, setIsOpen] = useState(false); const folder = isOpen ? getIconForOpenFolder(data.name) @@ -45,11 +78,23 @@ export default function SidebarFolder({ } }, [editing]); + // return ( + //
+ //
+ // ) + return ( setIsOpen((prev) => !prev)} - className="w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary rounded-sm cursor-pointer" + className={`${ + isDraggedOver ? "bg-secondary/50 rounded-t-sm" : "rounded-sm" + } w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary cursor-pointer`} > {isOpen ? ( -
+
{data.children.map((child) => @@ -110,6 +159,7 @@ export default function SidebarFolder({ selectFile={selectFile} handleRename={handleRename} handleDeleteFile={handleDeleteFile} + movingId={movingId} /> ) : ( ) )} diff --git a/frontend/components/editor/sidebar/index.tsx b/frontend/components/editor/sidebar/index.tsx index 0c725dd..e95b0db 100644 --- a/frontend/components/editor/sidebar/index.tsx +++ b/frontend/components/editor/sidebar/index.tsx @@ -3,24 +3,31 @@ import { FilePlus, FolderPlus, Loader2, Search, Sparkles } from "lucide-react"; import SidebarFile from "./file"; import SidebarFolder from "./folder"; -import { TFile, TFolder, TTab } from "@/lib/types"; -import { useState } from "react"; +import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"; +import { useEffect, useRef, useState } from "react"; import New from "./new"; import { Socket } from "socket.io-client"; -import Button from "@/components/ui/customButton"; import { Switch } from "@/components/ui/switch"; +import { + dropTargetForElements, + monitorForElements, +} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; + export default function Sidebar({ + sandboxData, files, selectFile, handleRename, handleDeleteFile, handleDeleteFolder, socket, + setFiles, addNew, ai, setAi, }: { + sandboxData: Sandbox; files: (TFile | TFolder)[]; selectFile: (tab: TTab) => void; handleRename: ( @@ -32,13 +39,65 @@ export default function Sidebar({ handleDeleteFile: (file: TFile) => void; handleDeleteFolder: (folder: TFolder) => void; socket: Socket; + setFiles: (files: (TFile | TFolder)[]) => void; addNew: (name: string, type: "file" | "folder") => void; ai: boolean; setAi: React.Dispatch>; }) { + const ref = useRef(null); // drop target + const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>( null ); + const [movingId, setMovingId] = useState(""); + + useEffect(() => { + const el = ref.current; + + if (el) { + return dropTargetForElements({ + element: el, + getData: () => ({ id: `projects/${sandboxData.id}` }), + canDrop: ({ source }) => { + const file = files.find((child) => child.id === source.data.id); + return !file; + }, + }); + } + }, [files]); + + useEffect(() => { + return monitorForElements({ + onDrop({ source, location }) { + const destination = location.current.dropTargets[0]; + if (!destination) { + return; + } + + const fileId = source.data.id as string; + const folderId = destination.data.id as string; + + const fileFolder = fileId.split("/").slice(0, -1).join("/"); + if (fileFolder === folderId) { + console.log("NO"); + return; + } + + console.log("move file", fileId, "to folder", folderId); + + setMovingId(fileId); + socket.emit( + "moveFile", + fileId, + folderId, + (response: (TFolder | TFile)[]) => { + setFiles(response); + setMovingId(""); + } + ); + }, + }); + }, []); return (
@@ -47,14 +106,16 @@ export default function Sidebar({
Explorer
@@ -64,7 +125,13 @@ export default function Sidebar({ */}
-
+
+ {/*
*/} {files.length === 0 ? (
@@ -79,6 +146,7 @@ export default function Sidebar({ selectFile={selectFile} handleRename={handleRename} handleDeleteFile={handleDeleteFile} + movingId={movingId} /> ) : ( ) )} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e8a0c80..cf92d08 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "sandbox", "version": "0.1.0", "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.1.7", "@clerk/nextjs": "^4.29.12", "@clerk/themes": "^1.7.12", "@hookform/resolvers": "^3.3.4", @@ -73,6 +74,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@atlaskit/pragmatic-drag-and-drop": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.1.7.tgz", + "integrity": "sha512-pCopubqXglL9VJ7YU+CS0CJwa62CqdGAS3QYfTsEgC844OOMiQ+G6HlSnBsZU9EV6DTHZHLooiTXFBqleib2ew==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "bind-event-listener": "^3.0.0", + "raf-schd": "^4.0.3" + } + }, "node_modules/@babel/runtime": { "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", @@ -1804,6 +1815,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bind-event-listener": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz", + "integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -3081,6 +3097,11 @@ "node": ">=8" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ad73fdb..6ee0100 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@atlaskit/pragmatic-drag-and-drop": "^1.1.7", "@clerk/nextjs": "^4.29.12", "@clerk/themes": "^1.7.12", "@hookform/resolvers": "^3.3.4",