fix: drag-resize working now

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-26 06:55:18 +02:00
parent 82ba43c7ba
commit 3556ab45b3
7 changed files with 257 additions and 1 deletions

View File

@@ -24,10 +24,18 @@ defmodule BDS.Desktop.Automation do
GenServer.call(session, {:click, selector}, @request_timeout)
end
def drag(session, selector, delta_x) when is_binary(selector) and is_integer(delta_x) do
GenServer.call(session, {:drag, selector, delta_x}, @request_timeout)
end
def press(session, shortcut) when is_binary(shortcut) do
GenServer.call(session, {:press, shortcut}, @request_timeout)
end
def reload(session) do
GenServer.call(session, :reload, @request_timeout)
end
def capture_screenshot(session, destination) when is_binary(destination) do
GenServer.call(session, {:capture_screenshot, destination}, @request_timeout)
end
@@ -82,11 +90,23 @@ defmodule BDS.Desktop.Automation do
{:reply, normalize_simple_reply(reply), state}
end
def handle_call({:drag, selector, delta_x}, _from, state) do
{reply, state} =
driver_request(state, %{"command" => "drag", "selector" => selector, "deltaX" => delta_x})
{:reply, normalize_simple_reply(reply), state}
end
def handle_call({:press, shortcut}, _from, state) do
{reply, state} = driver_request(state, %{"command" => "press", "shortcut" => shortcut})
{:reply, normalize_simple_reply(reply), state}
end
def handle_call(:reload, _from, state) do
{reply, state} = driver_request(state, %{"command" => "reload"})
{:reply, normalize_simple_reply(reply), state}
end
def handle_call({:capture_screenshot, destination}, _from, state) do
File.mkdir_p!(Path.dirname(destination))

View File

@@ -66,6 +66,14 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, open_sidebar_item(socket, params, :pin)}
end
def handle_event("sync_layout", params, socket) do
{:noreply, reload_shell(socket, sync_layout(socket.assigns.workbench, params))}
end
def handle_event("resize_panel", %{"target" => target, "width" => width}, socket) do
{:noreply, reload_shell(socket, resize_panel(socket.assigns.workbench, target, width))}
end
def handle_event("shortcut", params, socket) do
if ignore_shortcut?(params) do
{:noreply, socket}
@@ -439,6 +447,44 @@ defmodule BDS.Desktop.ShellLive do
Enum.find(tabs, &(&1.type == type and &1.id == id))
end
defp sync_layout(workbench, params) do
workbench
|> maybe_set_sidebar_width(Map.get(params, "sidebar_width"))
|> maybe_set_assistant_width(Map.get(params, "assistant_sidebar_width"))
end
defp resize_panel(workbench, "sidebar", width) do
workbench
|> Workbench.set_sidebar_width(parse_width(width))
|> Map.put(:sidebar_visible, true)
end
defp resize_panel(workbench, "assistant", width) do
workbench
|> Workbench.set_assistant_sidebar_width(parse_width(width))
|> Map.put(:assistant_sidebar_visible, true)
end
defp resize_panel(workbench, _target, _width), do: workbench
defp maybe_set_sidebar_width(workbench, nil), do: workbench
defp maybe_set_sidebar_width(workbench, width), do: Workbench.set_sidebar_width(workbench, parse_width(width))
defp maybe_set_assistant_width(workbench, nil), do: workbench
defp maybe_set_assistant_width(workbench, width) do
Workbench.set_assistant_sidebar_width(workbench, parse_width(width))
end
defp parse_width(width) when is_integer(width), do: width
defp parse_width(width) when is_binary(width) do
case Integer.parse(width) do
{parsed, _rest} -> parsed
:error -> 0
end
end
defp ignore_shortcut?(params) do
Map.get(params, "alt", false) or
Map.get(params, "contentEditable", false) or

View File

@@ -1,4 +1,4 @@
<div class="app" id="bds-shell-app" phx-window-keydown="shortcut">
<div class="app" id="bds-shell-app" phx-hook="AppShell" phx-window-keydown="shortcut">
<div class="window-titlebar" data-region="title-bar">
<div class="window-titlebar-menu-bar is-hidden">
<button class="window-titlebar-menu-button" type="button">File</button>

View File

