mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix(ui): gate chat sidebar mutations (#113947)
This commit is contained in:
committed by
GitHub
parent
25495ad0be
commit
c7fcfd3482
@@ -668,6 +668,9 @@ export class ChatPaneRender extends ChatPaneHeaderRender {
|
||||
focusVersion: state.sidebarFocusVersion,
|
||||
layout: sidebarLayout,
|
||||
narrow: this.paneWidth < SIDEBAR_NARROW_BREAKPOINT_PX,
|
||||
panelMutationEnabled: {
|
||||
chat: Boolean(board.activeTabId) && !board.activeTabReadOnly && board.provider.canMutate,
|
||||
},
|
||||
panelTemplates,
|
||||
primary,
|
||||
sessionKey: state.sessionKey,
|
||||
|
||||
@@ -47,6 +47,7 @@ describe("chat pane sidebar layout", () => {
|
||||
focusVersion: 0,
|
||||
layout,
|
||||
narrow,
|
||||
panelMutationEnabled: {},
|
||||
panelTemplates: { detail: html`<aside>Details</aside>` },
|
||||
primary: html`<main data-primary>Primary</main>`,
|
||||
sessionKey: "agent:main:current",
|
||||
@@ -107,6 +108,7 @@ describe("chat pane sidebar layout", () => {
|
||||
focusVersion: 0,
|
||||
layout: openSlot({ columns: [] }, "detail"),
|
||||
narrow: false,
|
||||
panelMutationEnabled: {},
|
||||
panelTemplates: { detail: html`<aside>Details</aside>` },
|
||||
primary: html`<main>Primary</main>`,
|
||||
sessionKey: "agent:main:current",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type SidebarColumn,
|
||||
type SidebarLayout,
|
||||
type SidebarPanel,
|
||||
type SidebarSlotId,
|
||||
} from "./sidebar-layout.ts";
|
||||
|
||||
const DETAIL_FULL_MESSAGE_MAX_CHARS = 500_000;
|
||||
@@ -31,6 +32,7 @@ export function renderSidebarRegion(params: {
|
||||
focusVersion: number;
|
||||
layout: SidebarLayout;
|
||||
narrow: boolean;
|
||||
panelMutationEnabled: Partial<Record<SidebarSlotId, boolean>>;
|
||||
panelTemplates: SidebarPanelTemplates;
|
||||
primary: TemplateResult;
|
||||
sessionKey: string;
|
||||
@@ -53,6 +55,7 @@ export function renderSidebarRegion(params: {
|
||||
.layout=${params.layout}
|
||||
.panelTemplates=${params.panelTemplates}
|
||||
.panelOpenUrls=${{ discussion: params.discussionOpenUrl }}
|
||||
.panelMutationEnabled=${params.panelMutationEnabled}
|
||||
.callbacks=${params.callbacks}
|
||||
.sessionKey=${params.sessionKey}
|
||||
.focusPanelId=${params.focusPanelId}
|
||||
|
||||
@@ -39,6 +39,8 @@ class ChatSidebarRegion extends OpenClawLightDomElement {
|
||||
@property({ attribute: false }) layout: SidebarLayout = { columns: [] };
|
||||
@property({ attribute: false }) panelTemplates: SidebarPanelTemplates = {};
|
||||
@property({ attribute: false }) panelOpenUrls: Partial<Record<SidebarSlotId, string | null>> = {};
|
||||
@property({ attribute: false }) panelMutationEnabled: Partial<Record<SidebarSlotId, boolean>> =
|
||||
{};
|
||||
@property({ attribute: false }) callbacks: SidebarRegionCallbacks | null = null;
|
||||
@property() sessionKey = "";
|
||||
@property() focusPanelId = "";
|
||||
@@ -122,6 +124,10 @@ class ChatSidebarRegion extends OpenClawLightDomElement {
|
||||
this.callbacks?.activatePanel(panelId);
|
||||
}
|
||||
|
||||
private canMutatePanel(slot: SidebarSlotId): boolean {
|
||||
return this.panelMutationEnabled[slot] !== false;
|
||||
}
|
||||
|
||||
private renderHeader(column: SidebarColumn, activePanelId: string, narrow: boolean) {
|
||||
const active = column.panels.find((panel) => panel.id === activePanelId) ?? column.panels[0];
|
||||
if (!active) {
|
||||
@@ -141,22 +147,25 @@ class ChatSidebarRegion extends OpenClawLightDomElement {
|
||||
without-scroll-controls
|
||||
@wa-tab-show=${(event: CustomEvent<{ name: string }>) => this.activate(event.detail.name)}
|
||||
>
|
||||
${column.panels.map(
|
||||
(panel) => html`
|
||||
${column.panels.map((panel) => {
|
||||
const draggable = !narrow && this.canMutatePanel(panel.slot);
|
||||
return html`
|
||||
<wa-tab
|
||||
class="sidebar-column__tab"
|
||||
panel=${panel.id}
|
||||
data-panel-id=${panel.id}
|
||||
.draggable=${!narrow}
|
||||
title=${t("chat.sidebarColumns.drag", { panel: panelTitle(panel.slot) })}
|
||||
.draggable=${draggable}
|
||||
title=${draggable
|
||||
? t("chat.sidebarColumns.drag", { panel: panelTitle(panel.slot) })
|
||||
: nothing}
|
||||
@dragstart=${(event: DragEvent) =>
|
||||
narrow ? undefined : this.startDrag(event, panel.id)}
|
||||
draggable ? this.startDrag(event, panel.id) : undefined}
|
||||
@dragend=${() => this.endDrag()}
|
||||
>
|
||||
${panelTitle(panel.slot)}
|
||||
</wa-tab>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
})}
|
||||
</wa-tab-group>
|
||||
<div class="sidebar-column__actions">
|
||||
${openUrl
|
||||
@@ -170,15 +179,17 @@ class ChatSidebarRegion extends OpenClawLightDomElement {
|
||||
>${icons.externalLink}</a
|
||||
>`
|
||||
: nothing}
|
||||
<button
|
||||
class="btn btn--ghost btn--icon"
|
||||
type="button"
|
||||
aria-label=${t("chat.sidebarColumns.close", { panel: panelTitle(active.slot) })}
|
||||
title=${t("chat.sidebarColumns.close", { panel: panelTitle(active.slot) })}
|
||||
@click=${() => this.callbacks?.closeSlot(active.slot)}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
${this.canMutatePanel(active.slot)
|
||||
? html`<button
|
||||
class="btn btn--ghost btn--icon"
|
||||
type="button"
|
||||
aria-label=${t("chat.sidebarColumns.close", { panel: panelTitle(active.slot) })}
|
||||
title=${t("chat.sidebarColumns.close", { panel: panelTitle(active.slot) })}
|
||||
@click=${() => this.callbacks?.closeSlot(active.slot)}
|
||||
>
|
||||
${icons.x}
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { html } from "lit";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import "../../../components/resizable-divider.ts";
|
||||
import { mergePanelIntoColumn, openSlot } from "../sidebar-layout.ts";
|
||||
import { mergePanelIntoColumn, openSlot, type SidebarSlotId } from "../sidebar-layout.ts";
|
||||
import "./chat-sidebar-region.runtime.ts";
|
||||
|
||||
type Region = HTMLElementTagNameMap["openclaw-chat-sidebar-region"] & {
|
||||
@@ -12,7 +12,10 @@ type Region = HTMLElementTagNameMap["openclaw-chat-sidebar-region"] & {
|
||||
|
||||
const regions: Region[] = [];
|
||||
|
||||
async function createRegion(narrow: boolean) {
|
||||
async function createRegion(
|
||||
narrow: boolean,
|
||||
panelMutationEnabled: Partial<Record<SidebarSlotId, boolean>> = {},
|
||||
) {
|
||||
const shell = document.createElement("div");
|
||||
shell.className = `sidebar-region ${narrow ? "sidebar-region--narrow" : ""}`;
|
||||
const region = document.createElement("openclaw-chat-sidebar-region") as Region;
|
||||
@@ -22,6 +25,7 @@ async function createRegion(narrow: boolean) {
|
||||
detail: html`<div data-panel="detail">Detail panel</div>`,
|
||||
discussion: html`<div data-panel="discussion">Discussion panel</div>`,
|
||||
};
|
||||
region.panelMutationEnabled = panelMutationEnabled;
|
||||
region.callbacks = {
|
||||
activatePanel: vi.fn(),
|
||||
closeSlot: vi.fn(),
|
||||
@@ -144,6 +148,51 @@ describe("chat sidebar region", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("gates chat close while keeping non-chat close controls functional", async () => {
|
||||
const region = await createRegion(false, { chat: false });
|
||||
const closeButtons = () =>
|
||||
Array.from(
|
||||
regionRoot(region).querySelectorAll<HTMLButtonElement>(".sidebar-column__actions button"),
|
||||
);
|
||||
const closeButton = (panel: string) =>
|
||||
closeButtons().find((button) => button.getAttribute("aria-label") === `Close ${panel}`);
|
||||
|
||||
expect(closeButton("Chat")).toBeUndefined();
|
||||
closeButton("Details")?.click();
|
||||
closeButton("Discussion")?.click();
|
||||
expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("detail");
|
||||
expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("discussion");
|
||||
|
||||
region.panelMutationEnabled = { chat: true };
|
||||
await region.updateComplete;
|
||||
|
||||
closeButton("Chat")?.click();
|
||||
closeButton("Details")?.click();
|
||||
closeButton("Discussion")?.click();
|
||||
expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("chat");
|
||||
expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("detail");
|
||||
expect(region.callbacks?.closeSlot).toHaveBeenCalledWith("discussion");
|
||||
});
|
||||
|
||||
it("gates chat dragging while keeping non-chat panels draggable", async () => {
|
||||
const region = await createRegion(false, { chat: false });
|
||||
const tabs = () =>
|
||||
Array.from(regionRoot(region).querySelectorAll<HTMLElement>(".sidebar-column__tab"));
|
||||
const tab = (panel: string) =>
|
||||
tabs().find((candidate) => candidate.textContent?.trim() === panel);
|
||||
|
||||
expect(tab("Chat")?.draggable).toBe(false);
|
||||
expect(tab("Details")?.draggable).toBe(true);
|
||||
expect(tab("Discussion")?.draggable).toBe(true);
|
||||
|
||||
region.panelMutationEnabled = { chat: true };
|
||||
await region.updateComplete;
|
||||
|
||||
expect(tab("Chat")?.draggable).toBe(true);
|
||||
expect(tab("Details")?.draggable).toBe(true);
|
||||
expect(tab("Discussion")?.draggable).toBe(true);
|
||||
});
|
||||
|
||||
it("routes boundary drops through the detach callback", async () => {
|
||||
const region = await createRegion(false);
|
||||
const source = regionRoot(region).querySelectorAll<HTMLElement>(".sidebar-column__tab")[1]!;
|
||||
|
||||
Reference in New Issue
Block a user