diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex
index 80a1159..c6950b2 100644
--- a/lib/bds/desktop/shell_live.ex
+++ b/lib/bds/desktop/shell_live.ex
@@ -172,16 +172,9 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:media_editor_drafts, %{})
|> assign(:media_editor_quick_actions_open, %{})
|> assign(:media_editor_post_pickers_open, %{})
- |> assign(:media_editor_post_picker_queries, %{})
- |> assign(:media_editor_save_states, %{})
- |> assign(:media_editor_translation_forms, %{})
- |> assign(:settings_editor_search, "")
- |> assign(:settings_editor_project_draft, %{})
- |> assign(:settings_editor_endpoint_models, %{})
- |> assign(:settings_editor_publishing_draft, %{})
- |> assign(:settings_editor_new_category, "")
- |> assign(:style_editor_theme, nil)
- |> assign(:style_editor_preview_mode, "auto")
+ |> assign(:media_editor_post_picker_queries, %{})
+ |> assign(:media_editor_save_states, %{})
+ |> assign(:media_editor_translation_forms, %{})
|> assign(:script_editor_drafts, %{})
|> assign(:template_editor_drafts, %{})
|> assign(:chat_editor_inputs, %{})
@@ -530,109 +523,10 @@ defmodule BDS.Desktop.ShellLive do
end
end
- def handle_event("change_settings_search", %{"query" => query}, socket) do
- {:noreply, SettingsEditor.update_search(socket, query, &reload_shell/2)}
- end
-
- def handle_event("change_settings_project", %{"settings_project" => params}, socket) do
- {:noreply, SettingsEditor.update_project_draft(socket, params, &reload_shell/2)}
- end
-
- def handle_event("change_settings_editor", %{"settings_editor" => params}, socket) do
- {:noreply, SettingsEditor.update_editor_draft(socket, params, &reload_shell/2)}
- end
-
- def handle_event("save_settings_editor", _params, socket) do
- {:noreply, SettingsEditor.save_editor(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("save_settings_project", _params, socket) do
- {:noreply, SettingsEditor.save_project(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("change_settings_publishing", %{"settings_publishing" => params}, socket) do
- {:noreply, SettingsEditor.update_publishing_draft(socket, params, &reload_shell/2)}
- end
-
- def handle_event("change_settings_ai", %{"settings_ai" => params}, socket) do
- {:noreply, SettingsEditor.update_ai_draft(socket, params, &reload_shell/2)}
- end
-
- def handle_event("refresh_settings_ai_models", %{"endpoint" => endpoint}, socket) do
- case BoundedAtoms.ai_endpoint(endpoint) do
- nil ->
- {:noreply, reload_shell(socket, socket.assigns.workbench)}
-
- endpoint_key ->
- {:noreply,
- SettingsEditor.refresh_ai_models(
- socket,
- endpoint_key,
- &reload_shell/2,
- &append_output_entry/5
- )}
- end
- end
-
- def handle_event("save_settings_ai", _params, socket) do
- {:noreply, SettingsEditor.save_ai(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("reset_settings_ai_prompt", _params, socket) do
- {:noreply, SettingsEditor.reset_ai_prompt(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("save_settings_publishing", _params, socket) do
- {:noreply, SettingsEditor.save_publishing(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("clear_settings_publishing", _params, socket) do
- {:noreply, SettingsEditor.clear_publishing(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("change_settings_new_category", %{"name" => name}, socket) do
- {:noreply, SettingsEditor.update_new_category(socket, name, &reload_shell/2)}
- end
-
- def handle_event("add_settings_category", _params, socket) do
- {:noreply, SettingsEditor.add_category(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("reset_settings_categories", _params, socket) do
- {:noreply, SettingsEditor.reset_categories(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("save_settings_category", %{"category_settings" => params}, socket) do
- {:noreply,
- SettingsEditor.save_category(socket, params, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("remove_settings_category", %{"category" => category}, socket) do
- {:noreply,
- SettingsEditor.remove_category(socket, category, &reload_shell/2, &append_output_entry/5)}
- end
-
def handle_event("settings_shell_command", %{"action" => action}, socket) do
{:noreply, apply_shell_command(socket, action)}
end
- def handle_event("toggle_settings_mcp_agent", %{"agent" => agent}, socket) do
- {:noreply,
- SettingsEditor.toggle_mcp_agent(socket, agent, &reload_shell/2, &append_output_entry/5)}
- end
-
- def handle_event("select_style_theme", %{"theme" => theme}, socket) do
- {:noreply, SettingsEditor.select_style_theme(socket, theme, &reload_shell/2)}
- end
-
- def handle_event("change_style_preview_mode", %{"mode" => mode}, socket) do
- {:noreply, SettingsEditor.change_style_preview_mode(socket, mode, &reload_shell/2)}
- end
-
- def handle_event("apply_style_theme", _params, socket) do
- {:noreply, SettingsEditor.apply_style_theme(socket, &reload_shell/2, &append_output_entry/5)}
- end
-
def handle_event("menu_editor_select_item", %{"item_id" => item_id}, socket) do
{:noreply, MenuEditor.select_item(socket, item_id, &reload_shell/2)}
end
@@ -1451,6 +1345,14 @@ defmodule BDS.Desktop.ShellLive do
{:noreply, reload_shell(socket, socket.assigns.workbench)}
end
+ def handle_info({:settings_output, title, message, level}, socket) do
+ {:noreply, append_output_entry(socket, title, message, nil, level)}
+ end
+
+ def handle_info(:settings_changed, socket) do
+ {:noreply, reload_shell(socket, socket.assigns.workbench)}
+ end
+
@impl true
def render(assigns) do
UILocale.put(assigns.page_language)
@@ -1521,7 +1423,6 @@ defmodule BDS.Desktop.ShellLive do
|> assign(:current_tab, current_tab(workbench))
|> assign_post_editor()
|> assign_media_editor()
- |> assign_settings_editor()
|> assign_menu_editor()
|> assign_code_entity_editor()
|> assign_chat_editor()
@@ -1576,10 +1477,6 @@ defmodule BDS.Desktop.ShellLive do
MediaEditor.assign_socket(socket)
end
- defp assign_settings_editor(socket) do
- SettingsEditor.assign_socket(socket)
- end
-
defp assign_menu_editor(socket) do
MenuEditor.assign_socket(socket)
end
@@ -1756,7 +1653,8 @@ defmodule BDS.Desktop.ShellLive do
end
defp save_current_tab(%{assigns: %{current_tab: %{type: :settings}}} = socket) do
- SettingsEditor.save_project(socket, &reload_shell/2, &append_output_entry/5)
+ send_update(SettingsEditor, id: "settings-editor", action: :save_project)
+ socket
end
defp save_current_tab(%{assigns: %{current_tab: %{type: :menu_editor}}} = socket) do
diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex
index 7838ef5..bdbb5ed 100644
--- a/lib/bds/desktop/shell_live/index.html.heex
+++ b/lib/bds/desktop/shell_live/index.html.heex
@@ -388,11 +388,16 @@
<% @current_tab.type == :media and @media_editor -> %>
- <% @current_tab.type == :settings and @settings_editor -> %>
-
-
- <% @current_tab.type == :style and @style_editor -> %>
-
+ <% @current_tab.type in [:settings, :style] and @current_project -> %>
+ <.live_component module={SettingsEditor} id="settings-editor"
+ project_id={@current_project.id}
+ current_project={@current_project}
+ projects={@projects}
+ workbench={@workbench}
+ current_tab={@current_tab}
+ tab_meta={@tab_meta}
+ offline_mode={@offline_mode}
+ />
<% @current_tab.type == :menu_editor and @menu_editor -> %>
diff --git a/lib/bds/desktop/shell_live/settings_editor.ex b/lib/bds/desktop/shell_live/settings_editor.ex
index a586d65..ddd1bca 100644
--- a/lib/bds/desktop/shell_live/settings_editor.ex
+++ b/lib/bds/desktop/shell_live/settings_editor.ex
@@ -1,7 +1,7 @@
defmodule BDS.Desktop.ShellLive.SettingsEditor do
@moduledoc false
- use Phoenix.Component
+ use Phoenix.LiveComponent
import Ecto.Query
@@ -22,57 +22,188 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
@settings_sections ~w(project editor content ai technology publishing data mcp)
@supported_languages ["en", "de", "fr", "it", "es"]
- defdelegate update_project_draft(socket, params, reload), to: ProjectSettings
- defdelegate save_project(socket, reload, append_output), to: ProjectSettings
- defdelegate update_editor_draft(socket, params, reload), to: EditorSettings
- defdelegate save_editor(socket, reload, append_output), to: EditorSettings
- defdelegate update_publishing_draft(socket, params, reload), to: PublishingSettings
- defdelegate save_publishing(socket, reload, append_output), to: PublishingSettings
- defdelegate clear_publishing(socket, reload, append_output), to: PublishingSettings
- defdelegate update_ai_draft(socket, params, reload), to: AISettings
- defdelegate refresh_ai_models(socket, endpoint_key, reload, append_output), to: AISettings
- defdelegate save_ai(socket, reload, append_output), to: AISettings
- defdelegate reset_ai_prompt(socket, reload, append_output), to: AISettings
- defdelegate update_new_category(socket, name, reload), to: ManagedCategories
- defdelegate add_category(socket, reload, append_output), to: ManagedCategories
- defdelegate reset_categories(socket, reload, append_output), to: ManagedCategories
- defdelegate save_category(socket, params, reload, append_output), to: ManagedCategories
- defdelegate remove_category(socket, category, reload, append_output), to: ManagedCategories
- defdelegate toggle_mcp_agent(socket, agent, reload, append_output), to: MCPConfig
- defdelegate select_style_theme(socket, theme, reload), to: StyleEditor
- defdelegate change_style_preview_mode(socket, mode, reload), to: StyleEditor
- defdelegate apply_style_theme(socket, reload, append_output), to: StyleEditor
defdelegate theme_display_name(theme), to: StyleEditor
defdelegate protected_category?(category), to: ManagedCategories
- @spec assign_socket(term()) :: term()
- def assign_socket(socket) do
- case socket.assigns[:current_tab] do
- %{type: :settings} ->
- socket
- |> assign(:settings_editor, build_settings(socket.assigns))
- |> assign(:style_editor, nil)
+ @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()}
+ @impl true
+ def update(%{action: :save_project} = assigns, socket) do
+ socket = assign(socket, Map.drop(assigns, [:action]))
+ socket = ProjectSettings.save_project(socket, reload_callback(), append_output_callback())
+ {:ok, socket}
+ end
- %{type: :style} ->
- socket
- |> assign(:settings_editor, nil)
- |> assign(:style_editor, StyleEditor.build_style(socket.assigns))
+ def update(assigns, socket) do
+ socket =
+ socket
+ |> assign(assigns)
+ |> initialize_state()
+ |> build_data()
- _other ->
- socket
- |> assign(:settings_editor, nil)
- |> assign(:style_editor, nil)
+ {:ok, socket}
+ end
+
+ @spec render(map()) :: Phoenix.LiveView.Rendered.t()
+ @impl true
+ def render(assigns) do
+ case assigns.current_tab do
+ %{type: :settings} -> settings_editor(assigns)
+ %{type: :style} -> style_editor(assigns)
+ _other -> ~H""
end
end
- @spec update_search(term(), term(), term()) :: term()
- def update_search(socket, query, reload) do
- socket
- |> assign(:settings_editor_search, to_string(query || ""))
- |> reload.(socket.assigns.workbench)
+ @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) ::
+ {:noreply, Phoenix.LiveView.Socket.t()}
+ @impl true
+ def handle_event("change_settings_search", %{"query" => query}, socket) do
+ socket =
+ socket
+ |> assign(:settings_editor_search, to_string(query || ""))
+ |> build_data()
+
+ {:noreply, socket}
end
- @spec build_settings(term()) :: term()
+ def handle_event("change_settings_project", %{"settings_project" => params}, socket) do
+ {:noreply, ProjectSettings.update_project_draft(socket, params, reload_callback())}
+ end
+
+ def handle_event("change_settings_editor", %{"settings_editor" => params}, socket) do
+ {:noreply, EditorSettings.update_editor_draft(socket, params, reload_callback())}
+ end
+
+ def handle_event("save_settings_editor", _params, socket) do
+ {:noreply, EditorSettings.save_editor(socket, reload_callback(), append_output_callback())}
+ end
+
+ def handle_event("save_settings_project", _params, socket) do
+ socket = ProjectSettings.save_project(socket, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ def handle_event("change_settings_publishing", %{"settings_publishing" => params}, socket) do
+ {:noreply, PublishingSettings.update_publishing_draft(socket, params, reload_callback())}
+ end
+
+ def handle_event("change_settings_ai", %{"settings_ai" => params}, socket) do
+ {:noreply, AISettings.update_ai_draft(socket, params, reload_callback())}
+ end
+
+ def handle_event("refresh_settings_ai_models", %{"endpoint" => endpoint}, socket) do
+ case BDS.BoundedAtoms.ai_endpoint(endpoint) do
+ nil ->
+ {:noreply, build_data(socket)}
+
+ endpoint_key ->
+ {:noreply,
+ AISettings.refresh_ai_models(
+ socket,
+ endpoint_key,
+ reload_callback(),
+ append_output_callback()
+ )}
+ end
+ end
+
+ def handle_event("save_settings_ai", _params, socket) do
+ socket = AISettings.save_ai(socket, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ def handle_event("reset_settings_ai_prompt", _params, socket) do
+ {:noreply, AISettings.reset_ai_prompt(socket, reload_callback(), append_output_callback())}
+ end
+
+ def handle_event("save_settings_publishing", _params, socket) do
+ socket = PublishingSettings.save_publishing(socket, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ def handle_event("clear_settings_publishing", _params, socket) do
+ {:noreply, PublishingSettings.clear_publishing(socket, reload_callback(), append_output_callback())}
+ end
+
+ def handle_event("change_settings_new_category", %{"name" => name}, socket) do
+ {:noreply, ManagedCategories.update_new_category(socket, name, reload_callback())}
+ end
+
+ def handle_event("add_settings_category", _params, socket) do
+ socket = ManagedCategories.add_category(socket, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ def handle_event("reset_settings_categories", _params, socket) do
+ socket = ManagedCategories.reset_categories(socket, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ def handle_event("save_settings_category", %{"category_settings" => params}, socket) do
+ socket = ManagedCategories.save_category(socket, params, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ def handle_event("remove_settings_category", %{"category" => category}, socket) do
+ socket = ManagedCategories.remove_category(socket, category, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ def handle_event("toggle_settings_mcp_agent", %{"agent" => agent}, socket) do
+ socket = MCPConfig.toggle_mcp_agent(socket, agent, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ def handle_event("select_style_theme", %{"theme" => theme}, socket) do
+ {:noreply, StyleEditor.select_style_theme(socket, theme, reload_callback())}
+ end
+
+ def handle_event("change_style_preview_mode", %{"mode" => mode}, socket) do
+ {:noreply, StyleEditor.change_style_preview_mode(socket, mode, reload_callback())}
+ end
+
+ def handle_event("apply_style_theme", _params, socket) do
+ socket = StyleEditor.apply_style_theme(socket, reload_callback(), append_output_callback())
+ notify_parent(:settings_changed)
+ {:noreply, socket}
+ end
+
+ defp initialize_state(socket) do
+ defaults = %{
+ settings_editor_search: "",
+ settings_editor_project_draft: %{},
+ settings_editor_editor_draft: %{},
+ settings_editor_ai_draft: %{},
+ settings_editor_publishing_draft: %{},
+ settings_editor_new_category: "",
+ settings_editor_endpoint_models: %{},
+ style_editor_theme: nil,
+ style_editor_preview_mode: "auto"
+ }
+
+ Enum.reduce(defaults, socket, fn {key, default}, acc ->
+ if is_nil(Map.get(acc.assigns, key)) do
+ assign(acc, key, default)
+ else
+ acc
+ end
+ end)
+ end
+
+ defp build_data(socket) do
+ socket
+ |> assign(:settings_editor, build_settings(socket.assigns))
+ |> assign(:style_editor, build_style(socket.assigns))
+ end
+
+ @spec build_settings(map()) :: term()
def build_settings(%{projects: %{active_project_id: nil}}), do: nil
def build_settings(assigns) do
@@ -148,9 +279,27 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
}
end
- @spec translated(term(), term()) :: term()
- def translated(text, bindings \\ %{}),
- do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
+ @spec build_style(map()) :: term()
+ def build_style(%{projects: %{active_project_id: nil}}), do: nil
+
+ def build_style(assigns) do
+ StyleEditor.build_style(assigns)
+ end
+
+ defp reload_callback do
+ fn socket, _workbench -> build_data(socket) end
+ end
+
+ defp append_output_callback do
+ fn socket, title, message, _details, level ->
+ send(self(), {:settings_output, title, message, level})
+ socket
+ end
+ end
+
+ defp notify_parent(message) do
+ send(self(), message)
+ end
defp current_settings_section(assigns) do
meta = current_tab_meta(assigns)
@@ -236,4 +385,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do
defp section_matches?(query, keywords),
do: Enum.any?(keywords, &String.contains?(&1, String.downcase(query)))
+
+ @spec translated(term(), term()) :: term()
+ def translated(text, bindings \\ %{}),
+ do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
end
diff --git a/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex b/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex
index a3baf5e..697ab3c 100644
--- a/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex
+++ b/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex
@@ -9,7 +9,7 @@
@@ -27,7 +27,7 @@
<%= translated("Project") %>
<%= translated("Blog identity, URLs, authoring defaults, and bookmarklet setup") %>
-
-
+
<% end %>
<%= if @settings_editor.technology_visible? do %>
-
-
+
<% end %>
<%= if @settings_editor.publishing_visible? do %>
-
-
+
<% end %>
@@ -342,7 +342,7 @@
<%= agent.config_path || translated("Not supported in the rewrite yet") %>
-
diff --git a/lib/bds/desktop/shell_live/settings_editor_html/style_editor.html.heex b/lib/bds/desktop/shell_live/settings_editor_html/style_editor.html.heex
index a4b72ec..a705d34 100644
--- a/lib/bds/desktop/shell_live/settings_editor_html/style_editor.html.heex
+++ b/lib/bds/desktop/shell_live/settings_editor_html/style_editor.html.heex
@@ -6,7 +6,7 @@
<%= for theme <- @style_editor.themes do %>
-
+
@@ -22,13 +22,13 @@
- <%= translated("Apply Theme") %>
+ <%= translated("Apply Theme") %>
diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs
index 418bf45..afb386a 100644
--- a/test/bds/desktop/shell_live_test.exs
+++ b/test/bds/desktop/shell_live_test.exs
@@ -1200,7 +1200,9 @@ defmodule BDS.Desktop.ShellLiveTest do
refute html =~ "Anthropic / Online API Key"
_html =
- render_change(view, "change_settings_ai", %{
+ view
+ |> element("#settings-editor-shell form[phx-change='change_settings_ai']")
+ |> render_change(%{
"settings_ai" => %{
"online_url" => "https://api.example.test/v1",
"online_api_key" => "online-secret",
@@ -1221,7 +1223,10 @@ defmodule BDS.Desktop.ShellLiveTest do
}
})
- _html = render_click(view, "save_settings_ai")
+ _html =
+ view
+ |> element("#settings-editor-shell button[phx-click='save_settings_ai']")
+ |> render_click()
assert {:ok, online_endpoint} = AI.get_endpoint(:online)
assert online_endpoint.url == "https://api.example.test/v1"
@@ -1268,7 +1273,9 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Refresh Offline Models"
_html =
- render_change(view, "change_settings_ai", %{
+ view
+ |> element("#settings-editor-shell form[phx-change='change_settings_ai']")
+ |> render_change(%{
"settings_ai" => %{
"online_url" => "https://api.example.test/v1",
"offline_url" => "http://localhost:11434/v1"
@@ -1277,7 +1284,7 @@ defmodule BDS.Desktop.ShellLiveTest do
html =
view
- |> element("button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='online']")
+ |> element("#settings-editor-shell button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='online']")
|> render_click()
assert html =~ ~s()
@@ -1285,7 +1292,7 @@ defmodule BDS.Desktop.ShellLiveTest do
html =
view
- |> element("button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='airplane']")
+ |> element("#settings-editor-shell button[phx-click='refresh_settings_ai_models'][phx-value-endpoint='airplane']")
|> render_click()
assert html =~ ~s()