@@ -3,7 +3,116 @@ document.addEventListener("DOMContentLoaded", () => {
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
const SIDEBAR_STORAGE_KEY = "bds-panel-sidebar";
const ASSISTANT_STORAGE_KEY = "bds-panel-assistant-sidebar";
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
const readStoredSize = (key, fallback, min, max) => {
const raw = window.localStorage.getItem(key);
if (!raw) {
return fallback;
}
const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed)) {
return fallback;
}
return clamp(parsed, min, max);
};
const shellWidth = (selector) => {
const shell = document.querySelector(selector);
if (!shell) {
return 0;
}
const width = Number.parseInt(shell.style.width || "0", 10);
return Number.isNaN(width) ? Math.round(shell.getBoundingClientRect().width) : width;
};
const setShellWidth = (selector, width) => {
const shell = document.querySelector(selector);
if (shell) {
shell.style.width = `${width}px`;
shell.classList.remove("is-hidden");
}
};
const persistWidth = (target, width) => {
const key = target === "assistant" ? ASSISTANT_STORAGE_KEY : SIDEBAR_STORAGE_KEY;
window.localStorage.setItem(key, String(width));
};
const Hooks = {
AppShell: {
mounted() {
this.syncStoredLayout();
this.handleMouseDown = (event) => {
const handle = event.target.closest("[data-role='resize-handle']");
if (!handle || !this.el.contains(handle)) {
return;
}
event.preventDefault();
const target = handle.dataset.resize;
const startX = event.clientX;
const startWidth =
target === "assistant"
? shellWidth("[data-testid='assistant-shell']")
: shellWidth("[data-testid='sidebar-shell']");
const min = target === "assistant" ? 280 : 200;
const max = target === "assistant" ? 640 : 500;
const invert = target === "assistant";
const onMouseMove = (moveEvent) => {
const delta = invert ? startX - moveEvent.clientX : moveEvent.clientX - startX;
const width = clamp(startWidth + delta, min, max);
const selector = target === "assistant" ? "[data-testid='assistant-shell']" : "[data-testid='sidebar-shell']";
setShellWidth(selector, width);
persistWidth(target, width);
};
const onMouseUp = (upEvent) => {
const delta = invert ? startX - upEvent.clientX : upEvent.clientX - startX;
const width = clamp(startWidth + delta, min, max);
persistWidth(target, width);
this.pushEvent("resize_panel", { target, width });
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
};
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
};
this.el.addEventListener("mousedown", this.handleMouseDown);
},
destroyed() {
this.el.removeEventListener("mousedown", this.handleMouseDown);
},
syncStoredLayout() {
this.pushEvent("sync_layout", {
sidebar_width: readStoredSize(SIDEBAR_STORAGE_KEY, shellWidth("[data-testid='sidebar-shell']"), 200, 500),
assistant_sidebar_width: readStoredSize(ASSISTANT_STORAGE_KEY, 360, 280, 640)
});
}
},
SidebarInteractions: {
mounted() {
this.handleDblClick = (event) => {

View File

@@ -43,7 +43,9 @@ for await (const line of rl) {
window_title: text("[data-testid='window-title']"),
active_view: document.querySelector("[data-testid='activity-button'][data-active='true']")?.dataset.view ?? null,
sidebar_visible: !hasClass("[data-testid='sidebar-shell']", "is-hidden"),
sidebar_width: document.querySelector("[data-testid='sidebar-shell']")?.getBoundingClientRect().width ?? 0,
assistant_visible: !hasClass("[data-testid='assistant-shell']", "is-hidden"),
assistant_width: document.querySelector("[data-testid='assistant-shell']")?.getBoundingClientRect().width ?? 0,
panel_visible: !hasClass(".panel-shell", "is-hidden"),
editor_title: text("[data-testid='editor-title']"),
activity_labels: texts("[data-testid='activity-button']", (node) => node.getAttribute("aria-label")),
@@ -70,6 +72,33 @@ for await (const line of rl) {
continue;
}
if (message.command === "drag") {
const locator = page.locator(message.selector);
const box = await locator.boundingBox();
if (!box) {
throw new Error(`unable to drag missing element: ${message.selector}`);
}
const startX = box.x + box.width / 2;
const startY = box.y + box.height / 2;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(startX + message.deltaX, startY, { steps: 10 });
await page.mouse.up();
await page.waitForTimeout(50);
console.log(JSON.stringify({ ref, status: "ok", result: "ok" }));
continue;
}
if (message.command === "reload") {
await page.reload({ waitUntil: "networkidle" });
await page.locator("#bds-shell-app").waitFor({ state: "visible" });
await page.waitForTimeout(100);
console.log(JSON.stringify({ ref, status: "ok", result: "ok" }));
continue;
}
if (message.command === "screenshot") {
await page.screenshot({ path: message.path, fullPage: false });
console.log(JSON.stringify({ ref, status: "ok", result: message.path }));

View File

@@ -71,6 +71,34 @@ defmodule BDS.Desktop.AutomationTest do
assert File.exists?(screenshot_path)
end
@tag timeout: 120_000
test "automation drags sidebar resize handle and restores width after reload" do
{:ok, session} = Automation.start_session()
on_exit(fn ->
Automation.stop_session(session)
end)
snapshot = Automation.snapshot(session)
assert snapshot.sidebar_width >= 279
assert snapshot.sidebar_width <= 281
assert :ok = Automation.drag(session, "[data-resize='sidebar']", 90)
snapshot = Automation.snapshot(session)
assert snapshot.sidebar_width >= 360
assert snapshot.sidebar_width <= 380
resized_width = snapshot.sidebar_width
assert :ok = Automation.reload(session)
snapshot = Automation.snapshot(session)
assert snapshot.sidebar_visible == true
assert snapshot.sidebar_width >= resized_width - 2
assert snapshot.sidebar_width <= resized_width + 2
end
@tag timeout: 120_000
test "automation stop_session shuts down the app and browser child processes it started" do
baseline = automation_process_counts()

View File

@@ -169,4 +169,28 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(class="sidebar-shell is-hidden")
assert html =~ ~s(style="width: 0px;")
end
test "layout hooks sync persisted widths and apply drag resizing" do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
assert html =~ ~s(style="width: 280px;")
html = render_hook(view, "sync_layout", %{"sidebar_width" => 420, "assistant_sidebar_width" => 480})
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(style="width: 420px;")
html =
view
|> element("[data-testid='toggle-assistant']")
|> render_click()
assert html =~ ~s(data-testid="assistant-shell")
assert html =~ ~s(style="width: 480px;")
html = render_hook(view, "resize_panel", %{"target" => "sidebar", "width" => 460})
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(style="width: 460px;")
end
end