fix: fixed CSM-017
This commit is contained in:
37
CODESMELL.md
37
CODESMELL.md
@@ -282,23 +282,26 @@
|
||||
|
||||
---
|
||||
|
||||
### CSM-017 — `send(self(), ...)` Component Chatter
|
||||
- **Files:** 25+ call sites across editor components:
|
||||
- `lib/bds/desktop/shell_live/script_editor.ex` (3 sends)
|
||||
- `lib/bds/desktop/shell_live/post_editor.ex` (2 sends)
|
||||
- `lib/bds/desktop/shell_live/template_editor.ex` (3 sends)
|
||||
- `lib/bds/desktop/shell_live/media_editor.ex` (2 sends)
|
||||
- `lib/bds/desktop/shell_live/chat_editor.ex` (1 send)
|
||||
- `lib/bds/desktop/shell_live/menu_editor.ex` (1 send)
|
||||
- `lib/bds/desktop/shell_live/settings_editor.ex` (2 sends)
|
||||
- `lib/bds/desktop/shell_live/misc_editor.ex` (4 sends)
|
||||
- `lib/bds/desktop/shell_live/tags_editor.ex` (2 sends)
|
||||
- `lib/bds/desktop/shell_live/import_editor.ex` (1 send)
|
||||
- `lib/bds/desktop/shell_live/overlay_manager.ex` (3 sends)
|
||||
- `lib/bds/desktop/main_window.ex` (1 send)
|
||||
- **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.
|
||||
- **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.
|
||||
- **Test:** Refactor one component; assert it no longer uses `send(self(), ...)`.
|
||||
### ~~CSM-017 — `send(self(), ...)` Component Chatter~~ ✅ FIXED
|
||||
- **Fixed:** 2026-05-09
|
||||
- **What was done:**
|
||||
- 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).
|
||||
- Replaced all 25+ `send(self(), ...)` calls across 11 editor components with `Notify.*` calls:
|
||||
- `post_editor.ex` — 13 calls (dirty, tab_meta, close_tab, output)
|
||||
- `media_editor.ex` — 7 calls (dirty, tab_meta, output)
|
||||
- `chat_editor.ex` — 15 calls (output, tab_meta, open_sidebar_item, plus chat-specific via `Notify.parent`)
|
||||
- `template_editor.ex` — 3 calls (close_tab, output, reload)
|
||||
- `script_editor.ex` — 3 calls (close_tab, output, reload)
|
||||
- `misc_editor.ex` — 4 calls (command, output, tab_meta_merge, open_sidebar_item)
|
||||
- `settings_editor.ex` — 2 calls (output, parent)
|
||||
- `tags_editor.ex` — 2 calls (output, parent)
|
||||
- `menu_editor.ex` — 1 call (output)
|
||||
- `import_editor.ex` — 2 calls (tab_meta, output)
|
||||
- `overlay_manager.ex` — 3 calls (parent for cross-component routing)
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,25 +9,73 @@ defmodule BDS.Desktop.ShellLive.Bridges do
|
||||
alias BDS.Desktop.ShellLive.{CliSync, SessionUtil}
|
||||
alias BDS.UI.Workbench
|
||||
|
||||
@refreshable_tab_meta_types [:import, :chat]
|
||||
|
||||
@spec handle_info(tuple() | atom(), Phoenix.LiveView.Socket.t(), map()) ::
|
||||
{: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
|
||||
|
||||
def handle_info({:import_editor_tab_meta, definition_id, title, subtitle}, socket, callbacks) do
|
||||
tab_meta =
|
||||
Map.put(socket.assigns.tab_meta, {:import, definition_id}, %{
|
||||
title: title,
|
||||
subtitle: subtitle || ""
|
||||
})
|
||||
def handle_info({:editor_tab_meta, type, id, updates}, socket, callbacks)
|
||||
when is_atom(type) and is_map(updates) do
|
||||
key = {type, id}
|
||||
current_meta = Map.get(socket.assigns.tab_meta, key, %{})
|
||||
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,
|
||||
socket
|
||||
|> assign(:tab_meta, tab_meta)
|
||||
|> callbacks.refresh_sidebar.(socket.assigns.workbench)}
|
||||
callbacks.refresh_layout.(socket, Workbench.close_tab(socket.assigns.workbench, type, id))}
|
||||
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
|
||||
send_update(ChatEditor,
|
||||
id: "chat-editor-#{conversation_id}",
|
||||
@@ -68,27 +116,6 @@ defmodule BDS.Desktop.ShellLive.Bridges do
|
||||
{:noreply, assign(socket, :chat_editor_request_refs, refs)}
|
||||
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
|
||||
{:noreply,
|
||||
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))}
|
||||
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
|
||||
{:noreply, CliSync.apply_entity_change(socket, payload, callbacks.refresh_content)}
|
||||
end
|
||||
@@ -155,126 +211,5 @@ defmodule BDS.Desktop.ShellLive.Bridges do
|
||||
{:noreply, socket}
|
||||
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}
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
|
||||
alias BDS.{AI, BoundedAtoms, MapUtils, Persistence}
|
||||
alias BDS.Desktop.ShellLive.ChatEditor.{MessageBuild, ModelSelection, ToolTracking}
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.Desktop.ShellLive.TabHelpers
|
||||
use Gettext, backend: BDS.Gettext
|
||||
|
||||
@@ -77,7 +78,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
{:noreply, assign(socket, :model_selector_open?, false) |> build_data()}
|
||||
|
||||
{: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()}
|
||||
end
|
||||
end
|
||||
@@ -129,14 +130,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
end
|
||||
|
||||
def handle_event("open_chat_settings", _params, socket) do
|
||||
notify_parent(
|
||||
{:open_sidebar_item,
|
||||
Notify.open_sidebar_item(
|
||||
%{
|
||||
"route" => "settings",
|
||||
"id" => "settings-ai",
|
||||
"title" => "Settings",
|
||||
"subtitle" => "AI"
|
||||
}, :pin}
|
||||
},
|
||||
:pin
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
@@ -203,10 +204,8 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
build_data(socket)
|
||||
|
||||
socket.assigns.offline_mode ->
|
||||
notify_parent(
|
||||
{:chat_editor_output, dgettext("ui", "Chat"),
|
||||
dgettext("ui", "Automatic AI actions stay gated by airplane mode."), "info"}
|
||||
)
|
||||
Notify.output(dgettext("ui", "Chat"),
|
||||
dgettext("ui", "Automatic AI actions stay gated by airplane mode."), "info")
|
||||
|
||||
build_data(socket)
|
||||
|
||||
@@ -227,7 +226,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
|
||||
: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
|
||||
|> assign(:input, "")
|
||||
@@ -254,7 +253,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
%{ref: ref} = _request ->
|
||||
: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
|
||||
|> assign(:request, nil)
|
||||
@@ -293,9 +292,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
assign(socket, :request, nil) |> build_data()
|
||||
|
||||
{:error, reason} ->
|
||||
notify_parent(
|
||||
{:chat_editor_output, dgettext("ui", "Chat"), format_error(reason), "error"}
|
||||
)
|
||||
Notify.output(dgettext("ui", "Chat"), format_error(reason), "error")
|
||||
|
||||
assign(socket, :request, nil) |> build_data()
|
||||
end
|
||||
@@ -347,7 +344,7 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
|> MapUtils.attr(:title)
|
||||
|
||||
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
|
||||
|
||||
socket
|
||||
@@ -368,14 +365,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
:open_post ->
|
||||
case Map.get(payload, "postId") || Map.get(payload, "post_id") do
|
||||
post_id when is_binary(post_id) and post_id != "" ->
|
||||
notify_parent(
|
||||
{:open_sidebar_item,
|
||||
Notify.open_sidebar_item(
|
||||
%{
|
||||
"route" => "post",
|
||||
"id" => post_id,
|
||||
"title" => TabHelpers.post_title(post_id),
|
||||
"subtitle" => TabHelpers.post_subtitle(post_id)
|
||||
}, :pin}
|
||||
},
|
||||
:pin
|
||||
)
|
||||
|
||||
assign(socket, :action_error, nil) |> build_data()
|
||||
@@ -387,14 +384,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
:open_media ->
|
||||
case Map.get(payload, "mediaId") || Map.get(payload, "media_id") do
|
||||
media_id when is_binary(media_id) and media_id != "" ->
|
||||
notify_parent(
|
||||
{:open_sidebar_item,
|
||||
Notify.open_sidebar_item(
|
||||
%{
|
||||
"route" => "media",
|
||||
"id" => media_id,
|
||||
"title" => TabHelpers.media_title(media_id),
|
||||
"subtitle" => TabHelpers.media_subtitle(media_id)
|
||||
}, :pin}
|
||||
},
|
||||
:pin
|
||||
)
|
||||
|
||||
assign(socket, :action_error, nil) |> build_data()
|
||||
@@ -404,14 +401,14 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
end
|
||||
|
||||
:open_settings ->
|
||||
notify_parent(
|
||||
{:open_sidebar_item,
|
||||
Notify.open_sidebar_item(
|
||||
%{
|
||||
"route" => "settings",
|
||||
"id" => "settings-ai",
|
||||
"title" => "Settings",
|
||||
"subtitle" => "AI"
|
||||
}, :pin}
|
||||
},
|
||||
:pin
|
||||
)
|
||||
|
||||
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") ||
|
||||
socket.assigns.conversation_id
|
||||
|
||||
notify_parent(
|
||||
{:open_sidebar_item,
|
||||
Notify.open_sidebar_item(
|
||||
%{
|
||||
"route" => "chat",
|
||||
"id" => chat_id,
|
||||
"title" => Map.get(payload, "title", "Chat"),
|
||||
"subtitle" => Map.get(payload, "subtitle", "")
|
||||
}, :pin}
|
||||
},
|
||||
:pin
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
view ->
|
||||
notify_parent({:chat_editor_switch_view, view})
|
||||
Notify.parent({:chat_editor_switch_view, view})
|
||||
assign(socket, :action_error, nil) |> build_data()
|
||||
end
|
||||
|
||||
:toggle_sidebar ->
|
||||
notify_parent({:chat_editor_toggle_sidebar})
|
||||
Notify.parent({:chat_editor_toggle_sidebar})
|
||||
assign(socket, :action_error, nil) |> build_data()
|
||||
|
||||
:toggle_panel ->
|
||||
notify_parent({:chat_editor_toggle_panel})
|
||||
Notify.parent({:chat_editor_toggle_panel})
|
||||
assign(socket, :action_error, nil) |> build_data()
|
||||
|
||||
:toggle_assistant_sidebar ->
|
||||
notify_parent({:chat_editor_toggle_assistant_sidebar})
|
||||
Notify.parent({:chat_editor_toggle_assistant_sidebar})
|
||||
assign(socket, :action_error, nil) |> build_data()
|
||||
|
||||
:unknown ->
|
||||
@@ -822,9 +819,6 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
||||
|
||||
# ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
defp notify_parent(message) do
|
||||
send(self(), message)
|
||||
end
|
||||
|
||||
defp active_project_id(socket) do
|
||||
socket.assigns[:project_id]
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
||||
|
||||
alias BDS.{AI, ImportAnalysis, ImportDefinitions, ImportExecution}
|
||||
alias BDS.Desktop.{FilePicker, FolderPicker, ShellData}
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
|
||||
alias BDS.Desktop.ShellLive.ImportEditor.{
|
||||
AnalysisState,
|
||||
@@ -641,12 +642,11 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
||||
defp maybe_update_tab_meta(socket, name) do
|
||||
title = name || dgettext("ui", "Untitled Import")
|
||||
|
||||
notify_parent(
|
||||
{:import_editor_tab_meta, socket.assigns.definition_id, title,
|
||||
Notify.tab_meta(:import, socket.assigns.definition_id, title,
|
||||
dgettext(
|
||||
"ui",
|
||||
"Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported."
|
||||
)}
|
||||
)
|
||||
)
|
||||
|
||||
socket
|
||||
@@ -1404,12 +1404,8 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
||||
|
||||
# ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
defp notify_parent(message) do
|
||||
send(self(), message)
|
||||
end
|
||||
|
||||
defp notify_output(socket, title, message, level \\ "info") do
|
||||
notify_parent({:import_editor_output, title, message, level})
|
||||
Notify.output(title, message, level)
|
||||
socket
|
||||
end
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
||||
import Ecto.Query
|
||||
|
||||
alias BDS.Desktop.{FilePicker}
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.{AI, I18n, Media}
|
||||
alias BDS.Media.Media, as: MediaRecord
|
||||
alias BDS.Media.Translation
|
||||
@@ -90,7 +91,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
||||
|> build_data()
|
||||
|
||||
if dirty? != was_dirty? do
|
||||
notify_parent({:media_editor_dirty, socket.assigns.media_id, dirty?})
|
||||
Notify.dirty(:media, socket.assigns.media_id, dirty?)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
@@ -123,12 +124,10 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
||||
|> assign(:dirty?, false)
|
||||
|> build_data()
|
||||
|
||||
notify_parent({:media_editor_dirty, media.id, false})
|
||||
Notify.dirty(:media, media.id, false)
|
||||
|
||||
notify_parent(
|
||||
{:media_editor_tab_meta, media.id, display_title(updated_media),
|
||||
updated_media.original_name || updated_media.mime_type || ""}
|
||||
)
|
||||
Notify.tab_meta(:media, media.id, display_title(updated_media),
|
||||
updated_media.original_name || updated_media.mime_type || "")
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
@@ -483,12 +482,10 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
||||
|> assign(:save_state, :saved)
|
||||
|> build_data()
|
||||
|
||||
notify_parent({:media_editor_dirty, media.id, false})
|
||||
Notify.dirty(:media, media.id, false)
|
||||
|
||||
notify_parent(
|
||||
{:media_editor_tab_meta, media.id, display_title(updated_media),
|
||||
updated_media.original_name || updated_media.mime_type || ""}
|
||||
)
|
||||
Notify.tab_meta(:media, media.id, display_title(updated_media),
|
||||
updated_media.original_name || updated_media.mime_type || "")
|
||||
|
||||
notify_output(socket, dgettext("ui", "Media"), dgettext("ui", "Media saved"))
|
||||
socket
|
||||
@@ -528,7 +525,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
||||
|> assign(:quick_actions_open?, false)
|
||||
|> build_data()
|
||||
|
||||
notify_parent({:media_editor_dirty, media.id, dirty?})
|
||||
Notify.dirty(:media, media.id, dirty?)
|
||||
socket
|
||||
end
|
||||
end
|
||||
@@ -569,12 +566,8 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_parent(message) do
|
||||
send(self(), message)
|
||||
end
|
||||
|
||||
defp notify_output(socket, title, message, level \\ "info") do
|
||||
send(self(), {:media_editor_output, title, message, level})
|
||||
Notify.output(title, message, level)
|
||||
socket
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
||||
|
||||
use Gettext, backend: BDS.Gettext
|
||||
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
|
||||
alias BDS.Desktop.ShellLive.MenuEditor.{
|
||||
DraftManagement,
|
||||
PageCategory,
|
||||
@@ -251,7 +253,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do
|
||||
end
|
||||
|
||||
defp notify_output(title, message, level) do
|
||||
send(self(), {:menu_editor_output, title, message, level})
|
||||
Notify.output(title, message, level)
|
||||
end
|
||||
|
||||
attr(:menu_editor, :map, required: true)
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
||||
import Ecto.Query
|
||||
|
||||
alias BDS.{Embeddings, Generation, Git, HelpDocs, Posts, Repo}
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.MapUtils
|
||||
alias BDS.Settings.Setting
|
||||
use Gettext, backend: BDS.Gettext
|
||||
@@ -358,19 +359,19 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do
|
||||
# ── Private helpers ────────────────────────────────────────────────────────
|
||||
|
||||
defp notify_command(action, params \\ %{}) do
|
||||
send(self(), {:misc_editor_command, action, params})
|
||||
Notify.command(action, params)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
defp notify_open_sidebar_item(params, intent) do
|
||||
send(self(), {:open_sidebar_item, params, intent})
|
||||
Notify.open_sidebar_item(params, intent)
|
||||
end
|
||||
|
||||
defp rerun_action(assigns) do
|
||||
|
||||
70
lib/bds/desktop/shell_live/notify.ex
Normal file
70
lib/bds/desktop/shell_live/notify.ex
Normal 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
|
||||
@@ -11,6 +11,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
|
||||
|
||||
alias BDS.Desktop.ShellLive.{
|
||||
MediaEditor,
|
||||
Notify,
|
||||
PostEditor,
|
||||
TabHelpers
|
||||
}
|
||||
@@ -170,7 +171,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
|
||||
"[#{result.original_name}](bds-media://#{result.media_id})"
|
||||
end
|
||||
|
||||
send(self(), {:post_editor_insert_content, post_id, syntax})
|
||||
Notify.parent({:post_editor_insert_content, post_id, syntax})
|
||||
socket
|
||||
end
|
||||
|
||||
@@ -195,7 +196,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
|
||||
end
|
||||
|
||||
if details do
|
||||
send(self(), {:post_editor_insert_content, post_id, details})
|
||||
Notify.parent({:post_editor_insert_content, post_id, details})
|
||||
end
|
||||
|
||||
socket
|
||||
@@ -213,7 +214,7 @@ defmodule BDS.Desktop.ShellLive.OverlayManager do
|
||||
socket =
|
||||
case {socket.assigns[:shell_overlay], current_tab} do
|
||||
{%{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
|
||||
|
||||
{%{kind: :language_picker}, %{type: :media, id: media_id}} ->
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|
||||
alias BDS.{AI, Posts, Preview}
|
||||
alias BDS.Desktop.ShellData
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, ListValues, Persistence, PostMetadata}
|
||||
alias BDS.Posts.Post
|
||||
alias BDS.Tags
|
||||
@@ -181,7 +182,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|> build_data()
|
||||
|
||||
if dirty? != was_dirty? do
|
||||
notify_parent({:post_editor_dirty, post_id, dirty?})
|
||||
Notify.dirty(:post, post_id, dirty?)
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
@@ -456,12 +457,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|> assign(:dirty?, false)
|
||||
|> build_data()
|
||||
|
||||
notify_parent(
|
||||
{:post_editor_tab_meta, post.id, record_title(record, refreshed_post),
|
||||
Atom.to_string(record_status(record))}
|
||||
)
|
||||
Notify.tab_meta(:post, post.id, record_title(record, refreshed_post),
|
||||
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"))
|
||||
socket
|
||||
|
||||
@@ -497,12 +496,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|> assign(:dirty?, false)
|
||||
|> build_data()
|
||||
|
||||
notify_parent(
|
||||
{:post_editor_tab_meta, post.id, record_title(record, refreshed_post),
|
||||
Atom.to_string(record_status(record))}
|
||||
)
|
||||
Notify.tab_meta(:post, post.id, record_title(record, refreshed_post),
|
||||
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"))
|
||||
socket
|
||||
|
||||
@@ -534,13 +531,11 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|> assign(:dirty?, false)
|
||||
|> build_data()
|
||||
|
||||
notify_parent(
|
||||
{:post_editor_tab_meta, post.id,
|
||||
Notify.tab_meta(:post, 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
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -555,7 +550,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|
||||
case Posts.delete_post(post_id) do
|
||||
{:ok, :deleted} ->
|
||||
notify_parent({:close_tab, :post, post_id})
|
||||
Notify.close_tab(:post, post_id)
|
||||
socket
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -642,7 +637,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|> assign(:quick_actions_open?, false)
|
||||
|> build_data()
|
||||
|
||||
notify_parent({:post_editor_dirty, post_id, false})
|
||||
Notify.dirty(:post, post_id, false)
|
||||
socket
|
||||
else
|
||||
{:error, reason} ->
|
||||
@@ -698,7 +693,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|> assign(:shell_overlay, nil)
|
||||
|> build_data()
|
||||
|
||||
notify_parent({:post_editor_dirty, post_id, true})
|
||||
Notify.dirty(:post, post_id, true)
|
||||
socket
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -741,7 +736,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|> put_component_draft_field(field_key(kind), updated)
|
||||
|> build_data()
|
||||
|
||||
notify_parent({:post_editor_dirty, socket.assigns.post_id, true})
|
||||
Notify.dirty(:post, socket.assigns.post_id, true)
|
||||
assign(socket, :dirty?, true)
|
||||
end
|
||||
end
|
||||
@@ -771,7 +766,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
||||
|> put_component_draft_field(field_key(kind), updated)
|
||||
|> build_data()
|
||||
|
||||
notify_parent({:post_editor_dirty, socket.assigns.post_id, true})
|
||||
Notify.dirty(:post, socket.assigns.post_id, true)
|
||||
assign(socket, :dirty?, true)
|
||||
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, :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
|
||||
send(self(), {:post_editor_output, title, message, level})
|
||||
Notify.output(title, message, level)
|
||||
socket
|
||||
end
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.ScriptEditor do
|
||||
use Phoenix.LiveComponent
|
||||
|
||||
alias BDS.{Scripts, Scripting}
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.Scripts.Script
|
||||
use Gettext, backend: BDS.Gettext
|
||||
|
||||
@@ -225,7 +226,7 @@ defmodule BDS.Desktop.ShellLive.ScriptEditor do
|
||||
|
||||
case Scripts.delete_script(script_id) do
|
||||
{:ok, _deleted} ->
|
||||
send(self(), {:close_tab, :scripts, script_id})
|
||||
Notify.close_tab(:scripts, script_id)
|
||||
socket
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -282,12 +283,12 @@ defmodule BDS.Desktop.ShellLive.ScriptEditor do
|
||||
end
|
||||
|
||||
defp notify_output(socket, title, message, level \\ "info") do
|
||||
send(self(), {:script_editor_output, title, message, level})
|
||||
Notify.output(title, message, level)
|
||||
socket
|
||||
end
|
||||
|
||||
defp notify_reload(socket) do
|
||||
send(self(), :reload_shell)
|
||||
Notify.reload()
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
||||
alias BDS.Desktop.ShellLive.SettingsEditor.AISettings
|
||||
alias BDS.Desktop.ShellLive.SettingsEditor.EditorSettings
|
||||
alias BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.Desktop.ShellLive.SettingsEditor.MCPConfig
|
||||
alias BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings
|
||||
alias BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings
|
||||
@@ -308,13 +309,13 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
|
||||
|
||||
defp append_output_callback do
|
||||
fn socket, title, message, _details, level ->
|
||||
send(self(), {:settings_output, title, message, level})
|
||||
Notify.output(title, message, level)
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_parent(message) do
|
||||
send(self(), message)
|
||||
Notify.parent(message)
|
||||
end
|
||||
|
||||
defp current_settings_section(assigns) do
|
||||
|
||||
@@ -6,6 +6,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
||||
import Ecto.Query
|
||||
|
||||
alias BDS.{Repo, Tags}
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.Posts.Post
|
||||
alias BDS.Tags.Tag
|
||||
alias BDS.Templates.Template
|
||||
@@ -292,11 +293,11 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
||||
defp noreply(socket), do: {:noreply, socket}
|
||||
|
||||
defp notify_parent(message) do
|
||||
send(self(), message)
|
||||
Notify.parent(message)
|
||||
end
|
||||
|
||||
defp notify_output(title, message, level) do
|
||||
send(self(), {:tags_editor_output, title, message, level})
|
||||
Notify.output(title, message, level)
|
||||
end
|
||||
|
||||
@spec tag_font_size(term(), term()) :: term()
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.TemplateEditor do
|
||||
use Phoenix.LiveComponent
|
||||
|
||||
alias BDS.{MCP, Templates}
|
||||
alias BDS.Desktop.ShellLive.Notify
|
||||
alias BDS.Templates.Template
|
||||
use Gettext, backend: BDS.Gettext
|
||||
|
||||
@@ -182,7 +183,7 @@ defmodule BDS.Desktop.ShellLive.TemplateEditor do
|
||||
|
||||
case Templates.delete_template(template_id, force: true) do
|
||||
{:ok, _deleted} ->
|
||||
send(self(), {:close_tab, :templates, template_id})
|
||||
Notify.close_tab(:templates, template_id)
|
||||
socket
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -231,12 +232,12 @@ defmodule BDS.Desktop.ShellLive.TemplateEditor do
|
||||
defp normalize_template_kind(_kind), do: :post
|
||||
|
||||
defp notify_output(socket, title, message, level \\ "info") do
|
||||
send(self(), {:template_editor_output, title, message, level})
|
||||
Notify.output(title, message, level)
|
||||
socket
|
||||
end
|
||||
|
||||
defp notify_reload(socket) do
|
||||
send(self(), :reload_shell)
|
||||
Notify.reload()
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
172
test/bds/csm017_component_chatter_test.exs
Normal file
172
test/bds/csm017_component_chatter_test.exs
Normal 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
|
||||
Reference in New Issue
Block a user