feat(desktop): make the Projects overview follow the selected section (#6335)

## Summary

Part 1 of 4 stacked PRs continuing the Projects work from #6003.

- The Projects overview now follows the selected section: the right-hand
context pod stays visible across the Projects / Repositories / Reviews /
Tasks / Channels tabs and shows section-specific people, stats, and
contribution graphs, so it reads as live context rather than a detached
summary.
- Entity list rows are consolidated onto one compact line (title first,
icons after, aligned dates/counts), shared across projects,
repositories, PRs, issues, and channel lists via `ProjectEntityListRow`.
- New `projectRelatedChannels` helper resolves the channels a project is
discussed in for the overview.

Follow-ups in this stack: part 2 (selectable workspaces), part 3
(context-aware collaboration), part 4 (navigation & detail-page polish).

## Test plan

- [x] Desktop unit tests (`pnpm test`) — pass
- [x] TypeScript (`tsc --noEmit`), Biome, clippy — clean via pre-push
gate
- [x] Playwright specs updated for the new overview behavior
(`projects-v3-screenshots.spec.ts`, `sidebar.spec.ts`) — pass locally in
the smoke project

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
thomaspblock
2026-08-19 21:55:47 -04:00
committed by GitHub
parent 09718fbb60
commit f88cda9eb8
47 changed files with 4013 additions and 1186 deletions

View File

@@ -57,7 +57,7 @@ test("top-level project lists align dates and overflow actions", async ({
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await expect(
page.getByRole("heading", { level: 1, name: "Projects" }),
page.getByRole("heading", { level: 2, name: "Projects", exact: true }),
).toBeVisible();
async function trailingPositions(
@@ -103,18 +103,9 @@ test("top-level project lists align dates and overflow actions", async ({
await expect(page.getByRole("menuitem", { name: "Local" })).toBeVisible();
await page.keyboard.press("Escape");
const projectRow = page.locator('[data-testid^="project-row-"]').first();
const projectPositions = await trailingPositions(projectRow, {
summaryTestId: "projects-row-summary",
});
// Project rows show the activity bar alone — counts stay in its tooltips.
await expect(
projectRow
.getByTestId("projects-row-summary")
.getByTestId("project-activity-bar"),
).toBeVisible();
await expect(
projectRow.getByTestId("projects-row-summary"),
).not.toContainText("commits");
const projectPositions = await trailingPositions(projectRow);
await expect(projectRow.getByTestId("projects-row-context")).toBeVisible();
await expect(projectRow.getByTestId("projects-row-people")).toBeVisible();
await page.getByTestId("projects-section-repositories").click();
await page.getByRole("button", { name: "Filter repositories" }).click();
@@ -126,17 +117,14 @@ test("top-level project lists align dates and overflow actions", async ({
await expect(page.getByTestId("repository-row-relay-tools")).toBeVisible();
const repositoryRow = page.getByTestId("repository-row-buzz");
await expect(
repositoryRow.getByTestId("repositories-row-summary"),
).toContainText("commits");
repositoryRow.getByTestId("repositories-row-project"),
).toBeVisible();
await expect(
repositoryRow.getByTestId("repositories-row-branch"),
).toContainText("main");
// Subtitle is the repository location (owner/repo for Buzz-hosted repos).
await expect(repositoryRow.locator("p")).toHaveText(/\/buzz$/);
repositoryRow.getByTestId("repositories-row-description"),
).toContainText(/Relay, desktop, and mobile|community platform/);
const repositoryPositions = await trailingPositions(repositoryRow, {
actionName: /More options for/,
dateTestId: "repositories-row-date",
summaryTestId: "repositories-row-summary",
});
// No summaryX comparison: repository rows carry text stats next to the bar
// while project rows show the bar alone, so the columns differ in width by
@@ -219,11 +207,11 @@ test("top-level project lists align dates and overflow actions", async ({
.locator('[data-testid^="project-row-"]')
.first();
await expect(
responsiveRepositoryRow.getByTestId("projects-row-summary"),
).toBeHidden();
responsiveRepositoryRow.getByTestId("projects-row-context"),
).toBeVisible();
await expect(
responsiveRepositoryRow.getByTestId("projects-row-people"),
).toBeHidden();
).toBeVisible();
await expect(
responsiveRepositoryRow.getByTestId("projects-row-date"),
).toBeVisible();
@@ -470,7 +458,13 @@ test("multi-repository projects switch the active repository", async ({
},
);
await projectRow.click();
await expect(page).toHaveURL(/\/projects\//);
await expect(page).not.toHaveURL(/\/projects\//);
await expect(projectRow).toHaveAttribute("aria-expanded", "false");
await expect(relayToolsRepository).toBeHidden();
await projectRow.click();
await expect(page).not.toHaveURL(/\/projects\//);
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect(relayToolsRepository).toBeVisible();
const projectSidebarMetrics = await sidebarScrollContent.evaluate(
(element) => {
@@ -496,7 +490,7 @@ test("multi-repository projects switch the active repository", async ({
return element.scrollTop;
});
await projectRow.click();
await expect(page).toHaveURL(/\/projects\//);
await expect(page).not.toHaveURL(/\/projects\//);
await expect(projectRow).toHaveAttribute("aria-expanded", "true");
await expect
.poll(() =>

View File

@@ -867,9 +867,11 @@ test("project pull request author rollover stays identity-only", async ({
),
).toBeVisible();
const authorLabel = (
await author.getByTestId("projects-pr-author-label").innerText()
).trim();
const authorLabel =
(
await author.getByTestId("projects-pr-author-label").textContent()
)?.trim() ?? "";
expect(authorLabel.length).toBeGreaterThan(0);
await author.hover();
const rollover = page.getByTestId("projects-pr-author-rollover");
await expect(rollover).toBeVisible();
@@ -898,9 +900,11 @@ test("project issue author rollover matches pull requests", async ({
),
).toBeVisible();
const authorLabel = (
await author.getByTestId("projects-issue-author-label").innerText()
).trim();
const authorLabel =
(
await author.getByTestId("projects-issue-author-label").textContent()
)?.trim() ?? "";
expect(authorLabel.length).toBeGreaterThan(0);
await author.hover();
const rollover = page.getByTestId("projects-issue-author-rollover");
await expect(rollover).toBeVisible();
@@ -993,7 +997,7 @@ test("project overview reports aggregate work-item failures", async ({
);
});
test("project overview does not paint a background behind its cards", async ({
test("project overview presents collapsible context beside grouped activity", async ({
page,
}) => {
await enableProjectsFeature(page);
@@ -1005,16 +1009,138 @@ test("project overview does not paint a background behind its cards", async ({
"background-color",
"rgba(0, 0, 0, 0)",
);
await expect(page.getByTestId("projects-overview-layout")).toHaveAttribute(
"data-project-context-detached",
"true",
);
const overviewLayout = page.getByTestId("projects-overview-layout");
const overviewContentPod = page.getByTestId("projects-overview-content-pod");
const appContentSurface = page
.locator("[data-buzz-content-surface]")
.filter({ has: overviewLayout })
.first();
await expect(appContentSurface).toHaveCSS(
"background-color",
"rgba(0, 0, 0, 0)",
);
await expect(appContentSurface).toHaveCSS("border-radius", "0px");
await expect(overviewContentPod).toHaveCSS("border-radius", "16px");
await expect(page.getByTestId("projects-workspace-chrome")).toContainText(
"Projects",
);
await expect(page.getByTestId("projects-workspace-chrome")).toContainText(
"Activity",
);
await expect(page.getByTestId("projects-page-tabs")).toBeVisible();
await expect(page.getByTestId("projects-page-header")).toContainText(
"Welcome to Activity",
);
await expect(page.getByTestId("projects-activity-search")).toBeVisible();
await expect(page.getByTestId("projects-activity-intro")).toContainText(
"Keep up with commits, reviews, and tasks",
);
await expect(
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Projects",
);
expect(
Math.round(
(await page.getByTestId("projects-overview-context-panel").boundingBox())
?.width ?? 0,
),
).toBe(280);
await expect(
page.getByTestId("projects-overview-create-project"),
).toContainText("Create project");
await expect(
page
.getByTestId("projects-overview-context-panel")
.getByTestId("projects-create-menu"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-stats-pod")).toBeVisible();
await expect(
page
.getByTestId("projects-overview-context-panel")
.getByTestId("projects-overview-people"),
).toBeVisible();
await expect(
page
.getByTestId("projects-overview-stats-pod")
.getByTestId("projects-overview-people"),
).toHaveCount(0);
await expect(
page
.getByTestId("projects-overview-context-panel")
.getByTestId("projects-overview-context-title"),
).toBeVisible();
await expect(
page
.getByTestId("projects-overview-context-panel")
.getByRole("heading", { name: "Details" }),
).toHaveCount(0);
await expect(
page
.getByTestId("projects-overview-context-panel")
.getByRole("heading", { name: "Activity" }),
).toHaveCount(0);
await expect(
page
.getByTestId("projects-overview-context-panel")
.getByTestId("projects-overview-activity"),
).toBeVisible();
await expect(
page.getByTestId("projects-activity-group").first(),
).toContainText(/This week|Last week|Earlier/);
const searchBox = await page
.getByTestId("projects-activity-search")
.boundingBox();
const tabMenuBox = await page.getByTestId("projects-page-tabs").boundingBox();
const firstTabBox = await page
.getByTestId("projects-section-all")
.boundingBox();
const introBox = await page
.getByTestId("projects-activity-intro")
.boundingBox();
const feedBox = await page
.getByTestId("projects-activity-group")
.first()
.boundingBox();
const contentPodBox = await overviewContentPod.boundingBox();
expect(searchBox).toBeTruthy();
expect(tabMenuBox).toBeTruthy();
expect(firstTabBox).toBeTruthy();
expect(introBox).toBeTruthy();
expect(feedBox).toBeTruthy();
expect(contentPodBox).toBeTruthy();
expect(searchBox?.x ?? 0).toBeLessThan(firstTabBox?.x ?? 0);
expect(Math.abs((searchBox?.y ?? 0) - (firstTabBox?.y ?? 0))).toBeLessThan(8);
expect(tabMenuBox?.y ?? 0).toBeLessThan(introBox?.y ?? 0);
expect(introBox?.y ?? 0).toBeLessThan(feedBox?.y ?? 0);
expect(feedBox?.width ?? 0).toBeLessThan((contentPodBox?.width ?? 0) - 48);
const feedCenter = (feedBox?.x ?? 0) + (feedBox?.width ?? 0) / 2;
const contentCenter =
(contentPodBox?.x ?? 0) + (contentPodBox?.width ?? 0) / 2;
expect(Math.abs(feedCenter - contentCenter)).toBeLessThan(24);
const stats = page.getByTestId("projects-overview-stat");
await expect(stats).toHaveCount(4);
for (let index = 0; index < 4; index += 1) {
await expect(stats.nth(index)).toHaveCSS(
"background-color",
"rgba(0, 0, 0, 0)",
);
await expect(stats.nth(index)).toHaveCSS("border-style", "solid");
}
await expect(stats).toHaveCount(5);
await expect(stats.nth(2)).toContainText("Channels");
const createBox = await page
.getByTestId("projects-overview-create-project")
.boundingBox();
const lastStatBox = await stats.last().boundingBox();
const peopleBox = await page
.getByTestId("projects-overview-people")
.boundingBox();
expect(createBox).toBeTruthy();
expect(lastStatBox).toBeTruthy();
expect(peopleBox).toBeTruthy();
expect(peopleBox?.y ?? 0).toBeGreaterThan(createBox?.y ?? 0);
expect(peopleBox?.y ?? 0).toBeGreaterThan(
(lastStatBox?.y ?? 0) + (lastStatBox?.height ?? 0) - 1,
);
const activityCards = page.getByTestId("projects-activity-card");
await expect(activityCards.first()).toBeVisible();
@@ -1026,6 +1152,104 @@ test("project overview does not paint a background behind its cards", async ({
);
await expect(activityCards.nth(index)).toHaveCSS("border-style", "solid");
}
await page.getByTestId("projects-section-channels").click();
await expect(page.getByTestId("projects-page-header")).toContainText(
"Channels",
);
await expect(
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Channels",
);
await expect(
page.getByTestId("projects-overview-create-project"),
).toHaveCount(0);
await expect(page.getByTestId("projects-overview-people")).toHaveCount(0);
await expect(page.getByTestId("projects-overview-activity")).toHaveCount(0);
await expect(stats).toHaveCount(3);
await expect(stats.first()).toContainText("Channels");
await expect(page.getByTestId("projects-overview-layout")).toHaveAttribute(
"data-project-context-detached",
"true",
);
const channelRows = page.getByTestId("project-channel-row");
// One row per project or repository binding: buzz, buzz · relay-tools,
// design-system, and relay-tools all bind the shared mock channel.
await expect(channelRows).toHaveCount(4);
await expect(channelRows.first()).toContainText("#general");
const channelsList = page.getByTestId("projects-channels-list");
await expect(channelsList).toContainText("buzz");
await expect(channelsList).toContainText("relay-tools");
await expect(channelsList).toContainText("design-system");
await expect(page.getByTestId("project-channel-project")).toHaveCount(4);
await expect(page.getByTestId("project-channel-repository")).toHaveCount(4);
await page.getByTestId("projects-section-projects").click();
await expect(page.getByTestId("projects-page-header")).toContainText(
"Projects",
);
await expect(
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Projects",
);
await expect(
page.getByTestId("projects-overview-create-project"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-people")).toBeVisible();
await expect(page.getByTestId("projects-overview-activity")).toBeVisible();
await page.getByTestId("projects-section-repositories").click();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Repositories",
);
await expect(
page.getByTestId("projects-overview-create-project"),
).toHaveCount(0);
await expect(stats.nth(1)).toContainText("Active tasks");
await expect(page.getByTestId("projects-overview-activity")).toBeVisible();
await page.getByTestId("projects-section-issues").click();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Tasks",
);
await expect(
page.getByTestId("projects-overview-create-issue"),
).toContainText("Create task");
await expect(page.getByTestId("projects-overview-activity")).toHaveCount(0);
await page.getByTestId("projects-section-prs").click();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Reviews",
);
await expect(
page.getByTestId("projects-overview-create-pull-request"),
).toContainText("Create review");
await expect(page.getByTestId("projects-overview-people")).toBeVisible();
await expect(page.getByTestId("projects-overview-activity")).toHaveCount(0);
await page.getByTestId("projects-section-all").click();
await expect(
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(page.getByTestId("projects-overview-context-title")).toHaveText(
"Projects",
);
await expect(page.getByTestId("projects-overview-activity")).toBeVisible();
await page.getByTestId("projects-overview-create-project").click();
await expect(page.getByTestId("create-project-dialog")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByTestId("create-project-dialog")).toBeHidden();
await page.getByTestId("projects-overview-context-toggle").click();
await expect(
page.getByTestId("projects-overview-context-panel"),
).not.toBeVisible();
await expect(
page.getByTestId("projects-overview-layout"),
).not.toHaveAttribute("data-project-context-detached", "true");
await expect(stats).toHaveCount(0);
await expect(activityCards.first()).toBeVisible();
await expect(page.getByTestId("projects-create-menu")).toBeVisible();
});
test("repository rows identify their git host", async ({ page }) => {
@@ -1081,7 +1305,6 @@ test("project subsections do not paint backgrounds behind list or grid items", a
"background-color",
"rgba(0, 0, 0, 0)",
);
await expect(listItems.nth(index)).toHaveCSS("border-style", "solid");
}
await page.getByRole("button", { name: "Grid layout" }).click();
@@ -1595,3 +1818,68 @@ test("project task can be created with a category from the tasks header", async
]);
expect(createdEvent?.tags).toContainEqual(["t", "change-request"]);
});
test("narrow layouts keep section context reachable through a sheet", async ({
page,
}) => {
await page.setViewportSize({ height: 720, width: 900 });
await enableProjectsFeature(page);
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
// Below the detached breakpoint the docked rail never renders, but the
// context toggle stays available.
await expect(page.getByTestId("projects-page-tabs")).toBeVisible();
await expect(page.getByTestId("projects-overview-context-panel")).toHaveCount(
0,
);
const contextToggle = page.getByTestId("projects-overview-context-toggle");
await expect(contextToggle).toBeVisible();
await expect(contextToggle).toHaveAttribute("aria-pressed", "false");
// Keyboard journey into the Tasks section: open the sheet from the toggle.
await page.getByTestId("projects-section-issues").click();
await expect(page.getByTestId("projects-page-header")).toContainText("Tasks");
await contextToggle.focus();
await page.keyboard.press("Enter");
const contextSheet = page.getByTestId("projects-overview-context-sheet");
await expect(contextSheet).toBeVisible();
await expect(
contextSheet.getByTestId("projects-overview-context-title"),
).toHaveText("Tasks");
await expect(contextToggle).toHaveAttribute("aria-pressed", "true");
// Escape dismisses the sheet and returns focus to the toggle.
await page.keyboard.press("Escape");
await expect(contextSheet).toBeHidden();
await expect(contextToggle).toBeFocused();
await expect(contextToggle).toHaveAttribute("aria-pressed", "false");
// The same journey follows the Reviews section.
await page.getByTestId("projects-section-prs").click();
await expect(page.getByTestId("projects-page-header")).toContainText(
"Reviews",
);
await contextToggle.focus();
await page.keyboard.press("Enter");
await expect(contextSheet).toBeVisible();
await expect(
contextSheet.getByTestId("projects-overview-context-title"),
).toHaveText("Reviews");
await page.keyboard.press("Escape");
await expect(contextSheet).toBeHidden();
// Selecting a section inside the sheet navigates and dismisses it: the
// Activity context lists every section as a stat row.
await page.getByTestId("projects-section-all").click();
await contextToggle.focus();
await page.keyboard.press("Enter");
await expect(contextSheet).toBeVisible();
await contextSheet
.getByTestId("projects-overview-stat")
.filter({ hasText: "Tasks" })
.click();
await expect(contextSheet).toBeHidden();
await expect(page.getByTestId("projects-page-header")).toContainText("Tasks");
});

View File

@@ -18,6 +18,32 @@ async function openBuzzProject(page: import("@playwright/test").Page) {
await projectEntry.click();
}
test("projects activity overview screenshot", async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem("buzz-theme", "light");
});
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await expect(page.getByTestId("projects-page-tabs")).toBeVisible();
await expect(page.getByTestId("projects-page-header")).toBeVisible();
await expect(page.getByTestId("projects-activity-search")).toBeVisible();
await expect(page.getByTestId("projects-activity-intro")).toContainText(
"Welcome to Activity",
);
await expect(
page.getByTestId("projects-overview-context-panel"),
).toBeVisible();
await expect(
page.getByTestId("projects-overview-create-project"),
).toBeVisible();
await expect(
page.getByTestId("projects-activity-group").first(),
).toBeVisible();
await waitForAnimations(page);
await page.screenshot({ path: `${SHOTS}/00-projects-pulse.png` });
});
test("sidebar project add flow browses before creating", async ({ page }) => {
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
@@ -473,20 +499,24 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
"Hide project context",
);
await expect(terminalIcon).toBeVisible();
// The icon is an inline SVG drawn with currentColor, so it must inherit
// the toggle button's text color to stay tinted with button state.
expect(
await terminalIcon.evaluate((element) => {
const style = getComputedStyle(element);
return {
backgroundColor: style.backgroundColor,
maskImage: style.maskImage || style.webkitMaskImage,
parentColor: getComputedStyle(element.parentElement ?? element).color,
};
}),
await terminalIcon.evaluate((element) => ({
color: getComputedStyle(element).color,
strokesCurrentColor: Array.from(element.querySelectorAll("rect")).some(
(rect) =>
rect.getAttribute("stroke") === "currentColor" ||
rect.getAttribute("fill") === "currentColor",
),
tagName: element.tagName.toLowerCase(),
})),
).toMatchObject({
backgroundColor: await terminalButton.evaluate(
color: await terminalButton.evaluate(
(element) => getComputedStyle(element).color,
),
maskImage: expect.not.stringMatching(/^none$/),
strokesCurrentColor: true,
tagName: "svg",
});
const [repositoryTabBounds, chatTabBounds] = await Promise.all([
repositoryPanelTab.boundingBox(),
@@ -511,6 +541,9 @@ test("projects v3 workspace screenshot states", async ({ page }) => {
await expect(agentContext).toBeVisible();
await expect(agentContext).toContainText("Overview");
await expect(agentContext).not.toContainText("Buzz /");
// The context rail reveals the chat panel with a width transition; measure
// only after it settles or the panel's unclipped box overhangs the rail.
await waitForAnimations(page);
const [
attachedSharedHeaderBackdropBounds,
tabMenuHeaderBounds,
@@ -1012,25 +1045,33 @@ test("projects v3 work-item list metadata", async ({ page }) => {
await page.getByTestId("open-projects-view").click();
await page.getByTestId("projects-section-prs").click();
const reviewTable = page.getByTestId("projects-list-container");
await expect(reviewTable.locator("thead")).toHaveClass(/sr-only/);
const reviewList = page.getByTestId("projects-list-container");
await expect(reviewList).toBeVisible();
const pullRequestRow = page.getByTestId(/^projects-pr-row-/).first();
await expect(pullRequestRow).toBeVisible();
await expect(pullRequestRow).toContainText("Review");
await expect(pullRequestRow).toContainText("opened this in");
await expect(pullRequestRow).toContainText(/from\s*feature\/mock-2-0/);
await expect(pullRequestRow).not.toContainText(/#[0-9a-f]{8}/);
await expect(pullRequestRow).toContainText(/relay-tools|buzz|design-system/);
await waitForAnimations(page);
await page.screenshot({ path: `${SHOTS}/05-pr-list-metadata.png` });
await page.getByTestId("projects-section-issues").click();
const taskTable = page.getByTestId("projects-list-container");
await expect(taskTable.locator("thead")).toHaveClass(/sr-only/);
const taskList = page.getByTestId("projects-list-container");
await expect(taskList).toBeVisible();
const issueRow = page.getByTestId(/^projects-issue-row-/).first();
await expect(issueRow).toBeVisible();
await expect(issueRow).toContainText("Issue");
await expect(issueRow).toContainText(/opened this in\s*relay-tools/);
await expect(issueRow).not.toContainText(/#[0-9a-f]{8}/);
await expect(issueRow).toContainText(/relay-tools|buzz|design-system/);
await waitForAnimations(page);
await page.screenshot({ path: `${SHOTS}/06-issue-list-metadata.png` });
await page.getByTestId("projects-section-channels").click();
const channelRow = page.getByTestId("project-channel-row").first();
await expect(channelRow).toBeVisible();
await expect(channelRow).toContainText("#general");
await expect(
page.getByTestId("project-channel-project").first(),
).toBeVisible();
await expect(
page.getByTestId("project-channel-repository").first(),
).toBeVisible();
await waitForAnimations(page);
await page.screenshot({ path: `${SHOTS}/07-channels-list.png` });
});