mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
fix(desktop): refine Projects overview and list presentation
Separate overview context and agent rails from the content pod, simplify project and repository rows, and align the surrounding navigation chrome and metadata. Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
@@ -20,10 +20,8 @@ import type {
|
||||
ProjectActivitySummary,
|
||||
} from "@/features/projects/hooks";
|
||||
import {
|
||||
formatExactTimestamp,
|
||||
getProjectUpdatedAt,
|
||||
listRowDescription,
|
||||
relativeTime,
|
||||
} from "@/features/projects/lib/projectsViewHelpers";
|
||||
import type { ProjectRepoUnavailableReason } from "@/features/projects/lib/projectRepoAvailability";
|
||||
import { projectShareLink } from "@/features/projects/lib/projectShareLinks";
|
||||
@@ -55,39 +53,6 @@ import { ProjectEntityListRow } from "./ProjectEntityListRow";
|
||||
import { PROJECT_GRID_CARD_BODY_CLASS } from "./projectGridCardStyles";
|
||||
import { ProjectListRowMenu } from "./ProjectListRowMenu";
|
||||
|
||||
function ProjectUpdatedLabel({
|
||||
profiles,
|
||||
project,
|
||||
summary,
|
||||
}: {
|
||||
profiles?: UserProfileLookup;
|
||||
project: Project;
|
||||
summary: ProjectActivitySummary | undefined;
|
||||
}) {
|
||||
const updatedAt = getProjectUpdatedAt(project, summary);
|
||||
const latestCommit = summary?.latestCommit;
|
||||
const authorLabel = latestCommit?.author
|
||||
? resolveUserLabel({ profiles, pubkey: latestCommit.author })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="whitespace-nowrap text-xs leading-4 text-muted-foreground/70">
|
||||
{relativeTime(updatedAt)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-96 break-words">
|
||||
{latestCommit
|
||||
? `${latestCommit.title || latestCommit.commit.slice(0, 7)}${
|
||||
authorLabel ? ` · ${authorLabel}` : ""
|
||||
} · ${formatExactTimestamp(latestCommit.createdAt)}`
|
||||
: `Created ${formatExactTimestamp(project.createdAt)}`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectPeopleStack({
|
||||
pubkeys,
|
||||
profiles,
|
||||
@@ -540,12 +505,7 @@ export function ProjectGridCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-auto relative z-10 flex shrink-0 items-center gap-1">
|
||||
<ProjectUpdatedLabel
|
||||
profiles={profiles}
|
||||
project={project}
|
||||
summary={summary}
|
||||
/>
|
||||
<div className="pointer-events-auto relative z-10 shrink-0">
|
||||
<ProjectActionsMenu
|
||||
canDelete={canDelete}
|
||||
disabled={deleteDisabled}
|
||||
@@ -613,6 +573,7 @@ export function ProjectListRow({
|
||||
<span>{repositoryCount}</span>
|
||||
</span>
|
||||
}
|
||||
affiliationClassName="w-auto"
|
||||
affiliationTestId="projects-row-context"
|
||||
affiliationTitle={`${repositoryCount} ${
|
||||
repositoryCount === 1 ? "repository" : "repositories"
|
||||
|
||||
@@ -131,6 +131,7 @@ export function ProjectEntitySelectControl({
|
||||
|
||||
export function ProjectEntityListRow({
|
||||
affiliation,
|
||||
affiliationClassName,
|
||||
affiliationTestId,
|
||||
affiliationTitle,
|
||||
beforeDate,
|
||||
@@ -158,6 +159,7 @@ export function ProjectEntityListRow({
|
||||
trailing,
|
||||
}: {
|
||||
affiliation?: React.ReactNode;
|
||||
affiliationClassName?: string;
|
||||
affiliationTestId?: string;
|
||||
affiliationTitle?: string;
|
||||
beforeDate?: React.ReactNode;
|
||||
@@ -292,7 +294,10 @@ export function ProjectEntityListRow({
|
||||
) : null}
|
||||
{affiliation ? (
|
||||
<span
|
||||
className="hidden w-36 shrink-0 truncate text-right text-xs text-muted-foreground/65 md:block"
|
||||
className={cn(
|
||||
"hidden w-36 shrink-0 truncate text-right text-xs text-muted-foreground/65 md:block",
|
||||
affiliationClassName,
|
||||
)}
|
||||
data-projects-text-priority="secondary"
|
||||
data-testid={affiliationTestId}
|
||||
title={
|
||||
|
||||
@@ -884,7 +884,10 @@ export function RepositoryFilesPanel({
|
||||
>
|
||||
<th className="px-4 py-3 text-left font-normal" colSpan={3}>
|
||||
{latestCommit ? (
|
||||
<div className="flex min-w-0 items-center justify-between gap-3 text-sm">
|
||||
<div
|
||||
className="flex min-w-0 items-center justify-between gap-3 text-xs"
|
||||
data-testid="project-repository-latest-commit-summary"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<UserAvatar
|
||||
accent={latestCommitProfile?.isAgent === true}
|
||||
@@ -950,7 +953,7 @@ export function RepositoryFilesPanel({
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<td className="min-w-52 px-3 py-2 align-middle transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35">
|
||||
<td className="min-w-52 px-3 py-2 align-middle transition-colors group-hover/repository-entry:rounded-l-md group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:rounded-l-md group-focus-visible/repository-entry:bg-muted/35">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<RepositoryEntryIcon entry={entry} />
|
||||
<span className="truncate font-medium text-foreground">
|
||||
@@ -964,7 +967,7 @@ export function RepositoryFilesPanel({
|
||||
profiles={profiles}
|
||||
/>
|
||||
</td>
|
||||
<td className="w-36 whitespace-nowrap p-2 text-right align-middle text-muted-foreground transition-colors group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:bg-muted/35">
|
||||
<td className="w-36 whitespace-nowrap p-2 text-right align-middle text-muted-foreground transition-colors group-hover/repository-entry:rounded-r-md group-hover/repository-entry:bg-muted/35 group-focus-visible/repository-entry:rounded-r-md group-focus-visible/repository-entry:bg-muted/35">
|
||||
{latestCommit ? (
|
||||
<time
|
||||
dateTime={new Date(
|
||||
|
||||
@@ -118,9 +118,7 @@ export function ProjectsListHeaderBar({
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap items-center justify-between gap-2 bg-muted/20 px-3 py-1.5",
|
||||
variant === "bar"
|
||||
? "rounded-xl border border-border/60"
|
||||
: "border-b border-border/60",
|
||||
variant === "bar" ? "rounded-xl" : "border-b border-border/60",
|
||||
)}
|
||||
data-testid="projects-list-header"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Info, MessageCircle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
|
||||
export function ProjectsOverviewChromeActions({
|
||||
chatOpen,
|
||||
contextOpen,
|
||||
onToggleChat,
|
||||
onToggleContext,
|
||||
sectionTitle,
|
||||
}: {
|
||||
chatOpen: boolean;
|
||||
contextOpen: boolean;
|
||||
onToggleChat: () => void;
|
||||
onToggleContext: () => void;
|
||||
sectionTitle: string;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
aria-label={`Chat with an agent about ${sectionTitle}`}
|
||||
aria-pressed={chatOpen}
|
||||
className="h-7 w-7 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
data-testid="projects-overview-chat-toggle"
|
||||
onClick={onToggleChat}
|
||||
size="icon"
|
||||
title={`Chat with an agent about ${sectionTitle}`}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<MessageCircle
|
||||
className={cn(
|
||||
"h-4 w-4 transition-opacity duration-200 ease-linear",
|
||||
chatOpen ? "opacity-100" : "opacity-60",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={
|
||||
contextOpen ? "Hide project context" : "Show project context"
|
||||
}
|
||||
aria-pressed={contextOpen}
|
||||
className="h-7 w-7 text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
data-testid="projects-overview-context-toggle"
|
||||
onClick={onToggleContext}
|
||||
size="icon"
|
||||
title={contextOpen ? "Hide project context" : "Show project context"}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Info
|
||||
className={cn(
|
||||
"h-4 w-4 transition-opacity duration-200 ease-linear",
|
||||
contextOpen ? "opacity-100" : "opacity-60",
|
||||
)}
|
||||
data-testid="projects-overview-context-icon"
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
} from "@/features/projects/lib/projectRepoHost";
|
||||
import { ProjectsActivityFeed } from "@/features/projects/ui/ProjectsActivityFeed";
|
||||
import { ProjectsChannelsList } from "@/features/projects/ui/ProjectsChannelsList";
|
||||
import { ProjectsOverviewChatToggle } from "@/features/projects/ui/ProjectsOverviewChatToggle";
|
||||
import {
|
||||
ProjectsOverviewContextSheet,
|
||||
ProjectsOverviewNarrowContextToggle,
|
||||
@@ -41,6 +40,7 @@ import {
|
||||
ProjectsOverviewContextPanel,
|
||||
ProjectsOverviewPanel,
|
||||
} from "@/features/projects/ui/ProjectsOverviewPanel";
|
||||
import { ProjectsOverviewChromeActions } from "@/features/projects/ui/ProjectsOverviewChromeActions";
|
||||
import { ProjectContextRail } from "@/features/projects/ui/ProjectContextRail";
|
||||
import {
|
||||
openAppSearch,
|
||||
@@ -705,22 +705,31 @@ export function ProjectsView() {
|
||||
) ?? [],
|
||||
summaries: activitySummariesQuery.data,
|
||||
};
|
||||
const overviewDetached = overviewPanelOpen && !isNarrowProjectsLayout;
|
||||
const contextOpen = isNarrowProjectsLayout
|
||||
? narrowContextOpen
|
||||
: overviewPanelOpen;
|
||||
const chromeActions = (
|
||||
<>
|
||||
<ProjectsOverviewNarrowContextToggle
|
||||
onToggle={() =>
|
||||
isNarrowProjectsLayout
|
||||
? setNarrowContextOpen((open) => !open)
|
||||
: setOverviewPanelOpen((open) => !open)
|
||||
}
|
||||
open={contextOpen}
|
||||
ref={contextToggleRef}
|
||||
/>
|
||||
</>
|
||||
const overviewChatOpen =
|
||||
selectionAgentContext !== null && !isNarrowProjectsLayout;
|
||||
const overviewContextOpen = overviewPanelOpen && !isNarrowProjectsLayout;
|
||||
const overviewDetached = overviewContextOpen || overviewChatOpen;
|
||||
const chromeActions = isNarrowProjectsLayout ? (
|
||||
<ProjectsOverviewNarrowContextToggle
|
||||
onToggle={() => setNarrowContextOpen((open) => !open)}
|
||||
open={contextOpen}
|
||||
ref={contextToggleRef}
|
||||
/>
|
||||
) : (
|
||||
<ProjectsOverviewChromeActions
|
||||
chatOpen={overviewChatOpen}
|
||||
contextOpen={overviewPanelOpen}
|
||||
onToggleChat={() =>
|
||||
setSelectionAgentContext((context) =>
|
||||
context ? null : overviewAgentContext,
|
||||
)
|
||||
}
|
||||
onToggleContext={() => setOverviewPanelOpen((open) => !open)}
|
||||
sectionTitle={projectsSectionTitle(filter)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -842,15 +851,6 @@ export function ProjectsView() {
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<ProjectsOverviewChatToggle
|
||||
active={selectionAgentContext !== null}
|
||||
onToggle={() =>
|
||||
setSelectionAgentContext((context) =>
|
||||
context ? null : overviewAgentContext,
|
||||
)
|
||||
}
|
||||
sectionTitle={projectsSectionTitle(filter)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
@@ -933,20 +933,29 @@ export function ProjectsView() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectionAgentContext && !isNarrowProjectsLayout ? (
|
||||
<ProjectAgentChatPanel
|
||||
canResetWidth={overviewAgentPanelWidth.canReset}
|
||||
context={selectionAgentContext}
|
||||
onClose={() => setSelectionAgentContext(null)}
|
||||
onResetWidth={overviewAgentPanelWidth.onResetWidth}
|
||||
onResizeStart={overviewAgentPanelWidth.onResizeStart}
|
||||
widthPx={overviewAgentPanelWidth.widthPx}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<ProjectContextRail
|
||||
open={overviewDetached}
|
||||
open={overviewChatOpen}
|
||||
panelWidthPx={overviewAgentPanelWidth.widthPx}
|
||||
resizing={overviewAgentPanelWidth.isResizing}
|
||||
testId="projects-overview-agent-rail"
|
||||
>
|
||||
{selectionAgentContext ? (
|
||||
<ProjectAgentChatPanel
|
||||
canResetWidth={overviewAgentPanelWidth.canReset}
|
||||
constrainToAvailableSpace={false}
|
||||
context={selectionAgentContext}
|
||||
detached
|
||||
onClose={() => setSelectionAgentContext(null)}
|
||||
onResetWidth={overviewAgentPanelWidth.onResetWidth}
|
||||
onResizeStart={overviewAgentPanelWidth.onResizeStart}
|
||||
widthPx={overviewAgentPanelWidth.widthPx}
|
||||
/>
|
||||
) : null}
|
||||
</ProjectContextRail>
|
||||
<ProjectContextRail
|
||||
open={overviewContextOpen}
|
||||
panelWidthPx={PROJECT_CONTEXT_PANEL_DEFAULT_WIDTH_PX}
|
||||
testId="projects-overview-context-rail"
|
||||
>
|
||||
|
||||
@@ -289,58 +289,83 @@ export function PullRequestReviewersRow({
|
||||
|
||||
return (
|
||||
<ProjectDetailMetaRow icon={Users} label="Reviewers">
|
||||
<div className="grid min-w-0 gap-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<div
|
||||
className="flex min-w-0 items-center gap-2"
|
||||
data-testid="project-reviewers-content"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden whitespace-nowrap">
|
||||
{showSummary && displayedDecisionActors.length === 0 ? (
|
||||
<span className="font-medium" data-testid={summaryTestId}>
|
||||
<span className="truncate font-medium" data-testid={summaryTestId}>
|
||||
{reviewSummary}
|
||||
</span>
|
||||
) : null}
|
||||
{displayedDecisionActors.map((pubkey, index) => {
|
||||
const label = labelForPubkey(pubkey, profiles);
|
||||
const hasApproved = approvedBy.has(pubkey);
|
||||
const hasRequestedChanges = changesRequestedBy.has(pubkey);
|
||||
const needsRereview = staleDecisionActors.has(pubkey);
|
||||
const DecisionIcon = hasApproved
|
||||
? Check
|
||||
: hasRequestedChanges
|
||||
? TriangleAlert
|
||||
: needsRereview
|
||||
? History
|
||||
: null;
|
||||
const decisionLabel = hasApproved
|
||||
? `Approved by ${label}`
|
||||
: hasRequestedChanges
|
||||
? `Changes requested by ${label}`
|
||||
: needsRereview
|
||||
? `Re-review needed from ${label}`
|
||||
: `Awaiting review from ${label}`;
|
||||
return (
|
||||
<React.Fragment key={pubkey}>
|
||||
{index > 0 ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="shrink-0 text-muted-foreground/50"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
className={cn(
|
||||
"flex min-w-0 shrink items-center gap-1 text-sm",
|
||||
hasApproved && "text-green-600 dark:text-green-400",
|
||||
hasRequestedChanges && "text-amber-600 dark:text-amber-400",
|
||||
!hasApproved &&
|
||||
!hasRequestedChanges &&
|
||||
"text-muted-foreground",
|
||||
)}
|
||||
data-testid="project-reviewer-decision"
|
||||
title={decisionLabel}
|
||||
>
|
||||
<span className="sr-only">{decisionLabel}</span>
|
||||
{DecisionIcon ? (
|
||||
<DecisionIcon
|
||||
aria-hidden="true"
|
||||
className="h-3.5 w-3.5 shrink-0"
|
||||
/>
|
||||
) : null}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="truncate"
|
||||
data-testid="project-reviewer-name"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{hasHistoricalDecision ? (
|
||||
<span className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
|
||||
<span className="flex min-w-0 items-center gap-1 truncate text-xs text-amber-600 dark:text-amber-400">
|
||||
<History className="h-3.5 w-3.5 shrink-0" />
|
||||
Earlier decision applies to another commit
|
||||
</span>
|
||||
) : null}
|
||||
{requestAction}
|
||||
</div>
|
||||
{displayedDecisionActors.map((pubkey) => {
|
||||
const label = labelForPubkey(pubkey, profiles);
|
||||
const hasApproved = approvedBy.has(pubkey);
|
||||
const hasRequestedChanges = changesRequestedBy.has(pubkey);
|
||||
const needsRereview = staleDecisionActors.has(pubkey);
|
||||
const DecisionIcon = hasApproved
|
||||
? Check
|
||||
: hasRequestedChanges
|
||||
? TriangleAlert
|
||||
: needsRereview
|
||||
? History
|
||||
: null;
|
||||
const decisionLabel = hasApproved
|
||||
? `Approved by ${label}`
|
||||
: hasRequestedChanges
|
||||
? `Changes requested by ${label}`
|
||||
: needsRereview
|
||||
? `Re-review needed from ${label}`
|
||||
: `Awaiting review from ${label}`;
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex min-w-0 items-center gap-1 text-sm",
|
||||
hasApproved && "text-green-600 dark:text-green-400",
|
||||
hasRequestedChanges && "text-amber-600 dark:text-amber-400",
|
||||
!hasApproved && !hasRequestedChanges && "text-muted-foreground",
|
||||
)}
|
||||
data-testid="project-reviewer-decision"
|
||||
key={pubkey}
|
||||
>
|
||||
{DecisionIcon ? (
|
||||
<DecisionIcon className="h-3.5 w-3.5 shrink-0" />
|
||||
) : null}
|
||||
<span className="truncate">{decisionLabel}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{requestAction}
|
||||
</div>
|
||||
</ProjectDetailMetaRow>
|
||||
);
|
||||
|
||||
@@ -18,10 +18,6 @@ import {
|
||||
selectionItemFromRepository,
|
||||
type ProjectSelectionItem,
|
||||
} from "@/features/projects/lib/projectSelection";
|
||||
import {
|
||||
formatExactTimestamp,
|
||||
relativeTime,
|
||||
} from "@/features/projects/lib/projectsViewHelpers";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import { useRelayOrigin } from "@/shared/lib/useRelayOrigin";
|
||||
@@ -155,27 +151,6 @@ function RepositoryIdentity({
|
||||
);
|
||||
}
|
||||
|
||||
function RepositoryUpdatedLabel({
|
||||
repository,
|
||||
summary,
|
||||
}: Pick<RepositoryItemProps, "repository" | "summary">) {
|
||||
const updatedAt = summary?.updatedAt || repository.createdAt;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="whitespace-nowrap text-xs leading-4 text-muted-foreground/70">
|
||||
{relativeTime(updatedAt)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{summary?.latestCommit
|
||||
? `${summary.latestCommit.title || summary.latestCommit.commit.slice(0, 7)} · ${formatExactTimestamp(summary.latestCommit.createdAt)}`
|
||||
: `Created ${formatExactTimestamp(repository.createdAt)}`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function repositoryPeople(
|
||||
repository: Repository,
|
||||
summary: ProjectActivitySummary | undefined,
|
||||
@@ -261,13 +236,12 @@ export function RepositoryGridCard(props: RepositoryItemProps) {
|
||||
>
|
||||
{repository.description || "A repository in this project."}
|
||||
</p>
|
||||
<div className="pointer-events-auto mt-auto flex items-center justify-between gap-3">
|
||||
<div className="pointer-events-auto mt-auto">
|
||||
<ProjectPeopleStack
|
||||
profiles={profiles}
|
||||
pubkeys={repositoryPeople(repository, summary)}
|
||||
workOwnerPubkey={repository.owner}
|
||||
/>
|
||||
<RepositoryUpdatedLabel repository={repository} summary={summary} />
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<ProjectStatsRow summary={summary} />
|
||||
|
||||
@@ -605,6 +605,26 @@ test("latest files commit opens its detail without a divider", async ({
|
||||
const latestCommit = page.getByTestId("project-repository-latest-commit");
|
||||
await expect(latestCommit).toBeVisible();
|
||||
await expect(latestCommit).toHaveCSS("border-bottom-width", "0px");
|
||||
await expect(
|
||||
page.getByTestId("project-repository-latest-commit-summary"),
|
||||
).toHaveCSS("font-size", "12px");
|
||||
await expect(
|
||||
page.getByTestId("project-repository-entry-row").first(),
|
||||
).toHaveCSS("font-size", "12px");
|
||||
const repositoryEntryRow = page
|
||||
.getByTestId("project-repository-entry-row")
|
||||
.first();
|
||||
const repositoryEntryCells = repositoryEntryRow.locator("td");
|
||||
await expect(repositoryEntryCells.first()).toHaveCSS("border-radius", "0px");
|
||||
await repositoryEntryRow.hover();
|
||||
await expect(repositoryEntryCells.first()).toHaveCSS(
|
||||
"border-top-left-radius",
|
||||
"8px",
|
||||
);
|
||||
await expect(repositoryEntryCells.last()).toHaveCSS(
|
||||
"border-top-right-radius",
|
||||
"8px",
|
||||
);
|
||||
await latestCommit.click();
|
||||
await expect(page.getByTestId("project-commit-detail")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -266,11 +266,20 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(page.getByTestId("project-review-summary")).toHaveCount(0);
|
||||
const reviewerDecision = page
|
||||
.getByTestId("project-reviewer-decision")
|
||||
.filter({ hasText: "bob" });
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("project-reviewer-decision")
|
||||
.filter({ hasText: "Awaiting review from bob" }),
|
||||
).toHaveCount(1);
|
||||
reviewerDecision.getByTestId("project-reviewer-name"),
|
||||
).toHaveText("bob");
|
||||
await expect(reviewerDecision).toHaveAttribute(
|
||||
"title",
|
||||
"Awaiting review from bob",
|
||||
);
|
||||
await expect(page.getByTestId("project-reviewers-content")).toHaveCSS(
|
||||
"flex-wrap",
|
||||
"nowrap",
|
||||
);
|
||||
|
||||
await waitForAnimations(page);
|
||||
await page.screenshot({
|
||||
@@ -296,9 +305,9 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId("project-review-summary")).toHaveCount(0);
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("project-reviewer-decision")
|
||||
.filter({ hasText: "Changes requested" }),
|
||||
page.locator(
|
||||
'[data-testid="project-reviewer-decision"][title^="Changes requested by "]',
|
||||
),
|
||||
).toHaveCount(1);
|
||||
const changeRequestEvent = await page.evaluate(() =>
|
||||
window.__BUZZ_E2E_SIGNED_EVENTS__
|
||||
@@ -380,9 +389,9 @@ test("PR creator/owner can toggle draft, request reviews, and approve", async ({
|
||||
).toHaveCount(0);
|
||||
await expect(page.getByTestId("project-review-summary")).toHaveCount(0);
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("project-reviewer-decision")
|
||||
.filter({ hasText: "Approved by" }),
|
||||
page.locator(
|
||||
'[data-testid="project-reviewer-decision"][title^="Approved by "]',
|
||||
),
|
||||
).toHaveCount(1);
|
||||
const approvalEvent = await page.evaluate(() =>
|
||||
window.__BUZZ_E2E_SIGNED_EVENTS__
|
||||
@@ -1108,6 +1117,14 @@ test("sidebar distinguishes the Projects overview from an open project", async (
|
||||
await projectsOverview.click();
|
||||
await expect(projectsOverview).toHaveAttribute("data-active", "true");
|
||||
await expect(sidebarProject).toHaveAttribute("data-active", "false");
|
||||
await expect(sidebarProject.locator("svg").first()).toHaveCSS(
|
||||
"opacity",
|
||||
"0.8",
|
||||
);
|
||||
await expect(sidebarProject.locator('[data-sidebar="menu-label"]')).toHaveCSS(
|
||||
"opacity",
|
||||
"0.8",
|
||||
);
|
||||
|
||||
await sidebarProject.click();
|
||||
await expect(projectsOverview).toHaveAttribute("data-active", "true");
|
||||
@@ -1605,7 +1622,9 @@ test("project overview presents collapsible context beside grouped activity", as
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("project overview content header toggles agent chat", async ({ page }) => {
|
||||
test("project overview chrome toggles a detached resizable agent chat", async ({
|
||||
page,
|
||||
}) => {
|
||||
await enableProjectsFeature(page);
|
||||
await installMockBridge(page);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
@@ -1622,25 +1641,36 @@ test("project overview content header toggles agent chat", async ({ page }) => {
|
||||
"Chat with an agent about Reviews",
|
||||
);
|
||||
await expect(overviewChat).toHaveAttribute("aria-pressed", "false");
|
||||
const [contentBox, chatBox] = await Promise.all([
|
||||
page.getByTestId("projects-overview-content-pod").boundingBox(),
|
||||
const overviewInfo = page.getByTestId("projects-overview-context-toggle");
|
||||
const [chatBox, infoBox] = await Promise.all([
|
||||
overviewChat.boundingBox(),
|
||||
overviewInfo.boundingBox(),
|
||||
]);
|
||||
expect(contentBox).not.toBeNull();
|
||||
expect(chatBox).not.toBeNull();
|
||||
expect(infoBox).not.toBeNull();
|
||||
expect(
|
||||
(contentBox?.x ?? 0) +
|
||||
(contentBox?.width ?? 0) -
|
||||
((chatBox?.x ?? 0) + (chatBox?.width ?? 0)),
|
||||
).toBeLessThanOrEqual(20);
|
||||
(infoBox?.x ?? 0) - ((chatBox?.x ?? 0) + (chatBox?.width ?? 0)),
|
||||
).toBeLessThanOrEqual(4);
|
||||
const contextRail = page.getByTestId("projects-overview-context-rail");
|
||||
await expect(overviewInfo).toHaveAttribute("aria-pressed", "true");
|
||||
await overviewInfo.click();
|
||||
await expect(overviewInfo).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(contextRail).toHaveCSS("width", "0px");
|
||||
await overviewChat.click();
|
||||
await expect(overviewChat).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(overviewInfo).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(contextRail).toHaveCSS("width", "0px");
|
||||
await expect(overviewChat).toBeVisible();
|
||||
const chatRail = page.getByTestId("projects-overview-agent-rail");
|
||||
await expect(chatRail.getByTestId("project-agent-chat-panel")).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("projects-overview-content-pod")
|
||||
.getByTestId("project-agent-chat-panel"),
|
||||
).toBeVisible();
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
chatRail.getByTestId("projects-overview-agent-rail-panel"),
|
||||
).toHaveCSS("border-radius", "16px");
|
||||
const agentHeader = page.getByTestId("project-agent-context");
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -1649,11 +1679,46 @@ test("project overview content header toggles agent chat", async ({ page }) => {
|
||||
),
|
||||
)
|
||||
.not.toBe("none");
|
||||
await expect(page.getByTestId("projects-overview-agent-rail")).toHaveCount(0);
|
||||
await expect(contextRail).toHaveAttribute("aria-hidden", "true");
|
||||
await overviewInfo.click();
|
||||
await expect(overviewInfo).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(contextRail).toHaveAttribute("aria-hidden", "false");
|
||||
await expect(
|
||||
page.getByTestId("projects-overview-context-panel"),
|
||||
).toBeVisible();
|
||||
const chatPanel = page.getByTestId("project-agent-chat-panel");
|
||||
const resizeHandle = chatPanel.getByTestId(
|
||||
"right-auxiliary-pane-resize-handle",
|
||||
);
|
||||
const [initialPanelBox, resizeHandleBox] = await Promise.all([
|
||||
chatPanel.boundingBox(),
|
||||
resizeHandle.boundingBox(),
|
||||
]);
|
||||
expect(initialPanelBox).not.toBeNull();
|
||||
expect(resizeHandleBox).not.toBeNull();
|
||||
const resizeStartX =
|
||||
(resizeHandleBox?.x ?? 0) + (resizeHandleBox?.width ?? 0) / 2;
|
||||
const resizeStartY =
|
||||
(resizeHandleBox?.y ?? 0) + (resizeHandleBox?.height ?? 0) / 2;
|
||||
await resizeHandle.dispatchEvent("pointerdown", {
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
clientX: resizeStartX,
|
||||
clientY: resizeStartY,
|
||||
pointerId: 1,
|
||||
pointerType: "mouse",
|
||||
});
|
||||
await expect(chatRail).toHaveAttribute("data-resizing", "true");
|
||||
await page.mouse.move(resizeStartX - 32, resizeStartY);
|
||||
await expect
|
||||
.poll(() =>
|
||||
chatPanel.evaluate((element) =>
|
||||
Math.round(element.getBoundingClientRect().width),
|
||||
),
|
||||
)
|
||||
.toBe(Math.round(initialPanelBox?.width ?? 0) + 32);
|
||||
await page.mouse.up();
|
||||
await expect(chatRail).toHaveAttribute("data-resizing", "false");
|
||||
await chatPanel.getByTestId("message-input").fill("Summarize these reviews");
|
||||
await chatPanel.getByTestId("message-input").press("Enter");
|
||||
const readSentContent = () =>
|
||||
@@ -1683,6 +1748,7 @@ test("project overview content header toggles agent chat", async ({ page }) => {
|
||||
await overviewChat.click();
|
||||
await expect(overviewChat).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(page.getByTestId("project-agent-chat-panel")).not.toBeVisible();
|
||||
await expect(chatRail).toHaveCSS("width", "0px");
|
||||
await expect(
|
||||
page.getByTestId("projects-overview-context-panel"),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -1122,9 +1122,24 @@ test("projects v3 work-item list metadata", async ({ page }) => {
|
||||
);
|
||||
|
||||
await page.getByTestId("projects-section-projects").click();
|
||||
await expectSinglePrimaryTextColumn(
|
||||
page.getByTestId(/^project-row-/).first(),
|
||||
await expect(page.getByTestId("projects-list-header")).toHaveCSS(
|
||||
"border-left-width",
|
||||
"0px",
|
||||
);
|
||||
const projectRow = page.getByTestId(/^project-row-/).first();
|
||||
await expectSinglePrimaryTextColumn(projectRow);
|
||||
const [projectTitleBox, repositoryCountBox] = await Promise.all([
|
||||
projectRow.locator('[data-projects-text-priority="primary"]').boundingBox(),
|
||||
projectRow.getByTestId("projects-row-context").boundingBox(),
|
||||
]);
|
||||
expect(projectTitleBox).not.toBeNull();
|
||||
expect(repositoryCountBox).not.toBeNull();
|
||||
expect(
|
||||
Math.round(
|
||||
(repositoryCountBox?.x ?? 0) -
|
||||
((projectTitleBox?.x ?? 0) + (projectTitleBox?.width ?? 0)),
|
||||
),
|
||||
).toBe(12);
|
||||
|
||||
await page.getByTestId("projects-section-repositories").click();
|
||||
await expectSinglePrimaryTextColumn(
|
||||
|
||||
Reference in New Issue
Block a user