Simplify new project dialog: always show repo and local folder fields

Remove the workspace setup toggle menu ("Where will work be done on this
project?") and instead always display both repo URL and local folder
inputs directly. Both fields are marked as optional with help tooltips
explaining their purpose. Repo is shown first, local folder second.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-19 07:17:49 -05:00
parent 9e21ef879f
commit 72a0e256a8

View File

@@ -23,10 +23,13 @@ import {
Calendar, Calendar,
Plus, Plus,
X, X,
FolderOpen, HelpCircle,
Github,
GitBranch,
} from "lucide-react"; } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { PROJECT_COLORS } from "@paperclipai/shared"; import { PROJECT_COLORS } from "@paperclipai/shared";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
import { MarkdownEditor, type MarkdownEditorRef } from "./MarkdownEditor"; import { MarkdownEditor, type MarkdownEditorRef } from "./MarkdownEditor";
@@ -41,8 +44,6 @@ const projectStatuses = [
{ value: "cancelled", label: "Cancelled" }, { value: "cancelled", label: "Cancelled" },
]; ];
type WorkspaceSetup = "none" | "local" | "repo" | "both";
export function NewProjectDialog() { export function NewProjectDialog() {
const { newProjectOpen, closeNewProject } = useDialog(); const { newProjectOpen, closeNewProject } = useDialog();
const { selectedCompanyId, selectedCompany } = useCompany(); const { selectedCompanyId, selectedCompany } = useCompany();
@@ -53,7 +54,6 @@ export function NewProjectDialog() {
const [goalIds, setGoalIds] = useState<string[]>([]); const [goalIds, setGoalIds] = useState<string[]>([]);
const [targetDate, setTargetDate] = useState(""); const [targetDate, setTargetDate] = useState("");
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [workspaceSetup, setWorkspaceSetup] = useState<WorkspaceSetup>("none");
const [workspaceLocalPath, setWorkspaceLocalPath] = useState(""); const [workspaceLocalPath, setWorkspaceLocalPath] = useState("");
const [workspaceRepoUrl, setWorkspaceRepoUrl] = useState(""); const [workspaceRepoUrl, setWorkspaceRepoUrl] = useState("");
const [workspaceError, setWorkspaceError] = useState<string | null>(null); const [workspaceError, setWorkspaceError] = useState<string | null>(null);
@@ -87,7 +87,6 @@ export function NewProjectDialog() {
setGoalIds([]); setGoalIds([]);
setTargetDate(""); setTargetDate("");
setExpanded(false); setExpanded(false);
setWorkspaceSetup("none");
setWorkspaceLocalPath(""); setWorkspaceLocalPath("");
setWorkspaceRepoUrl(""); setWorkspaceRepoUrl("");
setWorkspaceError(null); setWorkspaceError(null);
@@ -124,23 +123,16 @@ export function NewProjectDialog() {
} }
}; };
const toggleWorkspaceSetup = (next: WorkspaceSetup) => {
setWorkspaceSetup((prev) => (prev === next ? "none" : next));
setWorkspaceError(null);
};
async function handleSubmit() { async function handleSubmit() {
if (!selectedCompanyId || !name.trim()) return; if (!selectedCompanyId || !name.trim()) return;
const localRequired = workspaceSetup === "local" || workspaceSetup === "both";
const repoRequired = workspaceSetup === "repo" || workspaceSetup === "both";
const localPath = workspaceLocalPath.trim(); const localPath = workspaceLocalPath.trim();
const repoUrl = workspaceRepoUrl.trim(); const repoUrl = workspaceRepoUrl.trim();
if (localRequired && !isAbsolutePath(localPath)) { if (localPath && !isAbsolutePath(localPath)) {
setWorkspaceError("Local folder must be a full absolute path."); setWorkspaceError("Local folder must be a full absolute path.");
return; return;
} }
if (repoRequired && !isGitHubRepoUrl(repoUrl)) { if (repoUrl && !isGitHubRepoUrl(repoUrl)) {
setWorkspaceError("Repo must use a valid GitHub repo URL."); setWorkspaceError("Repo must use a valid GitHub repo URL.");
return; return;
} }
@@ -157,28 +149,15 @@ export function NewProjectDialog() {
...(targetDate ? { targetDate } : {}), ...(targetDate ? { targetDate } : {}),
}); });
const workspacePayloads: Array<Record<string, unknown>> = []; if (localPath || repoUrl) {
if (localRequired && repoRequired) { const workspacePayload: Record<string, unknown> = {
workspacePayloads.push({ name: localPath
name: deriveWorkspaceNameFromPath(localPath), ? deriveWorkspaceNameFromPath(localPath)
cwd: localPath, : deriveWorkspaceNameFromRepo(repoUrl),
repoUrl, ...(localPath ? { cwd: localPath } : {}),
}); ...(repoUrl ? { repoUrl } : {}),
} else if (localRequired) { };
workspacePayloads.push({ await projectsApi.createWorkspace(created.id, workspacePayload);
name: deriveWorkspaceNameFromPath(localPath),
cwd: localPath,
});
} else if (repoRequired) {
workspacePayloads.push({
name: deriveWorkspaceNameFromRepo(repoUrl),
repoUrl,
});
}
for (const workspacePayload of workspacePayloads) {
await projectsApi.createWorkspace(created.id, {
...workspacePayload,
});
} }
queryClient.invalidateQueries({ queryKey: queryKeys.projects.list(selectedCompanyId) }); queryClient.invalidateQueries({ queryKey: queryKeys.projects.list(selectedCompanyId) });
@@ -280,80 +259,51 @@ export function NewProjectDialog() {
</div> </div>
<div className="px-4 pb-3 space-y-3 border-t border-border"> <div className="px-4 pb-3 space-y-3 border-t border-border">
<div className="pt-3"> <div className="rounded-md border border-border p-2">
<p className="text-sm font-medium">Where will work be done on this project?</p> <div className="mb-1 flex items-center gap-1.5">
<p className="text-xs text-muted-foreground">Add a repo and/or local folder for this project.</p> <label className="block text-xs text-muted-foreground">Repo URL</label>
</div> <span className="text-xs text-muted-foreground/50">optional</span>
<div className="grid gap-2 sm:grid-cols-3"> <Tooltip delayDuration={300}>
<button <TooltipTrigger asChild>
type="button" <HelpCircle className="h-3 w-3 text-muted-foreground/50 cursor-help" />
className={cn( </TooltipTrigger>
"rounded-lg border px-3 py-3 text-left transition-colors", <TooltipContent side="top" className="max-w-[240px] text-xs">
workspaceSetup === "local" ? "border-foreground bg-accent/40" : "border-border hover:bg-accent/30", Link a GitHub repository so agents can clone, read, and push code for this project.
)} </TooltipContent>
onClick={() => toggleWorkspaceSetup("local")} </Tooltip>
> </div>
<div className="flex items-center gap-2 text-sm font-medium"> <input
<FolderOpen className="h-4 w-4" /> className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs outline-none"
A local folder value={workspaceRepoUrl}
</div> onChange={(e) => { setWorkspaceRepoUrl(e.target.value); setWorkspaceError(null); }}
<p className="mt-1 text-xs text-muted-foreground">Use a full path on this machine.</p> placeholder="https://github.com/org/repo"
</button> />
<button
type="button"
className={cn(
"rounded-lg border px-3 py-3 text-left transition-colors",
workspaceSetup === "repo" ? "border-foreground bg-accent/40" : "border-border hover:bg-accent/30",
)}
onClick={() => toggleWorkspaceSetup("repo")}
>
<div className="flex items-center gap-2 text-sm font-medium">
<Github className="h-4 w-4" />
A repo
</div>
<p className="mt-1 text-xs text-muted-foreground">Paste a GitHub URL.</p>
</button>
<button
type="button"
className={cn(
"rounded-lg border px-3 py-3 text-left transition-colors",
workspaceSetup === "both" ? "border-foreground bg-accent/40" : "border-border hover:bg-accent/30",
)}
onClick={() => toggleWorkspaceSetup("both")}
>
<div className="flex items-center gap-2 text-sm font-medium">
<GitBranch className="h-4 w-4" />
Both
</div>
<p className="mt-1 text-xs text-muted-foreground">Configure both repo and local folder.</p>
</button>
</div> </div>
{(workspaceSetup === "local" || workspaceSetup === "both") && ( <div className="rounded-md border border-border p-2">
<div className="rounded-md border border-border p-2"> <div className="mb-1 flex items-center gap-1.5">
<label className="mb-1 block text-xs text-muted-foreground">Local folder (full path)</label> <label className="block text-xs text-muted-foreground">Local folder</label>
<div className="flex items-center gap-2"> <span className="text-xs text-muted-foreground/50">optional</span>
<input <Tooltip delayDuration={300}>
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none" <TooltipTrigger asChild>
value={workspaceLocalPath} <HelpCircle className="h-3 w-3 text-muted-foreground/50 cursor-help" />
onChange={(e) => setWorkspaceLocalPath(e.target.value)} </TooltipTrigger>
placeholder="/absolute/path/to/workspace" <TooltipContent side="top" className="max-w-[240px] text-xs">
/> Set an absolute path on this machine where local agents will read and write files for this project.
<ChoosePathButton /> </TooltipContent>
</div> </Tooltip>
</div> </div>
)} <div className="flex items-center gap-2">
{(workspaceSetup === "repo" || workspaceSetup === "both") && (
<div className="rounded-md border border-border p-2">
<label className="mb-1 block text-xs text-muted-foreground">Repo URL</label>
<input <input
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs outline-none" className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none"
value={workspaceRepoUrl} value={workspaceLocalPath}
onChange={(e) => setWorkspaceRepoUrl(e.target.value)} onChange={(e) => { setWorkspaceLocalPath(e.target.value); setWorkspaceError(null); }}
placeholder="https://github.com/org/repo" placeholder="/absolute/path/to/workspace"
/> />
<ChoosePathButton />
</div> </div>
)} </div>
{workspaceError && ( {workspaceError && (
<p className="text-xs text-destructive">{workspaceError}</p> <p className="text-xs text-destructive">{workspaceError}</p>
)} )}