mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 12:07:51 +08:00
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:
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user