Files
buzz/desktop/src/features/projects/ui/ProjectsOverviewItems.tsx
Thomas Petersen ba185ba4f5 feat(desktop): polish selectable Projects workspaces
Unify grouped selection, contextual actions, agent chat, and local repository controls so Projects workflows stay focused across overview and detail views.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-17 08:37:31 -04:00

203 lines
6.0 KiB
TypeScript

import type { UserProfileLookup } from "@/features/profile/lib/identity";
import type {
Project,
ProjectActivitySummary,
Repository,
} from "@/features/projects/hooks";
import {
hasLocalCheckout,
hasLocalRepositoryCheckout,
} from "@/features/projects/lib/projectLocalRepos";
import type { ProjectRepoUnavailableReason } from "@/features/projects/lib/projectRepoAvailability";
import {
projectShareLink,
repositoryShareLink,
} from "@/features/projects/lib/projectShareLinks";
import {
isProjectOwnedByCurrentUser,
projectPeople,
type ProjectsFilter,
type ProjectsViewMode,
} from "@/features/projects/lib/projectsViewHelpers";
import {
selectionItemFromProject,
selectionItemFromRepository,
} from "@/features/projects/lib/projectSelection";
import {
EmptyFilteredState,
ProjectGridCard,
ProjectListRow,
} from "@/features/projects/ui/ProjectCards";
import {
RepositoryGridCard,
RepositoryListRow,
} from "@/features/projects/ui/RepositoryCards";
import { cn } from "@/shared/lib/cn";
export function ProjectsOverviewProjectItems({
currentPubkey,
deleteDisabled,
filter,
localRepoNames,
onDelete,
onOpen,
onOpenTerminal,
profiles,
repositoryUnavailableReasonFor,
summaries,
viewMode,
visibleProjects,
}: {
currentPubkey: string | undefined;
deleteDisabled: boolean;
filter: ProjectsFilter;
localRepoNames: Set<string>;
onDelete: (project: Project) => void;
onOpen: (project: Project) => void;
onOpenTerminal: (project: Project) => void;
profiles?: UserProfileLookup;
repositoryUnavailableReasonFor: (
project: Project,
) => ProjectRepoUnavailableReason | undefined;
summaries?: Record<string, ProjectActivitySummary>;
viewMode: ProjectsViewMode;
visibleProjects: Project[];
}) {
if (visibleProjects.length === 0) {
return <EmptyFilteredState />;
}
if (viewMode === "grid") {
return (
<div
className={cn(
"grid gap-3 md:grid-cols-2",
filter !== "all" && "xl:grid-cols-3",
)}
>
{visibleProjects.map((project) => {
const summary = summaries?.[project.id];
return (
<ProjectGridCard
canDelete={isProjectOwnedByCurrentUser(project, currentPubkey)}
deleteDisabled={deleteDisabled}
hasLocal={hasLocalCheckout(project, localRepoNames)}
key={project.id}
onDelete={onDelete}
onOpen={onOpen}
onOpenTerminal={onOpenTerminal}
people={projectPeople(project, summary)}
profiles={profiles}
project={project}
repositoryUnavailableReason={repositoryUnavailableReasonFor(
project,
)}
summary={summary}
/>
);
})}
</div>
);
}
return (
<div data-testid="projects-list-container">
{visibleProjects.map((project) => {
const summary = summaries?.[project.id];
const selectionRangeItems = visibleProjects.map((item) =>
selectionItemFromProject({
channelId: item.projectChannelId,
id: item.id,
owner: item.owner,
shareLink: projectShareLink(item),
title: item.name,
}),
);
return (
<ProjectListRow
canDelete={isProjectOwnedByCurrentUser(project, currentPubkey)}
deleteDisabled={deleteDisabled}
hasLocal={hasLocalCheckout(project, localRepoNames)}
key={project.id}
onDelete={onDelete}
onOpen={onOpen}
onOpenTerminal={onOpenTerminal}
people={projectPeople(project, summary)}
profiles={profiles}
project={project}
repositoryUnavailableReason={repositoryUnavailableReasonFor(
project,
)}
selectionRangeItems={selectionRangeItems}
summary={summary}
/>
);
})}
</div>
);
}
export function ProjectsOverviewRepositoryItems({
localRepoNames,
onOpen,
onOpenTerminal,
profiles,
summaries,
viewMode,
visibleRepositories,
}: {
localRepoNames: Set<string>;
onOpen: (project: Project, repository: Repository) => void;
onOpenTerminal: (repository: Repository) => void;
profiles?: UserProfileLookup;
summaries?: Record<string, ProjectActivitySummary>;
viewMode: ProjectsViewMode;
visibleRepositories: Array<{ project: Project; repository: Repository }>;
}) {
if (visibleRepositories.length === 0) {
return <EmptyFilteredState />;
}
if (viewMode === "grid") {
return (
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{visibleRepositories.map(({ project, repository }) => (
<RepositoryGridCard
hasLocal={hasLocalRepositoryCheckout(repository, localRepoNames)}
key={repository.repoAddress}
onOpen={onOpen}
onOpenTerminal={onOpenTerminal}
profiles={profiles}
project={project}
repository={repository}
summary={summaries?.[repository.repoAddress]}
/>
))}
</div>
);
}
return (
<div data-testid="projects-list-container">
{visibleRepositories.map(({ project, repository }) => (
<RepositoryListRow
hasLocal={hasLocalRepositoryCheckout(repository, localRepoNames)}
key={repository.repoAddress}
onOpen={onOpen}
onOpenTerminal={onOpenTerminal}
profiles={profiles}
project={project}
repository={repository}
selectionRangeItems={visibleRepositories.map((row) =>
selectionItemFromRepository({
channelId:
row.repository.channelId ?? row.project.projectChannelId,
id: row.repository.id,
owner: row.repository.owner,
shareLink: repositoryShareLink(row.repository),
title: row.repository.name,
}),
)}
summary={summaries?.[repository.repoAddress]}
/>
))}
</div>
);
}