mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 13:24:55 +01:00
Strip blue link styling, status badge, project chip from search rows (PAP-3468)
Issue rows on /search now drop the right-side StatusBadge and project chip —
status is already conveyed by the StatusIcon on the left, and the project
hexagon ("Board UI") was visual noise. The Link wrapper picks up the default
browser <a> styling (blue underline through children); explicitly setting
no-underline + text-inherit removes the stray blue line. Wrapped the row in
React.memo and dropped the now-unused projects fetch from Search.tsx.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
1b1c5f0349
commit
5d92571313
3 files changed
+13
-56
No files matched your search
@@ -1,10 +1,9 @@
|
||||
import type { ComponentType, SVGProps } from "react";
|
||||
import { memo, type ComponentType, type SVGProps } from "react";
|
||||
import { Bot, FileText, Hexagon, MessageSquare, Quote } from "lucide-react";
|
||||
import type { Agent, CompanySearchResult, Project } from "@paperclipai/shared";
|
||||
import type { Agent, CompanySearchResult } from "@paperclipai/shared";
|
||||
import { Link } from "@/lib/router";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { StatusIcon } from "../StatusIcon";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
import { Identity } from "../Identity";
|
||||
import { HighlightedText, type HighlightedTextProps } from "./HighlightedText";
|
||||
|
||||
@@ -47,15 +46,16 @@ function formatRelativeTime(input: string | null): string {
|
||||
export interface SearchResultRowProps {
|
||||
result: CompanySearchResult;
|
||||
agentsById?: ReadonlyMap<string, Pick<Agent, "id" | "name">>;
|
||||
projectsById?: ReadonlyMap<string, Pick<Project, "id" | "name">>;
|
||||
isActive?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SearchResultRow({
|
||||
const ROW_BASE =
|
||||
"group flex items-start gap-3 px-4 transition-colors no-underline text-inherit hover:bg-muted/40";
|
||||
|
||||
function SearchResultRowImpl({
|
||||
result,
|
||||
agentsById,
|
||||
projectsById,
|
||||
isActive,
|
||||
className,
|
||||
}: SearchResultRowProps) {
|
||||
@@ -63,11 +63,7 @@ export function SearchResultRow({
|
||||
return (
|
||||
<Link
|
||||
to={result.href}
|
||||
className={cn(
|
||||
"group flex items-start gap-3 px-4 py-2.5 transition-colors hover:bg-muted/40",
|
||||
isActive && "bg-muted/40",
|
||||
className,
|
||||
)}
|
||||
className={cn(ROW_BASE, "py-2.5", isActive && "bg-muted/40", className)}
|
||||
data-result-type="agent"
|
||||
>
|
||||
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
@@ -94,11 +90,7 @@ export function SearchResultRow({
|
||||
return (
|
||||
<Link
|
||||
to={result.href}
|
||||
className={cn(
|
||||
"group flex items-start gap-3 px-4 py-2.5 transition-colors hover:bg-muted/40",
|
||||
isActive && "bg-muted/40",
|
||||
className,
|
||||
)}
|
||||
className={cn(ROW_BASE, "py-2.5", isActive && "bg-muted/40", className)}
|
||||
data-result-type="project"
|
||||
>
|
||||
<Hexagon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
@@ -122,7 +114,6 @@ export function SearchResultRow({
|
||||
const assigneeName = issue.assigneeAgentId
|
||||
? agentsById?.get(issue.assigneeAgentId)?.name ?? null
|
||||
: null;
|
||||
const projectName = issue.projectId ? projectsById?.get(issue.projectId)?.name ?? null : null;
|
||||
const updated = formatRelativeTime(result.updatedAt ?? issue.updatedAt);
|
||||
const titleHighlights = result.snippets.find((snippet) => snippet.field === "title")?.highlights;
|
||||
const bodySnippets = result.snippets.filter((snippet) => snippet.field !== "title").slice(0, 2);
|
||||
@@ -131,11 +122,7 @@ export function SearchResultRow({
|
||||
<Link
|
||||
to={result.href}
|
||||
disableIssueQuicklook
|
||||
className={cn(
|
||||
"group flex items-start gap-3 px-4 py-3 transition-colors hover:bg-muted/40",
|
||||
isActive && "bg-muted/40",
|
||||
className,
|
||||
)}
|
||||
className={cn(ROW_BASE, "py-3", isActive && "bg-muted/40", className)}
|
||||
data-result-type="issue"
|
||||
>
|
||||
<div className="mt-1 shrink-0">
|
||||
@@ -154,17 +141,8 @@ export function SearchResultRow({
|
||||
className="line-clamp-2 min-w-0 flex-1 text-sm font-medium leading-snug text-foreground"
|
||||
/>
|
||||
<div className="ml-2 hidden shrink-0 items-center gap-2.5 text-xs text-muted-foreground sm:flex">
|
||||
<StatusBadge status={issue.status} />
|
||||
{assigneeName ? <Identity name={assigneeName} size="sm" /> : null}
|
||||
{projectName ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Hexagon className="h-3 w-3" />
|
||||
<span className="max-w-[10ch] truncate">{projectName}</span>
|
||||
</span>
|
||||
) : null}
|
||||
{updated ? (
|
||||
<span className="tabular-nums">{updated}</span>
|
||||
) : null}
|
||||
{updated ? <span className="tabular-nums">{updated}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
{bodySnippets.map((snippet, index) => (
|
||||
@@ -178,9 +156,7 @@ export function SearchResultRow({
|
||||
/>
|
||||
))}
|
||||
<div className="mt-1.5 flex items-center gap-2 text-xs text-muted-foreground sm:hidden">
|
||||
<StatusBadge status={issue.status} />
|
||||
{assigneeName ? <span className="truncate">{assigneeName}</span> : null}
|
||||
{projectName ? <span className="truncate">· {projectName}</span> : null}
|
||||
{updated ? <span className="ml-auto tabular-nums">{updated}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,6 +164,8 @@ export function SearchResultRow({
|
||||
);
|
||||
}
|
||||
|
||||
export const SearchResultRow = memo(SearchResultRowImpl);
|
||||
|
||||
interface SnippetLineProps {
|
||||
text: string;
|
||||
highlights?: HighlightedTextProps["highlights"];
|
||||
|
||||
+1
-19
@@ -20,13 +20,12 @@ import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useDialogActions } from "../context/DialogContext";
|
||||
import { searchApi } from "../api/search";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { loadRecentSearches, pushRecentSearch } from "../lib/recent-searches";
|
||||
import { PageTabBar, type PageTabItem } from "../components/PageTabBar";
|
||||
import { IssueGroupHeader } from "../components/IssueGroupHeader";
|
||||
import { SearchResultRow } from "../components/search/SearchResultRow";
|
||||
import type { Agent, Project } from "@paperclipai/shared";
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 250;
|
||||
const IDENTIFIER_PATTERN = /^[A-Z]+-\d+$/;
|
||||
@@ -202,24 +201,12 @@ export function Search() {
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const { data: projects } = useQuery({
|
||||
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const agentsById = useMemo<ReadonlyMap<string, Pick<Agent, "id" | "name">>>(() => {
|
||||
const map = new Map<string, Pick<Agent, "id" | "name">>();
|
||||
for (const agent of agents ?? []) map.set(agent.id, agent);
|
||||
return map;
|
||||
}, [agents]);
|
||||
|
||||
const projectsById = useMemo<ReadonlyMap<string, Pick<Project, "id" | "name">>>(() => {
|
||||
const map = new Map<string, Pick<Project, "id" | "name">>();
|
||||
for (const project of projects ?? []) map.set(project.id, project);
|
||||
return map;
|
||||
}, [projects]);
|
||||
|
||||
// Persist recent searches once we have a successful response with a non-empty query.
|
||||
useEffect(() => {
|
||||
if (!selectedCompanyId) return;
|
||||
@@ -409,7 +396,6 @@ export function Search() {
|
||||
totalResults={totalResults}
|
||||
isFetching={isFetching && !!data}
|
||||
agentsById={agentsById}
|
||||
projectsById={projectsById}
|
||||
/>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
@@ -438,7 +424,6 @@ interface SearchTabContentProps {
|
||||
totalResults: number;
|
||||
isFetching: boolean;
|
||||
agentsById: ReadonlyMap<string, Pick<Agent, "id" | "name">>;
|
||||
projectsById: ReadonlyMap<string, Pick<Project, "id" | "name">>;
|
||||
}
|
||||
|
||||
function SearchTabContent({
|
||||
@@ -460,7 +445,6 @@ function SearchTabContent({
|
||||
totalResults,
|
||||
isFetching,
|
||||
agentsById,
|
||||
projectsById,
|
||||
}: SearchTabContentProps) {
|
||||
if (showInitialState) {
|
||||
return (
|
||||
@@ -623,7 +607,6 @@ function SearchTabContent({
|
||||
key={`${result.type}:${result.id}:${result.href}`}
|
||||
result={result}
|
||||
agentsById={agentsById}
|
||||
projectsById={projectsById}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -638,7 +621,6 @@ function SearchTabContent({
|
||||
key={`${result.type}:${result.id}:${result.href}`}
|
||||
result={result}
|
||||
agentsById={agentsById}
|
||||
projectsById={projectsById}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,6 @@ import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { storybookAgents, storybookProjects, storybookIssues } from "../fixtures/paperclipData";
|
||||
|
||||
const agentsById = new Map(storybookAgents.map((agent) => [agent.id, agent]));
|
||||
const projectsById = new Map(storybookProjects.map((project) => [project.id, project]));
|
||||
|
||||
type IssueResultOverrides = Omit<Partial<CompanySearchResult>, "issue"> & {
|
||||
issue?: Partial<NonNullable<CompanySearchResult["issue"]>>;
|
||||
@@ -282,7 +281,6 @@ function SearchPagePreview({
|
||||
key={result.id}
|
||||
result={result}
|
||||
agentsById={agentsById}
|
||||
projectsById={projectsById}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -580,7 +578,6 @@ function SearchStories() {
|
||||
key={result.id}
|
||||
result={result}
|
||||
agentsById={agentsById}
|
||||
projectsById={projectsById}
|
||||
/>
|
||||
))}
|
||||
{fixtureAgents.map((result) => (
|
||||
|
||||
Reference in new issue
Block a user