From ce54e973ad5e118a6e49cdcdce7dd05024c43c6a Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 3 May 2026 09:19:27 +0200 Subject: [PATCH] chore: converted preferences editor to live component --- lib/bds/desktop/shell_live.ex | 128 +-------- lib/bds/desktop/shell_live/index.html.heex | 15 +- lib/bds/desktop/shell_live/settings_editor.ex | 243 ++++++++++++++---- .../settings_editor.html.heex | 38 +-- .../style_editor.html.heex | 6 +- test/bds/desktop/shell_live_test.exs | 17 +- 6 files changed, 255 insertions(+), 192 deletions(-) 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 @@

<%= translated("Settings") %>

-
@@ -27,7 +27,7 @@

<%= translated("Project") %>

<%= translated("Blog identity, URLs, authoring defaults, and bookmarklet setup") %>

-
+
@@ -97,7 +97,7 @@

<%= translated("Bookmarklet copy support is wired through the desktop runtime and project public URL.") %>

-
+
<% end %> @@ -107,7 +107,7 @@

<%= translated("Editor") %>

<%= translated("Default editing mode and diff presentation") %>

-
+
@@ -136,7 +136,7 @@
-
+
<% end %> @@ -183,11 +183,11 @@
-
+
- +
@@ -198,12 +198,12 @@
- - + +
-
+
<% end %> @@ -211,13 +211,13 @@ <%= if @settings_editor.ai_visible? do %>

<%= translated("AI") %>

<%= translated("OpenAI-compatible endpoints, model routing, airplane mode, and system prompt") %>

-
+
- +
@@ -250,7 +250,7 @@
- +
@@ -297,14 +297,14 @@ <% end %> -
+
<% end %> <%= if @settings_editor.technology_visible? do %>

<%= translated("Technology") %>

<%= translated("Application-level runtime behavior and semantic indexing") %>

-
+
@@ -314,20 +314,20 @@

<%= translated("Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.") %>

-
+
<% end %> <%= if @settings_editor.publishing_visible? do %>

<%= translated("Publishing") %>

<%= translated("Deployment credentials for upload tasks") %>

-
+
-
+
<% 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 %> - +
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()