feat: delete buttons on sidebar entries

This commit is contained in:
2026-05-02 09:15:54 +02:00
parent c118412f56
commit 07fab7d1ab
4 changed files with 522 additions and 121 deletions

View File

@@ -5,7 +5,7 @@ defmodule BDS.Desktop.ShellLive do
import Phoenix.HTML import Phoenix.HTML
alias BDS.{AI, BoundedAtoms} alias BDS.{AI, BoundedAtoms, ImportDefinitions, Media, Posts, Scripts}
alias BDS.CliSync.Watcher alias BDS.CliSync.Watcher
alias BDS.Desktop.{FolderPicker, Overlay, ShellData, UILocale} alias BDS.Desktop.{FolderPicker, Overlay, ShellData, UILocale}
@@ -419,67 +419,12 @@ defmodule BDS.Desktop.ShellLive do
|> reload_shell(workbench)} |> reload_shell(workbench)}
end end
def handle_event("delete_sidebar_template", %{"id" => template_id}, socket) do def handle_event(
case Templates.get_template(template_id) do "confirm_sidebar_delete",
%Templates.Template{project_id: project_id} %{"route" => route, "id" => id} = params,
when project_id == socket.assigns.projects.active_project_id -> socket
case Templates.delete_template(template_id) do ) do
{:ok, :deleted} -> {:noreply, request_sidebar_delete(socket, route, id, Map.get(params, "title"))}
workbench = Workbench.close_tab(socket.assigns.workbench, :templates, template_id)
tab_meta = Map.delete(socket.assigns.tab_meta, {:templates, template_id})
{:noreply,
socket
|> assign(:tab_meta, tab_meta)
|> reload_shell(workbench)}
{:error, reason} ->
{:noreply,
socket
|> append_output_entry(
translated("Delete") <> " " <> translated("Template"),
inspect(reason),
nil,
"error"
)
|> reload_shell(socket.assigns.workbench)}
end
_other ->
{:noreply,
socket
|> append_output_entry(
translated("Delete") <> " " <> translated("Template"),
inspect(:not_found),
nil,
"error"
)
|> reload_shell(socket.assigns.workbench)}
end
end
def handle_event("delete_sidebar_chat", %{"id" => conversation_id}, socket) do
case AI.delete_chat_conversation(conversation_id) do
{:ok, :deleted} ->
workbench = Workbench.close_tab(socket.assigns.workbench, :chat, conversation_id)
tab_meta = Map.delete(socket.assigns.tab_meta, {:chat, conversation_id})
{:noreply,
socket
|> assign(:tab_meta, tab_meta)
|> reload_shell(workbench)}
{:error, reason} ->
{:noreply,
socket
|> append_output_entry(
translated("sidebar.chat.deleteConversation"),
inspect(reason),
nil,
"error"
)
|> reload_shell(socket.assigns.workbench)}
end
end end
def handle_event("toggle_offline_mode", _params, socket) do def handle_event("toggle_offline_mode", _params, socket) do
@@ -1348,6 +1293,9 @@ defmodule BDS.Desktop.ShellLive do
socket = socket =
case {socket.assigns[:shell_overlay], current_tab} do case {socket.assigns[:shell_overlay], current_tab} do
{%{kind: :confirm_delete, delete_action: %{source: :sidebar, route: route, id: id}}, _tab} ->
execute_sidebar_delete(socket, route, id)
{%{kind: :ai_suggestions} = overlay, %{type: :post, id: post_id}} -> {%{kind: :ai_suggestions} = overlay, %{type: :post, id: post_id}} ->
PostEditor.apply_ai_suggestions( PostEditor.apply_ai_suggestions(
socket, socket,
@@ -1926,4 +1874,247 @@ defmodule BDS.Desktop.ShellLive do
|> append_output_entry(title, translated("Command completed"), details) |> append_output_entry(title, translated("Command completed"), details)
|> assign(:shell_overlay, nil) |> assign(:shell_overlay, nil)
end end
defp request_sidebar_delete(socket, route, id, fallback_title) do
case sidebar_delete_target(socket, route, id, fallback_title) do
{:ok, entity_name} ->
assign(socket, :shell_overlay, %{
kind: :confirm_delete,
title: sidebar_delete_title(route),
entity_name: entity_name,
entity_type: route,
reference_count: 0,
reference_list: [],
delete_action: %{source: :sidebar, route: route, id: id}
})
{:error, reason} ->
socket
|> assign(:shell_overlay, nil)
|> append_output_entry(sidebar_delete_title(route), inspect(reason), nil, "error")
|> reload_shell(socket.assigns.workbench)
end
end
defp execute_sidebar_delete(socket, route, id) do
case route do
"post" ->
socket
|> assign(:shell_overlay, nil)
|> PostEditor.delete_socket(id, &reload_shell/2, &append_output_entry/5)
"media" ->
socket
|> assign(:shell_overlay, nil)
|> MediaEditor.delete_socket(id, &reload_shell/2, &append_output_entry/5)
"scripts" ->
delete_sidebar_script(socket, id)
"templates" ->
delete_sidebar_template(socket, id)
"chat" ->
delete_sidebar_chat(socket, id)
"import" ->
delete_sidebar_import(socket, id)
_other ->
socket
|> assign(:shell_overlay, nil)
|> append_output_entry(translated("Delete"), inspect(:unsupported_route), nil, "error")
|> reload_shell(socket.assigns.workbench)
end
end
defp delete_sidebar_script(socket, script_id) do
case Scripts.delete_script(script_id) do
{:ok, :deleted} ->
workbench = Workbench.close_tab(socket.assigns.workbench, :scripts, script_id)
socket
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:scripts, script_id}))
|> assign(:script_editor_drafts, Map.delete(socket.assigns.script_editor_drafts, script_id))
|> reload_shell(workbench)
{:error, reason} ->
socket
|> assign(:shell_overlay, nil)
|> append_output_entry(sidebar_delete_title("scripts"), inspect(reason), nil, "error")
|> reload_shell(socket.assigns.workbench)
end
end
defp delete_sidebar_template(socket, template_id) do
case Templates.delete_template(template_id, force: true) do
{:ok, :deleted} ->
workbench = Workbench.close_tab(socket.assigns.workbench, :templates, template_id)
socket
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:templates, template_id}))
|> assign(
:template_editor_drafts,
Map.delete(socket.assigns.template_editor_drafts, template_id)
)
|> reload_shell(workbench)
{:error, reason} ->
socket
|> assign(:shell_overlay, nil)
|> append_output_entry(sidebar_delete_title("templates"), inspect(reason), nil, "error")
|> reload_shell(socket.assigns.workbench)
end
end
defp delete_sidebar_chat(socket, conversation_id) do
case AI.delete_chat_conversation(conversation_id) do
{:ok, :deleted} ->
workbench = Workbench.close_tab(socket.assigns.workbench, :chat, conversation_id)
socket
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:chat, conversation_id}))
|> reload_shell(workbench)
{:error, reason} ->
socket
|> assign(:shell_overlay, nil)
|> append_output_entry(sidebar_delete_title("chat"), inspect(reason), nil, "error")
|> reload_shell(socket.assigns.workbench)
end
end
defp delete_sidebar_import(socket, definition_id) do
case ImportDefinitions.delete_definition(definition_id) do
{:ok, :deleted} ->
workbench = Workbench.close_tab(socket.assigns.workbench, :import, definition_id)
socket
|> assign(:shell_overlay, nil)
|> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:import, definition_id}))
|> clear_import_editor_state(definition_id)
|> reload_shell(workbench)
{:error, reason} ->
socket
|> assign(:shell_overlay, nil)
|> append_output_entry(sidebar_delete_title("import"), inspect(reason), nil, "error")
|> reload_shell(socket.assigns.workbench)
end
end
defp clear_import_editor_state(socket, definition_id) do
socket
|> assign(
:import_editor_analysis_states,
Map.delete(socket.assigns.import_editor_analysis_states, definition_id)
)
|> assign(
:import_editor_analysis_task_refs,
Map.delete(socket.assigns.import_editor_analysis_task_refs, definition_id)
)
|> assign(
:import_editor_execution_states,
Map.delete(socket.assigns.import_editor_execution_states, definition_id)
)
|> assign(
:import_editor_execution_task_refs,
Map.delete(socket.assigns.import_editor_execution_task_refs, definition_id)
)
|> assign(:import_editor_sections, Map.delete(socket.assigns.import_editor_sections, definition_id))
|> assign(
:import_editor_taxonomy_edits,
Map.delete(socket.assigns.import_editor_taxonomy_edits, definition_id)
)
|> assign(
:import_editor_model_selectors_open,
Map.delete(socket.assigns.import_editor_model_selectors_open, definition_id)
)
|> assign(
:import_editor_selected_models,
Map.delete(socket.assigns.import_editor_selected_models, definition_id)
)
end
defp sidebar_delete_target(socket, route, id, fallback_title) do
active_project_id = socket.assigns.projects.active_project_id
case route do
"post" ->
case Posts.get_post(id) do
%{project_id: ^active_project_id} = post ->
{:ok, present_title(fallback_title) || present_title(post.title) || present_title(post.slug) || id}
_other ->
{:error, :not_found}
end
"media" ->
case Media.get_media(id) do
%{project_id: ^active_project_id} = media ->
{:ok,
present_title(fallback_title) || present_title(media.title) ||
present_title(media.original_name) || id}
_other ->
{:error, :not_found}
end
"scripts" ->
case Scripts.get_script(id) do
%{project_id: ^active_project_id} = script ->
{:ok, present_title(fallback_title) || present_title(script.title) || id}
_other ->
{:error, :not_found}
end
"templates" ->
case Templates.get_template(id) do
%{project_id: ^active_project_id} = template ->
{:ok, present_title(fallback_title) || present_title(template.title) || id}
_other ->
{:error, :not_found}
end
"chat" ->
case AI.get_chat_conversation(id) do
%{title: title} -> {:ok, present_title(fallback_title) || present_title(title) || id}
_other -> {:error, :not_found}
end
"import" ->
case ImportDefinitions.get_definition(id) do
%{project_id: ^active_project_id} = definition ->
{:ok, present_title(fallback_title) || present_title(definition.name) || id}
_other ->
{:error, :not_found}
end
_other ->
{:error, :unsupported_route}
end
end
defp sidebar_delete_title("chat"), do: translated("sidebar.chat.deleteConversation")
defp sidebar_delete_title("post"), do: translated("Delete") <> " " <> translated("Post")
defp sidebar_delete_title("media"), do: translated("Delete") <> " " <> translated("Media")
defp sidebar_delete_title("scripts"), do: translated("Delete") <> " " <> translated("Script")
defp sidebar_delete_title("templates"), do: translated("Delete") <> " " <> translated("Template")
defp sidebar_delete_title("import"), do: translated("Delete") <> " " <> translated("Import")
defp sidebar_delete_title(_route), do: translated("Delete")
defp present_title(value) when is_binary(value) do
case String.trim(value) do
"" -> nil
trimmed -> trimmed
end
end
defp present_title(_value), do: nil
end end

