fix: handling of tab titles on restore
This commit is contained in:
@@ -1652,6 +1652,7 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
dashboard = ShellData.dashboard(projects.active_project_id)
|
dashboard = ShellData.dashboard(projects.active_project_id)
|
||||||
git_badge_count = ShellData.git_badge_count(projects.active_project_id)
|
git_badge_count = ShellData.git_badge_count(projects.active_project_id)
|
||||||
active_view_id = Atom.to_string(workbench.active_view)
|
active_view_id = Atom.to_string(workbench.active_view)
|
||||||
|
tab_meta = TabHelpers.sync_tab_meta(workbench, socket.assigns[:tab_meta] || %{})
|
||||||
|
|
||||||
sidebar_data =
|
sidebar_data =
|
||||||
ShellData.sidebar_view(
|
ShellData.sidebar_view(
|
||||||
@@ -1675,6 +1676,7 @@ defmodule BDS.Desktop.ShellLive do
|
|||||||
task_status = localize_task_status(raw_task_status, page_language)
|
task_status = localize_task_status(raw_task_status, page_language)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|
|> assign(:tab_meta, tab_meta)
|
||||||
|> assign(:workbench, workbench)
|
|> assign(:workbench, workbench)
|
||||||
|> assign(:projects, projects)
|
|> assign(:projects, projects)
|
||||||
|> assign(:current_project, ShellData.current_project(projects))
|
|> assign(:current_project, ShellData.current_project(projects))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
|
|||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
alias BDS.Desktop.ShellData
|
alias BDS.Desktop.ShellData
|
||||||
alias BDS.{AI, BoundedAtoms, Media, Posts}
|
alias BDS.{AI, BoundedAtoms, ImportDefinitions, Media, Posts, Scripts, Templates}
|
||||||
alias BDS.Media.Media, as: MediaRecord
|
alias BDS.Media.Media, as: MediaRecord
|
||||||
alias BDS.Posts.Post
|
alias BDS.Posts.Post
|
||||||
alias BDS.UI.Registry
|
alias BDS.UI.Registry
|
||||||
@@ -25,7 +25,21 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_tab_title(%{type: :chat, id: conversation_id}), do: chat_title(conversation_id)
|
def sync_tab_meta(%{tabs: tabs}, tab_meta) when is_list(tabs) and is_map(tab_meta) do
|
||||||
|
Enum.reduce(tabs, %{}, fn tab, acc ->
|
||||||
|
key = {tab.type, tab.id}
|
||||||
|
existing_meta = Map.get(tab_meta, key, %{})
|
||||||
|
synced_meta = merge_missing_meta(existing_meta, derived_tab_meta(tab))
|
||||||
|
|
||||||
|
if map_size(synced_meta) == 0 do
|
||||||
|
acc
|
||||||
|
else
|
||||||
|
Map.put(acc, key, synced_meta)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_tab_meta(_workbench, tab_meta) when is_map(tab_meta), do: tab_meta
|
||||||
|
|
||||||
def default_tab_title(%{type: type, id: id}) do
|
def default_tab_title(%{type: type, id: id}) do
|
||||||
case Registry.editor_route(type) do
|
case Registry.editor_route(type) do
|
||||||
@@ -34,7 +48,6 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp default_tab_subtitle(%{type: :chat}), do: translated("AI conversations")
|
|
||||||
defp default_tab_subtitle(_tab), do: "Desktop workbench content routed through the Elixir shell."
|
defp default_tab_subtitle(_tab), do: "Desktop workbench content routed through the Elixir shell."
|
||||||
|
|
||||||
def tab_route_label(nil), do: translated("Dashboard")
|
def tab_route_label(nil), do: translated("Dashboard")
|
||||||
@@ -67,40 +80,32 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
|
|||||||
|
|
||||||
def post_title(post_id) do
|
def post_title(post_id) do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
%Post{} = post -> post.title || post.slug || post.id
|
%Post{} = post -> post_record_title(post)
|
||||||
_other -> "Post"
|
_other -> "Post"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_subtitle(post_id) do
|
def post_subtitle(post_id) do
|
||||||
case Posts.get_post(post_id) do
|
case Posts.get_post(post_id) do
|
||||||
%Post{} = post -> post.slug || "draft"
|
%Post{} = post -> post_record_subtitle(post)
|
||||||
_other -> "draft"
|
_other -> "draft"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def media_title(media_id) do
|
def media_title(media_id) do
|
||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
%MediaRecord{} = media -> media.title || media.filename || media.id
|
%MediaRecord{} = media -> media_record_title(media)
|
||||||
_other -> "Media"
|
_other -> "Media"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def media_subtitle(media_id) do
|
def media_subtitle(media_id) do
|
||||||
case Media.get_media(media_id) do
|
case Media.get_media(media_id) do
|
||||||
%MediaRecord{} = media -> media.filename || media.mime_type || "media"
|
%MediaRecord{} = media -> media_record_subtitle(media)
|
||||||
_other -> "media"
|
_other -> "media"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def chat_title(conversation_id) do
|
|
||||||
case AI.get_chat_conversation(conversation_id) do
|
|
||||||
%{title: title} when is_binary(title) and title != "" -> title
|
|
||||||
%{id: id} when is_binary(id) and id != "" -> id
|
|
||||||
_other -> "Chat"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_integer(value) when is_integer(value), do: value
|
def parse_integer(value) when is_integer(value), do: value
|
||||||
|
|
||||||
def parse_integer(value) do
|
def parse_integer(value) do
|
||||||
@@ -110,5 +115,118 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp derived_tab_meta(%{type: :post, id: post_id}) do
|
||||||
|
case Posts.get_post(post_id) do
|
||||||
|
%Post{} = post -> %{title: post_record_title(post), subtitle: post_record_subtitle(post)}
|
||||||
|
_other -> %{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp derived_tab_meta(%{type: :media, id: media_id}) do
|
||||||
|
case Media.get_media(media_id) do
|
||||||
|
%MediaRecord{} = media ->
|
||||||
|
%{title: media_record_title(media), subtitle: media_record_subtitle(media)}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp derived_tab_meta(%{type: :scripts, id: script_id}) do
|
||||||
|
case Scripts.get_script(script_id) do
|
||||||
|
%{title: title, id: id} ->
|
||||||
|
%{title: blank_to_nil(title) || id, subtitle: translated("Automation helpers")}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp derived_tab_meta(%{type: :templates, id: template_id}) do
|
||||||
|
case Templates.get_template(template_id) do
|
||||||
|
%{title: title, id: id} ->
|
||||||
|
%{title: blank_to_nil(title) || id, subtitle: translated("Site rendering")}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp derived_tab_meta(%{type: :chat, id: conversation_id}) do
|
||||||
|
case AI.get_chat_conversation(conversation_id) do
|
||||||
|
conversation when is_map(conversation) ->
|
||||||
|
%{title: chat_record_title(conversation), subtitle: translated("AI conversations")}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp derived_tab_meta(%{type: :import, id: definition_id}) do
|
||||||
|
case ImportDefinitions.get_definition(definition_id) do
|
||||||
|
%{name: name} ->
|
||||||
|
%{
|
||||||
|
title: blank_to_nil(name) || translated("importAnalysis.untitledImport"),
|
||||||
|
subtitle: translated("importAnalysis.headerDescription")
|
||||||
|
}
|
||||||
|
|
||||||
|
_other ->
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp derived_tab_meta(%{type: :git_diff, id: "git-working-tree"}) do
|
||||||
|
%{title: translated("Working tree"), subtitle: translated("Working tree and history")}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp derived_tab_meta(_tab), do: %{}
|
||||||
|
|
||||||
|
defp merge_missing_meta(existing_meta, fresh_meta) do
|
||||||
|
existing_meta
|
||||||
|
|> maybe_put_missing(:title, Map.get(fresh_meta, :title))
|
||||||
|
|> maybe_put_missing(:subtitle, Map.get(fresh_meta, :subtitle))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_put_missing(meta, key, value) do
|
||||||
|
cond do
|
||||||
|
blank_to_nil(value) == nil ->
|
||||||
|
meta
|
||||||
|
|
||||||
|
existing_value_present?(Map.get(meta, key)) ->
|
||||||
|
meta
|
||||||
|
|
||||||
|
true ->
|
||||||
|
Map.put(meta, key, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp existing_value_present?(value) do
|
||||||
|
if is_binary(value), do: String.trim(value) != "", else: false
|
||||||
|
end
|
||||||
|
|
||||||
|
defp post_record_title(%Post{} = post), do: blank_to_nil(post.title) || blank_to_nil(post.slug) || post.id
|
||||||
|
|
||||||
|
defp post_record_subtitle(%Post{} = post), do: Atom.to_string(post.status)
|
||||||
|
|
||||||
|
defp media_record_title(%MediaRecord{} = media) do
|
||||||
|
blank_to_nil(media.title) || blank_to_nil(media.original_name) || media.id
|
||||||
|
end
|
||||||
|
|
||||||
|
defp media_record_subtitle(%MediaRecord{} = media) do
|
||||||
|
blank_to_nil(media.original_name) || blank_to_nil(media.mime_type) || "media"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp chat_record_title(%{title: title, id: id}), do: blank_to_nil(title) || id
|
||||||
|
|
||||||
|
defp blank_to_nil(value) do
|
||||||
|
value
|
||||||
|
|> to_string()
|
||||||
|
|> String.trim()
|
||||||
|
|> case do
|
||||||
|
"" -> nil
|
||||||
|
trimmed -> trimmed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp translated(text), do: ShellData.translate(text, %{}, BDS.Desktop.UILocale.current())
|
defp translated(text), do: ShellData.translate(text, %{}, BDS.Desktop.UILocale.current())
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -728,6 +728,115 @@ defmodule BDS.Desktop.ShellLiveTest do
|
|||||||
assert has_element?(view, ".chat-panel-title-main", "Editorial Plan")
|
assert has_element?(view, ".chat-panel-title-main", "Editorial Plan")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "workbench session restore rehydrates entity tab titles from backing records", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
assert {:ok, post} = Posts.create_post(%{project_id: project.id, title: "Restored Post"})
|
||||||
|
|
||||||
|
source_path = Path.join(temp_dir, "restored-media.txt")
|
||||||
|
File.write!(source_path, "media body")
|
||||||
|
|
||||||
|
assert {:ok, media} =
|
||||||
|
Media.import_media(%{
|
||||||
|
project_id: project.id,
|
||||||
|
source_path: source_path,
|
||||||
|
title: "Restored Media"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, script} =
|
||||||
|
Scripts.create_script(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Restored Script",
|
||||||
|
kind: :utility,
|
||||||
|
content: "print(\"ok\")",
|
||||||
|
entrypoint: "main",
|
||||||
|
enabled: true
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, template} =
|
||||||
|
Templates.create_template(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Restored Template",
|
||||||
|
kind: :post,
|
||||||
|
content: "",
|
||||||
|
enabled: true
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, definition} =
|
||||||
|
ImportDefinitions.create_definition(%{
|
||||||
|
project_id: project.id,
|
||||||
|
name: "Restored Import"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, conversation} = AI.start_chat(%{title: "Restored Chat"})
|
||||||
|
|
||||||
|
posts_dir = Path.join(temp_dir, "posts")
|
||||||
|
File.mkdir_p!(posts_dir)
|
||||||
|
|
||||||
|
git_file_path = Path.join(posts_dir, "restore.md")
|
||||||
|
File.write!(git_file_path, "Old content\n")
|
||||||
|
init_git_repo!(temp_dir, "initial")
|
||||||
|
File.write!(git_file_path, "New content\n")
|
||||||
|
|
||||||
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
|
session_payload =
|
||||||
|
Workbench.new()
|
||||||
|
|> Workbench.open_tab(:post, post.id, :pin)
|
||||||
|
|> Workbench.open_tab(:media, media.id, :pin)
|
||||||
|
|> Workbench.open_tab(:scripts, script.id, :pin)
|
||||||
|
|> Workbench.open_tab(:templates, template.id, :pin)
|
||||||
|
|> Workbench.open_tab(:import, definition.id, :pin)
|
||||||
|
|> Workbench.open_tab(:chat, conversation.id, :pin)
|
||||||
|
|> Workbench.open_tab(:git_diff, "git-working-tree", :pin)
|
||||||
|
|> Session.serialize()
|
||||||
|
|
||||||
|
_html = render_hook(view, "restore_workbench_session", %{"session" => session_payload})
|
||||||
|
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
".tab[data-tab-type='post'][data-tab-id='#{post.id}'] .tab-title",
|
||||||
|
"Restored Post"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
".tab[data-tab-type='media'][data-tab-id='#{media.id}'] .tab-title",
|
||||||
|
"Restored Media"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
".tab[data-tab-type='scripts'][data-tab-id='#{script.id}'] .tab-title",
|
||||||
|
"Restored Script"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
".tab[data-tab-type='templates'][data-tab-id='#{template.id}'] .tab-title",
|
||||||
|
"Restored Template"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
".tab[data-tab-type='import'][data-tab-id='#{definition.id}'] .tab-title",
|
||||||
|
"Restored Import"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
".tab[data-tab-type='chat'][data-tab-id='#{conversation.id}'] .tab-title",
|
||||||
|
"Restored Chat"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
".tab[data-tab-type='git_diff'][data-tab-id='git-working-tree'] .tab-title",
|
||||||
|
"Working tree"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
test "metadata diff refresh reruns after workbench session restore", %{project: project} do
|
test "metadata diff refresh reruns after workbench session restore", %{project: project} do
|
||||||
:ok = BDS.Tasks.clear_finished()
|
:ok = BDS.Tasks.clear_finished()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user