Skip to content
Merged
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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export default defineConfig({
"**/send-channel-binding.spec.ts",
"**/project-cold-start.spec.ts",
"**/project-commit-detail.spec.ts",
"**/project-empty-state-alignment.spec.ts",
"**/project-inbox.spec.ts",
"**/projects-v3-screenshots.spec.ts",
"**/project-issue-comments.spec.ts",
Expand Down
11 changes: 10 additions & 1 deletion desktop/src/features/projects/ui/ProjectCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Folders,
GitCommit,
GitPullRequest,
Plus,
TerminalSquare,
Trash2,
} from "lucide-react";
Expand Down Expand Up @@ -304,7 +305,11 @@ function RepositoryUnavailableIndicator({
);
}

export function EmptyState() {
export function EmptyState({
onCreateProject,
}: {
onCreateProject: () => void;
}) {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-4 py-16 text-center">
<Folders className="h-10 w-10 text-muted-foreground/40" />
Expand All @@ -314,6 +319,10 @@ export function EmptyState() {
Projects published to this relay will appear here.
</p>
</div>
<Button onClick={onCreateProject} size="sm" type="button">
<Plus className="h-4 w-4" />
Create project
</Button>
</div>
);
}
Expand Down
36 changes: 36 additions & 0 deletions desktop/src/features/projects/ui/ProjectCreationDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { toast } from "sonner";

import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { useCreateProjectMutation } from "@/features/projects/useCreateProject";
import { CreateProjectDialog } from "@/features/projects/ui/CreateProjectDialog";

/** Shared project-creation flow for populated and first-run project views. */
export function ProjectCreationDialog({
onOpenChange,
open,
}: {
onOpenChange: (open: boolean) => void;
open: boolean;
}) {
const { goProject } = useAppNavigation();
const createProjectMutation = useCreateProjectMutation();

return (
<CreateProjectDialog
isCreating={createProjectMutation.isPending}
onCreate={async (input) => {
const result = await createProjectMutation.mutateAsync(input);
if (result.compatibilityWarning) {
toast.warning("Created as a standalone project", {
description: result.compatibilityWarning,
});
} else {
toast.success(`Project "${result.project.name}" created.`);
}
await goProject(result.project.id);
}}
onOpenChange={onOpenChange}
open={open}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ export function ProjectHomeContextPanel({

return (
<div
className="space-y-4 px-2 pb-8 pt-1"
className="space-y-4 px-2 pb-8 pt-3"
data-testid="project-home-context-panel"
>
<ContextSection testId="project-home-context-workspace">
Expand Down
29 changes: 11 additions & 18 deletions desktop/src/features/projects/ui/ProjectsView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from "react";
import { toast } from "sonner";

import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { useManagedAgentsQuery } from "@/features/agents/hooks";
Expand All @@ -17,7 +16,6 @@ import {
useProjectsWorkItemsQuery,
} from "@/features/projects/hooks";
import { useRepositoryActivitySummariesQuery } from "@/features/projects/repositoryActivityHooks";
import { useCreateProjectMutation } from "@/features/projects/useCreateProject";
import { isExplicitProject } from "@/features/projects/projectModels";
import { projectsWithWorkItemRepositories } from "@/features/projects/projectWorkItems";
import { useProjectsRepoSnapshotsQuery } from "@/features/projects/useProjectsRepoSnapshots";
Expand Down Expand Up @@ -52,7 +50,7 @@ import {
ProjectsOverviewProjectItems,
ProjectsOverviewRepositoryItems,
} from "@/features/projects/ui/ProjectsOverviewItems";
import { CreateProjectDialog } from "@/features/projects/ui/CreateProjectDialog";
import { ProjectCreationDialog } from "@/features/projects/ui/ProjectCreationDialog";
import { CreateProjectIssueDialog } from "@/features/projects/ui/CreateProjectIssueDialog";
import { CreatePullRequestDialog } from "@/features/projects/ui/CreatePullRequestDialog";
import { ProjectAgentChatPanel } from "@/features/projects/ui/ProjectAgentChatPanel";
Expand Down Expand Up @@ -183,7 +181,6 @@ export function ProjectsView() {
const [createIssueOpen, setCreateIssueOpen] = React.useState(false);
const [createPullRequestOpen, setCreatePullRequestOpen] =
React.useState(false);
const createProjectMutation = useCreateProjectMutation();
const [storedViewMode, setStoredViewMode] =
React.useState<ProjectsViewMode | null>(() => readStoredViewMode());
const [sort, setSort] = React.useState<ProjectsSort>(() => readStoredSort());
Expand Down Expand Up @@ -566,7 +563,15 @@ export function ProjectsView() {
}

if (projectReadModels.length === 0) {
return <EmptyState />;
return (
<>
<ProjectCreationDialog
onOpenChange={setCreateProjectOpen}
open={createProjectOpen}
/>
<EmptyState onCreateProject={() => setCreateProjectOpen(true)} />
</>
);
}

const projectItems = (
Expand Down Expand Up @@ -740,19 +745,7 @@ export function ProjectsView() {
overviewDetached ? "projects-overview-content-pod" : undefined
}
>
<CreateProjectDialog
isCreating={createProjectMutation.isPending}
onCreate={async (input) => {
const result = await createProjectMutation.mutateAsync(input);
if (result.compatibilityWarning) {
toast.warning("Created as a standalone project", {
description: result.compatibilityWarning,
});
} else {
toast.success(`Project "${result.project.name}" created.`);
}
await goProject(result.project.id);
}}
<ProjectCreationDialog
onOpenChange={setCreateProjectOpen}
open={createProjectOpen}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ test("activity pod shows workspace details without a create action", () => {
},
});

assert.equal(context.title, "Projects");
assert.equal(context.title, "Activity");
assert.equal(context.detailsTitle, "Details");
assert.equal(context.action, null);
assert.deepEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ export function projectsOverviewContext(
section: "prs",
},
],
title: "Projects",
title: "Activity",
};
}

Expand Down
6 changes: 6 additions & 0 deletions desktop/src/testing/e2eBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,8 @@ declare global {
__BUZZ_E2E_REPOSITORY_ONLY_PROJECTS__?: boolean;
/** Leaves broad project enumeration pending while scoped project queries remain available. */
__BUZZ_E2E_DEFER_FULL_PROJECT_QUERIES__?: boolean;
/** Omits all seeded project and repository events for empty-state tests. */
__BUZZ_E2E_EMPTY_PROJECTS__?: boolean;
/** Project-scoped events accepted by the mock relay. */
__BUZZ_E2E_ACCEPTED_PROJECT_EVENTS__?: Array<{
content: string;
Expand Down Expand Up @@ -5912,6 +5914,10 @@ function writeMockProjectBranch(
}

function buildMockProjectEvents(): RelayEvent[] {
if (window.__BUZZ_E2E_EMPTY_PROJECTS__) {
return [];
}

const events: RelayEvent[] = [];
const daySeconds = 86_400;
const now = Math.floor(Date.now() / 1000);
Expand Down
69 changes: 69 additions & 0 deletions desktop/tests/e2e/project-empty-state-alignment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { expect, test } from "@playwright/test";

import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";

const ALIGNMENT_TOLERANCE_PX = 2;

async function enableProjectsFeature(page: import("@playwright/test").Page) {
await page.addInitScript(() => {
window.localStorage.setItem(
"buzz-feature-overrides-v1",
JSON.stringify({ projects: true }),
);
});
}

async function addProjectToSidebar(
page: import("@playwright/test").Page,
dtag: string,
) {
await page.getByTestId("sidebar-projects-section-label").hover();
await page.getByTestId("sidebar-projects-create").click();
const browser = page.getByTestId("project-browser-dialog");
await browser.getByRole("searchbox", { name: "Search projects" }).fill(dtag);
await browser.getByTestId(`project-browser-result-${dtag}`).click();
await expect(browser).toBeHidden();
await expect(page.getByTestId(`sidebar-project-${dtag}`)).toBeVisible();
}

test("first-time project empty state opens project creation", async ({
page,
}) => {
await enableProjectsFeature(page);
await page.addInitScript(() => {
window.__BUZZ_E2E_EMPTY_PROJECTS__ = true;
});
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();

await expect(
page.getByRole("main").getByText("No projects yet"),
).toBeVisible();
await page.getByRole("button", { name: "Create project" }).click();
await expect(page.getByTestId("create-project-dialog")).toBeVisible();
});

test("project home context aligns with the channel header", async ({
page,
}) => {
await enableProjectsFeature(page);
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await addProjectToSidebar(page, "buzz");
await waitForAnimations(page);

const [headerTitleBox, tasksBox] = await Promise.all([
page.getByTestId("chat-title").boundingBox(),
page.getByTestId("project-home-context-tasks").boundingBox(),
]);
expect(headerTitleBox).not.toBeNull();
expect(tasksBox).not.toBeNull();
const headerTitleCenter =
(headerTitleBox?.y ?? 0) + (headerTitleBox?.height ?? 0) / 2;
const tasksCenter = (tasksBox?.y ?? 0) + (tasksBox?.height ?? 0) / 2;
expect(Math.abs(headerTitleCenter - tasksCenter)).toBeLessThanOrEqual(
ALIGNMENT_TOLERANCE_PX,
);
});
6 changes: 3 additions & 3 deletions desktop/tests/e2e/project-pr-review.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1722,7 +1722,7 @@ test("project overview presents collapsible context beside grouped activity", as
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Projects",
"Activity",
);
expect(
Math.round(
Expand Down Expand Up @@ -1757,7 +1757,7 @@ test("project overview presents collapsible context beside grouped activity", as
await expect(
page
.getByTestId("projects-overview-context-panel")
.getByRole("heading", { name: "Activity" }),
.getByRole("heading", { level: 3, name: "Activity" }),
).toHaveCount(0);
await expect(
page
Expand Down Expand Up @@ -1932,7 +1932,7 @@ test("project overview presents collapsible context beside grouped activity", as
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Projects",
"Activity",
);
await expect(page.getByTestId("projects-overview-activity")).toHaveCount(0);

Expand Down
Loading