View File

@@ -271,28 +271,45 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
</div> </div>
<div class="sidebar-list"> <div class="sidebar-list">
<%= for item <- Map.get(section, :items, []) do %> <%= for item <- Map.get(section, :items, []) do %>
<button <div class="sidebar-item-row" data-item-id={item.id}>
class={["sidebar-item", "sidebar-post-item", "post-type-post", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "selected")]} <button
data-testid="sidebar-open-item" class={["sidebar-item", "sidebar-post-item", "post-type-post", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "selected")]}
data-route={item.route} data-testid="sidebar-open-item"
data-item-id={item.id} data-route={item.route}
data-open-title={item.title} data-item-id={item.id}
data-open-subtitle={format_sidebar_timestamp(item.meta_timestamp)} data-open-title={item.title}
type="button" data-open-subtitle={format_sidebar_timestamp(item.meta_timestamp)}
phx-click="open_sidebar_item" type="button"
phx-value-route={item.route} phx-click="open_sidebar_item"
phx-value-id={item.id} phx-value-route={item.route}
phx-value-title={item.title} phx-value-id={item.id}
phx-value-subtitle={format_sidebar_timestamp(item.meta_timestamp)} phx-value-title={item.title}
> phx-value-subtitle={format_sidebar_timestamp(item.meta_timestamp)}
<span class="post-type-icon" title="post">●</span> >
<span class="sidebar-item-content"> <span class="post-type-icon" title="post">●</span>
<span class="sidebar-item-title-row"> <span class="sidebar-item-content">
<span class="sidebar-item-title"><%= item.title %></span> <span class="sidebar-item-title-row">
<span class="sidebar-item-title"><%= item.title %></span>
</span>
<span class="sidebar-item-meta"><%= format_sidebar_timestamp(item.meta_timestamp) %></span>
</span> </span>
<span class="sidebar-item-meta"><%= format_sidebar_timestamp(item.meta_timestamp) %></span> </button>
</span> <%= if sidebar_deletable?(item.route) do %>
</button> <button
class="sidebar-delete-button"
data-testid={sidebar_delete_testid(item.route)}
data-item-id={item.id}
type="button"
title={sidebar_delete_title(item.route)}
phx-click="confirm_sidebar_delete"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={item.title}
>
×
</button>
<% end %>
</div>
<% end %> <% end %>
</div> </div>
</section> </section>
@@ -310,34 +327,51 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
<%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %> <%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %>
<div class="sidebar-list media-grid"> <div class="sidebar-list media-grid">
<%= for item <- Map.get(@sidebar_data, :items, []) do %> <%= for item <- Map.get(@sidebar_data, :items, []) do %>
<button <div class="media-item-row" data-item-id={item.id}>
class={["media-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "selected")]} <button
data-testid="sidebar-open-item" class={["media-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "selected")]}
data-route={item.route} data-testid="sidebar-open-item"
data-item-id={item.id} data-route={item.route}
data-open-title={item.title} data-item-id={item.id}
data-open-subtitle={item.meta} data-open-title={item.title}
type="button" data-open-subtitle={item.meta}
title={item.title} type="button"
phx-click="open_sidebar_item" title={item.title}
phx-value-route={item.route} phx-click="open_sidebar_item"
phx-value-id={item.id} phx-value-route={item.route}
phx-value-title={item.title} phx-value-id={item.id}
phx-value-subtitle={item.meta} phx-value-title={item.title}
> phx-value-subtitle={item.meta}
<span class={media_thumbnail_class(item)}> >
<%= if image_media?(item) do %> <span class={media_thumbnail_class(item)}>
<span class="media-thumbnail-fallback"><%= media_thumbnail_glyph(item.mime_type) %></span> <%= if image_media?(item) do %>
<img class="media-thumbnail-image" src={"/media-thumbnail/#{item.id}"} alt="" loading="lazy" decoding="async" /> <span class="media-thumbnail-fallback"><%= media_thumbnail_glyph(item.mime_type) %></span>
<% else %> <img class="media-thumbnail-image" src={"/media-thumbnail/#{item.id}"} alt="" loading="lazy" decoding="async" />
<span class="media-thumbnail-fallback"><%= media_thumbnail_glyph(item.mime_type) %></span> <% else %>
<% end %> <span class="media-thumbnail-fallback"><%= media_thumbnail_glyph(item.mime_type) %></span>
</span> <% end %>
<span class="media-item-info"> </span>
<span class="media-item-name"><%= item.title %></span> <span class="media-item-info">
<span class="media-item-size"><%= item.meta %></span> <span class="media-item-name"><%= item.title %></span>
</span> <span class="media-item-size"><%= item.meta %></span>
</button> </span>
</button>
<%= if sidebar_deletable?(item.route) do %>
<button
class="sidebar-delete-button"
data-testid={sidebar_delete_testid(item.route)}
data-item-id={item.id}
type="button"
title={sidebar_delete_title(item.route)}
phx-click="confirm_sidebar_delete"
phx-value-route={item.route}
phx-value-id={item.id}
phx-value-title={item.title}
>
×
</button>
<% end %>
</div>
<% end %> <% end %>
</div> </div>
<% else %> <% else %>
@@ -353,7 +387,7 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
<%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %> <%= if Enum.any?(Map.get(@sidebar_data, :items, [])) do %>
<div class={if(template_sidebar?(@sidebar_data), do: "chat-list-items", else: "settings-nav-list")}> <div class={if(template_sidebar?(@sidebar_data), do: "chat-list-items", else: "settings-nav-list")}>
<%= for item <- Map.get(@sidebar_data, :items, []) do %> <%= for item <- Map.get(@sidebar_data, :items, []) do %>
<%= if item.route in ["templates", "chat"] do %> <%= if sidebar_deletable?(item.route) do %>
<div <div
class={["chat-list-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "active")]} class={["chat-list-item", if(sidebar_item_selected?(@workbench, item.route, item.id), do: "active")]}
data-item-id={item.id} data-item-id={item.id}
@@ -378,13 +412,15 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
</span> </span>
</button> </button>
<button <button
class="chat-item-delete" class="sidebar-delete-button"
data-testid={if(item.route == "chat", do: "sidebar-delete-chat", else: "sidebar-delete-template")} data-testid={sidebar_delete_testid(item.route)}
data-item-id={item.id} data-item-id={item.id}
type="button" type="button"
title={if(item.route == "chat", do: translated("sidebar.chat.deleteConversation"), else: translated("Delete") <> " " <> translated("Template"))} title={sidebar_delete_title(item.route)}
phx-click={if(item.route == "chat", do: "delete_sidebar_chat", else: "delete_sidebar_template")} phx-click="confirm_sidebar_delete"
phx-value-route={item.route}
phx-value-id={item.id} phx-value-id={item.id}
phx-value-title={item.title}
> >
× ×
</button> </button>
@@ -466,6 +502,24 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do
defp translated(text, bindings \\ %{}), defp translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
defp sidebar_deletable?(route), do: route in ["post", "media", "scripts", "templates", "chat", "import"]
defp sidebar_delete_testid("post"), do: "sidebar-delete-post"
defp sidebar_delete_testid("media"), do: "sidebar-delete-media"
defp sidebar_delete_testid("scripts"), do: "sidebar-delete-script"
defp sidebar_delete_testid("templates"), do: "sidebar-delete-template"
defp sidebar_delete_testid("chat"), do: "sidebar-delete-chat"
defp sidebar_delete_testid("import"), do: "sidebar-delete-import"
defp sidebar_delete_testid(route), do: "sidebar-delete-#{route}"
defp sidebar_delete_title("chat"), do: translated("sidebar.chat.deleteConversation")
defp sidebar_delete_title("post"), do: translated("Delete") <> " " <> translated("Post")
defp sidebar_delete_title("media"), do: translated("Delete") <> " " <> translated("Media")
defp sidebar_delete_title("scripts"), do: translated("Delete") <> " " <> translated("Script")
defp sidebar_delete_title("templates"), do: translated("Delete") <> " " <> translated("Template")
defp sidebar_delete_title("import"), do: translated("Delete") <> " " <> translated("Import")
defp sidebar_delete_title(_route), do: translated("Delete")
defp template_sidebar?(sidebar_data), do: Map.get(sidebar_data, :title) == "Templates" defp template_sidebar?(sidebar_data), do: Map.get(sidebar_data, :title) == "Templates"
defp group_year_month_counts(entries) do defp group_year_month_counts(entries) do

View File

@@ -6819,6 +6819,11 @@ button svg * {
flex-direction: column; flex-direction: column;
} }
.sidebar-item-row {
display: flex;
align-items: stretch;
}
.sidebar-item { .sidebar-item {
width: 100%; width: 100%;
display: flex; display: flex;
@@ -6921,6 +6926,12 @@ button svg * {
padding: 4px; padding: 4px;
} }
.media-item-row {
display: flex;
align-items: stretch;
gap: 4px;
}
.media-item { .media-item {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -7004,6 +7015,12 @@ button svg * {
color: var(--vscode-descriptionForeground); color: var(--vscode-descriptionForeground);
} }
.sidebar-item-row .sidebar-item,
.media-item-row .media-item {
flex: 1;
min-width: 0;
}
.sidebar-actions { .sidebar-actions {
display: flex; display: flex;
gap: 4px; gap: 4px;
@@ -8197,27 +8214,38 @@ button.import-taxonomy-pill {
font-size: 12px; font-size: 12px;
} }
.chat-item-delete { .sidebar-delete-button {
background: transparent; background: transparent;
border: none; border: none;
color: var(--vscode-descriptionForeground); color: var(--vscode-descriptionForeground);
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 16px;
line-height: 1; line-height: 1;
padding: 0 4px; padding: 0 6px;
flex-shrink: 0;
opacity: 0; opacity: 0;
transition: opacity 0.15s, color 0.15s; transition: opacity 0.15s, color 0.15s;
} }
.chat-list-item:hover .chat-item-delete, .sidebar-item-row:hover .sidebar-delete-button,
.chat-list-item.active .chat-item-delete { .sidebar-item-row .sidebar-item.selected ~ .sidebar-delete-button,
.media-item-row:hover .sidebar-delete-button,
.media-item-row .media-item.selected ~ .sidebar-delete-button,
.chat-list-item:hover .sidebar-delete-button,
.chat-list-item.active .sidebar-delete-button {
opacity: 1; opacity: 1;
} }
.chat-item-delete:hover { .sidebar-delete-button:hover {
color: var(--vscode-errorForeground); color: var(--vscode-errorForeground);
} }
.sidebar-item-row .sidebar-item.selected ~ .sidebar-delete-button,
.media-item-row .media-item.selected ~ .sidebar-delete-button {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}
.settings-nav-list { .settings-nav-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -289,6 +289,12 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='sidebar-delete-chat'][data-item-id='#{created_chat.id}']") |> element("[data-testid='sidebar-delete-chat'][data-item-id='#{created_chat.id}']")
|> render_click() |> render_click()
assert Repo.get(BDS.AI.ChatConversation, created_chat.id)
assert html =~ "confirm-delete-modal"
assert html =~ created_chat.title
html = render_click(view, "overlay_confirm", %{})
refute Repo.get(BDS.AI.ChatConversation, created_chat.id) refute Repo.get(BDS.AI.ChatConversation, created_chat.id)
refute html =~ ~s(data-tab-id="#{created_chat.id}") refute html =~ ~s(data-tab-id="#{created_chat.id}")
@@ -324,6 +330,122 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-settings-scroll-target="settings-section-ai") assert html =~ ~s(data-settings-scroll-target="settings-section-ai")
end end
test "database-backed sidebar entries require confirmation before deletion", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
title: "Sidebar Delete Post",
content: "delete me"
})
media_source_path = Path.join(temp_dir, "sidebar-delete-media.txt")
File.write!(media_source_path, "media body")
assert {:ok, media} =
Media.import_media(%{
project_id: project.id,
source_path: media_source_path,
title: "Sidebar Delete Media"
})
assert {:ok, script} =
Scripts.create_script(%{
project_id: project.id,
title: "Sidebar Delete Script",
kind: :utility,
content: "print(\"delete\")",
entrypoint: "main",
enabled: true
})
assert {:ok, template} =
Templates.create_template(%{
project_id: project.id,
title: "Sidebar Delete Template",
kind: :post,
content: "<article>{{ post.content }}</article>",
enabled: true
})
assert {:ok, conversation} = AI.start_chat(%{title: "Sidebar Delete Chat"})
assert {:ok, definition} =
ImportDefinitions.create_definition(%{
project_id: project.id,
name: "Sidebar Delete Import"
})
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
cases = [
%{
view: "posts",
id: post.id,
title: post.title,
testid: "sidebar-delete-post",
exists?: fn -> Repo.get(Post, post.id) != nil end
},
%{
view: "media",
id: media.id,
title: media.title,
testid: "sidebar-delete-media",
exists?: fn -> Repo.get(BDS.Media.Media, media.id) != nil end
},
%{
view: "scripts",
id: script.id,
title: script.title,
testid: "sidebar-delete-script",
exists?: fn -> Repo.get(BDS.Scripts.Script, script.id) != nil end
},
%{
view: "templates",
id: template.id,
title: template.title,
testid: "sidebar-delete-template",
exists?: fn -> Repo.get(BDS.Templates.Template, template.id) != nil end
},
%{
view: "chat",
id: conversation.id,
title: conversation.title,
testid: "sidebar-delete-chat",
exists?: fn -> Repo.get(BDS.AI.ChatConversation, conversation.id) != nil end
},
%{
view: "import",
id: definition.id,
title: definition.name,
testid: "sidebar-delete-import",
exists?: fn -> Repo.get(ImportDefinitions.ImportDefinition, definition.id) != nil end
}
]
Enum.each(cases, fn sidebar_case ->
html = render_click(view, "select_view", %{"view" => sidebar_case.view})
assert html =~ ~s(data-testid="#{sidebar_case.testid}")
html =
view
|> element("[data-testid='#{sidebar_case.testid}'][data-item-id='#{sidebar_case.id}']")
|> render_click()
assert sidebar_case.exists?.()
assert html =~ "confirm-delete-modal"
assert html =~ sidebar_case.title
html = render_click(view, "overlay_confirm", %{})
refute sidebar_case.exists?.()
refute html =~ sidebar_case.title
end)
end
test "shell live refreshes the posts sidebar when the CLI watcher broadcasts an entity change", test "shell live refreshes the posts sidebar when the CLI watcher broadcasts an entity change",
%{project: project} do %{project: project} do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) {:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
@@ -3292,6 +3414,12 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='sidebar-delete-template'][data-item-id='#{template.id}']") |> element("[data-testid='sidebar-delete-template'][data-item-id='#{template.id}']")
|> render_click() |> render_click()
assert BDS.Repo.get(BDS.Templates.Template, template.id)
assert html =~ "confirm-delete-modal"
assert html =~ template.title
html = render_click(view, "overlay_confirm", %{})
assert BDS.Repo.get(BDS.Templates.Template, template.id) == nil assert BDS.Repo.get(BDS.Templates.Template, template.id) == nil
refute html =~ "Sidebar Template" refute html =~ "Sidebar Template"
refute html =~ ~s(data-tab-type="templates") refute html =~ ~s(data-tab-type="templates")