Skip to content

Commit c7cae51

Browse files
feat(editor)!!: fully refactored package based editor for dx and new features (#1149)
* feat(FileSidebar, CodeEditor, CodeAndPreview): implement file deletion functionality - Added a dropdown menu in FileSidebar for deleting files, integrating the handleDeleteFile function. - Updated CodeAndPreview and CodeEditor components to support file deletion, including necessary state management and error handling. - Introduced DeleteFileProps interface for type safety in file deletion operations. - Enhanced useFileManagement hook to include handleDeleteFile logic, ensuring proper API interaction and user feedback through toast notifications. * refactor(CodeAndPreview, useFileManagement, useUpdatePackageFilesMutation): enhance file management logic - Improved the hasUnsavedChanges logic in CodeAndPreview to account for file count discrepancies. - Refactored handleDeleteFile in useFileManagement to streamline file deletion and state updates. - Added logic in useUpdatePackageFilesMutation to handle deletion of files that no longer exist in the package, ensuring proper API interaction and state management. * Update CodeAndPreview.tsx * refactor(FileSidebar): improve file path handling and variable naming - Updated variable names for clarity, changing 'path' to 'filePath', 'parts' to 'pathSegments', 'current' to 'currentNode', and 'isFile' to 'isLeafNode'. - Enhanced logic for constructing file paths and handling file selection, ensuring consistency and readability. - Adjusted conditions for file and folder identification to improve code maintainability. * refactor: update currentFile type to allow null in multiple components - Changed the type of currentFile from string to string | null in FileSidebar, CodeAndPreview, CodeEditor, and CodeEditorHeader components to handle cases where no file is selected. - Updated related logic to ensure proper handling of null values, improving robustness and preventing potential runtime errors. * refactor(CodeAndPreview, useUpdatePackageFilesMutation): rename initialFilesLoad to initiallyLoadedFiles - Updated the variable name from 'initialFilesLoad' to 'initiallyLoadedFiles' in CodeAndPreview and useUpdatePackageFilesMutation for improved clarity and consistency. - Adjusted related logic to reflect the new naming, ensuring the functionality remains intact while enhancing code readability. * refactor(useUpdatePackageFilesMutation): rename newpackage to newPackage for consistency - Updated the variable name from 'newpackage' to 'newPackage' in useUpdatePackageFilesMutation to enhance clarity and maintain consistency with naming conventions. - Adjusted related logic to ensure the functionality remains intact while improving code readability. * refactor(CodeAndPreview, useFileManagement): consolidate file management logic and enhance save functionality - Moved the handleSave and fsMap functions from CodeAndPreview to useFileManagement for better separation of concerns and improved code organization. - Updated handleSave to utilize the new parameters and ensure proper state management and file saving logic. - Refactored the file existence checks in handleCreateFile and handleDeleteFile to use the packageFilesWithContent prop for consistency. - Enhanced toast notifications for better user feedback during file operations. * Update src/components/package-port/CodeEditorHeader.tsx Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> * 1 - local files amangement * refactor: improve file management logic and enhance code readability across multiple components * test: add test for create_or_update allowing empty content_text in package files * 69 * fix: ensure currentFile is defined before formatting and streamline file extension handling in CodeEditorHeader * fix: correct currentFile access in CodeEditorHeader to ensure proper content retrieval * fix: refine currentFile handling in CodeEditorHeader for improved file type detection and toast messaging * feat: implement toggleSidebar function in FileSidebar for improved sidebar management and reset state * style: update DropdownMenuContent styling in FileSidebar for improved appearance and positioning * style: adjust width of Select component in CodeEditorHeader for better layout consistency * fix: update item ID generation in FileSidebar and ensure TreeActions always reflects selection state * refactor: streamline file selection logic in useFileManagement and update related components for consistency --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
1 parent c880e5d commit c7cae51

10 files changed

Lines changed: 566 additions & 398 deletions

File tree

bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,32 @@ test("create_or_update - 404 for non-existent package", async () => {
278278
}
279279
})
280280

