mirror of
https://github.com/block/buzz.git
synced 2026-11-09 10:51:42 +00:00
Projects, repositories, issues, and pull requests had no shareable reference — the only way to point someone at one was to describe it. Each now offers "Copy link" in its row menu (plus a copy button in the project, issue, and pull request detail headers), yielding a `buzz://` deep link that renders as a preview card in chat and opens the entity in-app when clicked or opened from the OS. Completes the entity-link format for this: adds the `buzz://project` scheme on both sides (TS builders/parser and the buzz-cli mirror, with `projects create` emitting `link`), resolves repository and project card titles from their announcement events, and registers the four entity hosts with the Tauri deep-link handler. Coordinates whose d-tag falls outside the link charset simply have no share affordance, rather than producing a link the recipient cannot parse. Signed-off-by: Thomas Petersen <thomasp@squareup.com>
334 lines
10 KiB
TypeScript
334 lines
10 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 { repositoryShareLink } from "@/features/projects/lib/projectShareLinks";
|
|
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 { CopyShareLinkMenuItem } from "./CopyShareLinkMenuItem";
|
|
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}`}>
|
|
<CopyShareLinkMenuItem
|
|
link={repositoryShareLink(repository)}
|
|
testId={`repository-copy-link-${repository.dtag}`}
|
|
/>
|
|
<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>
|
|
);
|
|
}
|