fix: fixed CSM-017

This commit is contained in:
2026-05-09 17:33:51 +02:00
parent e4452ca504
commit 5c17751d55
15 changed files with 463 additions and 301 deletions

View File

@@ -282,23 +282,26 @@
--- ---
### CSM-017 — `send(self(), ...)` Component Chatter ### ~~CSM-017 — `send(self(), ...)` Component Chatter~~ ✅ FIXED
- **Files:** 25+ call sites across editor components: - **Fixed:** 2026-05-09
- `lib/bds/desktop/shell_live/script_editor.ex` (3 sends) - **What was done:**
- `lib/bds/desktop/shell_live/post_editor.ex` (2 sends) - Created `BDS.Desktop.ShellLive.Notify` — a single dispatch module that standardizes all parent communication from LiveComponent editors. Provides typed functions: `output/3`, `output/4`, `tab_meta/4`, `tab_meta_merge/3`, `close_tab/2`, `reload/0`, `dirty/3`, `command/2`, `open_sidebar_item/2`, and `parent/1` (escape hatch for chat-specific messages).
- `lib/bds/desktop/shell_live/template_editor.ex` (3 sends) - Replaced all 25+ `send(self(), ...)` calls across 11 editor components with `Notify.*` calls:
- `lib/bds/desktop/shell_live/media_editor.ex` (2 sends) - `post_editor.ex` — 13 calls (dirty, tab_meta, close_tab, output)
- `lib/bds/desktop/shell_live/chat_editor.ex` (1 send) - `media_editor.ex` — 7 calls (dirty, tab_meta, output)
- `lib/bds/desktop/shell_live/menu_editor.ex` (1 send) - `chat_editor.ex` — 15 calls (output, tab_meta, open_sidebar_item, plus chat-specific via `Notify.parent`)
- `lib/bds/desktop/shell_live/settings_editor.ex` (2 sends) - `template_editor.ex` — 3 calls (close_tab, output, reload)
- `lib/bds/desktop/shell_live/misc_editor.ex` (4 sends) - `script_editor.ex` — 3 calls (close_tab, output, reload)
- `lib/bds/desktop/shell_live/tags_editor.ex` (2 sends) - `misc_editor.ex` — 4 calls (command, output, tab_meta_merge, open_sidebar_item)
- `lib/bds/desktop/shell_live/import_editor.ex` (1 send) - `settings_editor.ex` — 2 calls (output, parent)
- `lib/bds/desktop/shell_live/overlay_manager.ex` (3 sends) - `tags_editor.ex` — 2 calls (output, parent)
- `lib/bds/desktop/main_window.ex` (1 send) - `menu_editor.ex` — 1 call (output)
- **What:** Components send messages to the parent via `send(self(), ...)`, forcing a broad `handle_info` in `ShellLive`. Each message type must be handled in the parent, creating tight coupling. - `import_editor.ex` — 2 calls (tab_meta, output)
- **Fix:** Prefer `Phoenix.LiveView.send_update/2` for targeted component updates, or delegate through a single dispatch module that translates actions into specific state changes. - `overlay_manager.ex` — 3 calls (parent for cross-component routing)
- **Test:** Refactor one component; assert it no longer uses `send(self(), ...)`. - Consolidated Bridges from 30+ editor-specific `handle_info` clauses to 4 generic handlers: `{:editor_output, ...}`, `{:editor_tab_meta, ...}`, `{:editor_dirty, ...}`, `{:editor_command, ...}`.
- Removed 18 editor-specific message atoms from Bridges (`:post_editor_output`, `:media_editor_output`, `:post_editor_dirty`, `:media_editor_dirty`, `:post_editor_tab_meta`, etc.).
- Kept chat-specific messages (`{:chat_editor_task_started, ...}`, `{:chat_editor_toggle_sidebar}`, etc.) and cross-component routing (`{:post_editor_insert_content, ...}`) in Bridges since they originate from AI streaming or overlay actions, not from editor self-notification.
- Added 24 tests in `test/bds/csm017_component_chatter_test.exs`: 11 source-level tests asserting no `send(self(), ...)` in any editor file, 1 aggregate test verifying all shell_live `send(self(), ...)` calls are in `notify.ex`, 2 Bridges tests verifying old patterns are gone and new generic handlers exist, 10 Notify API tests verifying each function sends the correct message.
--- ---

View File

