working file renaming

This commit is contained in:
Ishaan Dey 2024-04-27 14:23:09 -04:00
parent 76b2fc7e0f
commit 676f88a7ce
10 changed files with 252 additions and 22 deletions

View File

@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}); });
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getSandboxFiles = void 0;
const getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () { const getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () {
const sandboxRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`); const sandboxRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`);
const sandboxData = yield sandboxRes.json(); const sandboxData = yield sandboxRes.json();
@ -17,6 +18,7 @@ const getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () {
// console.log("processedFiles.fileData:", processedFiles.fileData) // console.log("processedFiles.fileData:", processedFiles.fileData)
return processedFiles; return processedFiles;
}); });
exports.getSandboxFiles = getSandboxFiles;
const processFiles = (paths, id) => __awaiter(void 0, void 0, void 0, function* () { const processFiles = (paths, id) => __awaiter(void 0, void 0, void 0, function* () {
const root = { id: "/", type: "folder", name: "/", children: [] }; const root = { id: "/", type: "folder", name: "/", children: [] };
const fileData = []; const fileData = [];
@ -75,4 +77,3 @@ const fetchFileContent = (fileId) => __awaiter(void 0, void 0, void 0, function*
return ""; return "";
} }
}); });
exports.default = getSandboxFiles;

View File

@ -17,7 +17,7 @@ const dotenv_1 = __importDefault(require("dotenv"));
const http_1 = require("http"); const http_1 = require("http");
const socket_io_1 = require("socket.io"); const socket_io_1 = require("socket.io");
const zod_1 = require("zod"); const zod_1 = require("zod");
const getSandboxFiles_1 = __importDefault(require("./getSandboxFiles")); const utils_1 = require("./utils");
dotenv_1.default.config(); dotenv_1.default.config();
const app = (0, express_1.default)(); const app = (0, express_1.default)();
const port = process.env.PORT || 4000; const port = process.env.PORT || 4000;
@ -64,15 +64,22 @@ io.use((socket, next) => __awaiter(void 0, void 0, void 0, function* () {
})); }));
io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () { io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
const data = socket.data; const data = socket.data;
const sandboxFiles = yield (0, getSandboxFiles_1.default)(data.id); const sandboxFiles = yield (0, utils_1.getSandboxFiles)(data.id);
socket.emit("loaded", sandboxFiles.files); socket.emit("loaded", sandboxFiles.files);
socket.on("getFile", (fileId, callback) => { socket.on("getFile", (fileId, callback) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId); const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) if (!file)
return; return;
// console.log("file " + file.id + ": ", file.data) console.log("file " + file.id + ": ", file.data);
callback(file.data); callback(file.data);
}); });
socket.on("renameFile", (fileId, newName) => __awaiter(void 0, void 0, void 0, function* () {
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file)
return;
yield (0, utils_1.renameFile)(fileId, newName, file.data);
file.id = newName;
}));
})); }));
httpServer.listen(port, () => { httpServer.listen(port, () => {
console.log(`Server running on port ${port}`); console.log(`Server running on port ${port}`);

92
backend/server/dist/utils.js vendored Normal file
View File

@ -0,0 +1,92 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.renameFile = exports.getSandboxFiles = void 0;
const getSandboxFiles = (id) => __awaiter(void 0, void 0, void 0, function* () {
const sandboxRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`);
const sandboxData = yield sandboxRes.json();
const paths = sandboxData.objects.map((obj) => obj.key);
const processedFiles = yield processFiles(paths, id);
// console.log("processedFiles.fileData:", processedFiles.fileData)
return processedFiles;
});
exports.getSandboxFiles = getSandboxFiles;
const processFiles = (paths, id) => __awaiter(void 0, void 0, void 0, function* () {
const root = { id: "/", type: "folder", name: "/", children: [] };
const fileData = [];
paths.forEach((path) => {
const allParts = path.split("/");
if (allParts[1] !== id) {
console.log("invalid path!!!!");
return;
}
const parts = allParts.slice(2);
let current = root;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const isFile = i === parts.length - 1 && part.includes(".");
const existing = current.children.find((child) => child.name === part);
if (existing) {
if (!isFile) {
current = existing;
}
}
else {
if (isFile) {
const file = { id: path, type: "file", name: part };
current.children.push(file);
fileData.push({ id: path, data: "" });
}
else {
const folder = {
id: path, // issue todo: for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css
type: "folder",
name: part,
children: [],
};
current.children.push(folder);
current = folder;
}
}
}
});
yield Promise.all(fileData.map((file) => __awaiter(void 0, void 0, void 0, function* () {
const data = yield fetchFileContent(file.id);
file.data = data;
})));
return {
files: root.children,
fileData,
};
});
const fetchFileContent = (fileId) => __awaiter(void 0, void 0, void 0, function* () {
try {
const fileRes = yield fetch(`https://storage.ishaan1013.workers.dev/api?fileId=${fileId}`);
return yield fileRes.text();
}
catch (error) {
console.error("ERROR fetching file:", error);
return "";
}
});
const renameFile = (fileId, newName, data) => __awaiter(void 0, void 0, void 0, function* () {
const parts = fileId.split("/");
const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName;
const res = yield fetch(`https://storage.ishaan1013.workers.dev/api/rename`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ fileId, newFileId, data }),
});
return res.ok;
});
exports.renameFile = renameFile;

