Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions apps/web/src/routes/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,21 +508,29 @@ function NewProjectDialog(props: { onCreated: () => void }) {
setPending(true);
setError(null);

const project = await createProjectServerFn({
data: { name, slug },
});
try {
const project = await createProjectServerFn({
data: { name, slug },
});

setPending(false);
if (!project) {
setError("Unable to create project. Please try again.");
return;
}

if (!project) {
setError("Unable to create project. Please try again.");
return;
setName("");
setSlug("");
setOpen(false);
props.onCreated();
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: "Unable to create project. Please try again.",
);
} finally {
setPending(false);
}
Comment on lines +511 to 533
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create-project error-handling path (server throws -> error message shown + pending resets) isn’t exercised by automated tests. Since the repo already has Playwright e2e coverage, adding a test that forces createProjectServerFn to fail (or intercepts the network request) and asserts the dialog exits the “Creating…” state and displays the error would prevent regressions.

Copilot uses AI. Check for mistakes.

setName("");
setSlug("");
setOpen(false);
props.onCreated();
}

return (
Expand Down
92 changes: 65 additions & 27 deletions apps/web/src/routes/dashboard/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function DashboardLayout() {
const { user, hasWorkspace } = useRouteContext({ from: "/dashboard" });
const navigate = useNavigate();
const [profileOpen, setProfileOpen] = useState(false);
const [workspaceOpen, setWorkspaceOpen] = useState(false);
const orgQuery = useQuery(organizationQueryOptions());
const projectsQuery = useQuery({
...projectsQueryOptions(),
Expand All @@ -88,11 +89,11 @@ function DashboardLayout() {

const currentPath = typeof window === "undefined"
? ""
: window.location.pathname;
const currentSearch = typeof window === "undefined"
? new URLSearchParams()
: new URLSearchParams(window.location.search);
const currentProjectId = currentSearch.get("projectId") ?? "";
: window.location.pathname.replace(/\/$/, "");
const rawSearch = typeof window === "undefined"
? ""
: window.location.search;
const currentProjectId = new URLSearchParams(rawSearch).get("projectId") ?? "";

useEffect(() => {
if (!hasWorkspace) {
Expand Down Expand Up @@ -131,18 +132,18 @@ function DashboardLayout() {
return;
}

const params = new URLSearchParams(currentSearch);
const params = new URLSearchParams(rawSearch);
params.set("projectId", preferredProjectId);
if (currentPath === "/dashboard/analytics" && !params.get("range")) {
params.set("range", "30d");
}
window.history.replaceState({}, "", `${currentPath}?${params.toString()}`);
window.dispatchEvent(new PopStateEvent("popstate"));
window.history.replaceState(window.history.state, "", `${currentPath}?${params.toString()}`);
window.dispatchEvent(new PopStateEvent("popstate", { state: window.history.state }));
}
}, [
currentPath,
currentProjectId,
currentSearch,
rawSearch,
hasWorkspace,
navigate,
organization,
Expand All @@ -154,7 +155,7 @@ function DashboardLayout() {
writeStoredProjectId(organization.id, projectId);
}

const params = new URLSearchParams(currentSearch);
const params = new URLSearchParams(rawSearch);
if (projectId) {
params.set("projectId", projectId);
} else {
Expand All @@ -166,8 +167,8 @@ function DashboardLayout() {
}

const query = params.toString();
window.history.replaceState({}, "", query ? `${currentPath}?${query}` : currentPath);
window.dispatchEvent(new PopStateEvent("popstate"));
window.history.replaceState(window.history.state, "", query ? `${currentPath}?${query}` : currentPath);
window.dispatchEvent(new PopStateEvent("popstate", { state: window.history.state }));
}

return (
Expand Down Expand Up @@ -281,21 +282,58 @@ function DashboardLayout() {
</nav>

<div className="relative border-t border-stone-200 pt-3 dark:border-stone-800">
<Link
to="/dashboard/workspace"
className="mb-3 flex w-full items-center gap-2.5 rounded-lg border border-stone-200 px-2.5 py-2 text-left text-sm transition hover:bg-stone-100 dark:border-stone-800 dark:hover:bg-stone-800"
>
<Settings className="h-4 w-4 text-stone-500 dark:text-stone-400" />
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-stone-900 dark:text-stone-100">
{organization?.name ?? "Create workspace"}
</p>
<p className="truncate text-xs text-stone-500 dark:text-stone-400">
{organization?.slug ?? "Workspace required before creating a project"}
</p>
</div>
<ChevronDown className="h-4 w-4 shrink-0 text-stone-400" />
</Link>
{workspaceOpen ? (
<>
<div
className="fixed inset-0 z-40"
onClick={() => setWorkspaceOpen(false)}
/>
<div className="absolute bottom-full left-0 z-50 mb-2 w-full rounded-xl border border-stone-200 bg-white p-3 shadow-lg dark:border-stone-700 dark:bg-stone-900">
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workspace dropdown backdrop only closes on click; there’s no keyboard handling (e.g., Escape) so keyboard users can get stuck with the menu open. Consider matching the pattern used in apps/web/src/routes/dashboard/api-keys.tsx:549-554 by adding an onKeyDown Escape handler (and/or a document keydown listener while open) so the menu can be dismissed without a pointer.

Suggested change
<div className="absolute bottom-full left-0 z-50 mb-2 w-full rounded-xl border border-stone-200 bg-white p-3 shadow-lg dark:border-stone-700 dark:bg-stone-900">
<div
className="absolute bottom-full left-0 z-50 mb-2 w-full rounded-xl border border-stone-200 bg-white p-3 shadow-lg dark:border-stone-700 dark:bg-stone-900"
onKeyDown={(event) => {
if (event.key === "Escape") {
setWorkspaceOpen(false);
}
}}
>

Copilot uses AI. Check for mistakes.
<div className="mb-2 px-1">
<p className="truncate text-sm font-medium text-stone-900 dark:text-stone-100">
{organization?.name ?? "No workspace"}
</p>
<p className="truncate text-xs text-stone-500 dark:text-stone-400">
{organization?.slug ?? ""}
</p>
</div>
<div className="flex flex-col gap-0.5">
<Link
to="/dashboard/workspace"
onClick={() => setWorkspaceOpen(false)}
className="flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm text-stone-700 transition hover:bg-stone-100 dark:text-stone-300 dark:hover:bg-stone-800"
>
<Settings className="h-4 w-4 text-stone-500 dark:text-stone-400" />
Workspace settings
</Link>
</div>
</div>
</>
) : null}
<div className="mb-3 flex w-full items-center overflow-hidden rounded-lg border border-stone-200 dark:border-stone-800">
<Link
to="/dashboard/workspace"
className="flex flex-1 items-center gap-2.5 px-2.5 py-2 text-sm transition hover:bg-stone-100 dark:hover:bg-stone-800"
>
<Settings className="h-4 w-4 shrink-0 text-stone-500 dark:text-stone-400" />
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-stone-900 dark:text-stone-100">
{organization?.name ?? "Create workspace"}
</p>
<p className="truncate text-xs text-stone-500 dark:text-stone-400">
{organization?.slug ?? "Workspace required"}
</p>
</div>
</Link>
<button
type="button"
onClick={() => setWorkspaceOpen(!workspaceOpen)}
aria-label="Workspace menu"
className="flex items-center justify-center px-2.5 py-2 text-stone-400 transition hover:bg-stone-100 hover:text-stone-600 dark:hover:bg-stone-800 dark:hover:text-stone-300 border-l border-stone-200 dark:border-stone-800"
>
<ChevronDown className="h-4 w-4" />
</button>
Comment on lines +328 to +335
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New workspace dropdown behavior isn’t covered by existing Playwright e2e tests (no tests reference the “Workspace menu” button / “Workspace settings” entry). Adding a regression test that opens the dropdown via the chevron button and verifies the “Workspace settings” link navigates correctly would help prevent future sidebar regressions.

Copilot uses AI. Check for mistakes.
</div>
</div>

{/* Profile area */}
Expand Down
Loading