From 783bc372e718cfb4610be849ba71c553e4ad9bbb Mon Sep 17 00:00:00 2001 From: Vincent Colombo Date: Fri, 7 Aug 2026 11:51:47 -0500 Subject: [PATCH 1/2] fix(desktop): keep Create controls on empty Projects view When a relay has zero projects, ProjectsView early-returned a bare EmptyState before the header, toolbar, and ProjectsCreateMenu mounted, so the first project could only be created from the CLI. Render the empty state inside the normal layout instead, and give it a 'Create your first project' CTA wired to the existing CreateProjectDialog. Ported from closed PR #3620 onto current main. Fixes #2585, fixes #2622, fixes #3470, fixes #3504, fixes #4009 Signed-off-by: Vincent Colombo --- desktop/playwright.config.ts | 1 + .../src/features/projects/ui/ProjectCards.tsx | 24 ++++++- .../src/features/projects/ui/ProjectsView.tsx | 10 +-- .../tests/e2e/projects-empty-create.spec.ts | 68 +++++++++++++++++++ 4 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 desktop/tests/e2e/projects-empty-create.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index c1ea0e061b..59c22f5cf7 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -108,6 +108,7 @@ export default defineConfig({ "**/project-inbox.spec.ts", "**/project-issue-comments.spec.ts", "**/project-pr-review.spec.ts", + "**/projects-empty-create.spec.ts", "**/persona-model-combobox-screenshots.spec.ts", "**/drafts-screenshots.spec.ts", "**/drafts-all-fix-screenshots.spec.ts", diff --git a/desktop/src/features/projects/ui/ProjectCards.tsx b/desktop/src/features/projects/ui/ProjectCards.tsx index ed28ddd583..d3b4d66aa3 100644 --- a/desktop/src/features/projects/ui/ProjectCards.tsx +++ b/desktop/src/features/projects/ui/ProjectCards.tsx @@ -4,6 +4,7 @@ import { Folders, GitCommit, GitPullRequest, + Plus, TerminalSquare, Trash2, } from "lucide-react"; @@ -313,9 +314,16 @@ function RepositoryUnavailableIndicator({ ); } -export function EmptyState() { +export function EmptyState({ + onCreateProject, +}: { + onCreateProject?: () => void; +} = {}) { return ( -
+

No projects yet

@@ -323,6 +331,18 @@ export function EmptyState() { Projects published to this relay will appear here.

+ {onCreateProject ? ( + + ) : null}
); } diff --git a/desktop/src/features/projects/ui/ProjectsView.tsx b/desktop/src/features/projects/ui/ProjectsView.tsx index 72a514cea5..42b0da7520 100644 --- a/desktop/src/features/projects/ui/ProjectsView.tsx +++ b/desktop/src/features/projects/ui/ProjectsView.tsx @@ -597,10 +597,6 @@ export function ProjectsView() { ); } - if (projects.length === 0) { - return ; - } - const projectItems = visibleProjects.length === 0 ? ( @@ -859,7 +855,11 @@ export function ProjectsView() {
- {filter === "all" ? ( + {projects.length === 0 ? ( + setCreateProjectOpen(true)} + /> + ) : filter === "all" ? ( { + window.localStorage.setItem(key, JSON.stringify({ projects: true })); + }, + { key: FEATURE_OVERRIDES_STORAGE_KEY }, + ); +} + +/** + * Hide every seeded mock project so Projects renders the true empty state. + * + * Addresses must match MOCK_PROJECT_SEEDS (and the kind:30621 "buzz" project + * announcement) in `desktop/src/testing/e2eBridge.ts`. If a seed is added or + * renamed there without updating this list, the list stays populated and the + * `projects-empty-state` assertion below fails. + */ +async function hideAllMockProjects(page: import("@playwright/test").Page) { + const hiddenCards = [ + `30621:${DEFAULT_MOCK_PUBKEY}:buzz`, + `30617:${DEFAULT_MOCK_PUBKEY}:buzz`, + `30617:${TEST_IDENTITIES.alice.pubkey}:relay-tools`, + `30617:${TEST_IDENTITIES.bob.pubkey}:design-system`, + ]; + await page.addInitScript((cards) => { + window.localStorage.setItem( + "buzz.projects.hidden-cards.v1", + JSON.stringify(cards), + ); + }, hiddenCards); +} + +test("empty Projects keeps Create controls and offers an empty-state CTA", async ({ + page, +}) => { + await enableProjectsFeature(page); + await hideAllMockProjects(page); + await installMockBridge(page); + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.getByTestId("open-projects-view").click(); + + // The chrome (header + toolbar + create menu) must mount with zero projects. + await expect(page.getByTestId("projects-empty-state")).toBeVisible({ + timeout: 10_000, + }); + await expect(page.getByTestId("projects-create-menu")).toBeVisible(); + + // Entry point 1: the toolbar "+" create menu. + await page.getByTestId("projects-create-menu").hover(); + await page.getByRole("menuitem", { name: "Project" }).click(); + await expect(page.getByTestId("create-project-dialog")).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(page.getByTestId("create-project-dialog")).not.toBeVisible(); + + // Entry point 2: the empty-state CTA. + await page.getByTestId("projects-empty-create-project").click(); + await expect(page.getByTestId("create-project-dialog")).toBeVisible(); + await expect(page.getByTestId("create-project-name")).toBeVisible(); +}); From 9d326a81be5caf50faf0dbb3cd3f0e9db4684267 Mon Sep 17 00:00:00 2001 From: Vincent Colombo Date: Fri, 7 Aug 2026 12:27:21 -0500 Subject: [PATCH 2/2] test(desktop): use toBeHidden for dialog dismissal assertion Match the rest of the Projects e2e suite, which asserts dismissal of create-project-dialog with toBeHidden() rather than not.toBeVisible(). Signed-off-by: Vincent Colombo --- desktop/tests/e2e/projects-empty-create.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/tests/e2e/projects-empty-create.spec.ts b/desktop/tests/e2e/projects-empty-create.spec.ts index e6fa60273b..5fd8d7349b 100644 --- a/desktop/tests/e2e/projects-empty-create.spec.ts +++ b/desktop/tests/e2e/projects-empty-create.spec.ts @@ -59,7 +59,7 @@ test("empty Projects keeps Create controls and offers an empty-state CTA", async await page.getByRole("menuitem", { name: "Project" }).click(); await expect(page.getByTestId("create-project-dialog")).toBeVisible(); await page.keyboard.press("Escape"); - await expect(page.getByTestId("create-project-dialog")).not.toBeVisible(); + await expect(page.getByTestId("create-project-dialog")).toBeHidden(); // Entry point 2: the empty-state CTA. await page.getByTestId("projects-empty-create-project").click();