diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 47b740a678d..1cb51869a62 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, @@ -62,29 +62,20 @@ type OpenNewMultiResult = | { canceled: false; multi: true; results: FolderOutcome[] } | OpenNewError; -/** - * Initializes a git repository in the given path with an initial commit. - * Reused by openNew, openFromPath, and initGitAndOpen. - */ async function initGitRepo(path: string): Promise<{ defaultBranch: string }> { const git = simpleGit(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") || @@ -103,11 +94,6 @@ async function initGitRepo(path: string): Promise<{ defaultBranch: string }> { return { defaultBranch }; } -/** - * Creates or updates a project record in the database. - * If a project with the same mainRepoPath exists, updates lastOpenedAt. - * Otherwise, creates a new project. - */ function upsertProject(mainRepoPath: string, defaultBranch: string): Project { const name = basename(mainRepoPath); @@ -140,22 +126,15 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project { return project; } -/** - * Ensures a project has a main (branch) workspace. - * If one doesn't exist, creates it automatically. - * This is called after opening/creating a project to provide a default workspace. - */ async function ensureMainWorkspace(project: Project): Promise { const existingBranchWorkspace = getBranchWorkspace(project.id); - // If branch workspace already exists, just touch it and return if (existingBranchWorkspace) { touchWorkspace(existingBranchWorkspace.id); setLastActiveWorkspace(existingBranchWorkspace.id); return; } - // Get current branch from main repo const branch = await getCurrentBranch(project.mainRepoPath); if (!branch) { console.warn( @@ -164,8 +143,7 @@ async function ensureMainWorkspace(project: Project): Promise { return; } - // Insert new branch workspace with conflict handling for race conditions - // The unique partial index (projectId WHERE type='branch') prevents duplicates + // Unique partial index (projectId WHERE type='branch') prevents duplicates const insertResult = localDb .insert(workspaces) .values({ @@ -181,7 +159,6 @@ async function ensureMainWorkspace(project: Project): Promise { const wasExisting = insertResult.length === 0; - // Only shift existing workspaces if we successfully inserted if (!wasExisting) { const newWorkspaceId = insertResult[0].id; const projectWorkspaces = localDb @@ -205,7 +182,6 @@ async function ensureMainWorkspace(project: Project): Promise { } } - // Get the workspace (either newly created or existing from race condition) const workspace = insertResult[0] ?? getBranchWorkspace(project.id); if (!workspace) { @@ -230,45 +206,33 @@ async function ensureMainWorkspace(project: Project): Promise { } } -// Safe filename regex: letters, numbers, dots, underscores, hyphens, spaces, and common unicode -// Allows most valid Git repo names while avoiding path traversal characters +// Callers must additionally reject dot-only names (".", "..") to prevent path traversal const SAFE_REPO_NAME_REGEX = /^[a-zA-Z0-9._\- ]+$/; const ALLOWED_URL_PROTOCOLS = new Set(["http:", "https:", "ssh:", "git:"]); const SSH_GIT_URL_REGEX = /^[\w.-]+@[\w.-]+:[\w./-]+$/; -/** - * Extracts and validates a repository name from a git URL. - * Handles HTTP/HTTPS URLs, SSH-style URLs (git@host:user/repo), and edge cases. - */ function extractRepoName(urlInput: string): string | null { - // Normalize: trim whitespace and strip trailing slashes let normalized = urlInput.trim().replace(/\/+$/, ""); if (!normalized) return null; let repoSegment: string | undefined; - // Try parsing as HTTP/HTTPS URL first try { const parsed = new URL(normalized); if (parsed.protocol === "http:" || parsed.protocol === "https:") { - // Get pathname and strip query/hash (URL constructor handles this) const pathname = parsed.pathname; repoSegment = pathname.split("/").filter(Boolean).pop(); } } catch { - // Not a valid URL, try SSH-style parsing + // Not a standard URL — fall through to SSH-style parsing } - // Fallback to SSH-style parsing (git@github.com:user/repo.git) if (!repoSegment) { - // Handle SSH format: git@host:path or just path segments const colonIndex = normalized.indexOf(":"); if (colonIndex !== -1 && !normalized.includes("://")) { - // SSH-style: take everything after the colon normalized = normalized.slice(colonIndex + 1); } - // Split by '/' and get the last segment repoSegment = normalized.split("/").filter(Boolean).pop(); } @@ -279,13 +243,10 @@ function extractRepoName(urlInput: string): string | null { try { repoSegment = decodeURIComponent(repoSegment); - } catch { - // Invalid encoding, continue with raw value - } + } catch {} repoSegment = repoSegment.trim(); - // Validate against safe filename regex if (!repoSegment || !SAFE_REPO_NAME_REGEX.test(repoSegment)) { return null; } @@ -334,6 +295,28 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { .all(); }), + selectDirectory: publicProcedure + .input( + z.object({ + defaultPath: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + const window = getWindow(); + if (!window) { + return { canceled: true as const, path: null }; + } + const result = await dialog.showOpenDialog(window, { + properties: ["openDirectory", "createDirectory"], + title: "Select Directory", + defaultPath: input.defaultPath, + }); + if (result.canceled || result.filePaths.length === 0) { + return { canceled: true as const, path: null }; + } + return { canceled: false as const, path: result.filePaths[0] }; + }), + getBranches: publicProcedure .input(z.object({ projectId: z.string() })) .query( @@ -359,14 +342,11 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { const git = simpleGit(project.mainRepoPath); - // Check if origin remote exists let hasOrigin = false; try { const remotes = await git.getRemotes(); hasOrigin = remotes.some((r) => r.name === "origin"); - } catch { - // If we can't get remotes, assume no origin - } + } catch {} const branchSummary = await git.branch(["-a"]); @@ -383,13 +363,11 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { } } - // Get branch dates for sorting - fetch from both local and remote const branchMap = new Map< string, { lastCommitDate: number; isLocal: boolean; isRemote: boolean } >(); - // First, get remote branch dates (if origin exists) if (hasOrigin) { try { const remoteBranchInfo = await git.raw([ @@ -422,7 +400,6 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { }); } } catch { - // Fallback for remote branches for (const name of remoteBranchSet) { branchMap.set(name, { lastCommitDate: 0, @@ -433,7 +410,6 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { } } - // Then, add local-only branches try { const localBranchInfo = await git.raw([ "for-each-ref", @@ -797,6 +773,67 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { } }), + createEmptyRepo: publicProcedure + .input( + z.object({ + name: z + .string() + .min(1) + .refine( + (val) => SAFE_REPO_NAME_REGEX.test(val) && !/^\.+$/.test(val), + { + message: + "Name can only contain letters, numbers, dots, underscores, hyphens, and spaces", + }, + ), + parentDir: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + try { + const repoPath = join(input.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 }); + + let defaultBranch: string; + try { + ({ defaultBranch } = await initGitRepo(repoPath)); + } catch (gitErr) { + await rm(repoPath, { recursive: true, force: true }); + throw gitErr; + } + 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}`, + }; + } + }), + update: publicProcedure .input( z.object({ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/CloneRepoTab/CloneRepoTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/CloneRepoTab/CloneRepoTab.tsx new file mode 100644 index 00000000000..90e1de9f046 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/CloneRepoTab/CloneRepoTab.tsx @@ -0,0 +1,69 @@ +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 { + onError: (error: string) => void; + parentDir: string; +} + +export function CloneRepoTab({ onError, parentDir }: CloneRepoTabProps) { + const [url, setUrl] = useState(""); + const cloneRepo = electronTrpc.projects.cloneRepo.useMutation(); + const { handleResult, handleError, isCreatingWorkspace } = + useProjectCreationHandler(onError); + + const isLoading = cloneRepo.isPending || isCreatingWorkspace; + + const handleClone = () => { + if (!url.trim()) { + onError("Please enter a repository URL"); + return; + } + if (!parentDir.trim()) { + onError("Please select a project location"); + return; + } + + cloneRepo.mutate( + { url: url.trim(), targetDirectory: parentDir.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/routes/_authenticated/_onboarding/new-project/components/CloneRepoTab/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/CloneRepoTab/index.ts new file mode 100644 index 00000000000..3f48db1cc11 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/CloneRepoTab/index.ts @@ -0,0 +1 @@ +export { CloneRepoTab } from "./CloneRepoTab"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/EmptyRepoTab/EmptyRepoTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/EmptyRepoTab/EmptyRepoTab.tsx new file mode 100644 index 00000000000..9cc1d4daa8a --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/EmptyRepoTab/EmptyRepoTab.tsx @@ -0,0 +1,70 @@ +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 { + onError: (error: string) => void; + parentDir: string; +} + +export function EmptyRepoTab({ onError, parentDir }: EmptyRepoTabProps) { + const [name, setName] = useState(""); + const createEmptyRepo = electronTrpc.projects.createEmptyRepo.useMutation(); + const { handleResult, handleError, isCreatingWorkspace } = + useProjectCreationHandler(onError); + + const isLoading = createEmptyRepo.isPending || isCreatingWorkspace; + + const handleCreate = () => { + const trimmed = name.trim(); + if (!trimmed) { + onError("Please enter a repository name"); + return; + } + if (!parentDir.trim()) { + onError("Please select a project location"); + return; + } + + createEmptyRepo.mutate( + { name: trimmed, parentDir: parentDir.trim() }, + { + 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/routes/_authenticated/_onboarding/new-project/components/EmptyRepoTab/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/EmptyRepoTab/index.ts new file mode 100644 index 00000000000..1d45c599563 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/EmptyRepoTab/index.ts @@ -0,0 +1 @@ +export { EmptyRepoTab } from "./EmptyRepoTab"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/PathSelector/PathSelector.tsx b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/PathSelector/PathSelector.tsx new file mode 100644 index 00000000000..bb6068453c2 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/PathSelector/PathSelector.tsx @@ -0,0 +1,58 @@ +import { Button } from "@superset/ui/button"; +import { Input } from "@superset/ui/input"; +import { LuFolderOpen } from "react-icons/lu"; +import { electronTrpc } from "renderer/lib/electron-trpc"; + +interface PathSelectorProps { + value: string; + onChange: (path: string) => void; + disabled?: boolean; +} + +export function PathSelector({ value, onChange, disabled }: PathSelectorProps) { + const selectDirectory = electronTrpc.projects.selectDirectory.useMutation(); + + const handleBrowse = () => { + selectDirectory.mutate( + { defaultPath: value || undefined }, + { + onSuccess: (result) => { + if (!result.canceled && result.path) { + onChange(result.path); + } + }, + }, + ); + }; + + return ( +
+ +
+ onChange(e.target.value)} + disabled={disabled} + className="flex-1 font-mono text-xs" + /> + +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/PathSelector/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/PathSelector/index.ts new file mode 100644 index 00000000000..43707207dad --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/PathSelector/index.ts @@ -0,0 +1 @@ +export { PathSelector } from "./PathSelector"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/TemplateTab/TemplateTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/TemplateTab/TemplateTab.tsx new file mode 100644 index 00000000000..bb2c757a927 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/TemplateTab/TemplateTab.tsx @@ -0,0 +1,83 @@ +import { cn } from "@superset/ui/utils"; +import { + LuBraces, + LuGlobe, + LuLayoutDashboard, + LuServer, + LuSmartphone, + LuTerminal, +} from "react-icons/lu"; + +const TEMPLATES = [ + { + name: "Next.js", + description: "Full-stack React framework with SSR and API routes", + icon: LuGlobe, + color: "text-white bg-black", + }, + { + name: "Vite + React", + description: "Fast build tool with React and TypeScript", + icon: LuBraces, + color: "text-white bg-violet-500", + }, + { + name: "Express API", + description: "Minimal Node.js REST API server", + icon: LuServer, + color: "text-white bg-green-600", + }, + { + name: "Astro", + description: "Content-focused static site generator", + icon: LuLayoutDashboard, + color: "text-white bg-orange-500", + }, + { + name: "React Native", + description: "Cross-platform mobile app with Expo", + icon: LuSmartphone, + color: "text-white bg-blue-500", + }, + { + name: "CLI Tool", + description: "Command-line application with TypeScript", + icon: LuTerminal, + color: "text-white bg-zinc-700", + }, +]; + +export function TemplateTab() { + return ( +
+ {TEMPLATES.map((template) => ( + + ))} +
+

Templates coming soon

+
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/TemplateTab/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/TemplateTab/index.ts new file mode 100644 index 00000000000..90e3ad36444 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/components/TemplateTab/index.ts @@ -0,0 +1 @@ +export { TemplateTab } from "./TemplateTab"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/constants.ts b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/constants.ts new file mode 100644 index 00000000000..4876ba106bf --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/constants.ts @@ -0,0 +1 @@ +export type NewProjectMode = "empty" | "clone" | "template"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/hooks/useProjectCreationHandler/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/hooks/useProjectCreationHandler/index.ts new file mode 100644 index 00000000000..323a8ef3605 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/hooks/useProjectCreationHandler/index.ts @@ -0,0 +1 @@ +export { useProjectCreationHandler } from "./useProjectCreationHandler"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/hooks/useProjectCreationHandler/useProjectCreationHandler.ts b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/hooks/useProjectCreationHandler/useProjectCreationHandler.ts new file mode 100644 index 00000000000..1c481745cc1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/hooks/useProjectCreationHandler/useProjectCreationHandler.ts @@ -0,0 +1,41 @@ +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useCreateWorkspace } from "renderer/react-query/workspaces"; + +export function useProjectCreationHandler(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 }, + { + onSuccess: () => resetState?.(), + onError: (err) => onError(err.message || "Failed to open project"), + }, + ); + } 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/routes/_authenticated/_onboarding/new-project/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/page.tsx new file mode 100644 index 00000000000..72c8e997bef --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_onboarding/new-project/page.tsx @@ -0,0 +1,144 @@ +import { Button } from "@superset/ui/button"; +import { cn } from "@superset/ui/utils"; +import { createFileRoute, Link } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { HiArrowLeft } from "react-icons/hi2"; +import { + LuFolderPlus, + LuGitBranch, + LuLayoutTemplate, + LuX, +} from "react-icons/lu"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { CloneRepoTab } from "./components/CloneRepoTab"; +import { EmptyRepoTab } from "./components/EmptyRepoTab"; +import { PathSelector } from "./components/PathSelector"; +import { TemplateTab } from "./components/TemplateTab"; +import type { NewProjectMode } from "./constants"; + +export const Route = createFileRoute( + "/_authenticated/_onboarding/new-project/", +)({ + component: NewProjectPage, +}); + +const OPTIONS: { + mode: NewProjectMode; + label: string; + description: string; + icon: typeof LuFolderPlus; +}[] = [ + { + mode: "empty", + label: "Empty", + description: "New git repository from scratch", + icon: LuFolderPlus, + }, + { + mode: "clone", + label: "Clone", + description: "Clone from a remote URL", + icon: LuGitBranch, + }, + { + mode: "template", + label: "Template", + description: "Start from a project template", + icon: LuLayoutTemplate, + }, +]; + +function NewProjectPage() { + const [mode, setMode] = useState("empty"); + const [error, setError] = useState(null); + const [parentDir, setParentDir] = useState(""); + + const { data: homeDir } = electronTrpc.window.getHomeDir.useQuery(); + + useEffect(() => { + if (parentDir || !homeDir) return; + setParentDir(`${homeDir}/.superset/projects`); + }, [homeDir, parentDir]); + + return ( +
+
+ +
+ +
+
+
+

New Project

+ + + +
+ {OPTIONS.map((option) => { + const selected = mode === option.mode; + return ( + + ); + })} +
+ + {mode === "empty" && ( + + )} + {mode === "clone" && ( + + )} + {mode === "template" && } + + {error && ( +
+ {error} + +
+ )} +
+
+
+
+ ); +} 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/index.tsx b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx index 52947461dd4..e1cc546fde3 100644 --- a/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/StartView/index.tsx @@ -2,16 +2,14 @@ import { Button } from "@superset/ui/button"; import { cn } from "@superset/ui/utils"; import { useNavigate } from "@tanstack/react-router"; import { useCallback, useEffect, useState } from "react"; -import { LuFolderGit, LuFolderOpen, LuX } from "react-icons/lu"; +import { LuFolderOpen, LuPlus, LuX } from "react-icons/lu"; import { useOpenProject } from "renderer/react-query/projects"; import { SupersetLogo } from "renderer/routes/sign-in/components/SupersetLogo"; -import { CloneRepoDialog } from "./CloneRepoDialog"; export function StartView() { const navigate = useNavigate(); const { openNew, openFromPath, isPending } = useOpenProject(); const [error, setError] = useState(null); - const [isCloneDialogOpen, setIsCloneDialogOpen] = useState(false); const [isDragOver, setIsDragOver] = useState(false); useEffect(() => { @@ -121,10 +119,6 @@ export function StartView() { [isPending, openFromPath, navigate], ); - const handleCloneError = (errorMessage: string) => { - setError(errorMessage); - }; - return (
{/* biome-ignore lint/a11y/noStaticElementInteractions: Drop zone for external files */} @@ -159,7 +153,7 @@ export function StartView() { > {isDragOver ? (
- + Drop git project @@ -187,16 +181,17 @@ export function StartView() { )} > - Don't have a local repo? + Or start a new project
@@ -216,12 +211,6 @@ export function StartView() { )} - - setIsCloneDialogOpen(false)} - onError={handleCloneError} - /> ); } 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 d0115726363..3e7a3a95e97 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx @@ -7,11 +7,10 @@ import { } from "@superset/ui/dropdown-menu"; import { toast } from "@superset/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { useState } from "react"; +import { useNavigate } from "@tanstack/react-router"; import { LuFolderGit, LuFolderOpen, LuFolderPlus } from "react-icons/lu"; import { useOpenProject } from "renderer/react-query/projects"; import { useCreateBranchWorkspace } from "renderer/react-query/workspaces"; -import { CloneRepoDialog } from "../StartView/CloneRepoDialog"; import { STROKE_WIDTH } from "./constants"; interface WorkspaceSidebarFooterProps { @@ -21,9 +20,9 @@ interface WorkspaceSidebarFooterProps { export function WorkspaceSidebarFooter({ isCollapsed = false, }: WorkspaceSidebarFooterProps) { + const navigate = useNavigate(); const { openNew, isPending: isOpenPending } = useOpenProject(); const createBranchWorkspace = useCreateBranchWorkspace(); - const [isCloneDialogOpen, setIsCloneDialogOpen] = useState(false); const handleOpenProject = async () => { try { @@ -49,99 +48,67 @@ export function WorkspaceSidebarFooter({ } }; - const handleCloneError = (error: string) => { - toast.error("Failed to clone repository", { - description: error, - }); - }; - const isLoading = isOpenPending || createBranchWorkspace.isPending; if (isCollapsed) { return ( - <> -
- - - - - - - - Add repository - - - - - Open project - - setIsCloneDialogOpen(true)} - disabled={isLoading} - > - - Clone repo - - - -
- setIsCloneDialogOpen(false)} - onError={handleCloneError} - /> - - ); - } - - return ( - <> -
+
- - - + + + + + + + Add repository + Open project - setIsCloneDialogOpen(true)} - disabled={isLoading} - > + navigate({ to: "/new-project" })}> - Clone repo + New project
- setIsCloneDialogOpen(false)} - onError={handleCloneError} - /> - + ); + } + + return ( +
+ + + + + + + + Open project + + navigate({ to: "/new-project" })}> + + New project + + + +
); } diff --git a/bun.lock b/bun.lock index 66ab6dc2c04..f5e159c0023 100644 --- a/bun.lock +++ b/bun.lock @@ -109,7 +109,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "0.0.81", + "version": "0.0.82", "dependencies": { "@ai-sdk/react": "^3.0.0", "@better-auth/stripe": "1.4.18",