281+
test("create_or_update - allow empty content_text", async () => {
282+
const { axios } = await getTestServer()
283+
284+
const packageResponse = await axios.post("/api/packages/create", {
285+
name: "@test/package-files-create-or-update-error",
286+
description: "A test package for error cases",
287+
})
288+
const createdPackage = packageResponse.data.package
289+
290+
const releaseResponse = await axios.post("/api/package_releases/create", {
291+
package_id: createdPackage.package_id,
292+
version: "1.0.0",
293+
})
294+
const createdRelease = releaseResponse.data.package_release
295+
const response = await axios.post("/api/package_files/create_or_update", {
296+
package_release_id: createdRelease.package_release_id,
297+
file_path: "/test.js",
298+
content_text: "",
299+
})
300+
console.log(response.data.package_file)
301+
expect(response.status).toBe(200)
302+
expect(response.data.ok).toBe(true)
303+
expect(response.data.package_file).toBeDefined()
304+
expect(response.data.package_file.content_text).toBe("")
305+
})
306+
281307
test("create_or_update - 400 for missing content", async () => {
282308
const { axios } = await getTestServer()
283309

fake-snippets-api/routes/api/package_files/create_or_update.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const routeSpec = {
2828
}, "Cannot specify both package_release_id and package_name_with_version")
2929
.refine((v) => {
3030
if (v.content_base64 && v.content_text) return false
31-
if (!v.content_base64 && !v.content_text) return false
31+
if (!v.content_base64 && v.content_text === undefined) return false
3232
return true
3333
}, "Either content_base64 or content_text is required"),
3434
jsonResponse: z.object({
@@ -136,7 +136,7 @@ export default withRouteSpec(routeSpec)(async (req, ctx) => {
136136
exisitingFile.package_file_id,
137137
{
138138
content_text:
139-
content_text ||
139+
content_text ??
140140
(content_base64
141141
? Buffer.from(content_base64, "base64").toString()
142142
: null),
@@ -167,7 +167,7 @@ export default withRouteSpec(routeSpec)(async (req, ctx) => {
167167
package_release_id: packageReleaseId,
168168
file_path,
169169
content_text:
170-
content_text ||
170+
content_text ??
171171
(content_base64
172172
? Buffer.from(content_base64, "base64").toString()
173173
: null),

src/components/FileSidebar.tsx

Lines changed: 111 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
import React, { useState } from "react"
22
import { cn } from "@/lib/utils"
3-
import { File, Folder, PanelRightOpen, Plus } from "lucide-react"
3+
import { File, Folder, MoreVertical, PanelRightOpen, Plus } from "lucide-react"
44
import { TreeView, TreeDataItem } from "@/components/ui/tree-view"
55
import { isHiddenFile } from "./ViewPackagePage/utils/is-hidden-file"
66
import { Input } from "@/components/ui/input"
7-
import { CreateFileProps } from "./package-port/CodeAndPreview"
8-
7+
import {
8+
DropdownMenu,
9+
DropdownMenuContent,
10+
DropdownMenuGroup,
11+
DropdownMenuItem,
12+
DropdownMenuTrigger,
13+
} from "./ui/dropdown-menu"
14+
import type {
15+
ICreateFileProps,
16+
ICreateFileResult,
17+
IDeleteFileProps,
18+
IDeleteFileResult,
19+
} from "@/hooks/useFileManagement"
20+
import { useToast } from "@/hooks/use-toast"
921
type FileName = string
1022

1123
interface FileSidebarProps {
1224
files: Record<FileName, string>
13-
currentFile: FileName
25+
currentFile: FileName | null
1426
onFileSelect: (filename: FileName) => void
1527
className?: string
1628
fileSidebarState: ReturnType<typeof useState<boolean>>
17-
handleCreateFile: (props: CreateFileProps) => void
29+
handleCreateFile: (props: ICreateFileProps) => ICreateFileResult
30+
handleDeleteFile: (props: IDeleteFileProps) => IDeleteFileResult
1831
}
1932

2033
const FileSidebar: React.FC<FileSidebarProps> = ({
@@ -24,11 +37,13 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
2437
className,
2538
fileSidebarState,
2639
handleCreateFile,
40+
handleDeleteFile,
2741
}) => {
2842
const [sidebarOpen, setSidebarOpen] = fileSidebarState
2943
const [newFileName, setNewFileName] = useState("")
3044
const [isCreatingFile, setIsCreatingFile] = useState(false)
3145
const [errorMessage, setErrorMessage] = useState("")
46+
const { toast } = useToast()
3247

3348
const transformFilesToTreeData = (
3449
files: Record<FileName, string>,
@@ -38,38 +53,85 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
3853
}
3954
const root: Record<string, TreeNode> = {}
4055

41-
Object.keys(files).forEach((path) => {
42-
const startsWithSlash = path.startsWith("/")
43-
const parts = (startsWithSlash ? path.slice(1) : path).trim().split("/")
44-
let current = root
56+
Object.keys(files).forEach((filePath) => {
57+
const hasLeadingSlash = filePath.startsWith("/")
58+
const pathSegments = (hasLeadingSlash ? filePath.slice(1) : filePath)
59+
.trim()
60+
.split("/")
61+
let currentNode: Record<string, TreeNode> = root
4562

46-
parts.forEach((part, index) => {
47-
const isFile = index === parts.length - 1
48-
const parentPath = parts.slice(0, index).join("/")
49-
const currentPath = parentPath ? `${parentPath}/${part}` : part
50-
const evaluatedFilePath = startsWithSlash
51-
? `/${currentPath}`
52-
: currentPath
63+
pathSegments.forEach((segment, segmentIndex) => {
64+
const isLeafNode = segmentIndex === pathSegments.length - 1
65+
const ancestorPath = pathSegments.slice(0, segmentIndex).join("/")
66+
const relativePath = ancestorPath
67+
? `${ancestorPath}/${segment}`
68+
: segment
69+
const absolutePath = hasLeadingSlash ? `/${relativePath}` : relativePath
70+
const itemId = absolutePath
5371
if (
54-
!current[part] &&
55-
(!isHiddenFile(currentPath) ||
72+
!currentNode[segment] &&
73+
(!isHiddenFile(relativePath) ||
5674
isHiddenFile(
57-
currentFile.startsWith("/") ? currentFile.slice(1) : currentFile,
75+
currentFile?.startsWith("/")
76+
? currentFile.slice(1)
77+
: currentFile || "",
5878
))
5979
) {
60-
current[part] = {
61-
id: currentPath,
62-
name: isFile ? part : part,
63-
icon: isFile ? File : Folder,
64-
onClick: isFile ? () => onFileSelect(evaluatedFilePath) : undefined,
65-
draggable: isFile,
66-
droppable: !isFile,
67-
children: isFile ? undefined : {},
80+
currentNode[segment] = {
81+
id: itemId,
82+
name: isLeafNode ? segment : segment,
83+
icon: isLeafNode ? File : Folder,
84+
onClick: isLeafNode ? () => onFileSelect(absolutePath) : undefined,
85+
draggable: false,
86+
droppable: !isLeafNode,
87+
children: isLeafNode ? undefined : {},
88+
actions: (
89+
<>
90+
<DropdownMenu key={itemId}>
91+
<DropdownMenuTrigger asChild>
92+
<MoreVertical className="w-4 h-4 text-gray-500 hover:text-gray-700" />
93+
</DropdownMenuTrigger>
94+
<DropdownMenuContent
95+
className="w-48 bg-white shadow-lg rounded-md border-4 z-[100] border-white"
96+
style={{
97+
position: "absolute",
98+
top: "100%",
99+
left: "0",
100+
marginTop: "0.5rem",
101+
width: "8rem",
102+
padding: "0.01rem",
103+
}}
104+
>
105+
<DropdownMenuGroup>
106+
<DropdownMenuItem
107+
onClick={() => {
108+
const { fileDeleted } = handleDeleteFile({
109+
filename: relativePath,
110+
onError: (error) => {
111+
toast({
112+
title: `Error deleting file ${relativePath}`,
113+
description: error.message,
114+
})
115+
},
116+
})
117+
if (fileDeleted) {
118+
setErrorMessage("")
119+
}
120+
}}
121+
className="flex items-center px-4 py-1 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
122+
>
123+
Delete
124+
</DropdownMenuItem>
125+
</DropdownMenuGroup>
126+
</DropdownMenuContent>
127+
</DropdownMenu>
128+
</>
129+
),
68130
}
69131
}
70132

71-
if (!isFile && current[part].children) {
72-
current = current[part].children
133+
if (!isLeafNode && currentNode[segment].children) {
134+
currentNode = currentNode[segment].children
73135
}
74136
})
75137
})
@@ -90,15 +152,26 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
90152
}
91153

92154
const treeData = transformFilesToTreeData(files)
93-
155+
// console.log("treeData", files)
94156
const handleCreateFileInline = () => {
95-
handleCreateFile({
157+
const { newFileCreated } = handleCreateFile({
96158
newFileName,
97-
setErrorMessage,
98-
onFileSelect,
99-
setNewFileName,
100-
setIsCreatingFile,
159+
onError: (error) => {
160+
setErrorMessage(error.message)
161+
},
101162
})
163+
if (newFileCreated) {
164+
setIsCreatingFile(false)
165+
setNewFileName("")
166+
setErrorMessage("")
167+
}
168+
}
169+
170+
const toggleSidebar = () => {
171+
setSidebarOpen(!sidebarOpen)
172+
setErrorMessage("")
173+
setIsCreatingFile(false)
174+
setNewFileName("")
102175
}
103176

104177
return (
@@ -110,7 +183,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
110183
)}
111184
>
112185
<button
113-
onClick={() => setSidebarOpen(!sidebarOpen)}
186+
onClick={toggleSidebar}
114187
className={`z-[99] mt-2 ml-2 text-gray-400 scale-90 transition-opacity duration-200 ${
115188
!sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"
116189
}`}
@@ -129,6 +202,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
129202
<Input
130203
autoFocus
131204
value={newFileName}
205+
spellCheck={false}
132206
onChange={(e) => setNewFileName(e.target.value)}
133207
onBlur={handleCreateFileInline}
134208
onKeyDown={(e) => {
@@ -149,7 +223,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
149223
)}
150224
<TreeView
151225
data={treeData}
152-
initialSelectedItemId={currentFile}
226+
initialSelectedItemId={currentFile || ""}
153227
onSelectChange={(item) => {
154228
if (item?.onClick) {
155229
item.onClick()

0 commit comments

Comments
 (0)