View File

@ -5,7 +5,7 @@ import { Server } from "socket.io"
import { z } from "zod" import { z } from "zod"
import { User } from "./types" import { User } from "./types"
import getSandboxFiles from "./getSandboxFiles" import { getSandboxFiles, renameFile } from "./utils"
dotenv.config() dotenv.config()
@ -75,9 +75,16 @@ io.on("connection", async (socket) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId) const file = sandboxFiles.fileData.find((f) => f.id === fileId)
if (!file) return if (!file) return
// console.log("file " + file.id + ": ", file.data) console.log("file " + file.id + ": ", file.data)
callback(file.data) callback(file.data)
}) })
socket.on("renameFile", async (fileId: string, newName: string) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId)
if (!file) return
await renameFile(fileId, newName, file.data)
file.id = newName
})
}) })
httpServer.listen(port, () => { httpServer.listen(port, () => {

View File

@ -8,7 +8,7 @@ import {
User, User,
} from "./types" } from "./types"
const getSandboxFiles = async (id: string) => { export const getSandboxFiles = async (id: string) => {
const sandboxRes = await fetch( const sandboxRes = await fetch(
`https://storage.ishaan1013.workers.dev/api?sandboxId=${id}` `https://storage.ishaan1013.workers.dev/api?sandboxId=${id}`
) )
@ -50,7 +50,7 @@ const processFiles = async (paths: string[], id: string) => {
fileData.push({ id: path, data: "" }) fileData.push({ id: path, data: "" })
} else { } else {
const folder: TFolder = { const folder: TFolder = {
id: path, id: path, // issue todo: for example, folder "src" ID is: projects/a7vgttfqbgy403ratp7du3ln/src/App.css
type: "folder", type: "folder",
name: part, name: part,
children: [], children: [],
@ -87,4 +87,20 @@ const fetchFileContent = async (fileId: string): Promise<string> => {
} }
} }
export default getSandboxFiles export const renameFile = async (
fileId: string,
newName: string,
data: string
) => {
const parts = fileId.split("/")
const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName
const res = await fetch(`https://storage.ishaan1013.workers.dev/api/rename`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ fileId, newFileId, data }),
})
return res.ok
}

View File

@ -43,6 +43,20 @@ export default {
} else if (method === 'POST') { } else if (method === 'POST') {
return new Response('Hello, world!'); return new Response('Hello, world!');
} else return methodNotAllowed; } else return methodNotAllowed;
} else if (path === '/api/rename' && method === 'POST') {
const renameSchema = z.object({
fileId: z.string(),
newFileId: z.string(),
data: z.string(),
});
const body = await request.json();
const { fileId, newFileId, data } = renameSchema.parse(body);
await env.R2.delete(fileId);
await env.R2.put(newFileId, data);
return success;
} else if (path === '/api/init' && method === 'POST') { } else if (path === '/api/init' && method === 'POST') {
const initSchema = z.object({ const initSchema = z.object({
sandboxId: z.string(), sandboxId: z.string(),

View File

@ -86,6 +86,7 @@ export default function CodeEditor({
setTabs((prev) => { setTabs((prev) => {
const exists = prev.find((t) => t.id === tab.id) const exists = prev.find((t) => t.id === tab.id)
if (exists) { if (exists) {
// console.log("exists")
setActiveId(exists.id) setActiveId(exists.id)
return prev return prev
} }
@ -116,16 +117,45 @@ export default function CodeEditor({
setTabs((prev) => prev.filter((t) => t.id !== tab.id)) setTabs((prev) => prev.filter((t) => t.id !== tab.id))
} }
const handleFileNameChange = (id: string, newName: string) => { // Note: add renaming validation:
// In general: must not contain / or \ or whitespace, not empty, no duplicates
// Files: must contain dot
// Folders: must not contain dot
const handleRename = (
id: string,
newName: string,
oldName: string,
type: "file" | "folder"
) => {
// Validation
if (
newName === oldName ||
newName.includes("/") ||
newName.includes("\\") ||
newName.includes(" ") ||
(type === "file" && !newName.includes(".")) ||
(type === "folder" && newName.includes("."))
) {
return false
}
// Action
socket.emit("renameFile", id, newName) socket.emit("renameFile", id, newName)
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab)) prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
) )
return true
} }
return ( return (
<> <>
<Sidebar files={files} selectFile={selectFile} /> <Sidebar
files={files}
selectFile={selectFile}
handleRename={handleRename}
/>
<ResizablePanelGroup direction="horizontal"> <ResizablePanelGroup direction="horizontal">
<ResizablePanel <ResizablePanel
className="p-2 flex flex-col" className="p-2 flex flex-col"

View File

@ -8,9 +8,16 @@ import { useEffect, useRef, useState } from "react"
export default function SidebarFile({ export default function SidebarFile({
data, data,
selectFile, selectFile,
handleRename,
}: { }: {
data: TFile data: TFile
selectFile: (file: TTab) => void selectFile: (file: TTab) => void
handleRename: (
id: string,
newName: string,
oldName: string,
type: "file" | "folder"
) => boolean
}) { }) {
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)
@ -22,6 +29,19 @@ export default function SidebarFile({
} }
}, [editing]) }, [editing])
const renameFile = () => {
const renamed = handleRename(
data.id,
inputRef.current?.value ?? data.name,
data.name,
"file"
)
if (!renamed && inputRef.current) {
inputRef.current.value = data.name
}
setEditing(false)
}
return ( return (
<button <button
onClick={() => selectFile({ ...data, saved: true })} onClick={() => selectFile({ ...data, saved: true })}
@ -41,18 +61,17 @@ export default function SidebarFile({
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
console.log("submit") renameFile()
setEditing(false)
}} }}
> >
<input <input
ref={inputRef} ref={inputRef}
className={`bg-transparent w-full ${ className={`bg-transparent outline-foreground w-full ${
editing ? "" : "pointer-events-none" editing ? "" : "pointer-events-none"
}`} }`}
disabled={!editing} disabled={!editing}
defaultValue={data.name} defaultValue={data.name}
onBlur={() => setEditing(false)} onBlur={() => renameFile()}
/> />
</form> </form>
</button> </button>

View File

@ -1,7 +1,7 @@
"use client" "use client"
import Image from "next/image" import Image from "next/image"
import { useState } from "react" import { useEffect, useRef, useState } from "react"
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js" import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
import { TFile, TFolder, TTab } from "./types" import { TFile, TFolder, TTab } from "./types"
import SidebarFile from "./file" import SidebarFile from "./file"
@ -9,19 +9,38 @@ import SidebarFile from "./file"
export default function SidebarFolder({ export default function SidebarFolder({
data, data,
selectFile, selectFile,
handleRename,
}: { }: {
data: TFolder data: TFolder
selectFile: (file: TTab) => void selectFile: (file: TTab) => void
handleRename: (
id: string,
newName: string,
oldName: string,
type: "file" | "folder"
) => boolean
}) { }) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const folder = isOpen const folder = isOpen
? getIconForOpenFolder(data.name) ? getIconForOpenFolder(data.name)
: getIconForFolder(data.name) : getIconForFolder(data.name)
const [editing, setEditing] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (editing) {
inputRef.current?.focus()
}
}, [editing])
return ( return (
<> <>
<div <div
onClick={() => setIsOpen((prev) => !prev)} onClick={() => setIsOpen((prev) => !prev)}
onDoubleClick={() => {
setEditing(true)
}}
className="w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary rounded-sm cursor-pointer" className="w-full flex items-center h-7 px-1 transition-colors hover:bg-secondary rounded-sm cursor-pointer"
> >
<Image <Image
@ -31,7 +50,26 @@ export default function SidebarFolder({
height={18} height={18}
className="mr-2" className="mr-2"
/> />
{data.name} <form
onSubmit={(e) => {
e.preventDefault()
console.log("file renamed")
setEditing(false)
}}
>
<input
ref={inputRef}
className={`bg-transparent outline-foreground w-full ${
editing ? "" : "pointer-events-none"
}`}
disabled={!editing}
defaultValue={data.name}
onBlur={() => {
console.log("file renamed")
setEditing(false)
}}
/>
</form>
</div> </div>
{isOpen ? ( {isOpen ? (
<div className="flex w-full items-stretch"> <div className="flex w-full items-stretch">
@ -43,12 +81,14 @@ export default function SidebarFolder({
key={child.id} key={child.id}
data={child} data={child}
selectFile={selectFile} selectFile={selectFile}
handleRename={handleRename}
/> />
) : ( ) : (
<SidebarFolder <SidebarFolder
key={child.id} key={child.id}
data={child} data={child}
selectFile={selectFile} selectFile={selectFile}
handleRename={handleRename}
/> />
) )
)} )}

View File

@ -5,17 +5,19 @@ import SidebarFile from "./file"
import SidebarFolder from "./folder" import SidebarFolder from "./folder"
import { TFile, TFolder, TTab } from "./types" import { TFile, TFolder, TTab } from "./types"
// Note: add renaming validation:
// In general: must not contain / or \ or whitespace, not empty, no duplicates
// Files: must contain dot
// Folders: must not contain dot
export default function Sidebar({ export default function Sidebar({
files, files,
selectFile, selectFile,
handleRename,
}: { }: {
files: (TFile | TFolder)[] files: (TFile | TFolder)[]
selectFile: (tab: TTab) => void selectFile: (tab: TTab) => void
handleRename: (
id: string,
newName: string,
oldName: string,
type: "file" | "folder"
) => boolean
}) { }) {
return ( return (
<div className="h-full w-56 select-none flex flex-col text-sm items-start p-2"> <div className="h-full w-56 select-none flex flex-col text-sm items-start p-2">
@ -45,12 +47,14 @@ export default function Sidebar({
key={child.id} key={child.id}
data={child} data={child}
selectFile={selectFile} selectFile={selectFile}
handleRename={handleRename}
/> />
) : ( ) : (
<SidebarFolder <SidebarFolder
key={child.id} key={child.id}
data={child} data={child}
selectFile={selectFile} selectFile={selectFile}
handleRename={handleRename}
/> />
) )
) )