import { useRef, useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useDialog } from "../context/DialogContext"; import { useCompany } from "../context/CompanyContext"; import { projectsApi } from "../api/projects"; import { goalsApi } from "../api/goals"; import { assetsApi } from "../api/assets"; import { queryKeys } from "../lib/queryKeys"; import { Dialog, DialogContent, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Maximize2, Minimize2, Target, Calendar, Plus, X, FolderOpen, Github, GitBranch, } from "lucide-react"; import { PROJECT_COLORS } from "@paperclipai/shared"; import { cn } from "../lib/utils"; import { MarkdownEditor, type MarkdownEditorRef } from "./MarkdownEditor"; import { StatusBadge } from "./StatusBadge"; import { ChoosePathButton } from "./PathInstructionsModal"; const projectStatuses = [ { value: "backlog", label: "Backlog" }, { value: "planned", label: "Planned" }, { value: "in_progress", label: "In Progress" }, { value: "completed", label: "Completed" }, { value: "cancelled", label: "Cancelled" }, ]; type WorkspaceSetup = "none" | "local" | "repo" | "both"; const REPO_ONLY_CWD_SENTINEL = "/__paperclip_repo_only__"; export function NewProjectDialog() { const { newProjectOpen, closeNewProject } = useDialog(); const { selectedCompanyId, selectedCompany } = useCompany(); const queryClient = useQueryClient(); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [status, setStatus] = useState("planned"); const [goalIds, setGoalIds] = useState([]); const [targetDate, setTargetDate] = useState(""); const [expanded, setExpanded] = useState(false); const [workspaceSetup, setWorkspaceSetup] = useState("none"); const [workspaceLocalPath, setWorkspaceLocalPath] = useState(""); const [workspaceRepoUrl, setWorkspaceRepoUrl] = useState(""); const [workspaceError, setWorkspaceError] = useState(null); const [statusOpen, setStatusOpen] = useState(false); const [goalOpen, setGoalOpen] = useState(false); const descriptionEditorRef = useRef(null); const { data: goals } = useQuery({ queryKey: queryKeys.goals.list(selectedCompanyId!), queryFn: () => goalsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && newProjectOpen, }); const createProject = useMutation({ mutationFn: (data: Record) => projectsApi.create(selectedCompanyId!, data), }); const uploadDescriptionImage = useMutation({ mutationFn: async (file: File) => { if (!selectedCompanyId) throw new Error("No company selected"); return assetsApi.uploadImage(selectedCompanyId, file, "projects/drafts"); }, }); function reset() { setName(""); setDescription(""); setStatus("planned"); setGoalIds([]); setTargetDate(""); setExpanded(false); setWorkspaceSetup("none"); setWorkspaceLocalPath(""); setWorkspaceRepoUrl(""); setWorkspaceError(null); } const isAbsolutePath = (value: string) => value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value); const isGitHubRepoUrl = (value: string) => { try { const parsed = new URL(value); const host = parsed.hostname.toLowerCase(); if (host !== "github.com" && host !== "www.github.com") return false; const segments = parsed.pathname.split("/").filter(Boolean); return segments.length >= 2; } catch { return false; } }; const deriveWorkspaceNameFromPath = (value: string) => { const normalized = value.trim().replace(/[\\/]+$/, ""); const segments = normalized.split(/[\\/]/).filter(Boolean); return segments[segments.length - 1] ?? "Local folder"; }; const deriveWorkspaceNameFromRepo = (value: string) => { try { const parsed = new URL(value); const segments = parsed.pathname.split("/").filter(Boolean); const repo = segments[segments.length - 1]?.replace(/\.git$/i, "") ?? ""; return repo || "GitHub repo"; } catch { return "GitHub repo"; } }; const toggleWorkspaceSetup = (next: WorkspaceSetup) => { setWorkspaceSetup((prev) => (prev === next ? "none" : next)); setWorkspaceError(null); }; async function handleSubmit() { if (!selectedCompanyId || !name.trim()) return; const localRequired = workspaceSetup === "local" || workspaceSetup === "both"; const repoRequired = workspaceSetup === "repo" || workspaceSetup === "both"; const localPath = workspaceLocalPath.trim(); const repoUrl = workspaceRepoUrl.trim(); if (localRequired && !isAbsolutePath(localPath)) { setWorkspaceError("Local folder must be a full absolute path."); return; } if (repoRequired && !isGitHubRepoUrl(repoUrl)) { setWorkspaceError("Repo workspace must use a valid GitHub repo URL."); return; } setWorkspaceError(null); try { const created = await createProject.mutateAsync({ name: name.trim(), description: description.trim() || undefined, status, color: PROJECT_COLORS[Math.floor(Math.random() * PROJECT_COLORS.length)], ...(goalIds.length > 0 ? { goalIds } : {}), ...(targetDate ? { targetDate } : {}), }); const workspacePayloads: Array> = []; if (localRequired && repoRequired) { workspacePayloads.push({ name: deriveWorkspaceNameFromPath(localPath), cwd: localPath, repoUrl, }); } else if (localRequired) { workspacePayloads.push({ name: deriveWorkspaceNameFromPath(localPath), cwd: localPath, }); } else if (repoRequired) { workspacePayloads.push({ name: deriveWorkspaceNameFromRepo(repoUrl), cwd: REPO_ONLY_CWD_SENTINEL, repoUrl, }); } for (const workspacePayload of workspacePayloads) { await projectsApi.createWorkspace(created.id, { ...workspacePayload, }); } queryClient.invalidateQueries({ queryKey: queryKeys.projects.list(selectedCompanyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(created.id) }); reset(); closeNewProject(); } catch { // surface through createProject.isError } } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); handleSubmit(); } } const selectedGoals = (goals ?? []).filter((g) => goalIds.includes(g.id)); const availableGoals = (goals ?? []).filter((g) => !goalIds.includes(g.id)); return ( { if (!open) { reset(); closeNewProject(); } }} > {/* Header */}
{selectedCompany && ( {selectedCompany.name.slice(0, 3).toUpperCase()} )} New project
{/* Name */}
setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Tab" && !e.shiftKey) { e.preventDefault(); descriptionEditorRef.current?.focus(); } }} autoFocus />
{/* Description */}
{ const asset = await uploadDescriptionImage.mutateAsync(file); return asset.contentPath; }} />

Where will work be done on this project?

Add local folder and/or GitHub repo workspace hints.

{(workspaceSetup === "local" || workspaceSetup === "both") && (
setWorkspaceLocalPath(e.target.value)} placeholder="/absolute/path/to/workspace" />
)} {(workspaceSetup === "repo" || workspaceSetup === "both") && (
setWorkspaceRepoUrl(e.target.value)} placeholder="https://github.com/org/repo" />
)} {workspaceError && (

{workspaceError}

)}
{/* Property chips */}
{/* Status */} {projectStatuses.map((s) => ( ))} {selectedGoals.map((goal) => ( {goal.title} ))} {selectedGoals.length === 0 && ( )} {availableGoals.map((g) => ( ))} {selectedGoals.length > 0 && availableGoals.length === 0 && (
All goals already selected.
)}
{/* Target date */}
setTargetDate(e.target.value)} placeholder="Target date" />
{/* Footer */}
{createProject.isError ? (

Failed to create project.

) : ( )}
); }