mirror of
https://github.com/block/buzz.git
synced 2026-11-02 07:22:55 +00:00
## Summary Follow-up batch on the Projects overview (continues merged #1677): - **Repository access restrictions** — repositories the viewer can't reach are surfaced with a reason instead of failing silently. Channel-ACL denials (which arrive as the same 404 as a missing repo, for anti-enumeration) are re-classified using the repository's channel binding and the viewer's memberships (`useRepositoryAccess.ts`, `projectRepoAvailability.ts`). - **Projects loads in seconds instead of minutes** — enumeration no longer crawls every kind:5 deletion event on the relay. It fetches project/repo announcements first, then queries deletions scoped to those coordinates via chunked `#a` filters (3 queries instead of hundreds on staging). - **Activity feed layout polish** — bare event-type glyph beside the headline (no badge circle), timeline spine runs through the avatars connecting consecutive cards, linkable actor/project names are bold in theme foreground, rounded hover state, alignment fixes. - **Create button pinned** — the "+" create menu is pinned to the pane's top-right corner (equal 16px insets) and no longer scrolls away with the page header. - **List controls as a table header** — the scope selector (left) and sort + layout toggle (right) render as the first row of the list container on the Projects/Repositories/PRs/Issues tabs; in card view the identical bar stands alone with the cards below (`ProjectsListHeaderBar.tsx`). - **Repository rows show the git location** — subtitle is `github.com/org/repo` for external repos or `owner/repo` (resolved profile name) for Buzz-hosted ones, instead of repeating the project name (`repositoryDisplayPath`). - **Uniform work-item row heights** — issue rows previously ran the author chip in inline flow, letting the 20px avatar grow the line box ~3px taller than PR rows; both lists now share the same flex subtitle. 📸 Screenshots: [feed layout / pinned button](https://github.com/block/buzz/pull/5073#issuecomment-5214114069) · [list header / repo subtitles / row heights](https://github.com/block/buzz/pull/5073#issuecomment-5217480712). Note: two empty `chore: retrigger CI` commits exist on the branch from working around the Aug 6 GitHub Actions incident; happy to drop them with a signoff rebase before undrafting if preferred. Latest `main` is merged in (`a0cc35220`). ## Test plan - [x] Desktop unit tests (4,493 pass after merging main), Biome, tsc - [x] New unit tests for scoped deletion enumeration and repo availability re-classification - [x] New unit tests for `repositoryDisplayPath` (external, Buzz-hosted, unresolvable) - [x] Screenshot verification of feed layout, connector spine, and pinned button (top + scrolled states) — posted to the PR - [x] Screenshot verification of the list header row (list + card), repo subtitles, and matching PR/issue row heights — posted to the PR - [ ] Manual pass against staging (projects list load time, restricted-repo states) --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com>
328 lines
9.8 KiB
TypeScript
328 lines
9.8 KiB
TypeScript
import { FolderGit2, GitBranch, Globe, SquareTerminal } from "lucide-react";
|
|
|
|
import type {
|
|
Project,
|
|
ProjectActivitySummary,
|
|
Repository,
|
|
} from "@/features/projects/hooks";
|
|
import {
|
|
resolveUserLabel,
|
|
type UserProfileLookup,
|
|
} from "@/features/profile/lib/identity";
|
|
import {
|
|
projectRepoHostForRepository,
|
|
repositoryDisplayPath,
|
|
} from "@/features/projects/lib/projectRepoHost";
|
|
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";
|
|
import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark";
|
|
import { Card } from "@/shared/ui/card";
|
|
import { DropdownMenuItem } from "@/shared/ui/dropdown-menu";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
|
import {
|
|
PROJECT_LIST_ROW_CLASS,
|
|
PROJECT_LIST_ROW_DATE_CLASS,
|
|
PROJECT_LIST_ROW_PREVIEW_CLASS,
|
|
PROJECT_LIST_ROW_TITLE_CLASS,
|
|
PROJECT_LIST_ROW_TRAILING_CLASS,
|
|
} from "./projectListRowStyles";
|
|
import {
|
|
ProjectActivityBar,
|
|
ProjectPeopleStack,
|
|
ProjectStatsRow,
|
|
} from "./ProjectCards";
|
|
import { GitHubMark } from "./GitHubMark";
|
|
import { ProjectListRowMenu } from "./ProjectListRowMenu";
|
|
import { projectTerminalLabel } from "./useOpenProjectTerminal";
|
|
|
|
export type RepositoryListItem = {
|
|
project: Project;
|
|
repository: Repository;
|
|
};
|
|
|
|
type RepositoryItemProps = RepositoryListItem & {
|
|
hasLocal: boolean;
|
|
onOpen: (project: Project, repository: Repository) => void;
|
|
onOpenTerminal: (repository: Repository) => void;
|
|
profiles?: UserProfileLookup;
|
|
summary?: ProjectActivitySummary;
|
|
};
|
|
|
|
function RepositoryHostIcon({ repository }: { repository: Repository }) {
|
|
const host = projectRepoHostForRepository(repository, useRelayOrigin());
|
|
const label =
|
|
host.kind === "buzz"
|
|
? "Buzz-hosted repository"
|
|
: host.kind === "external"
|
|
? `Git data hosted on ${host.host}`
|
|
: "Repository host";
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span
|
|
aria-label={label}
|
|
className="pointer-events-auto flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-muted/40 text-muted-foreground"
|
|
data-testid="repository-host-icon"
|
|
role="img"
|
|
>
|
|
{host.kind === "buzz" ? (
|
|
<BuzzMark className="h-4.5 w-5" />
|
|
) : host.kind === "external" && host.host === "github.com" ? (
|
|
<GitHubMark className="h-4.5 w-4.5" />
|
|
) : host.kind === "external" ? (
|
|
<Globe className="h-4.5 w-4.5" />
|
|
) : (
|
|
<FolderGit2 className="h-4.5 w-4.5" />
|
|
)}
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{label}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
function RepositoryOpenButton({
|
|
onOpen,
|
|
project,
|
|
repository,
|
|
}: Pick<RepositoryItemProps, "onOpen" | "project" | "repository">) {
|
|
return (
|
|
<button
|
|
className="absolute inset-0 z-0 cursor-pointer"
|
|
onClick={() => onOpen(project, repository)}
|
|
type="button"
|
|
>
|
|
<span className="sr-only">View {repository.name}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function RepositoryIdentity({
|
|
inlineBranch = false,
|
|
profiles,
|
|
project,
|
|
repository,
|
|
}: Pick<RepositoryItemProps, "profiles" | "project" | "repository"> & {
|
|
inlineBranch?: boolean;
|
|
}) {
|
|
// Where the git data lives beats repeating the (often identical) project
|
|
// name — "github.com/block/buzz" for external repos, "owner/repo" for
|
|
// Buzz-hosted ones.
|
|
const displayPath = repositoryDisplayPath(
|
|
repository,
|
|
useRelayOrigin(),
|
|
resolveUserLabel({ pubkey: repository.owner, profiles }),
|
|
);
|
|
return (
|
|
<>
|
|
<RepositoryHostIcon repository={repository} />
|
|
<div className="min-w-0">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<span className={PROJECT_LIST_ROW_TITLE_CLASS}>
|
|
{repository.name}
|
|
</span>
|
|
</div>
|
|
<p className={PROJECT_LIST_ROW_PREVIEW_CLASS}>
|
|
{displayPath ?? project.name}
|
|
{inlineBranch ? ` · ${repository.defaultBranch}` : ""}
|
|
</p>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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,
|
|
) {
|
|
return [
|
|
...new Set(
|
|
[
|
|
repository.owner,
|
|
...repository.contributors,
|
|
...(summary?.participantPubkeys ?? []),
|
|
].map(normalizePubkey),
|
|
),
|
|
];
|
|
}
|
|
|
|
function RepositoryActionsMenu({
|
|
hasLocal,
|
|
onOpenTerminal,
|
|
repository,
|
|
}: Pick<RepositoryItemProps, "hasLocal" | "onOpenTerminal" | "repository">) {
|
|
return (
|
|
<ProjectListRowMenu label={`More options for ${repository.name}`}>
|
|
<DropdownMenuItem
|
|
onSelect={(event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onOpenTerminal(repository);
|
|
}}
|
|
>
|
|
<SquareTerminal className="h-4 w-4" />
|
|
{projectTerminalLabel(hasLocal)}
|
|
</DropdownMenuItem>
|
|
</ProjectListRowMenu>
|
|
);
|
|
}
|
|
|
|
export function RepositoryGridCard(props: RepositoryItemProps) {
|
|
const {
|
|
hasLocal,
|
|
onOpen,
|
|
onOpenTerminal,
|
|
profiles,
|
|
project,
|
|
repository,
|
|
summary,
|
|
} = props;
|
|
return (
|
|
<Card
|
|
className="group relative flex min-h-40 flex-col overflow-hidden border-border/60 bg-transparent shadow-none transition-colors duration-150 hover:bg-muted/20"
|
|
data-testid={`repository-card-${repository.dtag}`}
|
|
>
|
|
<RepositoryOpenButton
|
|
onOpen={onOpen}
|
|
project={project}
|
|
repository={repository}
|
|
/>
|
|
<div className="pointer-events-none relative z-10 flex min-h-0 flex-1 flex-col p-4">
|
|
<div className="flex min-w-0 items-start gap-2.5">
|
|
<RepositoryIdentity
|
|
inlineBranch
|
|
profiles={profiles}
|
|
project={project}
|
|
repository={repository}
|
|
/>
|
|
<div className="pointer-events-auto ml-auto">
|
|
<RepositoryActionsMenu
|
|
hasLocal={hasLocal}
|
|
onOpenTerminal={onOpenTerminal}
|
|
repository={repository}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p className="line-clamp-2 min-h-10 py-3 text-sm text-muted-foreground">
|
|
{repository.description || "A repository in this project."}
|
|
</p>
|
|
<div className="pointer-events-auto mt-auto flex items-center justify-between gap-3">
|
|
<ProjectPeopleStack
|
|
profiles={profiles}
|
|
pubkeys={repositoryPeople(repository, summary)}
|
|
workOwnerPubkey={repository.owner}
|
|
/>
|
|
<RepositoryUpdatedLabel repository={repository} summary={summary} />
|
|
</div>
|
|
<div className="mt-2">
|
|
<ProjectStatsRow summary={summary} />
|
|
<div className="mt-2">
|
|
<ProjectActivityBar summary={summary} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function RepositoryListRow(props: RepositoryItemProps) {
|
|
const {
|
|
hasLocal,
|
|
onOpen,
|
|
onOpenTerminal,
|
|
profiles,
|
|
project,
|
|
repository,
|
|
summary,
|
|
} = props;
|
|
return (
|
|
<div
|
|
className={cn(PROJECT_LIST_ROW_CLASS, "py-3")}
|
|
data-testid={`repository-row-${repository.dtag}`}
|
|
>
|
|
<RepositoryOpenButton
|
|
onOpen={onOpen}
|
|
project={project}
|
|
repository={repository}
|
|
/>
|
|
<div className="pointer-events-none relative z-10 flex min-w-0 flex-1 items-center gap-2.5">
|
|
<div className="flex min-w-0 flex-1 items-center gap-2.5">
|
|
<RepositoryIdentity
|
|
profiles={profiles}
|
|
project={project}
|
|
repository={repository}
|
|
/>
|
|
</div>
|
|
<div
|
|
className={cn(PROJECT_LIST_ROW_TRAILING_CLASS, "pointer-events-auto")}
|
|
>
|
|
<div
|
|
className="hidden w-24 shrink-0 items-center gap-1.5 text-xs text-muted-foreground md:flex"
|
|
data-testid="repositories-row-branch"
|
|
>
|
|
<GitBranch className="h-3.5 w-3.5 shrink-0" />
|
|
<span className="truncate">{repository.defaultBranch}</span>
|
|
</div>
|
|
<div
|
|
className="hidden items-center gap-3 xl:flex"
|
|
data-testid="repositories-row-summary"
|
|
>
|
|
<ProjectStatsRow fixedColumns summary={summary} />
|
|
<div className="w-20 shrink-0">
|
|
<ProjectActivityBar summary={summary} />
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="hidden w-24 shrink-0 justify-end lg:flex"
|
|
data-testid="repositories-row-people"
|
|
>
|
|
<ProjectPeopleStack
|
|
profiles={profiles}
|
|
pubkeys={repositoryPeople(repository, summary)}
|
|
workOwnerPubkey={repository.owner}
|
|
/>
|
|
</div>
|
|
<div
|
|
className={PROJECT_LIST_ROW_DATE_CLASS}
|
|
data-testid="repositories-row-date"
|
|
>
|
|
<RepositoryUpdatedLabel repository={repository} summary={summary} />
|
|
</div>
|
|
<RepositoryActionsMenu
|
|
hasLocal={hasLocal}
|
|
onOpenTerminal={onOpenTerminal}
|
|
repository={repository}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|