fix: permanent tabs back

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-26 06:40:11 +02:00
parent ad8d13cb69
commit 29922b8058
4 changed files with 124 additions and 20 deletions

View File

@@ -57,23 +57,12 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, reload_shell(socket, workbench)}
end
def handle_event("open_sidebar_item", %{"route" => route, "id" => id} = params, socket) do
route_atom = sidebar_route_atom(route)
tab_id = tab_id_for_route(route_atom, id)
def handle_event("open_sidebar_item", %{"route" => _route, "id" => _id} = params, socket) do
{:noreply, open_sidebar_item(socket, params, :preview)}
end
workbench =
Workbench.open_tab(socket.assigns.workbench, route_atom, tab_id, tab_intent(route_atom))
tab_meta =
Map.put(socket.assigns.tab_meta, {route_atom, tab_id}, %{
title: Map.get(params, "title", ""),
subtitle: Map.get(params, "subtitle", "")
})
{:noreply,
socket
|> assign(:tab_meta, tab_meta)
|> reload_shell(workbench)}
def handle_event("pin_sidebar_item", %{"route" => _route, "id" => _id} = params, socket) do
{:noreply, open_sidebar_item(socket, params, :pin)}
end
def handle_event("select_tab", %{"type" => type, "id" => id}, socket) do
@@ -181,6 +170,8 @@ defmodule BDS.Desktop.ShellLive do
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
data-open-title={item.title}
data-open-subtitle={format_sidebar_timestamp(item.meta_timestamp)}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
@@ -218,6 +209,8 @@ defmodule BDS.Desktop.ShellLive do
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
data-open-title={item.title}
data-open-subtitle={item.meta}
type="button"
title={item.title}
phx-click="open_sidebar_item"
@@ -259,6 +252,8 @@ defmodule BDS.Desktop.ShellLive do
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
data-open-title={item.title}
data-open-subtitle={translated(item.meta || "")}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
@@ -290,6 +285,8 @@ defmodule BDS.Desktop.ShellLive do
data-testid="sidebar-open-item"
data-route={item.route}
data-item-id={item.id}
data-open-title={translated(item.title)}
data-open-subtitle={translated(Map.get(@sidebar_data, :subtitle, ""))}
type="button"
phx-click="open_sidebar_item"
phx-value-route={item.route}
@@ -433,6 +430,24 @@ defmodule BDS.Desktop.ShellLive do
Enum.find(tabs, &(&1.type == type and &1.id == id))
end
defp open_sidebar_item(socket, params, intent) do
route_atom = sidebar_route_atom(Map.fetch!(params, "route"))
tab_id = tab_id_for_route(route_atom, Map.fetch!(params, "id"))
workbench =
Workbench.open_tab(socket.assigns.workbench, route_atom, tab_id, tab_intent(route_atom, intent))
tab_meta =
Map.put(socket.assigns.tab_meta, {route_atom, tab_id}, %{
title: Map.get(params, "title", ""),
subtitle: Map.get(params, "subtitle", "")
})
socket
|> assign(:tab_meta, tab_meta)
|> reload_shell(workbench)
end
defp sidebar_route_atom(route) when is_atom(route), do: route
defp sidebar_route_atom(route) when is_binary(route), do: String.to_existing_atom(route)
@@ -443,10 +458,10 @@ defmodule BDS.Desktop.ShellLive do
end
end
defp tab_intent(route) do
defp tab_intent(route, requested_intent) do
case Registry.editor_route(route) do
%{singleton: true} -> :pin
_other -> :preview
_other -> requested_intent
end
end

View File

@@ -93,7 +93,7 @@
style={"width: #{@workbench.sidebar_width}px;"}
>
<div class="sidebar" data-region="sidebar">
<div class="sidebar-content sidebar-body">
<div id="sidebar-content" class="sidebar-content sidebar-body" phx-hook="SidebarInteractions">
<div class="sidebar-section">
<div class="sidebar-section-header">
<span><%= String.upcase(sidebar_header_label(@sidebar_header)) %></span>

View File

@@ -3,8 +3,36 @@ document.addEventListener("DOMContentLoaded", () => {
.querySelector("meta[name='csrf-token']")
.getAttribute("content");
const Hooks = {
SidebarInteractions: {
mounted() {
this.handleDblClick = (event) => {
const button = event.target.closest("[data-testid='sidebar-open-item']");
if (!button || !this.el.contains(button)) {
return;
}
this.pushEvent("pin_sidebar_item", {
route: button.dataset.route,
id: button.dataset.itemId,
title: button.dataset.openTitle || "",
subtitle: button.dataset.openSubtitle || ""
});
};
this.el.addEventListener("dblclick", this.handleDblClick);
},
destroyed() {
this.el.removeEventListener("dblclick", this.handleDblClick);
}
}
};
const liveSocket = new LiveView.LiveSocket("/live", Phoenix.Socket, {
params: { _csrf_token: csrfToken }
params: { _csrf_token: csrfToken },
hooks: Hooks
});
liveSocket.connect();

View File

@@ -64,4 +64,65 @@ defmodule BDS.Desktop.ShellLiveTest do
refute html =~ ~s(data-tab-type="settings")
assert html =~ ~s(class="tab-bar-empty")
end
test "sidebar open supports preview and pin intents for entity tabs" do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
html =
render_click(view, "open_sidebar_item", %{
"route" => "post",
"id" => "post-1",
"title" => "First Post",
"subtitle" => "draft"
})
assert html =~ ~s(data-tab-type="post")
assert html =~ ~s(data-tab-id="post-1")
assert html =~ ~s(class="tab active transient")
html =
render_click(view, "pin_sidebar_item", %{
"route" => "post",
"id" => "post-1",
"title" => "First Post",
"subtitle" => "draft"
})
assert html =~ ~s(data-tab-id="post-1")
refute html =~ ~s(class="tab active transient")
html =
render_click(view, "open_sidebar_item", %{
"route" => "post",
"id" => "page-1",
"title" => "About Page",
"subtitle" => "page"
})
assert html =~ ~s(data-tab-id="post-1")
assert html =~ ~s(data-tab-id="page-1")
assert String.contains?(html, ">First Post<")
assert String.contains?(html, ">About Page<")
_html =
render_click(view, "pin_sidebar_item", %{
"route" => "media",
"id" => "media-1",
"title" => "hero.png",
"subtitle" => "12 KB"
})
html =
render_click(view, "open_sidebar_item", %{
"route" => "media",
"id" => "media-2",
"title" => "cover.png",
"subtitle" => "8 KB"
})
assert html =~ ~s(data-tab-id="media-1")
assert html =~ ~s(data-tab-id="media-2")
assert String.contains?(html, ">hero.png<")
assert String.contains?(html, ">cover.png<")
end
end