diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx index 894f9e7e2b3..197e01342f8 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx @@ -14,15 +14,23 @@ import { cn } from "@superset/ui/utils"; import { useNavigate, useParams } from "@tanstack/react-router"; import { useState } from "react"; import { HiChevronRight, HiMiniPlus } from "react-icons/hi2"; -import { LuFolderOpen, LuPalette, LuSettings, LuX } from "react-icons/lu"; +import { + LuFolderOpen, + LuPalette, + LuPencil, + LuSettings, + LuX, +} from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useUpdateProject } from "renderer/react-query/projects/useUpdateProject"; import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { useProjectRename } from "renderer/screens/main/hooks/useProjectRename"; import { PROJECT_COLOR_DEFAULT, PROJECT_COLORS, } from "shared/constants/project-colors"; import { STROKE_WIDTH } from "../constants"; +import { RenameInput } from "../RenameInput"; import { CloseProjectDialog } from "./CloseProjectDialog"; import { ProjectThumbnail } from "./ProjectThumbnail"; @@ -57,6 +65,7 @@ export function ProjectHeader({ const navigate = useNavigate(); const params = useParams({ strict: false }) as { workspaceId?: string }; const [isCloseDialogOpen, setIsCloseDialogOpen] = useState(false); + const rename = useProjectRename(projectId, projectName); const closeProject = electronTrpc.projects.close.useMutation({ onMutate: async ({ id }) => { @@ -200,6 +209,11 @@ export function ProjectHeader({ + + + Rename + + {/* Main clickable area */} - + {rename.isRenaming ? ( +
+ + +
+ ) : ( + + )} {/* Add workspace button */} @@ -304,6 +337,11 @@ export function ProjectHeader({ + + + Rename + + Open in Finder diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/RenameInput/RenameInput.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/RenameInput/RenameInput.tsx new file mode 100644 index 00000000000..09633e6db8d --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/RenameInput/RenameInput.tsx @@ -0,0 +1,55 @@ +import { useEffect, useRef } from "react"; + +interface RenameInputProps { + value: string; + onChange: (value: string) => void; + onSubmit: () => void; + onCancel: () => void; + className?: string; +} + +export function RenameInput({ + value, + onChange, + onSubmit, + onCancel, + className, +}: RenameInputProps) { + const inputRef = useRef(null); + + useEffect(() => { + // Delay to allow context menu to fully close + const timer = setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, 100); + return () => clearTimeout(timer); + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation(); + if (e.key === "Enter") { + e.preventDefault(); + onSubmit(); + } else if (e.key === "Escape") { + e.preventDefault(); + onCancel(); + } + }; + + return ( + onChange(e.target.value)} + onBlur={onSubmit} + onKeyDown={handleKeyDown} + onClick={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + className={className} + /> + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/RenameInput/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/RenameInput/index.ts new file mode 100644 index 00000000000..987187bef58 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/RenameInput/index.ts @@ -0,0 +1 @@ +export { RenameInput } from "./RenameInput"; diff --git a/apps/desktop/src/renderer/screens/main/hooks/useProjectRename/index.ts b/apps/desktop/src/renderer/screens/main/hooks/useProjectRename/index.ts new file mode 100644 index 00000000000..6ad5a3f5e92 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/hooks/useProjectRename/index.ts @@ -0,0 +1 @@ +export { useProjectRename } from "./useProjectRename"; diff --git a/apps/desktop/src/renderer/screens/main/hooks/useProjectRename/useProjectRename.ts b/apps/desktop/src/renderer/screens/main/hooks/useProjectRename/useProjectRename.ts new file mode 100644 index 00000000000..f1bea2b0bee --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/hooks/useProjectRename/useProjectRename.ts @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { useUpdateProject } from "renderer/react-query/projects/useUpdateProject"; + +export function useProjectRename(projectId: string, projectName: string) { + const [isRenaming, setIsRenaming] = useState(false); + const [renameValue, setRenameValue] = useState(projectName); + const updateProject = useUpdateProject(); + + useEffect(() => { + setRenameValue(projectName); + }, [projectName]); + + const startRename = () => { + setIsRenaming(true); + }; + + const submitRename = () => { + const trimmedValue = renameValue.trim(); + if (trimmedValue && trimmedValue !== projectName) { + updateProject.mutate({ + id: projectId, + patch: { name: trimmedValue }, + }); + } else { + setRenameValue(projectName); + } + setIsRenaming(false); + }; + + const cancelRename = () => { + setRenameValue(projectName); + setIsRenaming(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + submitRename(); + } else if (e.key === "Escape") { + e.preventDefault(); + cancelRename(); + } + }; + + return { + isRenaming, + renameValue, + setRenameValue, + startRename, + submitRename, + cancelRename, + handleKeyDown, + }; +}