diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/content.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/content.tsx index 7bf965c55..f8b251826 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/content.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/content.tsx @@ -27,7 +27,7 @@ import { PlusCircleIcon } from "@phosphor-icons/react"; import { type AdminOwnedProject } from "@hexclave/next"; import { runAsynchronouslyWithAlert, wait } from "@hexclave/shared/dist/utils/promises"; import { useSearchParams } from "next/navigation"; -import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { type FormEvent, type KeyboardEvent, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ProjectOnboardingWizard } from "./project-onboarding-wizard"; import { @@ -113,6 +113,87 @@ function PageClientInner() { router.replace(query.length > 0 ? `/new-project?${query}` : "/new-project"); }, [router, searchParams]); + const createProject = useCallback(async () => { + if (!beginPendingAction(creatingProjectRef, setCreatingProject)) { + return; + } + + try { + const trimmedProjectName = projectName.trim(); + if (trimmedProjectName.length === 0) { + throw new Error("Project name is required."); + } + + const firstTeam = teams.at(0); + const teamId = selectedTeamId ?? user.selectedTeam?.id ?? firstTeam?.id; + if (teamId === undefined) { + throw new Error("Select a team before creating the project."); + } + + const newProject = await user.createProject({ + displayName: trimmedProjectName, + teamId, + onboardingStatus: "config_choice", + }); + + setProjectStatuses((previous) => { + const next = new Map(previous); + next.set(newProject.id, "config_choice"); + return next; + }); + setProjectOnboardingStates((previous) => { + const next = new Map(previous); + next.set(newProject.id, null); + return next; + }); + + if (redirectToNeonConfirmWith != null) { + const confirmSearchParams = new URLSearchParams(redirectToNeonConfirmWith); + confirmSearchParams.set("default_selected_project_id", newProject.id); + router.push(`/integrations/neon/confirm?${confirmSearchParams.toString()}`); + await wait(2000); + return; + } + + if (redirectToConfirmWith != null) { + const confirmSearchParams = new URLSearchParams(redirectToConfirmWith); + confirmSearchParams.set("default_selected_project_id", newProject.id); + router.push(`/integrations/custom/confirm?${confirmSearchParams.toString()}`); + await wait(2000); + return; + } + + updateSearchParams({ + project_id: newProject.id, + mode: null, + }); + } finally { + endPendingAction(creatingProjectRef, setCreatingProject); + } + }, [projectName, redirectToConfirmWith, redirectToNeonConfirmWith, router, selectedTeamId, teams, updateSearchParams, user]); + + const handleCreateProjectSubmit = useCallback((event: FormEvent) => { + event.preventDefault(); + if (!hasProjectName || creatingProject) { + return; + } + + runAsynchronouslyWithAlert(createProject()); + }, [createProject, creatingProject, hasProjectName]); + + const handleProjectNameKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key !== "Enter") { + return; + } + + event.preventDefault(); + if (!hasProjectName || creatingProject) { + return; + } + + runAsynchronouslyWithAlert(createProject()); + }, [createProject, creatingProject, hasProjectName]); + const selectedProject = useMemo(() => { if (selectedProjectId == null) { return null; @@ -278,108 +359,52 @@ function PageClientInner() { -
-
- - setProjectName(event.target.value)} - placeholder="My Project" - /> -
- -
- -
- ({ value: team.id, label: team.displayName }))} +
+
+
+ + setProjectName(event.target.value)} + onKeyDown={handleProjectNameKeyDown} + placeholder="My Project" /> - setIsCreateTeamOpen(true)} className="rounded-xl sm:min-w-[152px]"> - - Create Team - +
+ +
+ +
+ ({ value: team.id, label: team.displayName }))} + /> + setIsCreateTeamOpen(true)} className="rounded-xl sm:min-w-[152px]"> + + Create Team + +
-
- - router.push("/projects")} disabled={creatingProject}> - Cancel - - { - if (!beginPendingAction(creatingProjectRef, setCreatingProject)) { - return; - } - - return runAsynchronouslyWithAlert(async () => { - const trimmedProjectName = projectName.trim(); - if (trimmedProjectName.length === 0) { - throw new Error("Project name is required."); - } - - const firstTeam = teams.at(0); - const teamId = selectedTeamId ?? user.selectedTeam?.id ?? firstTeam?.id; - if (teamId === undefined) { - throw new Error("Select a team before creating the project."); - } - - try { - const newProject = await user.createProject({ - displayName: trimmedProjectName, - teamId, - onboardingStatus: "config_choice", - }); - - setProjectStatuses((previous) => { - const next = new Map(previous); - next.set(newProject.id, "config_choice"); - return next; - }); - setProjectOnboardingStates((previous) => { - const next = new Map(previous); - next.set(newProject.id, null); - return next; - }); - - if (redirectToNeonConfirmWith != null) { - const confirmSearchParams = new URLSearchParams(redirectToNeonConfirmWith); - confirmSearchParams.set("default_selected_project_id", newProject.id); - router.push(`/integrations/neon/confirm?${confirmSearchParams.toString()}`); - await wait(2000); - return; - } - - if (redirectToConfirmWith != null) { - const confirmSearchParams = new URLSearchParams(redirectToConfirmWith); - confirmSearchParams.set("default_selected_project_id", newProject.id); - router.push(`/integrations/custom/confirm?${confirmSearchParams.toString()}`); - await wait(2000); - return; - } - - updateSearchParams({ - project_id: newProject.id, - mode: null, - }); - } finally { - endPendingAction(creatingProjectRef, setCreatingProject); - } - }); - }} - > - Create Project - - + + router.push("/projects")} disabled={creatingProject}> + Cancel + + + Create Project + + + diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.test.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.test.tsx index 3f21dddf1..8fdd140b6 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.test.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.test.tsx @@ -88,6 +88,21 @@ describe("new project page data loading", () => { }); }); +describe("new project creation dialog", () => { + it("uses native form submission for create-project keyboard access", () => { + const testDir = dirname(fileURLToPath(import.meta.url)); + const source = readFileSync(join( + testDir, + "page-client-parts/content.tsx", + ), "utf-8"); + + expect(source).toContain("
"); + expect(source).toContain("onKeyDown={handleProjectNameKeyDown}"); + expect(source).toContain('type="submit"'); + expect(source).toContain("Create Project"); + }); +}); + describe("OnboardingPage", () => { it("uses hover-exit-only transitions and accessible labels for progress dots", () => { render(