Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 57 additions & 13 deletions src/components/FileSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface FileSidebarProps {
pkg?: Package
isLoadingFiles?: boolean
loadingProgress?: string | null
preservedDirectories: Set<string>
}

const FileSidebar: React.FC<FileSidebarProps> = ({
Expand All @@ -45,6 +46,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
pkg,
isLoadingFiles = true,
loadingProgress = null,
preservedDirectories,
}) => {
const [sidebarOpen, setSidebarOpen] = fileSidebarState
const [newFileName, setNewFileName] = useState("")
Expand All @@ -54,7 +56,10 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
string | null
>(null)
const [openDropdownId, setOpenDropdownId] = useState<string | null>(null)
const selectedItemId = useMemo(() => currentFile || "", [currentFile])
const selectedItemId = useMemo(() => {
if (selectedFolderForCreation) return selectedFolderForCreation
return currentFile || ""
}, [currentFile, selectedFolderForCreation])
const canModifyFiles = Boolean(pkg) && !isLoadingFiles

const onFolderSelect = (folderPath: string) => {
Expand All @@ -75,6 +80,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
setSelectedFolderForCreation,
openDropdownId,
setOpenDropdownId,
preservedDirectories,
})

const getCurrentFolderPath = (): string => {
Expand All @@ -95,30 +101,40 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
return hasLeadingSlash ? `/${folderPath}` : folderPath
}

return hasLeadingSlash ? "/" : ""
return hasLeadingSlash ? "/" : "/" // Default to root slash if project is slashed
}

