Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
67 changes: 67 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,67 @@
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.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