mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ui): resize empty chat side panel (#130366)
* fix(ui): resize empty side panel * test(ui): preserve empty sidebar geometry expectations
This commit is contained in:
committed by
GitHub
parent
a2051ff3a9
commit
f290d6bc81
@@ -18,6 +18,7 @@ const suite = createControlUiE2eSuite({
|
||||
|
||||
const sessionKey = "agent:main:rail-tabs";
|
||||
const proofDir = process.env.OPENCLAW_UI_RAIL_PROOF_DIR?.trim();
|
||||
const videoDir = process.env.OPENCLAW_UI_RAIL_VIDEO_DIR?.trim();
|
||||
|
||||
const historyMessages = Array.from({ length: 10 }, (_, index) => ({
|
||||
id: `rail-tabs-${index}`,
|
||||
@@ -362,6 +363,9 @@ suite.define(() => {
|
||||
{
|
||||
colorScheme: themeMode,
|
||||
locale: "en-US",
|
||||
...(videoDir && themeMode === "light"
|
||||
? { recordVideo: { dir: videoDir, size: { height: 900, width: 1600 } } }
|
||||
: {}),
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1600 },
|
||||
},
|
||||
@@ -822,7 +826,44 @@ suite.define(() => {
|
||||
await page.locator(".chat-side-panel-toggle").click();
|
||||
await sidePanel(page).locator(".side-panel-empty--selector").waitFor();
|
||||
expect(await sidePanel(page).locator("wa-tab").count()).toBe(0);
|
||||
const emptyDividerBox = await divider.boundingBox();
|
||||
expect(emptyDividerBox).not.toBeNull();
|
||||
await page.mouse.move(
|
||||
emptyDividerBox!.x + 1,
|
||||
emptyDividerBox!.y + emptyDividerBox!.height / 2,
|
||||
);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(
|
||||
emptyDividerBox!.x - 70,
|
||||
emptyDividerBox!.y + emptyDividerBox!.height / 2,
|
||||
);
|
||||
await page.mouse.up();
|
||||
await expect
|
||||
.poll(() =>
|
||||
sidePanel(page).evaluate((element) => element.getBoundingClientRect().width),
|
||||
)
|
||||
.toBeGreaterThan(resizedWidth + 50);
|
||||
const emptyResizedWidth = await sidePanel(page).evaluate(
|
||||
(element) => element.getBoundingClientRect().width,
|
||||
);
|
||||
await divider.evaluate((element) => element.blur());
|
||||
await captureRichPanel(page, `rails-tabs-empty-resized-${themeMode}`);
|
||||
|
||||
await page.reload();
|
||||
await page.locator(".chat-group").first().waitFor();
|
||||
await sidePanel(page).locator(".side-panel-empty--selector").waitFor();
|
||||
expect(await divider.boundingBox()).not.toBeNull();
|
||||
await expect
|
||||
.poll(() =>
|
||||
sidePanel(page).evaluate((element) => element.getBoundingClientRect().width),
|
||||
)
|
||||
.toBeCloseTo(emptyResizedWidth, 0);
|
||||
await openFromEmpty(page, "Terminal");
|
||||
await expect
|
||||
.poll(() =>
|
||||
sidePanel(page).evaluate((element) => element.getBoundingClientRect().width),
|
||||
)
|
||||
.toBeCloseTo(emptyResizedWidth, 0);
|
||||
const terminalLabel = sidePanel(page)
|
||||
.locator(sidePanelTabLabelSelector)
|
||||
.filter({ hasText: "Terminal" });
|
||||
|
||||
@@ -204,7 +204,10 @@ describe("chat pane sidebar layout", () => {
|
||||
layout: { ...openSlot({ columns: [] }, "chat"), open },
|
||||
paneWidth: 1_400,
|
||||
});
|
||||
expect(empty).toMatchObject({ columns: [], open: false });
|
||||
expect(empty).toMatchObject({
|
||||
columns: [{ panels: [], activePanelId: "", height: 360, width: 480 }],
|
||||
open: false,
|
||||
});
|
||||
|
||||
const withDetail = resolveSidebarLayoutForBoard({
|
||||
board: board("hidden", "chat"),
|
||||
|
||||
@@ -134,7 +134,8 @@ describe("chat pane session discussion", () => {
|
||||
expect(action?.ariaLabel).toBe("Hide discussion");
|
||||
expect(action?.getAttribute("aria-pressed")).toBe("true");
|
||||
action?.click();
|
||||
expect(state.sidebarLayout.columns).toEqual([]);
|
||||
expect(state.sidebarLayout.columns[0]?.panels).toEqual([]);
|
||||
expect(state.sidebarLayout.open).toBe(false);
|
||||
expect(updateSidebarLayout).toHaveBeenCalledTimes(2);
|
||||
|
||||
container.remove();
|
||||
|
||||
@@ -921,7 +921,7 @@ describe("chat pane keyboard shortcuts", () => {
|
||||
expect(press().defaultPrevented).toBe(true);
|
||||
expect(state.sidebarLayout.columns[0]?.panels.map((panel) => panel.slot)).toEqual(["terminal"]);
|
||||
expect(press().defaultPrevented).toBe(true);
|
||||
expect(state.sidebarLayout.columns).toEqual([]);
|
||||
expect(state.sidebarLayout.columns[0]?.panels).toEqual([]);
|
||||
expect(state.sidebarLayout.open).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,7 +275,7 @@ class ChatSidebarRegion extends OpenClawLightDomElement {
|
||||
}
|
||||
|
||||
private renderBody(column?: SidebarColumn) {
|
||||
if (!column) {
|
||||
if (!column || column.panels.length === 0) {
|
||||
return html`<div id="chat-side-panel-content" class="side-panel__body">
|
||||
${this.renderEmpty()}
|
||||
</div>`;
|
||||
@@ -359,7 +359,7 @@ class ChatSidebarRegion extends OpenClawLightDomElement {
|
||||
style=${styleMap({ width, height })}
|
||||
aria-label=${t("chat.sidePanel.label")}
|
||||
>
|
||||
${column
|
||||
${column?.panels.length
|
||||
? this.renderHeader(column)
|
||||
: html`<header class="rail-header side-panel__header side-panel__header--empty">
|
||||
<strong class="side-panel__empty-header-title">${t("chat.sidePanel.label")}</strong>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import "../../../components/resizable-divider.ts";
|
||||
import {
|
||||
openSlot,
|
||||
setSidebarOpen,
|
||||
setSidebarDock,
|
||||
setSidebarExpanded,
|
||||
type SidebarLayout,
|
||||
@@ -219,7 +220,7 @@ describe("chat sidebar region", () => {
|
||||
});
|
||||
|
||||
it("opens into a type selector instead of restoring a previous tab", async () => {
|
||||
const region = await createRegion({ columns: [], open: true, expanded: false });
|
||||
const region = await createRegion(setSidebarOpen({ columns: [], expanded: false }, true));
|
||||
const selector = root(region).querySelector(".side-panel-empty--selector");
|
||||
|
||||
expect(selector?.querySelector(".side-panel-empty__title")?.textContent).toBe("Open a tab");
|
||||
@@ -321,8 +322,9 @@ describe("chat sidebar region", () => {
|
||||
});
|
||||
|
||||
it("offers expand and minimize controls in the no-tabs selector", async () => {
|
||||
const region = await createRegion({ columns: [], open: true });
|
||||
const region = await createRegion(setSidebarOpen({ columns: [] }, true));
|
||||
expect(root(region).querySelector<HTMLElement>(".side-panel")?.style.width).toBe("480px");
|
||||
expect(root(region).querySelector("resizable-divider")).not.toBeNull();
|
||||
root(region).querySelector<HTMLButtonElement>(".side-panel__expand")?.click();
|
||||
root(region).querySelector<HTMLButtonElement>(".side-panel__minimize")?.click();
|
||||
expect(region.callbacks?.setExpanded).toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -76,9 +76,6 @@ export function normalizeSidebarLayout(value: unknown): SidebarLayout {
|
||||
usedSlots.add(rawPanel.slot);
|
||||
columnPanels.push({ id: panelId, slot: rawPanel.slot });
|
||||
}
|
||||
if (columnPanels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const requestedActiveId =
|
||||
typeof rawColumn.activePanelId === "string" ? rawColumn.activePanelId.trim() : "";
|
||||
activePanelId = panelIds.get(requestedActiveId) ?? activePanelId;
|
||||
@@ -92,20 +89,21 @@ export function normalizeSidebarLayout(value: unknown): SidebarLayout {
|
||||
: height;
|
||||
panels.push(...columnPanels);
|
||||
}
|
||||
const columns = panels.length
|
||||
? [
|
||||
{
|
||||
id: usedColumnIds.values().next().value ?? "side-panel-column",
|
||||
side: "right" as const,
|
||||
panels,
|
||||
activePanelId: panels.some((panel) => panel.id === activePanelId)
|
||||
? activePanelId
|
||||
: panels[0]!.id,
|
||||
height,
|
||||
width,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const columns =
|
||||
usedColumnIds.size > 0 || value.open === true
|
||||
? [
|
||||
{
|
||||
id: usedColumnIds.values().next().value ?? "side-panel-column",
|
||||
side: "right" as const,
|
||||
panels,
|
||||
activePanelId: panels.some((panel) => panel.id === activePanelId)
|
||||
? activePanelId
|
||||
: (panels[0]?.id ?? ""),
|
||||
height,
|
||||
width,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
return {
|
||||
columns,
|
||||
dock: value.dock === "bottom" ? "bottom" : "right",
|
||||
|
||||
@@ -73,7 +73,19 @@ describe("sidebar layout", () => {
|
||||
|
||||
it("collapses the panel after its final tab closes", () => {
|
||||
const closed = closeSlot(openSlot({ columns: [] }, "detail"), "detail");
|
||||
expect(closed).toEqual({ columns: [], open: false });
|
||||
expect(closed).toEqual({
|
||||
columns: [
|
||||
{
|
||||
id: "side-panel-column",
|
||||
side: "right",
|
||||
panels: [],
|
||||
activePanelId: "",
|
||||
height: 360,
|
||||
width: 480,
|
||||
},
|
||||
],
|
||||
open: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not reopen a minimized panel when another tab remains", () => {
|
||||
@@ -160,6 +172,21 @@ describe("sidebar layout", () => {
|
||||
|
||||
it("deduplicates slots and repairs untrusted persisted values", () => {
|
||||
expect(normalizeSidebarLayout(null)).toEqual({ columns: [], open: false, expanded: false });
|
||||
expect(normalizeSidebarLayout({ columns: [], open: true })).toEqual({
|
||||
columns: [
|
||||
{
|
||||
id: "side-panel-column",
|
||||
side: "right",
|
||||
panels: [],
|
||||
activePanelId: "",
|
||||
height: 360,
|
||||
width: 480,
|
||||
},
|
||||
],
|
||||
dock: "right",
|
||||
open: true,
|
||||
expanded: false,
|
||||
});
|
||||
expect(
|
||||
normalizeSidebarLayout({
|
||||
columns: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
SidebarColumn,
|
||||
SidebarDock,
|
||||
SidebarLayout,
|
||||
SidebarPanel,
|
||||
@@ -28,6 +29,17 @@ function cloneLayout(layout: SidebarLayout): SidebarLayout {
|
||||
return structuredClone(layout);
|
||||
}
|
||||
|
||||
function createSidebarColumn(): SidebarColumn {
|
||||
return {
|
||||
id: "side-panel-column",
|
||||
side: "right",
|
||||
panels: [],
|
||||
activePanelId: "",
|
||||
height: SIDEBAR_DEFAULT_HEIGHT_PX,
|
||||
width: SIDEBAR_DEFAULT_WIDTH_PX,
|
||||
};
|
||||
}
|
||||
|
||||
function clampWidth(width: number): number {
|
||||
return Math.min(SIDEBAR_MAX_WIDTH_PX, Math.max(SIDEBAR_MIN_WIDTH_PX, width));
|
||||
}
|
||||
@@ -63,16 +75,13 @@ function nextPanelId(layout: SidebarLayout, slot: SidebarSlotId): string {
|
||||
}
|
||||
|
||||
function removePanel(layout: SidebarLayout, panelId: string): SidebarPanel | null {
|
||||
for (let columnIndex = 0; columnIndex < layout.columns.length; columnIndex += 1) {
|
||||
const column = layout.columns[columnIndex]!;
|
||||
for (const column of layout.columns) {
|
||||
const panelIndex = column.panels.findIndex((panel) => panel.id === panelId);
|
||||
if (panelIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
const panel = column.panels.splice(panelIndex, 1)[0]!;
|
||||
if (column.panels.length === 0) {
|
||||
layout.columns.splice(columnIndex, 1);
|
||||
} else if (column.activePanelId === panelId) {
|
||||
if (column.activePanelId === panelId) {
|
||||
column.activePanelId =
|
||||
column.panels[Math.min(panelIndex, column.panels.length - 1)]?.id ?? "";
|
||||
}
|
||||
@@ -95,22 +104,9 @@ export function openSlot(layout: SidebarLayout, slot: SidebarSlotId): SidebarLay
|
||||
return next;
|
||||
}
|
||||
const panel: SidebarPanel = { id: nextPanelId(next, slot), slot };
|
||||
const column = next.columns[0];
|
||||
if (column) {
|
||||
column.panels.push(panel);
|
||||
column.activePanelId = panel.id;
|
||||
} else {
|
||||
next.columns = [
|
||||
{
|
||||
id: "side-panel-column",
|
||||
side: "right",
|
||||
panels: [panel],
|
||||
activePanelId: panel.id,
|
||||
height: SIDEBAR_DEFAULT_HEIGHT_PX,
|
||||
width: SIDEBAR_DEFAULT_WIDTH_PX,
|
||||
},
|
||||
];
|
||||
}
|
||||
const column = (next.columns[0] ??= createSidebarColumn());
|
||||
column.panels.push(panel);
|
||||
column.activePanelId = panel.id;
|
||||
next.open = true;
|
||||
return next;
|
||||
}
|
||||
@@ -122,7 +118,7 @@ export function closeSlot(layout: SidebarLayout, slot: SidebarSlotId): SidebarLa
|
||||
.find((entry) => entry.slot === slot);
|
||||
if (panel) {
|
||||
removePanel(next, panel.id);
|
||||
if (next.columns.length === 0) {
|
||||
if (next.columns.every((column) => column.panels.length === 0)) {
|
||||
next.open = false;
|
||||
}
|
||||
}
|
||||
@@ -162,7 +158,12 @@ export function reorderPanel(
|
||||
}
|
||||
|
||||
export function setSidebarOpen(layout: SidebarLayout, open: boolean): SidebarLayout {
|
||||
return { ...cloneLayout(layout), open };
|
||||
const next = cloneLayout(layout);
|
||||
if (open) {
|
||||
next.columns[0] ??= createSidebarColumn();
|
||||
}
|
||||
next.open = open;
|
||||
return next;
|
||||
}
|
||||
|
||||
export function setSidebarExpanded(layout: SidebarLayout, expanded: boolean): SidebarLayout {
|
||||
|
||||
Reference in New Issue
Block a user