fix(frontend): make recent chat rows fully clickable (#3733)

* fix(frontend): make recent chat rows fully clickable

* Address recent chat row review comments
This commit is contained in:
zgenu 2026-06-24 09:48:59 +08:00 committed by GitHub
parent cc1df2d038
commit 11415875c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 105 additions and 80 deletions

View File

@ -241,95 +241,91 @@ export function RecentChatList() {
className="group/side-menu-item"
>
<SidebarMenuButton isActive={isActive} asChild>
<div>
<Link
className="text-muted-foreground flex min-w-0 items-center gap-1.5 pr-7 whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
href={pathOfThread(thread)}
>
<ThreadChannelIcon source={channelSource} />
<span className="min-w-0 truncate">
{titleOfThread(thread)}
</span>
{channelSource && (
<span
className="bg-muted text-muted-foreground ml-auto inline-flex h-5 max-w-14 shrink-0 items-center rounded-md px-1.5 text-[10px] font-medium"
title={`${channelSource.label} channel`}
>
<span className="truncate">
{channelSource.label}
</span>
<Link
className="text-muted-foreground min-w-0 whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
href={pathOfThread(thread)}
>
<ThreadChannelIcon source={channelSource} />
<span className="min-w-0 truncate">
{titleOfThread(thread)}
</span>
{channelSource && (
<span
className="bg-muted text-muted-foreground ml-auto inline-flex h-5 max-w-14 shrink-0 items-center rounded-md px-1.5 text-[10px] font-medium"
title={`${channelSource.label} channel`}
>
<span className="truncate">
{channelSource.label}
</span>
)}
</Link>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="bg-background/50 hover:bg-background"
>
<MoreHorizontal />
<span className="sr-only">{t.common.more}</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={"right"}
align={"start"}
>
</span>
)}
</Link>
</SidebarMenuButton>
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="bg-background/50 hover:bg-background after:left-0!"
>
<MoreHorizontal />
<span className="sr-only">{t.common.more}</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={"right"}
align={"start"}
>
<DropdownMenuItem
onSelect={() =>
handleRenameClick(
thread.thread_id,
titleOfThread(thread),
)
}
>
<Pencil className="text-muted-foreground" />
<span>{t.common.rename}</span>
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => handleShare(thread)}
>
<Share2 className="text-muted-foreground" />
<span>{t.common.share}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Download className="text-muted-foreground" />
<span>{t.common.export}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem
onSelect={() =>
handleRenameClick(
thread.thread_id,
titleOfThread(thread),
)
handleExport(thread, "markdown")
}
>
<Pencil className="text-muted-foreground" />
<span>{t.common.rename}</span>
<FileText className="text-muted-foreground" />
<span>{t.common.exportAsMarkdown}</span>
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => handleShare(thread)}
onSelect={() => handleExport(thread, "json")}
>
<Share2 className="text-muted-foreground" />
<span>{t.common.share}</span>
<FileJson className="text-muted-foreground" />
<span>{t.common.exportAsJSON}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Download className="text-muted-foreground" />
<span>{t.common.export}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem
onSelect={() =>
handleExport(thread, "markdown")
}
>
<FileText className="text-muted-foreground" />
<span>{t.common.exportAsMarkdown}</span>
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() =>
handleExport(thread, "json")
}
>
<FileJson className="text-muted-foreground" />
<span>{t.common.exportAsJSON}</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => handleDelete(thread)}
>
<Trash2 className="text-muted-foreground" />
<span>{t.common.delete}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</SidebarMenuButton>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => handleDelete(thread)}
>
<Trash2 className="text-muted-foreground" />
<span>{t.common.delete}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</SidebarMenuItem>
);
})}

View File

@ -53,6 +53,35 @@ test.describe("Thread history", () => {
await expect(page).toHaveURL(new RegExp(MOCK_THREAD_ID));
});
test("clicking blank space in a sidebar thread row navigates to it", async ({
page,
}) => {
mockLangGraphAPI(page, { threads: THREADS });
await page.goto("/workspace/chats/new");
const sidebar = page.locator("[data-sidebar='sidebar']");
const firstThreadItem = sidebar
.locator("[data-sidebar='menu-item']")
.filter({ hasText: "First conversation" })
.first();
await expect(firstThreadItem).toBeVisible({ timeout: 15_000 });
const firstThreadLink = firstThreadItem.getByRole("link");
await expect(firstThreadLink).toBeVisible();
const box = await firstThreadLink.boundingBox();
expect(box).not.toBeNull();
if (!box) {
return;
}
await firstThreadLink.click({ position: { x: 4, y: box.height / 2 } });
await page.waitForURL(`**/workspace/chats/${MOCK_THREAD_ID}`);
await expect(page).toHaveURL(new RegExp(MOCK_THREAD_ID));
});
test("existing thread loads historical messages", async ({ page }) => {
mockLangGraphAPI(page, { threads: THREADS });