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")).toBeHidden(); + + // 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(); +});