const constructFilePath = (fileName: string): string => {
const trimmedFileName = fileName.trim()
let trimmedFileName = fileName.trim()

if (!trimmedFileName) {
return ""
}

trimmedFileName = trimmedFileName.replace(/\/+/g, "/")

const currentFolder = getCurrentFolderPath()

if (trimmedFileName.startsWith("/")) {
return trimmedFileName
}

if (!currentFolder || currentFolder === "/") {
const result =
currentFolder === "/" ? `/${trimmedFileName}` : trimmedFileName
return result
return currentFolder === "/" ? `/${trimmedFileName}` : trimmedFileName
}

const result = `${currentFolder}/${trimmedFileName}`
return result
const normFolder = currentFolder.replace(/^\/|\/$/g, "")
const normFileName = trimmedFileName.replace(/^\/|\/$/g, "")

if (
normFileName === normFolder ||
normFileName.startsWith(`${normFolder}/`)
) {
const hasLeadingSlash = currentFolder.startsWith("/")
return hasLeadingSlash ? `/${normFileName}` : normFileName
}

return `${currentFolder}/${trimmedFileName}`
}
const handleCreateFileInline = () => {
const finalFileName = constructFilePath(newFileName)
Expand Down Expand Up @@ -169,15 +185,32 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
!sidebarOpen ? "w-0 overflow-hidden" : "w-[14rem]",
className,
)}
onClick={(e) => {
if (e.target === e.currentTarget) {
setSelectedFolderForCreation("/")
onFileSelect("")
}
}}
>
<div className="flex items-center justify-between px-2 py-2">
<div
className="flex items-center justify-between px-2 py-2"
onClick={(e) => {
if (e.target === e.currentTarget) {
setSelectedFolderForCreation("/")
onFileSelect("")
}
}}
>
<button
onClick={toggleSidebar}
onClick={(e) => {
e.stopPropagation()
toggleSidebar()
}}
className={`text-gray-400 scale-90 transition-opacity duration-200 ${!sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"}`}
>
<PanelRightOpen />
</button>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
{isLoadingFiles && (
<div className="flex items-center gap-1">
<Loader2 className="w-3 h-3 animate-spin text-gray-400" />
Expand All @@ -196,7 +229,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
</div>
</div>
{isCreatingFile && (
<div className="p-2">
<div className="p-2" onClick={(e) => e.stopPropagation()}>
<Input
autoFocus
value={newFileName}
Expand Down Expand Up @@ -251,12 +284,23 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
</div>
</div>
)}
<div className="flex-1 border-t h-full">
<div
className="flex-1 border-t h-full overflow-y-auto"
onClick={(e) => {
if (e.target === e.currentTarget) {
setSelectedFolderForCreation("/")
onFileSelect("")
}
}}
>
<TreeView
data={treeData}
setSelectedItemId={(value) => {
if (value && files[value]) {
onFileSelect(value)
} else if (!value) {
onFileSelect("")
setSelectedFolderForCreation(null)
}
}}
selectedItemId={selectedItemId}
Expand Down
16 changes: 13 additions & 3 deletions src/components/dialogs/view-ts-files-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import {
Copy,
Check,
FileText,
Code2,
FileCode,
Braces,
Menu,
Settings2,
} from "lucide-react"
import JSZip from "jszip"
import { saveAs } from "file-saver"
Expand Down Expand Up @@ -120,13 +122,21 @@ export const ViewTsFilesDialog: React.FC<ViewTsFilesDialogProps> = ({
}, [files, searchTerm])

const getFileIcon = (filename: string) => {
if (filename === "package.json") {
return <Settings2 className="w-4 h-4 text-gray-500" />
}
const ext = filename.split(".").pop()?.toLowerCase()
switch (ext) {
case "ts":
case "tsx":
return <Code2 className="w-4 h-4 text-blue-500" />
case "js":
case "jsx":
case "css":
return <FileCode className="w-4 h-4 text-blue-500" />
case "json":
return <FileText className="w-4 h-4 text-yellow-500" />
return <Braces className="w-4 h-4 text-yellow-500" />
case "md":
return <FileText className="w-4 h-4 text-gray-500" />
default:
return <File className="w-4 h-4 text-gray-500" />
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/package-port/CodeAndPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function CodeAndPreview({ pkg, projectUrl, isPackageFetched }: Props) {
initialFiles,
renameFile,
packageFilesMeta,
preservedDirectories,
} = useFileManagement({
templateCode: templateFromUrl?.code,
currentPackage: pkg,
Expand Down Expand Up @@ -258,6 +259,7 @@ export function CodeAndPreview({ pkg, projectUrl, isPackageFetched }: Props) {
)
}}
pkgFilesLoaded={!isLoading}
preservedDirectories={preservedDirectories}
/>
</div>
<div
Expand Down
3 changes: 3 additions & 0 deletions src/components/package-port/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const CodeEditor = ({
isFullyLoaded = false,
totalFilesCount = 0,
loadedFilesCount = 0,
preservedDirectories = new Set<string>(),
}: {
onCodeChange: (code: string, filename?: string) => void
files: PackageFile[]
Expand All @@ -97,6 +98,7 @@ export const CodeEditor = ({
isFullyLoaded?: boolean
totalFilesCount?: number
loadedFilesCount?: number
preservedDirectories: Set<string>
}) => {
const editorRef = useRef<HTMLDivElement>(null)
const viewRef = useRef<EditorView | null>(null)
Expand Down Expand Up @@ -821,6 +823,7 @@ export const CodeEditor = ({
loadingProgress={
totalFilesCount > 0 ? `${loadedFilesCount}/${totalFilesCount}` : null
}
preservedDirectories={preservedDirectories}
/>
<div className="flex flex-col flex-1 w-full min-w-0 h-full">
{showImportAndFormatButtons && (
Expand Down
4 changes: 4 additions & 0 deletions src/components/ui/tree-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
onDrop={(e) => {
handleDrop({ id: "", name: "parent_div" })
}}
onClick={(e) => {
e.stopPropagation()
handleSelectChange(undefined)
}}
></div>
</div>
)
Expand Down
122 changes: 122 additions & 0 deletions src/hooks/useDeleteFilesFromDirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useState, useEffect } from "react"
import type { PackageFile } from "@/types/package"
import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"

export interface IDeleteDirectoryProps {
directoryPath: string
onError: (error: Error) => void
}

export interface IDeleteDirectoryResult {
deleted: boolean
}

function getAncestorDirectories(dirPath: string): string[] {
const hasLeadingSlash = dirPath.startsWith("/")
const normalized = hasLeadingSlash ? dirPath.slice(1) : dirPath
const parts = normalized.split("/").filter(Boolean)
const ancestors: string[] = []
for (let i = 1; i < parts.length; i++) {
const ancestor = parts.slice(0, i).join("/")
ancestors.push(hasLeadingSlash ? `/${ancestor}` : ancestor)
}
return ancestors
}

export function useDeleteFilesFromDirectory({
localFiles,
setLocalFiles,
currentFile,
onFileSelect,
}: {
localFiles: PackageFile[]
setLocalFiles: (files: PackageFile[]) => void
currentFile: string | null
onFileSelect: (path: string) => void
}) {
const [preservedDirectories, setPreservedDirectories] = useState<
Set<string>
>(new Set())


useEffect(() => {
if (!localFiles) return
setPreservedDirectories((prev) => {
if (prev.size === 0) return prev
const next = new Set(prev)
let changed = false
for (const dir of prev) {
const prefix = dir.endsWith("/") ? dir : dir + "/"
const hasRealContent = localFiles.some((f) =>
f.path.startsWith(prefix),
)
if (hasRealContent) {
next.delete(dir)
changed = true
}
}
return changed ? next : prev
})
}, [localFiles])

const deleteDirectory = ({
directoryPath,
onError,
}: IDeleteDirectoryProps): IDeleteDirectoryResult => {
const dirPrefix = directoryPath.endsWith("/")
? directoryPath
: directoryPath + "/"
const hasFiles = localFiles.some((file) =>
file.path.startsWith(dirPrefix),
)

if (!hasFiles) {
if (preservedDirectories.has(directoryPath)) {
setPreservedDirectories(
(prev) => new Set([...prev].filter((d) => d !== directoryPath)),
)
return { deleted: true }
}
onError(new Error("Directory does not exist"))
return { deleted: false }
}

const updatedFiles = localFiles.filter(
(file) => !file.path.startsWith(dirPrefix),
)

const ancestors = getAncestorDirectories(directoryPath)
const emptyAncestors = ancestors.filter((ancestor) => {
const prefix = ancestor.endsWith("/") ? ancestor : ancestor + "/"
return !updatedFiles.some((file) => file.path.startsWith(prefix))
})

if (emptyAncestors.length > 0) {
setPreservedDirectories((prev) => {
const next = new Set(prev)
emptyAncestors.forEach((a) => next.add(a))
next.delete(directoryPath)
return next
})
} else if (preservedDirectories.has(directoryPath)) {
setPreservedDirectories(
(prev) => new Set([...prev].filter((d) => d !== directoryPath)),
)
}

setLocalFiles(updatedFiles)

if (currentFile?.startsWith(dirPrefix)) {
onFileSelect(
updatedFiles.filter((file) => !isHiddenFile(file.path))[0]?.path || "",
)
}

return { deleted: true }
}

return {
deleteDirectory,
preservedDirectories,
}
}
Loading
Loading