From d1420ed0711bee60d0295d64cf6fc2aa4b4b8f37 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 16 Feb 2026 18:30:42 -0800 Subject: [PATCH] feat(desktop): replace Clone Repository with unified New Project dialog Add a New Project dialog with three tabs (Empty, Clone, Template) replacing the single-purpose Clone Repository dialog. Extract shared initGitRepo helper and useProjectCreationHandler hook to eliminate duplication. --- .../src/lib/trpc/routers/projects/projects.ts | 260 +++++++++++++++--- .../components/StartView/CloneRepoDialog.tsx | 106 ------- .../NewProjectDialog/NewProjectDialog.tsx | 79 ++++++ .../components/CloneRepoTab/CloneRepoTab.tsx | 65 +++++ .../components/CloneRepoTab/index.ts | 1 + .../components/EmptyRepoTab/EmptyRepoTab.tsx | 66 +++++ .../components/EmptyRepoTab/index.ts | 1 + .../TemplateRepoTab/TemplateRepoTab.tsx | 136 +++++++++ .../components/TemplateRepoTab/index.ts | 1 + .../StartView/NewProjectDialog/constants.ts | 42 +++ .../hooks/useProjectCreationHandler/index.ts | 1 + .../useProjectCreationHandler.ts | 45 +++ .../StartView/NewProjectDialog/index.ts | 1 + .../main/components/StartView/index.tsx | 20 +- .../WorkspaceSidebarFooter.tsx | 32 +-- 15 files changed, 682 insertions(+), 174 deletions(-) delete mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/NewProjectDialog.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/CloneRepoTab/CloneRepoTab.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/CloneRepoTab/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/EmptyRepoTab/EmptyRepoTab.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/EmptyRepoTab/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/TemplateRepoTab/TemplateRepoTab.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/TemplateRepoTab/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/constants.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/hooks/useProjectCreationHandler/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/hooks/useProjectCreationHandler/useProjectCreationHandler.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/index.ts diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 107b5e1a996..8122b870224 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -1,5 +1,5 @@ import { existsSync, statSync } from "node:fs"; -import { access } from "node:fs/promises"; +import { access, mkdir, rm } from "node:fs/promises"; import { basename, join } from "node:path"; import { BRANCH_PREFIX_MODES, @@ -191,6 +191,51 @@ async function ensureMainWorkspace(project: Project): Promise { } } +/** + * Initializes a git repository, creates an initial commit, and returns the default branch name. + * Handles --initial-branch=main fallback for older Git versions and git config error detection. + */ +async function initGitRepo( + repoPath: string, + options?: { stageAll?: boolean; commitMessage?: string }, +): Promise { + const git = simpleGit(repoPath); + + try { + await git.init(["--initial-branch=main"]); + } catch { + await git.init(); + } + + const message = options?.commitMessage ?? "Initial commit"; + + try { + if (options?.stageAll) { + await git.add("."); + await git.commit(message); + } else { + await git.raw(["commit", "--allow-empty", "-m", message]); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + if ( + errorMessage.includes("empty ident") || + errorMessage.includes("user.email") || + errorMessage.includes("user.name") + ) { + throw new Error( + "Git user not configured. Please run:\n" + + ' git config --global user.name "Your Name"\n' + + ' git config --global user.email "you@example.com"', + ); + } + throw new Error(`Failed to create initial commit: ${errorMessage}`); + } + + const branchSummary = await git.branch(); + return branchSummary.current || "main"; +} + // Safe filename regex: letters, numbers, dots, underscores, hyphens, spaces, and common unicode // Allows most valid Git repo names while avoiding path traversal characters const SAFE_REPO_NAME_REGEX = /^[a-zA-Z0-9._\- ]+$/; @@ -577,48 +622,8 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { initGitAndOpen: publicProcedure .input(z.object({ path: z.string() })) .mutation(async ({ input }) => { - const git = simpleGit(input.path); - - // Initialize git repository with 'main' as default branch - // Try with --initial-branch=main (Git 2.28+), fall back to plain init - try { - await git.init(["--initial-branch=main"]); - } catch (err) { - // Likely an older Git version that doesn't support --initial-branch - console.warn( - "Git init with --initial-branch failed, using fallback:", - err, - ); - await git.init(); - } - - // Create initial commit so we have a valid branch ref - try { - await git.raw(["commit", "--allow-empty", "-m", "Initial commit"]); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - // Check for common git config issues - if ( - errorMessage.includes("empty ident") || - errorMessage.includes("user.email") || - errorMessage.includes("user.name") - ) { - throw new Error( - "Git user not configured. Please run:\n" + - ' git config --global user.name "Your Name"\n' + - ' git config --global user.email "you@example.com"', - ); - } - throw new Error(`Failed to create initial commit: ${errorMessage}`); - } - - // Get the current branch name (will be 'main' or 'master' depending on git version/config) - const branchSummary = await git.branch(); - const defaultBranch = branchSummary.current || "main"; - + const defaultBranch = await initGitRepo(input.path); const project = upsertProject(input.path, defaultBranch); - - // Auto-create main workspace if it doesn't exist await ensureMainWorkspace(project); track("project_opened", { @@ -785,6 +790,177 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { } }), + createEmptyRepo: publicProcedure + .input( + z.object({ + name: z + .string() + .min(1) + .refine((val) => SAFE_REPO_NAME_REGEX.test(val), { + message: + "Name can only contain letters, numbers, dots, underscores, hyphens, and spaces", + }), + }), + ) + .mutation(async ({ input }) => { + try { + const window = getWindow(); + if (!window) { + return { + canceled: false as const, + success: false as const, + error: "No window available", + }; + } + + const result = await dialog.showOpenDialog(window, { + properties: ["openDirectory", "createDirectory"], + title: "Select Location for New Repository", + }); + + if (result.canceled || result.filePaths.length === 0) { + return { canceled: true as const, success: false as const }; + } + + const parentDir = result.filePaths[0]; + const repoPath = join(parentDir, input.name); + + if (existsSync(repoPath)) { + return { + canceled: false as const, + success: false as const, + error: `A folder named "${input.name}" already exists at this location.`, + }; + } + + await mkdir(repoPath, { recursive: true }); + + const defaultBranch = await initGitRepo(repoPath); + const project = upsertProject(repoPath, defaultBranch); + await ensureMainWorkspace(project); + + track("project_opened", { + project_id: project.id, + method: "create_empty", + }); + + return { + canceled: false as const, + success: true as const, + project, + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + canceled: false as const, + success: false as const, + error: `Failed to create repository: ${errorMessage}`, + }; + } + }), + + createFromTemplate: publicProcedure + .input( + z.object({ + templateUrl: z + .string() + .min(1) + .refine( + (val) => { + try { + const parsed = new URL(val); + return ALLOWED_URL_PROTOCOLS.has(parsed.protocol); + } catch { + return SSH_GIT_URL_REGEX.test(val); + } + }, + { message: "Must be a valid Git URL (HTTPS or SSH)" }, + ), + name: z + .string() + .trim() + .optional() + .transform((v) => (v && v.length > 0 ? v : undefined)), + }), + ) + .mutation(async ({ input }) => { + try { + const window = getWindow(); + if (!window) { + return { + canceled: false as const, + success: false as const, + error: "No window available", + }; + } + + const result = await dialog.showOpenDialog(window, { + properties: ["openDirectory", "createDirectory"], + title: "Select Location for New Project", + }); + + if (result.canceled || result.filePaths.length === 0) { + return { canceled: true as const, success: false as const }; + } + + const parentDir = result.filePaths[0]; + const repoName = input.name || extractRepoName(input.templateUrl); + + if (!repoName) { + return { + canceled: false as const, + success: false as const, + error: "Could not determine project name from template URL", + }; + } + + const repoPath = join(parentDir, repoName); + + if (existsSync(repoPath)) { + return { + canceled: false as const, + success: false as const, + error: `A folder named "${repoName}" already exists at this location.`, + }; + } + + // Clone the template repo (shallow), then strip its history + const git = simpleGit(); + await git.clone(input.templateUrl, repoPath, ["--depth", "1"]); + await rm(join(repoPath, ".git"), { + recursive: true, + force: true, + }); + + const defaultBranch = await initGitRepo(repoPath, { + stageAll: true, + commitMessage: "Initial commit from template", + }); + const project = upsertProject(repoPath, defaultBranch); + await ensureMainWorkspace(project); + + track("project_opened", { + project_id: project.id, + method: "create_from_template", + }); + + return { + canceled: false as const, + success: true as const, + project, + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + canceled: false as const, + success: false as const, + error: `Failed to create project from template: ${errorMessage}`, + }; + } + }), + update: publicProcedure .input( z.object({ diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx deleted file mode 100644 index 8b404d736e1..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/StartView/CloneRepoDialog.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Button } from "@superset/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@superset/ui/dialog"; -import { Input } from "@superset/ui/input"; -import { useState } from "react"; -import { electronTrpc } from "renderer/lib/electron-trpc"; -import { useCreateWorkspace } from "renderer/react-query/workspaces"; - -interface CloneRepoDialogProps { - isOpen: boolean; - onClose: () => void; - onError: (error: string) => void; -} - -export function CloneRepoDialog({ - isOpen, - onClose, - onError, -}: CloneRepoDialogProps) { - const [url, setUrl] = useState(""); - const utils = electronTrpc.useUtils(); - const cloneRepo = electronTrpc.projects.cloneRepo.useMutation(); - const createWorkspace = useCreateWorkspace(); - - const isLoading = cloneRepo.isPending || createWorkspace.isPending; - - const handleClone = async () => { - if (!url.trim()) { - onError("Please enter a repository URL"); - return; - } - - cloneRepo.mutate( - { url: url.trim() }, - { - onSuccess: (result) => { - if (result.canceled) { - return; - } - - if (result.success && result.project) { - utils.projects.getRecents.invalidate(); - createWorkspace.mutate({ projectId: result.project.id }); - onClose(); - setUrl(""); - } else if (!result.success) { - onError(result.error ?? "Failed to clone repository"); - } - }, - onError: (err) => { - onError(err.message || "Failed to clone repository"); - }, - }, - ); - }; - - return ( - !open && onClose()}> - - - Clone Repository - - Enter a repository URL to clone it locally. - - - -
- - setUrl(e.target.value)} - placeholder="https:// or git@github.com:user/repo.git" - disabled={isLoading} - onKeyDown={(e) => { - if (e.key === "Enter" && !isLoading) { - handleClone(); - } - }} - autoFocus - /> -
- - - - - -
-
- ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/NewProjectDialog.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/NewProjectDialog.tsx new file mode 100644 index 00000000000..1c376674fac --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/NewProjectDialog.tsx @@ -0,0 +1,79 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@superset/ui/dialog"; +import { useState } from "react"; +import { CloneRepoTab } from "./components/CloneRepoTab"; +import { EmptyRepoTab } from "./components/EmptyRepoTab"; +import { TemplateRepoTab } from "./components/TemplateRepoTab"; +import type { NewProjectMode } from "./constants"; + +interface NewProjectDialogProps { + isOpen: boolean; + onClose: () => void; + onError: (error: string) => void; +} + +const TABS: { mode: NewProjectMode; label: string }[] = [ + { mode: "empty", label: "Empty" }, + { mode: "clone", label: "Clone" }, + { mode: "template", label: "Template" }, +]; + +export function NewProjectDialog({ + isOpen, + onClose, + onError, +}: NewProjectDialogProps) { + const [mode, setMode] = useState("empty"); + + const handleClose = () => { + onClose(); + setMode("empty"); + }; + + return ( + !open && handleClose()}> + + + New Project + + Create a new project or clone an existing repository. + + + +
+
+ {TABS.map((tab) => ( + + ))} +
+
+ + {mode === "empty" && ( + + )} + {mode === "clone" && ( + + )} + {mode === "template" && ( + + )} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/CloneRepoTab/CloneRepoTab.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/CloneRepoTab/CloneRepoTab.tsx new file mode 100644 index 00000000000..1e117de45e4 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/CloneRepoTab/CloneRepoTab.tsx @@ -0,0 +1,65 @@ +import { Button } from "@superset/ui/button"; +import { Input } from "@superset/ui/input"; +import { useState } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useProjectCreationHandler } from "../../hooks/useProjectCreationHandler"; + +interface CloneRepoTabProps { + onClose: () => void; + onError: (error: string) => void; +} + +export function CloneRepoTab({ onClose, onError }: CloneRepoTabProps) { + const [url, setUrl] = useState(""); + const cloneRepo = electronTrpc.projects.cloneRepo.useMutation(); + const { handleResult, handleError, isCreatingWorkspace } = + useProjectCreationHandler(onClose, onError); + + const isLoading = cloneRepo.isPending || isCreatingWorkspace; + + const handleClone = () => { + if (!url.trim()) { + onError("Please enter a repository URL"); + return; + } + + cloneRepo.mutate( + { url: url.trim() }, + { + onSuccess: (result) => handleResult(result, () => setUrl("")), + onError: handleError, + }, + ); + }; + + return ( +
+
+ + setUrl(e.target.value)} + placeholder="https:// or git@github.com:user/repo.git" + disabled={isLoading} + onKeyDown={(e) => { + if (e.key === "Enter" && !isLoading) { + handleClone(); + } + }} + autoFocus + /> +
+
+ +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/CloneRepoTab/index.ts b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/CloneRepoTab/index.ts new file mode 100644 index 00000000000..3f48db1cc11 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/CloneRepoTab/index.ts @@ -0,0 +1 @@ +export { CloneRepoTab } from "./CloneRepoTab"; diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/EmptyRepoTab/EmptyRepoTab.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/EmptyRepoTab/EmptyRepoTab.tsx new file mode 100644 index 00000000000..dc3ccfdabaf --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/EmptyRepoTab/EmptyRepoTab.tsx @@ -0,0 +1,66 @@ +import { Button } from "@superset/ui/button"; +import { Input } from "@superset/ui/input"; +import { useState } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useProjectCreationHandler } from "../../hooks/useProjectCreationHandler"; + +interface EmptyRepoTabProps { + onClose: () => void; + onError: (error: string) => void; +} + +export function EmptyRepoTab({ onClose, onError }: EmptyRepoTabProps) { + const [name, setName] = useState(""); + const createEmptyRepo = electronTrpc.projects.createEmptyRepo.useMutation(); + const { handleResult, handleError, isCreatingWorkspace } = + useProjectCreationHandler(onClose, onError); + + const isLoading = createEmptyRepo.isPending || isCreatingWorkspace; + + const handleCreate = () => { + const trimmed = name.trim(); + if (!trimmed) { + onError("Please enter a repository name"); + return; + } + + createEmptyRepo.mutate( + { name: trimmed }, + { + onSuccess: (result) => handleResult(result, () => setName("")), + onError: handleError, + }, + ); + }; + + return ( +
+
+ + setName(e.target.value)} + placeholder="my-project" + disabled={isLoading} + onKeyDown={(e) => { + if (e.key === "Enter" && !isLoading) { + handleCreate(); + } + }} + autoFocus + /> +
+
+ +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/EmptyRepoTab/index.ts b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/EmptyRepoTab/index.ts new file mode 100644 index 00000000000..1d45c599563 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/EmptyRepoTab/index.ts @@ -0,0 +1 @@ +export { EmptyRepoTab } from "./EmptyRepoTab"; diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/TemplateRepoTab/TemplateRepoTab.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/TemplateRepoTab/TemplateRepoTab.tsx new file mode 100644 index 00000000000..db0b7ea952a --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/TemplateRepoTab/TemplateRepoTab.tsx @@ -0,0 +1,136 @@ +import { Button } from "@superset/ui/button"; +import { Input } from "@superset/ui/input"; +import { cn } from "@superset/ui/utils"; +import { useState } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { PROJECT_TEMPLATES, type ProjectTemplate } from "../../constants"; +import { useProjectCreationHandler } from "../../hooks/useProjectCreationHandler"; + +interface TemplateRepoTabProps { + onClose: () => void; + onError: (error: string) => void; +} + +export function TemplateRepoTab({ onClose, onError }: TemplateRepoTabProps) { + const [selectedTemplate, setSelectedTemplate] = + useState(null); + const [customUrl, setCustomUrl] = useState(""); + const [nameOverride, setNameOverride] = useState(""); + const createFromTemplate = + electronTrpc.projects.createFromTemplate.useMutation(); + const { handleResult, handleError, isCreatingWorkspace } = + useProjectCreationHandler(onClose, onError); + + const isLoading = createFromTemplate.isPending || isCreatingWorkspace; + + const handleCreate = () => { + const templateUrl = selectedTemplate?.url || customUrl.trim(); + if (!templateUrl) { + onError("Please select a template or enter a custom URL"); + return; + } + + createFromTemplate.mutate( + { + templateUrl, + name: nameOverride.trim() || undefined, + }, + { + onSuccess: (result) => + handleResult(result, () => { + setSelectedTemplate(null); + setCustomUrl(""); + setNameOverride(""); + }), + onError: handleError, + }, + ); + }; + + return ( +
+
+ {PROJECT_TEMPLATES.map((template) => ( + + ))} +
+ +
+ + { + setCustomUrl(e.target.value); + if (e.target.value.trim()) { + setSelectedTemplate(null); + } + }} + placeholder="https://github.com/user/template-repo.git" + disabled={isLoading} + onKeyDown={(e) => { + if (e.key === "Enter" && !isLoading) { + handleCreate(); + } + }} + /> +
+ +
+ + setNameOverride(e.target.value)} + placeholder="Derived from template URL if empty" + disabled={isLoading} + onKeyDown={(e) => { + if (e.key === "Enter" && !isLoading) { + handleCreate(); + } + }} + /> +
+ +
+ +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/TemplateRepoTab/index.ts b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/TemplateRepoTab/index.ts new file mode 100644 index 00000000000..c13ff44f77d --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/components/TemplateRepoTab/index.ts @@ -0,0 +1 @@ +export { TemplateRepoTab } from "./TemplateRepoTab"; diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/constants.ts b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/constants.ts new file mode 100644 index 00000000000..f90f87c5511 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/constants.ts @@ -0,0 +1,42 @@ +export type NewProjectMode = "empty" | "clone" | "template"; + +export interface ProjectTemplate { + id: string; + name: string; + description: string; + url: string; + tags?: string[]; +} + +export const PROJECT_TEMPLATES: ProjectTemplate[] = [ + { + id: "nextjs", + name: "Next.js", + description: + "React framework with App Router, TypeScript, and Tailwind CSS", + url: "https://github.com/vercel/next.js/tree/canary/examples/hello-world", + tags: ["react", "typescript"], + }, + { + id: "vite-react", + name: "Vite + React", + description: "Fast React development with Vite, TypeScript, and ESLint", + url: "https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts", + tags: ["react", "typescript"], + }, + { + id: "astro", + name: "Astro", + description: + "Content-focused static site generator with island architecture", + url: "https://github.com/withastro/astro/tree/main/examples/minimal", + tags: ["static", "typescript"], + }, + { + id: "express", + name: "Express", + description: "Minimal Node.js web framework for APIs and web apps", + url: "https://github.com/expressjs/generator", + tags: ["node", "api"], + }, +]; diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/hooks/useProjectCreationHandler/index.ts b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/hooks/useProjectCreationHandler/index.ts new file mode 100644 index 00000000000..323a8ef3605 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/hooks/useProjectCreationHandler/index.ts @@ -0,0 +1 @@ +export { useProjectCreationHandler } from "./useProjectCreationHandler"; diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/hooks/useProjectCreationHandler/useProjectCreationHandler.ts b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/hooks/useProjectCreationHandler/useProjectCreationHandler.ts new file mode 100644 index 00000000000..efe81dca200 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/hooks/useProjectCreationHandler/useProjectCreationHandler.ts @@ -0,0 +1,45 @@ +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useCreateWorkspace } from "renderer/react-query/workspaces"; + +/** + * Shared handler for project creation mutation results. + * All three tabs (Empty, Clone, Template) use the same success/error pattern: + * check canceled → invalidate + createWorkspace + close → surface error. + */ +export function useProjectCreationHandler( + onClose: () => void, + onError: (error: string) => void, +) { + const utils = electronTrpc.useUtils(); + const createWorkspace = useCreateWorkspace(); + + const handleResult = ( + result: { + canceled?: boolean; + success?: boolean; + error?: string | null; + project?: { id: string } | null; + }, + resetState?: () => void, + ) => { + if (result.canceled) return; + if (result.success && result.project) { + utils.projects.getRecents.invalidate(); + createWorkspace.mutate({ projectId: result.project.id }); + onClose(); + resetState?.(); + } else if (!result.success && result.error) { + onError(result.error); + } + }; + + const handleError = (err: { message?: string }) => { + onError(err.message || "Operation failed"); + }; + + return { + handleResult, + handleError, + isCreatingWorkspace: createWorkspace.isPending, + }; +} diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/index.ts b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/index.ts new file mode 100644 index 00000000000..539d94095fd --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/StartView/NewProjectDialog/index.ts @@ -0,0 +1 @@ +export { NewProjectDialog } from "./NewProjectDialog"; diff --git a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx index cc2af19ae9a..65ad3a515b1 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx @@ -9,8 +9,8 @@ import { useOpenNew, } from "renderer/react-query/projects"; import { SupersetLogo } from "renderer/routes/sign-in/components/SupersetLogo"; -import { CloneRepoDialog } from "./CloneRepoDialog"; import { InitGitDialog } from "./InitGitDialog"; +import { NewProjectDialog } from "./NewProjectDialog"; export function StartView() { const navigate = useNavigate(); @@ -25,7 +25,7 @@ export function StartView() { const [pendingNavigateProjectId, setPendingNavigateProjectId] = useState< string | null >(null); - const [isCloneDialogOpen, setIsCloneDialogOpen] = useState(false); + const [isNewProjectDialogOpen, setIsNewProjectDialogOpen] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const isLoading = openNew.isPending || openFromPath.isPending; @@ -190,7 +190,7 @@ export function StartView() { [openFromPath, isLoading, navigate], ); - const handleCloneError = (errorMessage: string) => { + const handleNewProjectError = (errorMessage: string) => { setError(errorMessage); }; @@ -256,16 +256,16 @@ export function StartView() { )} > - Don't have a local repo? + Or start a new project @@ -307,10 +307,10 @@ export function StartView() { }} /> - setIsCloneDialogOpen(false)} - onError={handleCloneError} + setIsNewProjectDialogOpen(false)} + onError={handleNewProjectError} /> ); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx index 70ddf1a7590..f5ded828359 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx @@ -14,7 +14,7 @@ import { useOpenNew, } from "renderer/react-query/projects"; import { useCreateBranchWorkspace } from "renderer/react-query/workspaces"; -import { CloneRepoDialog } from "../StartView/CloneRepoDialog"; +import { NewProjectDialog } from "../StartView/NewProjectDialog"; import { STROKE_WIDTH } from "./constants"; interface WorkspaceSidebarFooterProps { @@ -26,7 +26,7 @@ export function WorkspaceSidebarFooter({ }: WorkspaceSidebarFooterProps) { const openNew = useOpenNew(); const createBranchWorkspace = useCreateBranchWorkspace(); - const [isCloneDialogOpen, setIsCloneDialogOpen] = useState(false); + const [isNewProjectDialogOpen, setIsNewProjectDialogOpen] = useState(false); const handleOpenProject = async () => { try { @@ -70,8 +70,8 @@ export function WorkspaceSidebarFooter({ } }; - const handleCloneError = (error: string) => { - toast.error("Failed to clone repository", { + const handleNewProjectError = (error: string) => { + toast.error("Failed to create project", { description: error, }); }; @@ -110,19 +110,19 @@ export function WorkspaceSidebarFooter({ Open project setIsCloneDialogOpen(true)} + onClick={() => setIsNewProjectDialogOpen(true)} disabled={isLoading} > - Clone repo + New project - setIsCloneDialogOpen(false)} - onError={handleCloneError} + setIsNewProjectDialogOpen(false)} + onError={handleNewProjectError} /> ); @@ -149,19 +149,19 @@ export function WorkspaceSidebarFooter({ Open project setIsCloneDialogOpen(true)} + onClick={() => setIsNewProjectDialogOpen(true)} disabled={isLoading} > - Clone repo + New project - setIsCloneDialogOpen(false)} - onError={handleCloneError} + setIsNewProjectDialogOpen(false)} + onError={handleNewProjectError} /> );