@@ -9,25 +9,73 @@ defmodule BDS.Desktop.ShellLive.Bridges do
alias BDS.Desktop.ShellLive.{CliSync, SessionUtil} alias BDS.Desktop.ShellLive.{CliSync, SessionUtil}
alias BDS.UI.Workbench alias BDS.UI.Workbench
@refreshable_tab_meta_types [:import, :chat]
@spec handle_info(tuple() | atom(), Phoenix.LiveView.Socket.t(), map()) :: @spec handle_info(tuple() | atom(), Phoenix.LiveView.Socket.t(), map()) ::
{:noreply, Phoenix.LiveView.Socket.t()} {:noreply, Phoenix.LiveView.Socket.t()}
def handle_info({:import_editor_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)} # ── Generic editor notifications (sent via Notify module) ────────────────
def handle_info({:editor_output, title, message, detail, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, detail, level)}
end end
def handle_info({:import_editor_tab_meta, definition_id, title, subtitle}, socket, callbacks) do def handle_info({:editor_tab_meta, type, id, updates}, socket, callbacks)
tab_meta = when is_atom(type) and is_map(updates) do
Map.put(socket.assigns.tab_meta, {:import, definition_id}, %{ key = {type, id}
title: title, current_meta = Map.get(socket.assigns.tab_meta, key, %{})
subtitle: subtitle || "" next_meta = Map.merge(current_meta, updates)
}) tab_meta = Map.put(socket.assigns.tab_meta, key, next_meta)
socket = assign(socket, :tab_meta, tab_meta)
if type in @refreshable_tab_meta_types do
{:noreply, callbacks.refresh_sidebar.(socket, socket.assigns.workbench)}
else
{:noreply, socket}
end
end
def handle_info({:editor_dirty, type, id, dirty?}, socket, _callbacks) do
workbench =
if dirty? do
Workbench.mark_dirty(socket.assigns.workbench, type, id)
else
Workbench.clear_dirty(socket.assigns.workbench, type, id)
end
{:noreply, assign(socket, :workbench, workbench)}
end
def handle_info({:editor_command, action, params}, socket, callbacks) do
{:noreply, callbacks.apply_shell_command.(socket, action, params)}
end
# ── Shared actions (already generic) ─────────────────────────────────────
def handle_info({:open_sidebar_item, params, intent}, socket, callbacks) do
{:noreply, callbacks.open_sidebar.(socket, params, intent)}
end
def handle_info(:reload_shell, socket, callbacks) do
{:noreply, callbacks.reload.(socket, socket.assigns.workbench)}
end
def handle_info({:close_tab, type, id}, socket, callbacks) do
{:noreply, {:noreply,
socket callbacks.refresh_layout.(socket, Workbench.close_tab(socket.assigns.workbench, type, id))}
|> assign(:tab_meta, tab_meta)
|> callbacks.refresh_sidebar.(socket.assigns.workbench)}
end end
def handle_info(:tags_changed, socket, callbacks) do
{:noreply, callbacks.refresh_content.(socket, socket.assigns.workbench)}
end
def handle_info(:settings_changed, socket, callbacks) do
{:noreply, callbacks.reload.(socket, socket.assigns.workbench)}
end
# ── Chat editor messages (sent from AI streaming, not from Notify) ──────
def handle_info({:chat_tool_call, conversation_id, tool_call}, socket, _callbacks) do def handle_info({:chat_tool_call, conversation_id, tool_call}, socket, _callbacks) do
send_update(ChatEditor, send_update(ChatEditor,
id: "chat-editor-#{conversation_id}", id: "chat-editor-#{conversation_id}",
@@ -68,27 +116,6 @@ defmodule BDS.Desktop.ShellLive.Bridges do
{:noreply, assign(socket, :chat_editor_request_refs, refs)} {:noreply, assign(socket, :chat_editor_request_refs, refs)}
end end
def handle_info({:chat_editor_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info({:chat_editor_tab_meta, conversation_id, title, subtitle}, socket, callbacks) do
tab_meta =
Map.put(socket.assigns.tab_meta, {:chat, conversation_id}, %{
title: title,
subtitle: subtitle || ""
})
{:noreply,
socket
|> assign(:tab_meta, tab_meta)
|> callbacks.refresh_sidebar.(socket.assigns.workbench)}
end
def handle_info({:open_sidebar_item, params, intent}, socket, callbacks) do
{:noreply, callbacks.open_sidebar.(socket, params, intent)}
end
def handle_info({:chat_editor_toggle_sidebar}, socket, callbacks) do def handle_info({:chat_editor_toggle_sidebar}, socket, callbacks) do
{:noreply, {:noreply,
callbacks.refresh_layout.(socket, Workbench.toggle_sidebar(socket.assigns.workbench))} callbacks.refresh_layout.(socket, Workbench.toggle_sidebar(socket.assigns.workbench))}
@@ -112,6 +139,35 @@ defmodule BDS.Desktop.ShellLive.Bridges do
callbacks.refresh_sidebar.(socket, Workbench.click_activity(socket.assigns.workbench, view))} callbacks.refresh_sidebar.(socket, Workbench.click_activity(socket.assigns.workbench, view))}
end end
# ── Post editor cross-component messages (sent from OverlayManager) ─────
def handle_info({:post_editor_insert_content, post_id, content}, socket, _callbacks) do
send_update(PostEditor,
id: "post-editor-#{post_id}",
action: :insert_content,
content: content
)
{:noreply, socket}
end
def handle_info({:post_editor_translate, post_id, language}, socket, _callbacks) do
send_update(PostEditor, id: "post-editor-#{post_id}", action: :translate, language: language)
{:noreply, socket}
end
def handle_info({:post_editor_apply_ai_suggestions, post_id, fields}, socket, _callbacks) do
send_update(PostEditor,
id: "post-editor-#{post_id}",
action: :apply_ai_suggestions,
fields: fields
)
{:noreply, socket}
end
# ── External system messages ─────────────────────────────────────────────
def handle_info({:entity_changed, payload}, socket, callbacks) when is_map(payload) do def handle_info({:entity_changed, payload}, socket, callbacks) when is_map(payload) do
{:noreply, CliSync.apply_entity_change(socket, payload, callbacks.refresh_content)} {:noreply, CliSync.apply_entity_change(socket, payload, callbacks.refresh_content)}
end end
@@ -155,126 +211,5 @@ defmodule BDS.Desktop.ShellLive.Bridges do
{:noreply, socket} {:noreply, socket}
end end
def handle_info({:tags_editor_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info(:tags_changed, socket, callbacks) do
{:noreply, callbacks.refresh_content.(socket, socket.assigns.workbench)}
end
def handle_info({:settings_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info(:settings_changed, socket, callbacks) do
{:noreply, callbacks.reload.(socket, socket.assigns.workbench)}
end
def handle_info({:menu_editor_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info({:script_editor_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info({:template_editor_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info({:misc_editor_output, title, message, _detail, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info({:misc_editor_command, action, params}, socket, callbacks) do
{:noreply, callbacks.apply_shell_command.(socket, action, params)}
end
def handle_info({:misc_editor_tab_meta, tab_type, tab_id, updates}, socket, _callbacks) do
key = {tab_type, tab_id}
current_meta = Map.get(socket.assigns.tab_meta, key, %{})
next_meta = Map.merge(current_meta, updates)
{:noreply, assign(socket, :tab_meta, Map.put(socket.assigns.tab_meta, key, next_meta))}
end
def handle_info({:post_editor_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info({:post_editor_dirty, post_id, dirty?}, socket, _callbacks) do
workbench =
if dirty? do
Workbench.mark_dirty(socket.assigns.workbench, :post, post_id)
else
Workbench.clear_dirty(socket.assigns.workbench, :post, post_id)
end
{:noreply, assign(socket, :workbench, workbench)}
end
def handle_info({:post_editor_tab_meta, post_id, title, subtitle}, socket, _callbacks) do
tab_meta =
Map.put(socket.assigns.tab_meta, {:post, post_id}, %{title: title, subtitle: subtitle})
{:noreply, assign(socket, :tab_meta, tab_meta)}
end
def handle_info({:post_editor_insert_content, post_id, content}, socket, _callbacks) do
send_update(PostEditor,
id: "post-editor-#{post_id}",
action: :insert_content,
content: content
)
{:noreply, socket}
end
def handle_info({:post_editor_translate, post_id, language}, socket, _callbacks) do
send_update(PostEditor, id: "post-editor-#{post_id}", action: :translate, language: language)
{:noreply, socket}
end
def handle_info({:post_editor_apply_ai_suggestions, post_id, fields}, socket, _callbacks) do
send_update(PostEditor,
id: "post-editor-#{post_id}",
action: :apply_ai_suggestions,
fields: fields
)
{:noreply, socket}
end
def handle_info({:media_editor_output, title, message, level}, socket, callbacks) do
{:noreply, callbacks.append_output.(socket, title, message, nil, level)}
end
def handle_info({:media_editor_dirty, media_id, dirty?}, socket, _callbacks) do
workbench =
if dirty? do
Workbench.mark_dirty(socket.assigns.workbench, :media, media_id)
else
Workbench.clear_dirty(socket.assigns.workbench, :media, media_id)
end
{:noreply, assign(socket, :workbench, workbench)}
end
def handle_info({:media_editor_tab_meta, media_id, title, subtitle}, socket, _callbacks) do
tab_meta =
Map.put(socket.assigns.tab_meta, {:media, media_id}, %{title: title, subtitle: subtitle})
{:noreply, assign(socket, :tab_meta, tab_meta)}
end
def handle_info(:reload_shell, socket, callbacks) do
{:noreply, callbacks.reload.(socket, socket.assigns.workbench)}
end
def handle_info({:close_tab, type, id}, socket, callbacks) do
{:noreply,
callbacks.refresh_layout.(socket, Workbench.close_tab(socket.assigns.workbench, type, id))}
end
def handle_info(_message, socket, _callbacks), do: {:noreply, socket} def handle_info(_message, socket, _callbacks), do: {:noreply, socket}
end end

View File

@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
alias BDS.{AI, BoundedAtoms, MapUtils, Persistence} alias BDS.{AI, BoundedAtoms, MapUtils, Persistence}
alias BDS.Desktop.ShellLive.ChatEditor.{MessageBuild, ModelSelection, ToolTracking} alias BDS.Desktop.ShellLive.ChatEditor.{MessageBuild, ModelSelection, ToolTracking}
alias BDS.Desktop.ShellLive.Notify
alias BDS.Desktop.ShellLive.TabHelpers alias BDS.Desktop.ShellLive.TabHelpers
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
@@ -77,7 +78,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
{:noreply, assign(socket, :model_selector_open?, false) |> build_data()} {:noreply, assign(socket, :model_selector_open?, false) |> build_data()}
{:error, reason} -> {:error, reason} ->
notify_parent({:chat_editor_output, dgettext("ui", "Chat"), inspect(reason), "error"}) Notify.output(dgettext("ui", "Chat"), inspect(reason), "error")
{:noreply, assign(socket, :model_selector_open?, false) |> build_data()} {:noreply, assign(socket, :model_selector_open?, false) |> build_data()}
end end
end end
@@ -129,14 +130,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
end end
def handle_event("open_chat_settings", _params, socket) do def handle_event("open_chat_settings", _params, socket) do
notify_parent( Notify.open_sidebar_item(
{:open_sidebar_item,
%{ %{
"route" => "settings", "route" => "settings",
"id" => "settings-ai", "id" => "settings-ai",
"title" => "Settings", "title" => "Settings",
"subtitle" => "AI" "subtitle" => "AI"
}, :pin} },
:pin
) )
{:noreply, socket} {:noreply, socket}
@@ -203,10 +204,8 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
build_data(socket) build_data(socket)
socket.assigns.offline_mode -> socket.assigns.offline_mode ->
notify_parent( Notify.output(dgettext("ui", "Chat"),
{:chat_editor_output, dgettext("ui", "Chat"), dgettext("ui", "Automatic AI actions stay gated by airplane mode."), "info")
dgettext("ui", "Automatic AI actions stay gated by airplane mode."), "info"}
)
build_data(socket) build_data(socket)
@@ -227,7 +226,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
:ok = allow_repo_sandbox(task.pid) :ok = allow_repo_sandbox(task.pid)
notify_parent({:chat_editor_task_started, conversation_id, task.ref}) Notify.parent({:chat_editor_task_started, conversation_id, task.ref})
socket socket
|> assign(:input, "") |> assign(:input, "")
@@ -254,7 +253,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
%{ref: ref} = _request -> %{ref: ref} = _request ->
:ok = AI.cancel_chat(conversation_id) :ok = AI.cancel_chat(conversation_id)
notify_parent({:chat_editor_task_cancelled, conversation_id, ref}) Notify.parent({:chat_editor_task_cancelled, conversation_id, ref})
socket socket
|> assign(:request, nil) |> assign(:request, nil)
@@ -293,9 +292,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
assign(socket, :request, nil) |> build_data() assign(socket, :request, nil) |> build_data()
{:error, reason} -> {:error, reason} ->
notify_parent( Notify.output(dgettext("ui", "Chat"), format_error(reason), "error")
{:chat_editor_output, dgettext("ui", "Chat"), format_error(reason), "error"}
)
assign(socket, :request, nil) |> build_data() assign(socket, :request, nil) |> build_data()
end end
@@ -347,7 +344,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|> MapUtils.attr(:title) |> MapUtils.attr(:title)
if is_binary(title) and String.trim(title) != "" do if is_binary(title) and String.trim(title) != "" do
notify_parent({:chat_editor_tab_meta, socket.assigns.conversation_id, title, ""}) Notify.tab_meta(:chat, socket.assigns.conversation_id, title, "")
end end
socket socket
@@ -368,14 +365,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
:open_post -> :open_post ->
case Map.get(payload, "postId") || Map.get(payload, "post_id") do case Map.get(payload, "postId") || Map.get(payload, "post_id") do
post_id when is_binary(post_id) and post_id != "" -> post_id when is_binary(post_id) and post_id != "" ->
notify_parent( Notify.open_sidebar_item(
{:open_sidebar_item,
%{ %{
"route" => "post", "route" => "post",
"id" => post_id, "id" => post_id,
"title" => TabHelpers.post_title(post_id), "title" => TabHelpers.post_title(post_id),
"subtitle" => TabHelpers.post_subtitle(post_id) "subtitle" => TabHelpers.post_subtitle(post_id)
}, :pin} },
:pin
) )
assign(socket, :action_error, nil) |> build_data() assign(socket, :action_error, nil) |> build_data()
@@ -387,14 +384,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
:open_media -> :open_media ->
case Map.get(payload, "mediaId") || Map.get(payload, "media_id") do case Map.get(payload, "mediaId") || Map.get(payload, "media_id") do
media_id when is_binary(media_id) and media_id != "" -> media_id when is_binary(media_id) and media_id != "" ->
notify_parent( Notify.open_sidebar_item(
{:open_sidebar_item,
%{ %{
"route" => "media", "route" => "media",
"id" => media_id, "id" => media_id,
"title" => TabHelpers.media_title(media_id), "title" => TabHelpers.media_title(media_id),
"subtitle" => TabHelpers.media_subtitle(media_id) "subtitle" => TabHelpers.media_subtitle(media_id)
}, :pin} },
:pin
) )
assign(socket, :action_error, nil) |> build_data() assign(socket, :action_error, nil) |> build_data()
@@ -404,14 +401,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
end end
:open_settings -> :open_settings ->
notify_parent( Notify.open_sidebar_item(
{:open_sidebar_item,
%{ %{
"route" => "settings", "route" => "settings",
"id" => "settings-ai", "id" => "settings-ai",
"title" => "Settings", "title" => "Settings",
"subtitle" => "AI" "subtitle" => "AI"
}, :pin} },
:pin
) )
assign(socket, :action_error, nil) |> build_data() assign(socket, :action_error, nil) |> build_data()
@@ -421,14 +418,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
Map.get(payload, "conversationId") || Map.get(payload, "conversation_id") || Map.get(payload, "conversationId") || Map.get(payload, "conversation_id") ||
socket.assigns.conversation_id socket.assigns.conversation_id
notify_parent( Notify.open_sidebar_item(
{:open_sidebar_item,
%{ %{
"route" => "chat", "route" => "chat",
"id" => chat_id, "id" => chat_id,
"title" => Map.get(payload, "title", "Chat"), "title" => Map.get(payload, "title", "Chat"),
"subtitle" => Map.get(payload, "subtitle", "") "subtitle" => Map.get(payload, "subtitle", "")
}, :pin} },
:pin
) )
assign(socket, :action_error, nil) |> build_data() assign(socket, :action_error, nil) |> build_data()
@@ -439,20 +436,20 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
set_action_error(socket, "Invalid payload for switchView action") set_action_error(socket, "Invalid payload for switchView action")
view -> view ->
notify_parent({:chat_editor_switch_view, view}) Notify.parent({:chat_editor_switch_view, view})
assign(socket, :action_error, nil) |> build_data() assign(socket, :action_error, nil) |> build_data()
end end
:toggle_sidebar -> :toggle_sidebar ->
notify_parent({:chat_editor_toggle_sidebar}) Notify.parent({:chat_editor_toggle_sidebar})
assign(socket, :action_error, nil) |> build_data() assign(socket, :action_error, nil) |> build_data()
:toggle_panel -> :toggle_panel ->
notify_parent({:chat_editor_toggle_panel}) Notify.parent({:chat_editor_toggle_panel})
assign(socket, :action_error, nil) |> build_data() assign(socket, :action_error, nil) |> build_data()
:toggle_assistant_sidebar -> :toggle_assistant_sidebar ->
notify_parent({:chat_editor_toggle_assistant_sidebar}) Notify.parent({:chat_editor_toggle_assistant_sidebar})
assign(socket, :action_error, nil) |> build_data() assign(socket, :action_error, nil) |> build_data()
:unknown -> :unknown ->
@@ -822,9 +819,6 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
# ── Private helpers ─────────────────────────────────────────────────────── # ── Private helpers ───────────────────────────────────────────────────────
defp notify_parent(message) do
send(self(), message)
end
defp active_project_id(socket) do defp active_project_id(socket) do
socket.assigns[:project_id] socket.assigns[:project_id]

View File

@@ -5,6 +5,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
alias BDS.{AI, ImportAnalysis, ImportDefinitions, ImportExecution} alias BDS.{AI, ImportAnalysis, ImportDefinitions, ImportExecution}
alias BDS.Desktop.{FilePicker, FolderPicker, ShellData} alias BDS.Desktop.{FilePicker, FolderPicker, ShellData}
alias BDS.Desktop.ShellLive.Notify
alias BDS.Desktop.ShellLive.ImportEditor.{ alias BDS.Desktop.ShellLive.ImportEditor.{
AnalysisState, AnalysisState,
@@ -641,12 +642,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
defp maybe_update_tab_meta(socket, name) do defp maybe_update_tab_meta(socket, name) do
title = name || dgettext("ui", "Untitled Import") title = name || dgettext("ui", "Untitled Import")
notify_parent( Notify.tab_meta(:import, socket.assigns.definition_id, title,
{:import_editor_tab_meta, socket.assigns.definition_id, title,
dgettext( dgettext(
"ui", "ui",
"Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported." "Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported."
)} )
) )
socket socket
@@ -1404,12 +1404,8 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
# ── Private helpers ─────────────────────────────────────────────────────── # ── Private helpers ───────────────────────────────────────────────────────
defp notify_parent(message) do
send(self(), message)
end
defp notify_output(socket, title, message, level \\ "info") do defp notify_output(socket, title, message, level \\ "info") do
notify_parent({:import_editor_output, title, message, level}) Notify.output(title, message, level)
socket socket
end end

View File

@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
import Ecto.Query import Ecto.Query
alias BDS.Desktop.{FilePicker} alias BDS.Desktop.{FilePicker}
alias BDS.Desktop.ShellLive.Notify
alias BDS.{AI, I18n, Media} alias BDS.{AI, I18n, Media}
alias BDS.Media.Media, as: MediaRecord alias BDS.Media.Media, as: MediaRecord
alias BDS.Media.Translation alias BDS.Media.Translation
@@ -90,7 +91,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|> build_data() |> build_data()
if dirty? != was_dirty? do if dirty? != was_dirty? do
notify_parent({:media_editor_dirty, socket.assigns.media_id, dirty?}) Notify.dirty(:media, socket.assigns.media_id, dirty?)
end end
{:noreply, socket} {:noreply, socket}
@@ -123,12 +124,10 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|> assign(:dirty?, false) |> assign(:dirty?, false)
|> build_data() |> build_data()
notify_parent({:media_editor_dirty, media.id, false}) Notify.dirty(:media, media.id, false)
notify_parent( Notify.tab_meta(:media, media.id, display_title(updated_media),
{:media_editor_tab_meta, media.id, display_title(updated_media), updated_media.original_name || updated_media.mime_type || "")
updated_media.original_name || updated_media.mime_type || ""}
)
{:noreply, socket} {:noreply, socket}
@@ -483,12 +482,10 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|> assign(:save_state, :saved) |> assign(:save_state, :saved)
|> build_data() |> build_data()
notify_parent({:media_editor_dirty, media.id, false}) Notify.dirty(:media, media.id, false)
notify_parent( Notify.tab_meta(:media, media.id, display_title(updated_media),
{:media_editor_tab_meta, media.id, display_title(updated_media), updated_media.original_name || updated_media.mime_type || "")
updated_media.original_name || updated_media.mime_type || ""}
)
notify_output(socket, dgettext("ui", "Media"), dgettext("ui", "Media saved")) notify_output(socket, dgettext("ui", "Media"), dgettext("ui", "Media saved"))
socket socket
@@ -528,7 +525,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|> assign(:quick_actions_open?, false) |> assign(:quick_actions_open?, false)
|> build_data() |> build_data()
notify_parent({:media_editor_dirty, media.id, dirty?}) Notify.dirty(:media, media.id, dirty?)
socket socket
end end
end end
@@ -569,12 +566,8 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
end end
end end
defp notify_parent(message) do
send(self(), message)
end
defp notify_output(socket, title, message, level \\ "info") do defp notify_output(socket, title, message, level \\ "info") do
send(self(), {:media_editor_output, title, message, level}) Notify.output(title, message, level)
socket socket
end end

View File

@@ -5,6 +5,8 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
alias BDS.Desktop.ShellLive.Notify
alias BDS.Desktop.ShellLive.MenuEditor.{ alias BDS.Desktop.ShellLive.MenuEditor.{
DraftManagement, DraftManagement,
PageCategory, PageCategory,
@@ -251,7 +253,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
end end
defp notify_output(title, message, level) do defp notify_output(title, message, level) do
send(self(), {:menu_editor_output, title, message, level}) Notify.output(title, message, level)
end end
attr(:menu_editor, :map, required: true) attr(:menu_editor, :map, required: true)

View File

@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
import Ecto.Query import Ecto.Query
alias BDS.{Embeddings, Generation, Git, HelpDocs, Posts, Repo} alias BDS.{Embeddings, Generation, Git, HelpDocs, Posts, Repo}
alias BDS.Desktop.ShellLive.Notify
alias BDS.MapUtils alias BDS.MapUtils
alias BDS.Settings.Setting alias BDS.Settings.Setting
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
@@ -358,19 +359,19 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
# ── Private helpers ──────────────────────────────────────────────────────── # ── Private helpers ────────────────────────────────────────────────────────
defp notify_command(action, params \\ %{}) do defp notify_command(action, params \\ %{}) do
send(self(), {:misc_editor_command, action, params}) Notify.command(action, params)
end end
defp notify_output(title, message, detail \\ nil, level \\ "info") do defp notify_output(title, message, detail \\ nil, level \\ "info") do
send(self(), {:misc_editor_output, title, message, detail, level}) Notify.output(title, message, detail, level)
end end
defp notify_tab_meta(tab_type, tab_id, updates) do defp notify_tab_meta(tab_type, tab_id, updates) do
send(self(), {:misc_editor_tab_meta, tab_type, tab_id, updates}) Notify.tab_meta_merge(tab_type, tab_id, updates)
end end
defp notify_open_sidebar_item(params, intent) do defp notify_open_sidebar_item(params, intent) do
send(self(), {:open_sidebar_item, params, intent}) Notify.open_sidebar_item(params, intent)
end end
defp rerun_action(assigns) do defp rerun_action(assigns) do

View File

@@ -0,0 +1,70 @@
defmodule BDS.Desktop.ShellLive.Notify do
@moduledoc """
Standardized parent notification API for LiveComponent editors.
Instead of each editor defining its own `notify_parent/1` and sending
editor-specific message atoms (e.g. `{:post_editor_output, ...}`),
all editors call functions from this module, which sends generic
messages that Bridges handles with a single clause per action type.
"""
@spec output(String.t(), String.t(), String.t()) :: :ok
def output(title, message, level) do
send(self(), {:editor_output, title, message, nil, level})
:ok
end
@spec output(String.t(), String.t(), String.t() | nil, String.t()) :: :ok
def output(title, message, detail, level) do
send(self(), {:editor_output, title, message, detail, level})
:ok
end
@spec tab_meta(atom(), term(), String.t(), String.t()) :: :ok
def tab_meta(type, id, title, subtitle) do
send(self(), {:editor_tab_meta, type, id, %{title: title, subtitle: subtitle || ""}})
:ok
end
@spec tab_meta_merge(atom(), term(), map()) :: :ok
def tab_meta_merge(type, id, updates) when is_map(updates) do
send(self(), {:editor_tab_meta, type, id, updates})
:ok
end
@spec close_tab(atom(), term()) :: :ok
def close_tab(type, id) do
send(self(), {:close_tab, type, id})
:ok
end
@spec reload :: :ok
def reload do
send(self(), :reload_shell)
:ok
end
@spec dirty(atom(), term(), boolean()) :: :ok
def dirty(type, id, dirty?) do
send(self(), {:editor_dirty, type, id, dirty?})
:ok
end
@spec command(atom() | String.t(), map()) :: :ok
def command(action, params \\ %{}) do
send(self(), {:editor_command, action, params})
:ok
end
@spec open_sidebar_item(map(), atom() | nil) :: :ok
def open_sidebar_item(params, intent \\ nil) do
send(self(), {:open_sidebar_item, params, intent})
:ok
end
@spec parent(term()) :: :ok
def parent(message) do
send(self(), message)
:ok
end
end

View File

@@ -11,6 +11,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
alias BDS.Desktop.ShellLive.{ alias BDS.Desktop.ShellLive.{
MediaEditor, MediaEditor,
Notify,
PostEditor, PostEditor,
TabHelpers TabHelpers
} }
@@ -170,7 +171,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
"[#{result.original_name}](bds-media://#{result.media_id})" "[#{result.original_name}](bds-media://#{result.media_id})"
end end
send(self(), {:post_editor_insert_content, post_id, syntax}) Notify.parent({:post_editor_insert_content, post_id, syntax})
socket socket
end end
@@ -195,7 +196,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
end end
if details do if details do
send(self(), {:post_editor_insert_content, post_id, details}) Notify.parent({:post_editor_insert_content, post_id, details})
end end
socket socket
@@ -213,7 +214,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
socket = socket =
case {socket.assigns[:shell_overlay], current_tab} do case {socket.assigns[:shell_overlay], current_tab} do
{%{kind: :language_picker}, %{type: :post, id: post_id}} -> {%{kind: :language_picker}, %{type: :post, id: post_id}} ->
send(self(), {:post_editor_translate, post_id, code}) Notify.parent({:post_editor_translate, post_id, code})
socket socket
{%{kind: :language_picker}, %{type: :media, id: media_id}} -> {%{kind: :language_picker}, %{type: :media, id: media_id}} ->

View File

@@ -5,6 +5,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
alias BDS.{AI, Posts, Preview} alias BDS.{AI, Posts, Preview}
alias BDS.Desktop.ShellData alias BDS.Desktop.ShellData
alias BDS.Desktop.ShellLive.Notify
alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, ListValues, Persistence, PostMetadata} alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, ListValues, Persistence, PostMetadata}
alias BDS.Posts.Post alias BDS.Posts.Post
alias BDS.Tags alias BDS.Tags
@@ -181,7 +182,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> build_data() |> build_data()
if dirty? != was_dirty? do if dirty? != was_dirty? do
notify_parent({:post_editor_dirty, post_id, dirty?}) Notify.dirty(:post, post_id, dirty?)
end end
{:noreply, socket} {:noreply, socket}
@@ -456,12 +457,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> assign(:dirty?, false) |> assign(:dirty?, false)
|> build_data() |> build_data()
notify_parent( Notify.tab_meta(:post, post.id, record_title(record, refreshed_post),
{:post_editor_tab_meta, post.id, record_title(record, refreshed_post), Atom.to_string(record_status(record)))
Atom.to_string(record_status(record))}
)
notify_parent({:post_editor_dirty, post.id, false}) Notify.dirty(:post, post.id, false)
notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post saved")) notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post saved"))
socket socket
@@ -497,12 +496,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> assign(:dirty?, false) |> assign(:dirty?, false)
|> build_data() |> build_data()
notify_parent( Notify.tab_meta(:post, post.id, record_title(record, refreshed_post),
{:post_editor_tab_meta, post.id, record_title(record, refreshed_post), Atom.to_string(record_status(record)))
Atom.to_string(record_status(record))}
)
notify_parent({:post_editor_dirty, post.id, false}) Notify.dirty(:post, post.id, false)
notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post published")) notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post published"))
socket socket
@@ -534,13 +531,11 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> assign(:dirty?, false) |> assign(:dirty?, false)
|> build_data() |> build_data()
notify_parent( Notify.tab_meta(:post, post.id,
{:post_editor_tab_meta, post.id,
restored_post.title || restored_post.slug || restored_post.id, restored_post.title || restored_post.slug || restored_post.id,
Atom.to_string(restored_post.status || :draft)} Atom.to_string(restored_post.status || :draft))
)
notify_parent({:post_editor_dirty, post.id, false}) Notify.dirty(:post, post.id, false)
socket socket
{:error, reason} -> {:error, reason} ->
@@ -555,7 +550,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
case Posts.delete_post(post_id) do case Posts.delete_post(post_id) do
{:ok, :deleted} -> {:ok, :deleted} ->
notify_parent({:close_tab, :post, post_id}) Notify.close_tab(:post, post_id)
socket socket
{:error, reason} -> {:error, reason} ->
@@ -642,7 +637,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> assign(:quick_actions_open?, false) |> assign(:quick_actions_open?, false)
|> build_data() |> build_data()
notify_parent({:post_editor_dirty, post_id, false}) Notify.dirty(:post, post_id, false)
socket socket
else else
{:error, reason} -> {:error, reason} ->
@@ -698,7 +693,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> assign(:shell_overlay, nil) |> assign(:shell_overlay, nil)
|> build_data() |> build_data()
notify_parent({:post_editor_dirty, post_id, true}) Notify.dirty(:post, post_id, true)
socket socket
{:error, reason} -> {:error, reason} ->
@@ -741,7 +736,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> put_component_draft_field(field_key(kind), updated) |> put_component_draft_field(field_key(kind), updated)
|> build_data() |> build_data()
notify_parent({:post_editor_dirty, socket.assigns.post_id, true}) Notify.dirty(:post, socket.assigns.post_id, true)
assign(socket, :dirty?, true) assign(socket, :dirty?, true)
end end
end end
@@ -771,7 +766,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|> put_component_draft_field(field_key(kind), updated) |> put_component_draft_field(field_key(kind), updated)
|> build_data() |> build_data()
notify_parent({:post_editor_dirty, socket.assigns.post_id, true}) Notify.dirty(:post, socket.assigns.post_id, true)
assign(socket, :dirty?, true) assign(socket, :dirty?, true)
end end
end end
@@ -807,12 +802,8 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
defp assign_query(socket, :tags, value), do: assign(socket, :tag_query, value) defp assign_query(socket, :tags, value), do: assign(socket, :tag_query, value)
defp assign_query(socket, :categories, value), do: assign(socket, :category_query, value) defp assign_query(socket, :categories, value), do: assign(socket, :category_query, value)
defp notify_parent(message) do
send(self(), message)
end
defp notify_output(socket, title, message, level \\ "info") do defp notify_output(socket, title, message, level \\ "info") do
send(self(), {:post_editor_output, title, message, level}) Notify.output(title, message, level)
socket socket
end end

View File

@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.ScriptEditor do
use Phoenix.LiveComponent use Phoenix.LiveComponent
alias BDS.{Scripts, Scripting} alias BDS.{Scripts, Scripting}
alias BDS.Desktop.ShellLive.Notify
alias BDS.Scripts.Script alias BDS.Scripts.Script
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
@@ -225,7 +226,7 @@ defmodule BDS.Desktop.ShellLive.ScriptEditor do
case Scripts.delete_script(script_id) do case Scripts.delete_script(script_id) do
{:ok, _deleted} -> {:ok, _deleted} ->
send(self(), {:close_tab, :scripts, script_id}) Notify.close_tab(:scripts, script_id)
socket socket
{:error, reason} -> {:error, reason} ->
@@ -282,12 +283,12 @@ defmodule BDS.Desktop.ShellLive.ScriptEditor do
end end
defp notify_output(socket, title, message, level \\ "info") do defp notify_output(socket, title, message, level \\ "info") do
send(self(), {:script_editor_output, title, message, level}) Notify.output(title, message, level)
socket socket
end end
defp notify_reload(socket) do defp notify_reload(socket) do
send(self(), :reload_shell) Notify.reload()
socket socket
end end
end end

View File

@@ -12,6 +12,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
alias BDS.Desktop.ShellLive.SettingsEditor.AISettings alias BDS.Desktop.ShellLive.SettingsEditor.AISettings
alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings
alias BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories alias BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories
alias BDS.Desktop.ShellLive.Notify
alias BDS.Desktop.ShellLive.SettingsEditor.MCPConfig alias BDS.Desktop.ShellLive.SettingsEditor.MCPConfig
alias BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings alias BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings
alias BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings alias BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings
@@ -308,13 +309,13 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
defp append_output_callback do defp append_output_callback do
fn socket, title, message, _details, level -> fn socket, title, message, _details, level ->
send(self(), {:settings_output, title, message, level}) Notify.output(title, message, level)
socket socket
end end
end end
defp notify_parent(message) do defp notify_parent(message) do
send(self(), message) Notify.parent(message)
end end
defp current_settings_section(assigns) do defp current_settings_section(assigns) do

View File

@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
import Ecto.Query import Ecto.Query
alias BDS.{Repo, Tags} alias BDS.{Repo, Tags}
alias BDS.Desktop.ShellLive.Notify
alias BDS.Posts.Post alias BDS.Posts.Post
alias BDS.Tags.Tag alias BDS.Tags.Tag
alias BDS.Templates.Template alias BDS.Templates.Template
@@ -292,11 +293,11 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
defp noreply(socket), do: {:noreply, socket} defp noreply(socket), do: {:noreply, socket}
defp notify_parent(message) do defp notify_parent(message) do
send(self(), message) Notify.parent(message)
end end
defp notify_output(title, message, level) do defp notify_output(title, message, level) do
send(self(), {:tags_editor_output, title, message, level}) Notify.output(title, message, level)
end end
@spec tag_font_size(term(), term()) :: term() @spec tag_font_size(term(), term()) :: term()

View File

@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.TemplateEditor do
use Phoenix.LiveComponent use Phoenix.LiveComponent
alias BDS.{MCP, Templates} alias BDS.{MCP, Templates}
alias BDS.Desktop.ShellLive.Notify
alias BDS.Templates.Template alias BDS.Templates.Template
use Gettext, backend: BDS.Gettext use Gettext, backend: BDS.Gettext
@@ -182,7 +183,7 @@ defmodule BDS.Desktop.ShellLive.TemplateEditor do
case Templates.delete_template(template_id, force: true) do case Templates.delete_template(template_id, force: true) do
{:ok, _deleted} -> {:ok, _deleted} ->
send(self(), {:close_tab, :templates, template_id}) Notify.close_tab(:templates, template_id)
socket socket
{:error, reason} -> {:error, reason} ->
@@ -231,12 +232,12 @@ defmodule BDS.Desktop.ShellLive.TemplateEditor do
defp normalize_template_kind(_kind), do: :post defp normalize_template_kind(_kind), do: :post
defp notify_output(socket, title, message, level \\ "info") do defp notify_output(socket, title, message, level \\ "info") do
send(self(), {:template_editor_output, title, message, level}) Notify.output(title, message, level)
socket socket
end end
defp notify_reload(socket) do defp notify_reload(socket) do
send(self(), :reload_shell) Notify.reload()
socket socket
end end
end end

View File

@@ -0,0 +1,172 @@
defmodule BDS.CSM017ComponentChatterTest do
use ExUnit.Case, async: true
@editor_files [
"lib/bds/desktop/shell_live/post_editor.ex",
"lib/bds/desktop/shell_live/media_editor.ex",
"lib/bds/desktop/shell_live/template_editor.ex",
"lib/bds/desktop/shell_live/script_editor.ex",
"lib/bds/desktop/shell_live/chat_editor.ex",
"lib/bds/desktop/shell_live/settings_editor.ex",
"lib/bds/desktop/shell_live/menu_editor.ex",
"lib/bds/desktop/shell_live/tags_editor.ex",
"lib/bds/desktop/shell_live/misc_editor.ex",
"lib/bds/desktop/shell_live/import_editor.ex",
"lib/bds/desktop/shell_live/overlay_manager.ex"
]
describe "no direct send(self(), ...) in editor components" do
for file <- @editor_files do
test "#{Path.basename(file)} uses Notify instead of send(self(), ...)" do
path = Path.join(File.cwd!(), unquote(file))
content = File.read!(path)
lines =
content
|> String.split("\n")
|> Enum.with_index(1)
|> Enum.filter(fn {line, _num} ->
String.contains?(line, "send(self(),") and
not String.contains?(line, "Process.send_after") and
not String.contains?(line, "# send(self()")
end)
assert lines == [],
"#{unquote(file)} still has direct send(self(), ...) calls at lines: #{inspect(Enum.map(lines, &elem(&1, 1)))}"
end
end
end
describe "Notify module is the single point of parent communication" do
test "all send(self(), ...) calls in shell_live/ are in notify.ex" do
shell_live_dir = Path.join(File.cwd!(), "lib/bds/desktop/shell_live")
send_self_files =
Path.wildcard(Path.join(shell_live_dir, "*.ex"))
|> Enum.filter(fn path ->
content = File.read!(path)
basename = Path.basename(path)
String.contains?(content, "send(self(),") and
not String.contains?(content, "Process.send_after(self(),") and
basename != "notify.ex" and
basename != "bridges.ex"
end)
|> Enum.reject(fn path ->
content = File.read!(path)
lines =
content
|> String.split("\n")
|> Enum.filter(fn line ->
String.contains?(line, "send(self(),") and
not String.contains?(line, "Process.send_after")
end)
Enum.empty?(lines)
end)
|> Enum.map(&Path.basename/1)
assert send_self_files == [],
"These files still have direct send(self(), ...) calls: #{inspect(send_self_files)}"
end
end
describe "Bridges handles generic message types" do
test "no editor-specific output handlers remain in Bridges" do
bridges_path = Path.join(File.cwd!(), "lib/bds/desktop/shell_live/bridges.ex")
content = File.read!(bridges_path)
old_patterns = [
":import_editor_output",
":chat_editor_output",
":tags_editor_output",
":settings_output",
":menu_editor_output",
":script_editor_output",
":template_editor_output",
":misc_editor_output",
":post_editor_output",
":media_editor_output",
":post_editor_dirty",
":media_editor_dirty",
":post_editor_tab_meta",
":media_editor_tab_meta",
":import_editor_tab_meta",
":chat_editor_tab_meta",
":misc_editor_tab_meta",
":misc_editor_command"
]
remaining =
Enum.filter(old_patterns, fn pattern ->
String.contains?(content, pattern)
end)
assert remaining == [],
"Bridges still contains editor-specific handlers: #{inspect(remaining)}"
end
test "Bridges has generic editor_output handler" do
bridges_path = Path.join(File.cwd!(), "lib/bds/desktop/shell_live/bridges.ex")
content = File.read!(bridges_path)
assert String.contains?(content, ":editor_output")
assert String.contains?(content, ":editor_tab_meta")
assert String.contains?(content, ":editor_dirty")
assert String.contains?(content, ":editor_command")
end
end
describe "Notify module API" do
test "output/3 sends {:editor_output, ...} message" do
BDS.Desktop.ShellLive.Notify.output("Title", "Message", "info")
assert_received {:editor_output, "Title", "Message", nil, "info"}
end
test "output/4 sends {:editor_output, ...} with detail" do
BDS.Desktop.ShellLive.Notify.output("Title", "Msg", "detail", "warning")
assert_received {:editor_output, "Title", "Msg", "detail", "warning"}
end
test "tab_meta/4 sends {:editor_tab_meta, ...}" do
BDS.Desktop.ShellLive.Notify.tab_meta(:post, "abc", "Title", "Subtitle")
assert_received {:editor_tab_meta, :post, "abc", %{title: "Title", subtitle: "Subtitle"}}
end
test "tab_meta_merge/3 sends {:editor_tab_meta, ...} with arbitrary updates" do
BDS.Desktop.ShellLive.Notify.tab_meta_merge(:misc, "id", %{title: "T"})
assert_received {:editor_tab_meta, :misc, "id", %{title: "T"}}
end
test "close_tab/2 sends {:close_tab, ...}" do
BDS.Desktop.ShellLive.Notify.close_tab(:templates, "t1")
assert_received {:close_tab, :templates, "t1"}
end
test "reload/0 sends :reload_shell" do
BDS.Desktop.ShellLive.Notify.reload()
assert_received :reload_shell
end
test "dirty/3 sends {:editor_dirty, ...}" do
BDS.Desktop.ShellLive.Notify.dirty(:post, "p1", true)
assert_received {:editor_dirty, :post, "p1", true}
end
test "command/2 sends {:editor_command, ...}" do
BDS.Desktop.ShellLive.Notify.command(:generate, %{force: true})
assert_received {:editor_command, :generate, %{force: true}}
end
test "open_sidebar_item/2 sends {:open_sidebar_item, ...}" do
BDS.Desktop.ShellLive.Notify.open_sidebar_item(%{"route" => "post"}, :pin)
assert_received {:open_sidebar_item, %{"route" => "post"}, :pin}
end
test "parent/1 sends arbitrary message" do
BDS.Desktop.ShellLive.Notify.parent({:custom, :message})
assert_received {:custom, :message}
end
end
end