diff --git a/desktop/src/features/projects/ui/DiscussionChannels.tsx b/desktop/src/features/projects/ui/DiscussionChannels.tsx index 58d8324215..03af4ca91a 100644 --- a/desktop/src/features/projects/ui/DiscussionChannels.tsx +++ b/desktop/src/features/projects/ui/DiscussionChannels.tsx @@ -273,7 +273,7 @@ const NAME_LIST_MAX = 3; function DiscussionMessagePreview({ content }: { content: string }) { return ( diff --git a/desktop/src/features/projects/ui/ProjectCommitCopyButton.tsx b/desktop/src/features/projects/ui/ProjectCommitCopyButton.tsx index 0930146e2d..faadaf5d2c 100644 --- a/desktop/src/features/projects/ui/ProjectCommitCopyButton.tsx +++ b/desktop/src/features/projects/ui/ProjectCommitCopyButton.tsx @@ -15,12 +15,16 @@ export function CopyTextButton({ text: string; }) { const [copied, setCopied] = React.useState(false); - const handleCopy = React.useCallback(() => { - void writeTextToClipboard(text).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2_000); - }); - }, [text]); + const handleCopy = React.useCallback( + (event: React.MouseEvent) => { + event.stopPropagation(); + void writeTextToClipboard(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2_000); + }); + }, + [text], + ); return ( event.stopPropagation()} type="button" > {copied ? ( diff --git a/desktop/src/features/projects/ui/ProjectEntityListRow.tsx b/desktop/src/features/projects/ui/ProjectEntityListRow.tsx index 10cd1f87a4..7493461132 100644 --- a/desktop/src/features/projects/ui/ProjectEntityListRow.tsx +++ b/desktop/src/features/projects/ui/ProjectEntityListRow.tsx @@ -311,13 +311,13 @@ export function ProjectEntityListRow({ {count != null ? ( - + {count} {countSuffix} diff --git a/desktop/src/features/projects/ui/ProjectRepositoryActionsPanel.tsx b/desktop/src/features/projects/ui/ProjectRepositoryActionsPanel.tsx index 78c59de03e..1caae6f6d3 100644 --- a/desktop/src/features/projects/ui/ProjectRepositoryActionsPanel.tsx +++ b/desktop/src/features/projects/ui/ProjectRepositoryActionsPanel.tsx @@ -531,6 +531,7 @@ export function ProjectRepositoryActionsPanel({ {!selectedIssue && !selectedPullRequest && diff --git a/desktop/src/features/projects/ui/ProjectRepositoryPanel.tsx b/desktop/src/features/projects/ui/ProjectRepositoryPanel.tsx index d784dc04cd..e28a9d913d 100644 --- a/desktop/src/features/projects/ui/ProjectRepositoryPanel.tsx +++ b/desktop/src/features/projects/ui/ProjectRepositoryPanel.tsx @@ -617,6 +617,7 @@ export function RepositoryFilesPanel({ profiles, fallbackAuthorPubkey, onContextChange, + onOpenCommit, sourceControls, unavailableMessage, }: { @@ -631,6 +632,7 @@ export function RepositoryFilesPanel({ kind: "file" | "folder"; path: string; }) => void; + onOpenCommit?: (commitHash: string) => void; /** Branch picker + remote/local toggle rendered in the panel header. */ sourceControls?: RepoSourceHeaderControls; unavailableMessage?: string; @@ -850,9 +852,36 @@ export function RepositoryFilesPanel({ ) : null} - + - + onOpenCommit(latestCommit.hash) + : undefined + } + onKeyDown={ + latestCommit && onOpenCommit + ? (event) => + handleRepositoryEntryKeyDown(event, () => + onOpenCommit(latestCommit.hash), + ) + : undefined + } + tabIndex={latestCommit && onOpenCommit ? 0 : undefined} + > {latestCommit ? ( @@ -904,9 +933,8 @@ export function RepositoryFilesPanel({ - {visibleEntries.map((entry, index) => { + {visibleEntries.map((entry) => { const latestCommit = entry.latestCommit; - const rowIsLast = index === visibleEntries.length - 1; const openEntry = () => openRepositoryEntry(entry, openPath, setSelectedFile); @@ -914,6 +942,7 @@ export function RepositoryFilesPanel({ @@ -921,12 +950,7 @@ export function RepositoryFilesPanel({ } tabIndex={0} > - + @@ -934,23 +958,13 @@ export function RepositoryFilesPanel({ - + - + {latestCommit ? ( + + {branchLabel} + + } + /> + + {communityIcon ? ( + + ) : ( + + 🐝 + + )} + - {showSummary ? ( + {showSummary && displayedDecisionActors.length === 0 ? ( {reviewSummary} @@ -317,32 +317,27 @@ export function PullRequestReviewersRow({ ? History : null; const decisionLabel = hasApproved - ? "Approved" + ? `Approved by ${label}` : hasRequestedChanges - ? "Changes requested" + ? `Changes requested by ${label}` : needsRereview - ? "Re-review needed" - : "Pending"; + ? `Re-review needed from ${label}` + : `Awaiting review from ${label}`; return ( - {label} - - {DecisionIcon ? : null} - {decisionLabel} - + {DecisionIcon ? ( + + ) : null} + {decisionLabel} ); })} diff --git a/desktop/tests/e2e/project-commit-detail.spec.ts b/desktop/tests/e2e/project-commit-detail.spec.ts index 0a663653ab..b32cdae810 100644 --- a/desktop/tests/e2e/project-commit-detail.spec.ts +++ b/desktop/tests/e2e/project-commit-detail.spec.ts @@ -585,6 +585,30 @@ test("multi-repository projects switch the active repository", async ({ .toBe(true); }); +test("latest files commit opens its detail without a divider", async ({ + page, +}) => { + await enableProjectsFeature(page); + await installMockBridge(page); + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.getByTestId("open-projects-view").click(); + await page.getByTestId("projects-section-projects").click(); + const projectEntry = page + .locator( + '[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]', + ) + .first(); + await expect(projectEntry).toBeVisible({ timeout: 10_000 }); + await projectEntry.click(); + await page.getByRole("tab", { name: "Files" }).click(); + + const latestCommit = page.getByTestId("project-repository-latest-commit"); + await expect(latestCommit).toBeVisible(); + await expect(latestCommit).toHaveCSS("border-bottom-width", "0px"); + await latestCommit.click(); + await expect(page.getByTestId("project-commit-detail")).toBeVisible(); +}); + test("commit detail opens from the commits feed with a diff", async ({ page, }) => { diff --git a/desktop/tests/e2e/project-pr-review.spec.ts b/desktop/tests/e2e/project-pr-review.spec.ts index fc9448ceba..0e03a9f7ca 100644 --- a/desktop/tests/e2e/project-pr-review.spec.ts +++ b/desktop/tests/e2e/project-pr-review.spec.ts @@ -204,6 +204,7 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({ ).toBeVisible(); await expect(page.getByTestId("project-context-chat-agent")).toBeVisible(); await expect(page.getByTestId("project-context-discuss")).toBeVisible(); + await expect(page.getByTestId("project-context-branch")).toContainText("→"); await expect(page.getByTestId("project-context-review-summary")).toHaveCount( 0, ); @@ -264,6 +265,12 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({ await expect(page.getByText("Requested a review from bob")).toBeVisible({ timeout: 10_000, }); + await expect(page.getByTestId("project-review-summary")).toHaveCount(0); + await expect( + page + .getByTestId("project-reviewer-decision") + .filter({ hasText: "Awaiting review from bob" }), + ).toHaveCount(1); await waitForAnimations(page); await page.screenshot({ @@ -287,9 +294,12 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({ .getByTestId("project-pull-request-timeline-row") .filter({ hasText: "requested changes" }), ).toBeVisible({ timeout: 10_000 }); - await expect(page.getByTestId("project-review-summary")).toHaveText( - "Changes requested", - ); + await expect(page.getByTestId("project-review-summary")).toHaveCount(0); + await expect( + page + .getByTestId("project-reviewer-decision") + .filter({ hasText: "Changes requested" }), + ).toHaveCount(1); const changeRequestEvent = await page.evaluate(() => window.__BUZZ_E2E_SIGNED_EVENTS__ ?.filter( @@ -368,10 +378,12 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({ await expect( page.getByRole("button", { name: "Approve", exact: true }), ).toHaveCount(0); - await expect(page.getByTestId("project-review-summary")).toHaveText( - "Awaiting review", - ); - await expect(page.getByText("Approved", { exact: true })).toBeVisible(); + await expect(page.getByTestId("project-review-summary")).toHaveCount(0); + await expect( + page + .getByTestId("project-reviewer-decision") + .filter({ hasText: "Approved by" }), + ).toHaveCount(1); const approvalEvent = await page.evaluate(() => window.__BUZZ_E2E_SIGNED_EVENTS__ ?.filter( diff --git a/desktop/tests/e2e/projects-v3-screenshots.spec.ts b/desktop/tests/e2e/projects-v3-screenshots.spec.ts index 93578ef552..fa1075dae8 100644 --- a/desktop/tests/e2e/projects-v3-screenshots.spec.ts +++ b/desktop/tests/e2e/projects-v3-screenshots.spec.ts @@ -40,7 +40,19 @@ test("projects activity overview screenshot", async ({ 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(); + const activityHeader = page.getByTestId("projects-page-header"); + const relayIcon = page.getByTestId("projects-activity-relay-icon"); + await expect(activityHeader).toBeVisible(); + await expect(relayIcon).toBeVisible(); + const [activityHeaderBox, relayIconBox] = await Promise.all([ + activityHeader.boundingBox(), + relayIcon.boundingBox(), + ]); + expect(activityHeaderBox).not.toBeNull(); + expect(relayIconBox).not.toBeNull(); + expect((relayIconBox?.y ?? 0) + (relayIconBox?.height ?? 0)).toBeLessThan( + activityHeaderBox?.y ?? 0, + ); await expect(page.getByTestId("projects-activity-search")).toBeVisible(); await expect(page.getByTestId("projects-activity-intro")).toContainText( "Projects Activity", @@ -785,6 +797,13 @@ test("projects v3 workspace screenshot states", async ({ page }) => { await expect( workspacePanel.getByTestId("project-repository-entry-icon").first(), ).toHaveCSS("border-radius", "8px"); + const repositoryEntryCell = workspacePanel + .getByTestId("project-repository-entry-row") + .first() + .locator("td") + .first(); + await expect(repositoryEntryCell).toHaveCSS("border-radius", "0px"); + await expect(repositoryEntryCell).toHaveCSS("border-bottom-width", "0px"); await chatPanelTab.click(); await expect(agentContext).toContainText("Files"); await expect( diff --git a/desktop/tests/e2e/sidebar.spec.ts b/desktop/tests/e2e/sidebar.spec.ts index 01d9d64d67..f2b3105812 100644 --- a/desktop/tests/e2e/sidebar.spec.ts +++ b/desktop/tests/e2e/sidebar.spec.ts @@ -531,6 +531,47 @@ test("aligns the sidebar search with the channel title outside the Buzz theme", expect(Math.abs(searchCenter - channelTitleCenter)).toBeLessThanOrEqual(2); }); +test("keeps only search pinned while primary navigation scrolls", async ({ + page, +}) => { + await loadTheme(page, "github-light"); + + const search = page.getByTestId("open-search"); + const primaryMenu = page.getByTestId("sidebar-primary-menu"); + const sidebarScroller = page.locator(".buzz-sidebar-scrollbar"); + const [initialSearchBox, initialMenuBox] = await Promise.all([ + search.boundingBox(), + primaryMenu.boundingBox(), + ]); + expect(initialSearchBox).not.toBeNull(); + expect(initialMenuBox).not.toBeNull(); + + const scrollTop = await sidebarScroller.evaluate((element) => { + element.scrollTop = Math.min( + 120, + Math.max(0, element.scrollHeight - element.clientHeight), + ); + return element.scrollTop; + }); + expect(scrollTop).toBeGreaterThan(0); + await expect + .poll(() => + sidebarScroller.evaluate((element) => Math.round(element.scrollTop)), + ) + .toBe(Math.round(scrollTop)); + + const [scrolledSearchBox, scrolledMenuBox] = await Promise.all([ + search.boundingBox(), + primaryMenu.boundingBox(), + ]); + expect(scrolledSearchBox).not.toBeNull(); + expect(scrolledMenuBox).not.toBeNull(); + expect( + Math.abs((scrolledSearchBox?.y ?? 0) - (initialSearchBox?.y ?? 0)), + ).toBeLessThanOrEqual(1); + expect(scrolledMenuBox?.y ?? 0).toBeLessThan(initialMenuBox?.y ?? 0); +}); + test("scales the sidebar backward while its chrome closes", async ({ page, }) => {