fix(playground): align Select All bulk-delete icon with session ⋮ column (#13313)

The bulk-delete trash icon in the Select All row sat off-axis from the
``⋮`` MoreMenu icons of the session rows below: lower (the wrapper had
``py-1 mb-1`` while session rows are ``h-8`` with no vertical padding)
and further left (the trash button was ``p-0`` inside an unpadded
column while the SessionMoreMenu trigger is ``h-8 w-8 p-2``).

Mirror the SessionSelector row shape exactly:

  - Outer wrapper: ``flex h-8 items-center justify-between`` — no extra
    vertical padding, no bottom margin.
  - Trash button: ``size="icon" h-8 w-8 p-2 rounded`` — matches the
    SessionMoreMenu trigger inner padding so the icon centers in the
    same 32×32 column as ``⋮`` below.
  - Inner padding moved to the Select All checkbox cluster (``px-2``)
    so the left edge still lines up with the SessionSelector internal
    padding.

Drive-bys (touched lines biome would flag otherwise):

  - Remove unused ``isDefaultSession`` local in the sessions map.
  - Sort the alertStore/flowStore imports.
  - Convert the Select All click target from ``<div onClick>`` to a
    real ``<button type="button" aria-pressed>`` so keyboard users can
    toggle it and the a11y lint warning no longer fires.

Adds ``__tests__/chat-sidebar.test.tsx`` covering: button hidden when
nothing selected, ``h-8 w-8 p-2`` button shape, host row uses ``h-8``
with no ``py-*`` / ``mb-*`` padding, and bulk delete fires with the
expected (default-session-excluded) selection.
This commit is contained in:
keval shah
2026-05-25 16:49:44 -04:00
committed by GitHub
parent f46c8a140c
commit 0a11614d7f
2 changed files with 139 additions and 30 deletions

View File

@ -0,0 +1,106 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { ChatSidebar } from "../chat-sidebar";
jest.mock(
"@/components/core/playgroundComponent/hooks/use-get-flow-id",
() => ({
useGetFlowId: () => "flow-1",
}),
);
type FlowState = { playgroundPage: boolean };
jest.mock("@/stores/flowStore", () => ({
__esModule: true,
default: <TResult,>(selector: (state: FlowState) => TResult) =>
selector({ playgroundPage: false }),
}));
type AlertState = { setSuccessData: jest.Mock };
jest.mock("@/stores/alertStore", () => ({
__esModule: true,
default: <TResult,>(selector: (state: AlertState) => TResult) =>
selector({ setSuccessData: jest.fn() }),
}));
jest.mock("../session-selector", () => ({
SessionSelector: ({
session,
onToggleSelect,
}: {
session: string;
onToggleSelect?: () => void;
}) => (
<button
type="button"
data-testid={`session-row-${session}`}
onClick={onToggleSelect}
>
{session}
</button>
),
}));
const baseProps = {
sessions: ["flow-1", "New Session 0", "New Session 1"],
onSessionSelect: jest.fn(),
currentSessionId: "flow-1",
onDeleteSession: jest.fn(),
onRenameSession: jest.fn().mockResolvedValue(undefined),
onBulkDeleteSessions: jest.fn(),
};
const checkAll = () =>
fireEvent.click(screen.getByTestId("select-all-checkbox"));
describe("ChatSidebar — Select All row alignment", () => {
it("does not render the bulk-delete button while nothing is selected", () => {
render(<ChatSidebar {...baseProps} />);
expect(screen.queryByTestId("bulk-delete-button")).not.toBeInTheDocument();
});
it("renders the bulk-delete button with the same h-8 w-8 p-2 shape as a SessionMoreMenu trigger", () => {
render(<ChatSidebar {...baseProps} />);
checkAll();
const trash = screen.getByTestId("bulk-delete-button");
expect(trash).toBeInTheDocument();
expect(trash.className).toContain("h-8");
expect(trash.className).toContain("w-8");
expect(trash.className).toContain("p-2");
// No leftover sm-button padding that would skew horizontal centering.
expect(trash.className).not.toContain("p-0");
});
it("hosts the bulk-delete button in an h-8 row with no extra vertical padding", () => {
render(<ChatSidebar {...baseProps} />);
checkAll();
const trash = screen.getByTestId("bulk-delete-button");
// Walk up to the Select All row wrapper. Its className must include
// `h-8` and must not add `py-1` / `py-*` padding that would push the
// trash button's center down relative to the SessionSelector rows.
const row = trash.closest("div.flex.items-center.justify-between");
expect(row).not.toBeNull();
expect(row!.className).toContain("h-8");
expect(row!.className).not.toMatch(/\bpy-\d/);
expect(row!.className).not.toMatch(/\bmb-\d/);
});
it("calls onBulkDeleteSessions with the checked sessions when clicked", () => {
const onBulkDeleteSessions = jest.fn();
render(
<ChatSidebar
{...baseProps}
onBulkDeleteSessions={onBulkDeleteSessions}
/>,
);
checkAll();
fireEvent.click(screen.getByTestId("bulk-delete-button"));
expect(onBulkDeleteSessions).toHaveBeenCalledTimes(1);
const [ids] = onBulkDeleteSessions.mock.calls[0];
expect(ids).toEqual(
expect.arrayContaining(["New Session 0", "New Session 1"]),
);
// Default session (currentFlowId) is non-selectable and must not be
// included in the bulk delete payload.
expect(ids).not.toContain("flow-1");
});
});

View File

@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Button } from "@/components/ui/button";
import useFlowStore from "@/stores/flowStore";
import useAlertStore from "@/stores/alertStore";
import useFlowStore from "@/stores/flowStore";
import { cn } from "@/utils/utils";
import { useGetFlowId } from "../../../hooks/use-get-flow-id";
import { SessionSelector } from "./session-selector";
@ -142,18 +142,23 @@ export function ChatSidebar({
) : (
<div className="flex flex-col gap-1">
{sessions.map((session, index) => {
const isDefaultSession = session === currentFlowId;
const isFirstNonDefaultSession =
index > 0 && sessions[index - 1] === currentFlowId;
return (
<div key={session}>
{/* Show Select All controls after the default session */}
{/* Show Select All controls after the default session.
Wrapper is sized + padded to mirror a SessionSelector
row (h-8, no extra vertical padding) so the trash
button lines up vertically and horizontally with the
`⋮` MoreMenu triggers in the session rows below. */}
{isFirstNonDefaultSession && selectableSessions.length > 0 && (
<div className="flex items-center justify-between px-2 py-1 mb-1">
<div
className="flex items-center gap-2 cursor-pointer"
<div className="flex h-8 items-center justify-between">
<button
type="button"
className="flex items-center gap-2 cursor-pointer px-2 bg-transparent border-0 p-0"
onClick={handleSelectAll}
aria-pressed={allSelected}
data-testid="select-all-checkbox"
>
<div className="flex items-center justify-center w-4 h-4 flex-shrink-0">
@ -170,31 +175,29 @@ export function ChatSidebar({
<span className="text-sm text-muted-foreground select-none">
{t("chat.selectAll")}
</span>
</div>
<div className="w-8 h-8 flex items-center justify-center">
{selectedSessions.size > 0 && (
<ShadTooltip
styleClasses="z-50"
content={t("chat.deleteSessionsCount", {
count: selectedSessions.size,
})}
side="top"
</button>
{selectedSessions.size > 0 && (
<ShadTooltip
styleClasses="z-50"
content={t("chat.deleteSessionsCount", {
count: selectedSessions.size,
})}
side="top"
>
<Button
data-testid="bulk-delete-button"
variant="ghost"
size="icon"
className="h-8 w-8 p-2 rounded text-status-red hover:text-status-red hover:bg-error-background"
onClick={handleBulkDelete}
>
<Button
data-testid="bulk-delete-button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-status-red hover:text-status-red hover:bg-error-background"
onClick={handleBulkDelete}
>
<ForwardedIconComponent
name="Trash2"
className="h-4 w-4"
/>
</Button>
</ShadTooltip>
)}
</div>
<ForwardedIconComponent
name="Trash2"
className="h-4 w-4"
/>
</Button>
</ShadTooltip>
)}
</div>
)}
<SessionSelector