Canvas: Fix project creation flow
Change-Id: I2373982b37807db17391149a7ad40ebd4a5894ed
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 8bb1a81..295bdf3 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -23,7 +23,7 @@
resp.header("Content-Type", "application/json");
resp.write(
JSON.stringify({
- id,
+ id: id.toString(),
}),
);
} catch (e) {
diff --git a/apps/canvas/front/src/ProjectSelect.tsx b/apps/canvas/front/src/ProjectSelect.tsx
index 6903c5e..4367bef 100644
--- a/apps/canvas/front/src/ProjectSelect.tsx
+++ b/apps/canvas/front/src/ProjectSelect.tsx
@@ -3,38 +3,59 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/ui/select";
import { Input } from "./components/ui/input";
import { Button } from "./components/ui/button";
-import { Dialog, DialogContent, DialogTrigger } from "./components/ui/dialog";
+import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "./components/ui/dialog";
import { useToast } from "@/hooks/use-toast";
import { Separator } from "./components/ui/separator";
import { Plus } from "lucide-react";
+import { z } from "zod";
+
+const createNewSchema = z.object({
+ id: z.string().min(1),
+});
export function ProjectSelect() {
const { toast } = useToast();
const store = useStateStore();
const [projects, setProjects] = useState<Project[]>([]);
const projectId = useProjectId();
+ // Track the selected project ID locally to ensure UI consistency
+ const [selectedId, setSelectedId] = useState<string | undefined>(projectId);
- const refreshProjects = useCallback(async () => {
- try {
- const resp = await fetch("/api/project");
- const projectList = await resp.json();
- const sortedProjects = [...projectList].sort((a, b) =>
- a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
- );
- setProjects(sortedProjects);
- } catch (e) {
- console.log(e);
- }
- }, [setProjects]);
-
+ // Keep local state in sync with global state
useEffect(() => {
- if (projects.length > 0 && (projectId == null || !projects.some((p) => p.id === projectId))) {
- store.setProject(projects[0].id);
- }
- }, [projectId, projects, store]);
+ setSelectedId(projectId);
+ }, [projectId]);
+
+ const refreshProjects = useCallback(
+ async (id?: string) => {
+ console.log("refreshProjects", id);
+ try {
+ const resp = await fetch("/api/project");
+ const projectList = await resp.json();
+ const sortedProjects = [...projectList].sort((a, b) =>
+ a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
+ );
+ setProjects(sortedProjects);
+ if (id && !sortedProjects.some((p) => p.id === id)) {
+ throw new Error("MUST NOT REACH!");
+ }
+ if (id == null) {
+ id = sortedProjects[0].id;
+ }
+ if (id !== selectedId) {
+ setSelectedId(id);
+ store.setProject(id);
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ },
+ [selectedId, store, setProjects, setSelectedId],
+ );
+
useEffect(() => {
refreshProjects();
- }, [refreshProjects]);
+ });
const [createNewOpen, setCreateNewOpen] = useState(false);
const onSelect = useCallback(
@@ -42,11 +63,13 @@
if (projectId === "create-new") {
setCreateNewOpen(true);
} else {
+ setSelectedId(projectId);
store.setProject(projectId);
}
},
- [store],
+ [store, setSelectedId],
);
+
const [name, setName] = useState<string | undefined>(undefined);
const updateName = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
@@ -54,9 +77,13 @@
},
[setName],
);
+
const createNew = useCallback(() => {
- console.log(name);
if (!name) {
+ toast({
+ variant: "destructive",
+ title: "Name is required",
+ });
return;
}
fetch("/api/project", {
@@ -70,12 +97,20 @@
})
.then(async (resp) => {
if (!resp.ok) {
+ return false;
+ }
+ const result = createNewSchema.safeParse(await resp.json());
+ if (!result.success) {
+ toast({
+ variant: "destructive",
+ title: `Failed to create project: ${name}`,
+ });
return;
}
- const { id } = await resp.json();
- await refreshProjects();
- store.setProject(id as string);
+ const { id } = result.data;
+ await refreshProjects(id);
setCreateNewOpen(false);
+ setName(undefined); // Clear the input for next time
toast({
title: `Created project: ${name}`,
});
@@ -87,32 +122,50 @@
title: `Failed to create project: ${name}`,
});
});
- }, [name, setCreateNewOpen, toast, store, refreshProjects]);
+ }, [name, setCreateNewOpen, toast, refreshProjects]);
+
return (
- <Select onValueChange={onSelect} value={projectId}>
- <SelectTrigger className="w-[200px] !border-none !shadow-none !focus:ring-0 !focus:ring-offset-0">
- <SelectValue placeholder="Choose Project" defaultValue={projectId} />
- </SelectTrigger>
- <SelectContent>
- {projects.map((p) => (
- <SelectItem key={p.id} value={p.id}>
- {p.name}
- </SelectItem>
- ))}
- <Separator />
- <SelectItem value={"create-new"}>
- <Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
- <DialogTrigger className="flex flex-row items-center">
+ <>
+ <Select onValueChange={onSelect} value={selectedId}>
+ <SelectTrigger className="w-[200px] !border-none !shadow-none !focus:ring-0 !focus:ring-offset-0">
+ <SelectValue placeholder="Choose Project" />
+ </SelectTrigger>
+ <SelectContent>
+ {projects.map((p) => (
+ <SelectItem key={p.id} value={p.id}>
+ {p.name}
+ </SelectItem>
+ ))}
+ <Separator />
+ <SelectItem value={"create-new"}>
+ <div className="flex flex-row items-center gap-1">
<Plus />
- Create New
- </DialogTrigger>
- <DialogContent>
- <Input type="text" placeholder="Name" onChange={updateName} />
- <Button onClick={createNew}>Create New</Button>
- </DialogContent>
- </Dialog>
- </SelectItem>
- </SelectContent>
- </Select>
+ <div>New project</div>
+ </div>
+ </SelectItem>
+ </SelectContent>
+ </Select>
+ <Dialog open={createNewOpen} onOpenChange={setCreateNewOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>New project</DialogTitle>
+ </DialogHeader>
+ <Input
+ type="text"
+ placeholder="Name"
+ onChange={updateName}
+ value={name || ""}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ createNew();
+ }
+ }}
+ />
+ <DialogFooter>
+ <Button onClick={createNew}>Create</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </>
);
}
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index f292c30..de04e9e 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -155,6 +155,7 @@
const edit = useCallback(async () => {
store.setMode("edit");
}, [store]);
+ // TODO(gio): refresh projects
const deleteProject = useCallback(async () => {
if (projectId == null) {
return;