FN-6767: make agents sidebar resizable
Make the Agents split view wider by default and persist user-adjusted sidebar widths. - Add a desktop/tablet resize handle with pointer and keyboard controls, clamped accessible values, and per-project storage.\n- Update split-layout CSS so mobile remains stacked while non-mobile views reserve a resize affordance column.\n- Cover default, stored, clamped, pointer, keyboard, mobile, and org-chart sidebar behaviors in tests.\n- Add a patch changeset for the published Fusion package.\n\nFiles changed:\n .changeset/fn-6767-agents-sidebar.md | 5 +\n packages/dashboard/app/components/AgentsView.css | 47 ++++++--\n packages/dashboard/app/components/AgentsView.tsx | 101 +++++++++++++++-\n .../app/components/__tests__/AgentsView.test.tsx | 132 +++++++++++++++++++++\n .../app/utils/__tests__/projectStorage.test.ts | 3 +-\n packages/dashboard/app/utils/projectStorage.ts | 1 +\n 6 files changed, 280 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6767 Fusion-Task-Lineage: 93605e16-7524-4577-bbcd-666c105a867b
This commit is contained in:
5
.changeset/fn-6767-agents-sidebar.md
Normal file
5
.changeset/fn-6767-agents-sidebar.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Make the Agents view sidebar wider by default on tablet and resizable with per-project persistence on non-mobile layouts.
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
|
|
||||||
.agents-split-layout {
|
.agents-split-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(calc(var(--space-xl) * 11 + var(--space-xs)), calc(var(--space-xl) * 13 + var(--space-lg))) minmax(0, 1fr);
|
grid-template-columns: minmax(calc(var(--space-xl) * 11 + var(--space-xs)), calc(var(--space-xl) * 13 + var(--space-lg))) var(--space-sm) minmax(0, 1fr);
|
||||||
gap: 0;
|
gap: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -235,6 +235,41 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agents-split-resize-handle {
|
||||||
|
position: relative;
|
||||||
|
width: var(--space-sm);
|
||||||
|
min-width: var(--space-sm);
|
||||||
|
cursor: col-resize;
|
||||||
|
background: transparent;
|
||||||
|
touch-action: none;
|
||||||
|
transition: background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:AgentsView 2026-06-20-00:00:
|
||||||
|
The resize affordance must match MissionManager's accessible split-pane handle while preserving a token-only, no-mobile-shell layout.
|
||||||
|
The base grid keeps a token-sized handle column as the no-JS fallback, while the React inline grid width owns desktop/tablet persistence.
|
||||||
|
*/
|
||||||
|
.agents-split-resize-handle::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: var(--space-xs);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-split-resize-handle:hover::before,
|
||||||
|
.agents-split-resize-handle:active::before {
|
||||||
|
background: color-mix(in srgb, var(--todo) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-split-resize-handle:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
}
|
||||||
|
|
||||||
.agents-split-detail {
|
.agents-split-detail {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1490,6 +1525,10 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agents-split-resize-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.agents-split-detail {
|
.agents-split-detail {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -1511,9 +1550,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (min-width: 769px) and (max-width: 1024px) {
|
|
||||||
.agents-split-layout {
|
|
||||||
grid-template-columns: minmax(calc(var(--space-xl) * 10), calc(var(--space-xl) * 11 + var(--space-md))) minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,6 +63,28 @@ const ORG_CHART_SCALE_MAX = 3;
|
|||||||
const ORG_CHART_KEYBOARD_PAN_STEP = 16;
|
const ORG_CHART_KEYBOARD_PAN_STEP = 16;
|
||||||
const ORG_CHART_OVERSCROLL = 32;
|
const ORG_CHART_OVERSCROLL = 32;
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:AgentsView 2026-06-20-00:00:
|
||||||
|
The Agents split view needs a wider tablet default than the old fixed CSS column and the sidebar must be user-resizable on non-mobile viewports.
|
||||||
|
Persist the clamped width per project so desktop and tablet users keep their preferred agent-list/detail balance without affecting the stacked mobile layout.
|
||||||
|
*/
|
||||||
|
const AGENTS_SIDEBAR_DEFAULT_WIDTH = 320;
|
||||||
|
const AGENTS_SIDEBAR_MIN_WIDTH = 260;
|
||||||
|
const AGENTS_SIDEBAR_MAX_WIDTH = 520;
|
||||||
|
const AGENTS_SIDEBAR_WIDTH_STORAGE_KEY = "kb-dashboard-agents-sidebar-width";
|
||||||
|
|
||||||
|
function clampAgentsSidebarWidth(width: number): number {
|
||||||
|
return Math.max(AGENTS_SIDEBAR_MIN_WIDTH, Math.min(AGENTS_SIDEBAR_MAX_WIDTH, width));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAgentsSidebarWidth(projectId?: string): number {
|
||||||
|
if (typeof window === "undefined") return AGENTS_SIDEBAR_DEFAULT_WIDTH;
|
||||||
|
const stored = getScopedItem(AGENTS_SIDEBAR_WIDTH_STORAGE_KEY, projectId);
|
||||||
|
const parsed = stored ? Number(stored) : NaN;
|
||||||
|
if (!Number.isFinite(parsed)) return AGENTS_SIDEBAR_DEFAULT_WIDTH;
|
||||||
|
return clampAgentsSidebarWidth(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
function getStateBadgeClass(state: AgentState): string {
|
function getStateBadgeClass(state: AgentState): string {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case "running":
|
case "running":
|
||||||
@@ -272,6 +294,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
|||||||
const [showSystemAgents, setShowSystemAgents] = useState(false);
|
const [showSystemAgents, setShowSystemAgents] = useState(false);
|
||||||
const viewportMode = useViewportMode();
|
const viewportMode = useViewportMode();
|
||||||
const isMobileViewport = viewportMode === "mobile";
|
const isMobileViewport = viewportMode === "mobile";
|
||||||
|
const [sidebarWidth, setSidebarWidth] = useState<number>(() => readAgentsSidebarWidth(projectId));
|
||||||
const [filterState, setFilterState] = useState<AgentState | "all">("all");
|
const [filterState, setFilterState] = useState<AgentState | "all">("all");
|
||||||
const { agents, stats, isLoading, loadAgents, refreshAgents } = useAgents(projectId, {
|
const { agents, stats, isLoading, loadAgents, refreshAgents } = useAgents(projectId, {
|
||||||
filterState,
|
filterState,
|
||||||
@@ -320,6 +343,10 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
|||||||
const controlsTriggerRef = useRef<HTMLButtonElement>(null);
|
const controlsTriggerRef = useRef<HTMLButtonElement>(null);
|
||||||
const controlsPanelId = useId();
|
const controlsPanelId = useId();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSidebarWidth(readAgentsSidebarWidth(projectId));
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = getScopedItem("fn-agent-view", projectId);
|
const saved = getScopedItem("fn-agent-view", projectId);
|
||||||
if (saved === "list" || saved === "board" || saved === "org") {
|
if (saved === "list" || saved === "board" || saved === "org") {
|
||||||
@@ -343,6 +370,59 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
|||||||
setScopedItem(ORG_CHART_LAYOUT_STORAGE_KEY, orgChartLayoutPreference, projectId);
|
setScopedItem(ORG_CHART_LAYOUT_STORAGE_KEY, orgChartLayoutPreference, projectId);
|
||||||
}, [orgChartLayoutPreference, projectId]);
|
}, [orgChartLayoutPreference, projectId]);
|
||||||
|
|
||||||
|
const persistSidebarWidth = useCallback((width: number) => {
|
||||||
|
try {
|
||||||
|
setScopedItem(AGENTS_SIDEBAR_WIDTH_STORAGE_KEY, String(width), projectId);
|
||||||
|
} catch {
|
||||||
|
// Ignore storage errors.
|
||||||
|
}
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
const handleSidebarResizeStart = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
|
if (isMobileViewport) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const handle = event.currentTarget;
|
||||||
|
if (typeof handle.setPointerCapture === "function") {
|
||||||
|
handle.setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startWidth = sidebarWidth;
|
||||||
|
let latestWidth = startWidth;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
|
||||||
|
const onPointerMove = (moveEvent: PointerEvent) => {
|
||||||
|
const deltaX = moveEvent.clientX - startX;
|
||||||
|
const nextWidth = clampAgentsSidebarWidth(startWidth + deltaX);
|
||||||
|
latestWidth = nextWidth;
|
||||||
|
setSidebarWidth(nextWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (upEvent: PointerEvent) => {
|
||||||
|
if (typeof handle.releasePointerCapture === "function") {
|
||||||
|
handle.releasePointerCapture(upEvent.pointerId);
|
||||||
|
}
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
document.removeEventListener("pointermove", onPointerMove);
|
||||||
|
document.removeEventListener("pointerup", onPointerUp);
|
||||||
|
persistSidebarWidth(latestWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("pointermove", onPointerMove);
|
||||||
|
document.addEventListener("pointerup", onPointerUp);
|
||||||
|
}, [isMobileViewport, persistSidebarWidth, sidebarWidth]);
|
||||||
|
|
||||||
|
const handleSidebarResizeKeyDown = useCallback((event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (isMobileViewport) return;
|
||||||
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
||||||
|
event.preventDefault();
|
||||||
|
const step = event.shiftKey ? 50 : 10;
|
||||||
|
const delta = event.key === "ArrowLeft" ? -step : step;
|
||||||
|
const nextWidth = clampAgentsSidebarWidth(sidebarWidth + delta);
|
||||||
|
setSidebarWidth(nextWidth);
|
||||||
|
persistSidebarWidth(nextWidth);
|
||||||
|
}, [isMobileViewport, persistSidebarWidth, sidebarWidth]);
|
||||||
|
|
||||||
const [editingRoleForAgent, setEditingRoleForAgent] = useState<string | null>(null);
|
const [editingRoleForAgent, setEditingRoleForAgent] = useState<string | null>(null);
|
||||||
const roleSelectRef = useRef<HTMLSelectElement>(null);
|
const roleSelectRef = useRef<HTMLSelectElement>(null);
|
||||||
const [updatingHeartbeatAgentId, setUpdatingHeartbeatAgentId] = useState<string | null>(null);
|
const [updatingHeartbeatAgentId, setUpdatingHeartbeatAgentId] = useState<string | null>(null);
|
||||||
@@ -1547,7 +1627,10 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="agents-split-layout">
|
<div
|
||||||
|
className="agents-split-layout"
|
||||||
|
style={isMobileViewport ? undefined : { gridTemplateColumns: `${sidebarWidth}px var(--space-sm) minmax(0, 1fr)` }}
|
||||||
|
>
|
||||||
<div className={`agents-split-sidebar${isMobileDetailOpen ? " agents-split-sidebar--hidden-mobile" : ""}`}>
|
<div className={`agents-split-sidebar${isMobileDetailOpen ? " agents-split-sidebar--hidden-mobile" : ""}`}>
|
||||||
<div className="agents-view-content">
|
<div className="agents-view-content">
|
||||||
{/* Agent Collection */}
|
{/* Agent Collection */}
|
||||||
@@ -1969,6 +2052,22 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isMobileViewport && (
|
||||||
|
<div
|
||||||
|
className="agents-split-resize-handle"
|
||||||
|
data-testid="agents-sidebar-resize-handle"
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-valuemin={AGENTS_SIDEBAR_MIN_WIDTH}
|
||||||
|
aria-valuemax={AGENTS_SIDEBAR_MAX_WIDTH}
|
||||||
|
aria-valuenow={sidebarWidth}
|
||||||
|
aria-label={t("agents.resizeSidebar", "Resize agents sidebar")}
|
||||||
|
tabIndex={0}
|
||||||
|
onPointerDown={handleSidebarResizeStart}
|
||||||
|
onKeyDown={handleSidebarResizeKeyDown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={`agents-split-detail${isMobileViewport && !selectedAgentId ? " agents-split-detail--hidden-mobile" : ""}`}>
|
<div className={`agents-split-detail${isMobileViewport && !selectedAgentId ? " agents-split-detail--hidden-mobile" : ""}`}>
|
||||||
{selectedAgentId ? (
|
{selectedAgentId ? (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const mockResizeObserverDisconnect = vi.fn();
|
|||||||
describe("AgentsView", () => {
|
describe("AgentsView", () => {
|
||||||
const mockAddToast = vi.fn();
|
const mockAddToast = vi.fn();
|
||||||
const projectId = "proj_123";
|
const projectId = "proj_123";
|
||||||
|
const agentsSidebarWidthKey = "kb-dashboard-agents-sidebar-width";
|
||||||
|
|
||||||
const mockAgents: Agent[] = [
|
const mockAgents: Agent[] = [
|
||||||
{
|
{
|
||||||
@@ -310,6 +311,121 @@ describe("AgentsView", () => {
|
|||||||
expect(container.querySelector(".agents-sidebar-quick-controls")).toBeNull();
|
expect(container.querySelector(".agents-sidebar-quick-controls")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(["desktop", "tablet"] as const)("renders an accessible resize handle on %s split layouts", async (mode) => {
|
||||||
|
mockViewportMode.mockReturnValue(mode);
|
||||||
|
const { container } = render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
|
||||||
|
|
||||||
|
const handle = await screen.findByTestId("agents-sidebar-resize-handle");
|
||||||
|
expect(handle).toHaveAttribute("role", "separator");
|
||||||
|
expect(handle).toHaveAttribute("aria-orientation", "vertical");
|
||||||
|
expect(handle).toHaveAttribute("aria-valuemin", "260");
|
||||||
|
expect(handle).toHaveAttribute("aria-valuemax", "520");
|
||||||
|
expect(handle).toHaveAttribute("aria-valuenow", "320");
|
||||||
|
expect(container.querySelector<HTMLElement>(".agents-split-layout")?.style.gridTemplateColumns).toBe("320px var(--space-sm) minmax(0, 1fr)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render the resize handle or inline split width on mobile", async () => {
|
||||||
|
mockViewportMode.mockReturnValue("mobile");
|
||||||
|
const { container } = render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Agents")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("agents-sidebar-resize-handle")).toBeNull();
|
||||||
|
expect(container.querySelector<HTMLElement>(".agents-split-layout")?.style.gridTemplateColumns).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ label: "no stored value", stored: null, expected: 320 },
|
||||||
|
{ label: "valid stored value", stored: "410", expected: 410 },
|
||||||
|
{ label: "corrupt stored value", stored: "not-a-number", expected: 320 },
|
||||||
|
{ label: "above max stored value", stored: "999", expected: 520 },
|
||||||
|
{ label: "below min stored value", stored: "10", expected: 260 },
|
||||||
|
])("initializes sidebar width from $label", async ({ stored, expected }) => {
|
||||||
|
if (stored !== null) {
|
||||||
|
localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { container } = render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
|
||||||
|
|
||||||
|
const handle = await screen.findByTestId("agents-sidebar-resize-handle");
|
||||||
|
expect(handle).toHaveAttribute("aria-valuenow", String(expected));
|
||||||
|
expect(container.querySelector<HTMLElement>(".agents-split-layout")?.style.gridTemplateColumns).toBe(`${expected}px var(--space-sm) minmax(0, 1fr)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports keyboard resizing with project-scoped persistence and clamping", async () => {
|
||||||
|
localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), "515");
|
||||||
|
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
|
||||||
|
|
||||||
|
const handle = await screen.findByTestId("agents-sidebar-resize-handle");
|
||||||
|
|
||||||
|
fireEvent.keyDown(handle, { key: "ArrowRight", shiftKey: true });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(handle).toHaveAttribute("aria-valuenow", "520");
|
||||||
|
expect(localStorage.getItem(scopedKey(agentsSidebarWidthKey, projectId))).toBe("520");
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.keyDown(handle, { key: "ArrowLeft", shiftKey: true });
|
||||||
|
expect(handle).toHaveAttribute("aria-valuenow", "470");
|
||||||
|
expect(localStorage.getItem(scopedKey(agentsSidebarWidthKey, projectId))).toBe("470");
|
||||||
|
|
||||||
|
fireEvent.keyDown(handle, { key: "ArrowLeft" });
|
||||||
|
expect(handle).toHaveAttribute("aria-valuenow", "460");
|
||||||
|
expect(localStorage.getItem(scopedKey(agentsSidebarWidthKey, projectId))).toBe("460");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps keyboard resizing at the minimum width", async () => {
|
||||||
|
localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), "260");
|
||||||
|
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
|
||||||
|
|
||||||
|
const handle = await screen.findByTestId("agents-sidebar-resize-handle");
|
||||||
|
fireEvent.keyDown(handle, { key: "ArrowLeft", shiftKey: true });
|
||||||
|
|
||||||
|
expect(handle).toHaveAttribute("aria-valuenow", "260");
|
||||||
|
expect(localStorage.getItem(scopedKey(agentsSidebarWidthKey, projectId))).toBe("260");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports pointer drag resizing with capture, cleanup, persistence, and max clamping", async () => {
|
||||||
|
localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), "500");
|
||||||
|
const { container } = render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
|
||||||
|
const handle = await screen.findByTestId("agents-sidebar-resize-handle");
|
||||||
|
const setPointerCapture = vi.fn();
|
||||||
|
const releasePointerCapture = vi.fn();
|
||||||
|
Object.defineProperty(handle, "setPointerCapture", { configurable: true, value: setPointerCapture });
|
||||||
|
Object.defineProperty(handle, "releasePointerCapture", { configurable: true, value: releasePointerCapture });
|
||||||
|
|
||||||
|
fireEvent.pointerDown(handle, { pointerId: 1, clientX: 300 });
|
||||||
|
expect(setPointerCapture).toHaveBeenCalledWith(1);
|
||||||
|
expect(document.body.style.userSelect).toBe("none");
|
||||||
|
|
||||||
|
fireEvent.pointerMove(document, { pointerId: 1, clientX: 400 });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(handle).toHaveAttribute("aria-valuenow", "520");
|
||||||
|
});
|
||||||
|
expect(container.querySelector<HTMLElement>(".agents-split-layout")?.style.gridTemplateColumns).toBe("520px var(--space-sm) minmax(0, 1fr)");
|
||||||
|
|
||||||
|
fireEvent.pointerUp(document, { pointerId: 1 });
|
||||||
|
expect(releasePointerCapture).toHaveBeenCalledWith(1);
|
||||||
|
expect(document.body.style.userSelect).toBe("");
|
||||||
|
expect(localStorage.getItem(scopedKey(agentsSidebarWidthKey, projectId))).toBe("520");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports pointer drag resizing with min clamping", async () => {
|
||||||
|
localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), "300");
|
||||||
|
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
|
||||||
|
const handle = await screen.findByTestId("agents-sidebar-resize-handle");
|
||||||
|
|
||||||
|
fireEvent.pointerDown(handle, { pointerId: 2, clientX: 300 });
|
||||||
|
fireEvent.pointerMove(document, { pointerId: 2, clientX: 0 });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(handle).toHaveAttribute("aria-valuenow", "260");
|
||||||
|
});
|
||||||
|
fireEvent.pointerUp(document, { pointerId: 2 });
|
||||||
|
|
||||||
|
expect(localStorage.getItem(scopedKey(agentsSidebarWidthKey, projectId))).toBe("260");
|
||||||
|
});
|
||||||
|
|
||||||
it("supports mobile drill-in detail with back navigation", async () => {
|
it("supports mobile drill-in detail with back navigation", async () => {
|
||||||
mockViewportMode.mockReturnValue("mobile");
|
mockViewportMode.mockReturnValue("mobile");
|
||||||
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||||
@@ -1345,6 +1461,22 @@ describe("AgentsView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not render the split resize handle in org chart view", async () => {
|
||||||
|
mockFetchOrgTree.mockResolvedValue(orgTree);
|
||||||
|
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
|
||||||
|
expect(await screen.findByTestId("agents-sidebar-resize-handle")).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Org Chart view" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container.querySelector(".agents-org-full-view")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("agents-sidebar-resize-handle")).toBeNull();
|
||||||
|
expect(container.querySelector(".agents-split-layout")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders org chart nodes and opens detail view when clicking a node", async () => {
|
it("renders org chart nodes and opens detail view when clicking a node", async () => {
|
||||||
mockFetchOrgTree.mockResolvedValue(orgTree);
|
mockFetchOrgTree.mockResolvedValue(orgTree);
|
||||||
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
const { container } = render(<AgentsView addToast={mockAddToast} />);
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ describe("projectStorage", () => {
|
|||||||
"kb-dashboard-list-selected-task",
|
"kb-dashboard-list-selected-task",
|
||||||
"kb-dashboard-list-sidebar-width",
|
"kb-dashboard-list-sidebar-width",
|
||||||
"kb-dashboard-mailbox-sidebar-width",
|
"kb-dashboard-mailbox-sidebar-width",
|
||||||
|
"kb-dashboard-agents-sidebar-width",
|
||||||
"kb-quick-entry-text",
|
"kb-quick-entry-text",
|
||||||
"kb-inline-create-text",
|
"kb-inline-create-text",
|
||||||
"fn-agent-view",
|
"fn-agent-view",
|
||||||
@@ -103,7 +104,7 @@ describe("projectStorage", () => {
|
|||||||
"fusion-plugin-dependency-graph:positions",
|
"fusion-plugin-dependency-graph:positions",
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
expect(PROJECT_STORAGE_KEYS).toHaveLength(25);
|
expect(PROJECT_STORAGE_KEYS).toHaveLength(26);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores branch filter values as scoped strings per project", () => {
|
it("stores branch filter values as scoped strings per project", () => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const PROJECT_STORAGE_KEYS: string[] = [
|
|||||||
"kb-dashboard-list-selected-task",
|
"kb-dashboard-list-selected-task",
|
||||||
"kb-dashboard-list-sidebar-width",
|
"kb-dashboard-list-sidebar-width",
|
||||||
"kb-dashboard-mailbox-sidebar-width",
|
"kb-dashboard-mailbox-sidebar-width",
|
||||||
|
"kb-dashboard-agents-sidebar-width",
|
||||||
"kb-quick-entry-text",
|
"kb-quick-entry-text",
|
||||||
"kb-inline-create-text",
|
"kb-inline-create-text",
|
||||||
"fn-agent-view",
|
"fn-agent-view",
|
||||||
|
|||||||
Reference in New Issue
Block a user