fix(webapp): the new-project dialog no longer opens itself on arrival (BEA-80) (#129)

A signed-in account with zero projects landed on / with the create dialog
already open over the onboarding page. The page renders its own "New
project" button (#ob-new), so the same call to action appeared twice and
the rear one was permanently pointer-intercepted — Playwright retried a
click against it for 30s before failing.

The auto-open was deliberate: "With no projects at all there is nothing
else on the page to do." That stopped being true when the empty state
gained the agent paste-prompt card — the route the docs treat as primary,
which the dialog was covering.

Delete the effect and its ref; fix the two comments that documented it;
rewrite the e2e test that asserted the auto-open by name.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Snow Lee (Sungwon)
2026-08-11 01:53:36 +09:00
committed by GitHub
co-authored by Claude Opus 5
parent 6bb7debe2b
commit 1894646093
5 changed files with 60 additions and 69 deletions
+7 -7
View File
@@ -79,20 +79,20 @@ test("join link accepts an invite after sign-in", async ({ page, browser }) => {
await ctx.close();
});
test("no projects: the create dialog opens itself, and the page behind is no dead end", async ({
test("no projects: the onboarding page renders with no dialog, and New project opens one", async ({
page,
}) => {
await login(page, "solo@example.com");
// With nothing to browse, the one useful action opens on arrival.
await expect(page.locator(".modal .start-points")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.locator(".modal-input")).toHaveCount(0);
// Nothing opens on arrival — the page's own button used to sit behind a
// dialog nobody asked for, pointer-blocked.
await expect(page.locator("[role=dialog]")).toHaveCount(0);
// Closing it leaves a page that says what to do and a way back in.
// The page says what to do, and the button works on the first attempt.
await expect(page.locator(".onboard h1")).toHaveText("Welcome to BearDrive");
await expect(page.locator(".ob-start h3")).toHaveText("Start a project");
await page.click("#ob-new", { timeout: 2000, trial: true }); // no retry loop
await page.click("#ob-new");
await expect(page.locator(".modal-input")).toBeVisible();
await expect(page.locator(".modal .start-points")).toBeVisible();
await page.keyboard.press("Escape");
// Dismissed once, it stays dismissed until asked for again.
await expect(page.locator(".modal-input")).toHaveCount(0);
+6 -15
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { postJSON } from "../api/http";
import type { InviteAccepted, Project, ProjectCreated, ServerConfig } from "../api/types";
import { useOrgs, usePending, useProjects, useHubRefresh } from "../hooks/useHub";
@@ -42,9 +42,11 @@ export default function HubApp({ config }: { config: ServerConfig }) {
const route = useMemo(() => parseRoute(loc, "hub"), [loc]);
// Creating a project is asked for from three places — the sidebar's +, the
// empty state's button, and the auto-open below — so the dialog and its one
// handler live here rather than in any of them.
// Creating a project is asked for from two places — the sidebar's + and the
// empty state's button — so the dialog and its one handler live here rather
// than in either of them. It never opens on its own: it used to on arrival
// with zero projects, which covered the empty state's agent paste-prompt
// and pointer-blocked its own "New project" button.
const [creating, setCreating] = useState(false);
// A read-only hub refuses creation server-side (403), so never offer it.
const canCreate = config.upload.enabled;
@@ -69,17 +71,6 @@ export default function HubApp({ config }: { config: ServerConfig }) {
}
};
// With no projects at all there is nothing else on the page to do, so the
// dialog opens itself. Once per mount, keyed off a ref rather than the
// empty state — otherwise closing it would immediately reopen it.
const autoOpened = useRef(false);
useEffect(() => {
if (autoOpened.current || joinToken) return;
if (!projects || projects.length > 0 || !canCreate) return;
autoOpened.current = true;
setCreating(true);
}, [projects, canCreate, joinToken]);
const newProjectDialog = creating ? (
<NewProjectDialog
templates={config.templates ?? []}
@@ -7,9 +7,9 @@ import { GuideCode, INSTALL_DOC } from "./ConnectGuide";
// a coding agent and let it do the whole thing. The by-hand route stays a
// docs link away.
//
// The create dialog also opens itself on arrival (HubApp): with no projects
// there is nothing else on this page to do. This page is what is left when
// someone closes it, so it must not be a dead end — hence the button.
// This is the first thing a zero-project account sees — nothing opens over
// it. Both cards are entry points: the button creates the project here, the
// paste-prompt hands the whole job to an agent.
export function EmptyState({ onNew, canCreate }: { onNew: () => void; canCreate: boolean }) {
return (
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
<script type="module" crossorigin src="/assets/index-wL8nzVtO.js"></script>
<script type="module" crossorigin src="/assets/index-BiaGRL-i.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BdCy9HmN.css">
</head>
<body>