chore: converted preferences editor to live component

This commit is contained in:
2026-05-03 09:19:27 +02:00
parent 6c7fde6b95
commit ce54e973ad
6 changed files with 255 additions and 192 deletions

View File

@@ -388,11 +388,16 @@
<% @current_tab.type == :media and @media_editor -> %>
<MediaEditor.media_editor media_editor={@media_editor} />
<% @current_tab.type == :settings and @settings_editor -> %>
<SettingsEditor.settings_editor settings_editor={@settings_editor} />
<% @current_tab.type == :style and @style_editor -> %>
<SettingsEditor.style_editor style_editor={@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 -> %>
<MenuEditor.menu_editor menu_editor={@menu_editor} />

View File

@@ -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

View File

@@ -9,7 +9,7 @@
<div class="settings-view">
<div class="settings-header">
<h2 data-testid="editor-title"><%= translated("Settings") %></h2>
<form class="settings-search" phx-change="change_settings_search">
<form class="settings-search" phx-change="change_settings_search" phx-target={@myself}>
<input type="text" name="query" value={@settings_editor.search_query} placeholder={translated("Search settings")} />
</form>
</div>
@@ -27,7 +27,7 @@
<h3><%= translated("Project") %></h3>
<p class="setting-section-description"><%= translated("Blog identity, URLs, authoring defaults, and bookmarklet setup") %></p>
</div>
<form class="setting-section-content" phx-change="change_settings_project">
<form class="setting-section-content" phx-change="change_settings_project" phx-target={@myself}>
<div class="setting-row">
<div class="setting-info">
<label class="setting-label"><%= translated("Project Name") %></label>
@@ -97,7 +97,7 @@
<div class="setting-control"><p class="setting-description"><%= translated("Bookmarklet copy support is wired through the desktop runtime and project public URL.") %></p></div>
</div>
</form>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_project"><%= translated("Save") %></button></div>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_project" phx-target={@myself}><%= translated("Save") %></button></div>
</div>
<% end %>
@@ -107,7 +107,7 @@
<h3><%= translated("Editor") %></h3>
<p class="setting-section-description"><%= translated("Default editing mode and diff presentation") %></p>
</div>
<form class="setting-section-content" phx-change="change_settings_editor">
<form class="setting-section-content" phx-change="change_settings_editor" phx-target={@myself}>
<div class="setting-row">
<div class="setting-info"><label class="setting-label"><%= translated("Default Editor Mode") %></label></div>
<div class="setting-control">
@@ -136,7 +136,7 @@
<div class="setting-control"><label><input type="checkbox" name="settings_editor[hide_unchanged_regions]" checked={@settings_editor.editor["hide_unchanged_regions"]} /> <%= translated("Collapse unchanged diff hunks") %></label></div>
</div>
</form>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_editor"><%= translated("Save") %></button></div>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_editor" phx-target={@myself}><%= translated("Save") %></button></div>
</div>
<% end %>
@@ -183,11 +183,11 @@
</td>
<td>
<div class="setting-input-group">
<form id={"category-form-#{category.name}"} phx-submit="save_settings_category">
<form id={"category-form-#{category.name}"} phx-submit="save_settings_category" phx-target={@myself}>
<input type="hidden" name="category_settings[category]" value={category.name} />
</form>
<button class="secondary" type="submit" form={"category-form-#{category.name}"}><%= translated("Save") %></button>
<button class="secondary" type="button" phx-click="remove_settings_category" phx-value-category={category.name} disabled={category.protected?}><%= translated("Remove") %></button>
<button class="secondary" type="button" phx-click="remove_settings_category" phx-target={@myself} phx-value-category={category.name} disabled={category.protected?}><%= translated("Remove") %></button>
</div>
</td>
</tr>
@@ -198,12 +198,12 @@
<div class="setting-info"><label class="setting-label"><%= translated("Add Category") %></label></div>
<div class="setting-control">
<div class="setting-input-group">
<input type="text" value={@settings_editor.new_category} phx-change="change_settings_new_category" name="name" />
<button class="primary" type="button" phx-click="add_settings_category"><%= translated("Add") %></button>
<input type="text" value={@settings_editor.new_category} phx-change="change_settings_new_category" phx-target={@myself} name="name" />
<button class="primary" type="button" phx-click="add_settings_category" phx-target={@myself}><%= translated("Add") %></button>
</div>
</div>
</div>
<div class="setting-actions"><button class="secondary" type="button" phx-click="reset_settings_categories"><%= translated("Reset to Defaults") %></button></div>
<div class="setting-actions"><button class="secondary" type="button" phx-click="reset_settings_categories" phx-target={@myself}><%= translated("Reset to Defaults") %></button></div>
</div>
</div>
<% end %>
@@ -211,13 +211,13 @@
<%= if @settings_editor.ai_visible? do %>
<div class="setting-section" id="settings-section-ai">
<div class="setting-section-header"><h3><%= translated("AI") %></h3><p class="setting-section-description"><%= translated("OpenAI-compatible endpoints, model routing, airplane mode, and system prompt") %></p></div>
<form class="setting-section-content" phx-change="change_settings_ai">
<form class="setting-section-content" phx-change="change_settings_ai" phx-target={@myself}>
<div class="setting-row">
<div class="setting-info"><label class="setting-label"><%= translated("Online Endpoint URL") %></label></div>
<div class="setting-control">
<div class="setting-input-group">
<input type="url" name="settings_ai[online_url]" value={@settings_editor.ai["online_url"]} />
<button class="secondary" type="button" phx-click="refresh_settings_ai_models" phx-value-endpoint="online"><%= translated("Refresh Online Models") %></button>
<button class="secondary" type="button" phx-click="refresh_settings_ai_models" phx-target={@myself} phx-value-endpoint="online"><%= translated("Refresh Online Models") %></button>
</div>
</div>
</div>
@@ -250,7 +250,7 @@
<div class="setting-control">
<div class="setting-input-group">
<input type="url" name="settings_ai[offline_url]" value={@settings_editor.ai["offline_url"]} />
<button class="secondary" type="button" phx-click="refresh_settings_ai_models" phx-value-endpoint="airplane"><%= translated("Refresh Offline Models") %></button>
<button class="secondary" type="button" phx-click="refresh_settings_ai_models" phx-target={@myself} phx-value-endpoint="airplane"><%= translated("Refresh Offline Models") %></button>
</div>
</div>
</div>
@@ -297,14 +297,14 @@
<% end %>
</datalist>
</form>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_ai"><%= translated("Save") %></button><button class="secondary" type="button" phx-click="reset_settings_ai_prompt"><%= translated("Reset to Default") %></button></div>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_ai" phx-target={@myself}><%= translated("Save") %></button><button class="secondary" type="button" phx-click="reset_settings_ai_prompt" phx-target={@myself}><%= translated("Reset to Default") %></button></div>
</div>
<% end %>
<%= if @settings_editor.technology_visible? do %>
<div class="setting-section" id="settings-section-technology">
<div class="setting-section-header"><h3><%= translated("Technology") %></h3><p class="setting-section-description"><%= translated("Application-level runtime behavior and semantic indexing") %></p></div>
<form class="setting-section-content" phx-change="change_settings_project">
<form class="setting-section-content" phx-change="change_settings_project" phx-target={@myself}>
<div class="setting-row">
<div class="setting-info"><label class="setting-label"><%= translated("Semantic Similarity") %></label></div>
<div class="setting-control"><label><input type="checkbox" name="settings_project[semantic_similarity_enabled]" checked={@settings_editor.technology["semantic_similarity_enabled"]} /> <%= translated("Enable duplicate search and related-post embeddings") %></label></div>
@@ -314,20 +314,20 @@
<div class="setting-control"><p class="setting-description"><%= translated("Scripting capabilities are configured at the application layer in the rewrite and do not expose runtime switching here.") %></p></div>
</div>
</form>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_project"><%= translated("Save") %></button></div>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_project" phx-target={@myself}><%= translated("Save") %></button></div>
</div>
<% end %>
<%= if @settings_editor.publishing_visible? do %>
<div class="setting-section" id="settings-section-publishing">
<div class="setting-section-header"><h3><%= translated("Publishing") %></h3><p class="setting-section-description"><%= translated("Deployment credentials for upload tasks") %></p></div>
<form class="setting-section-content" phx-change="change_settings_publishing">
<form class="setting-section-content" phx-change="change_settings_publishing" phx-target={@myself}>
<div class="setting-row"><div class="setting-info"><label class="setting-label"><%= translated("SSH Mode") %></label></div><div class="setting-control"><select name="settings_publishing[ssh_mode]"><option value="scp" selected={@settings_editor.publishing["ssh_mode"] == "scp"}>scp</option><option value="rsync" selected={@settings_editor.publishing["ssh_mode"] == "rsync"}>rsync</option></select></div></div>
<div class="setting-row"><div class="setting-info"><label class="setting-label"><%= translated("Host") %></label></div><div class="setting-control"><input type="text" name="settings_publishing[ssh_host]" value={@settings_editor.publishing["ssh_host"]} /></div></div>
<div class="setting-row"><div class="setting-info"><label class="setting-label"><%= translated("Username") %></label></div><div class="setting-control"><input type="text" name="settings_publishing[ssh_user]" value={@settings_editor.publishing["ssh_user"]} /></div></div>
<div class="setting-row"><div class="setting-info"><label class="setting-label"><%= translated("Remote Path") %></label></div><div class="setting-control"><input type="text" name="settings_publishing[ssh_remote_path]" value={@settings_editor.publishing["ssh_remote_path"]} /></div></div>
</form>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_publishing"><%= translated("Save") %></button><button class="secondary" type="button" phx-click="clear_settings_publishing"><%= translated("Clear") %></button></div>
<div class="setting-actions"><button class="primary" type="button" phx-click="save_settings_publishing" phx-target={@myself}><%= translated("Save") %></button><button class="secondary" type="button" phx-click="clear_settings_publishing" phx-target={@myself}><%= translated("Clear") %></button></div>
</div>
<% end %>
@@ -342,7 +342,7 @@
<p class="setting-description"><%= agent.config_path || translated("Not supported in the rewrite yet") %></p>
</div>
<div class="setting-control">
<button class="secondary" type="button" phx-click="toggle_settings_mcp_agent" phx-value-agent={agent.id} disabled={not agent.supported?}>
<button class="secondary" type="button" phx-click="toggle_settings_mcp_agent" phx-target={@myself} phx-value-agent={agent.id} disabled={not agent.supported?}>
<%= if agent.configured?, do: translated("Remove"), else: translated("Add") %>
</button>
</div>

View File

@@ -6,7 +6,7 @@
<div class="style-theme-picker" role="group" aria-label={translated("Theme picker")}>
<%= for theme <- @style_editor.themes do %>
<button type="button" class={["style-theme-option", if(theme.name == @style_editor.selected_theme, do: "selected")]} phx-click="select_style_theme" phx-value-theme={theme.name} aria-pressed={theme.name == @style_editor.selected_theme}>
<button type="button" class={["style-theme-option", if(theme.name == @style_editor.selected_theme, do: "selected")]} phx-click="select_style_theme" phx-target={@myself} phx-value-theme={theme.name} aria-pressed={theme.name == @style_editor.selected_theme}>
<span class="style-theme-swatch">
<span class="style-theme-tones" aria-hidden="true">
<span class="style-theme-tone style-theme-tone-accent" style={"background: linear-gradient(135deg, #{theme.accent_color}, #{theme.dark_bg_color})"}></span>
@@ -22,13 +22,13 @@
<div class="style-apply-row">
<label class="style-preview-mode-control">
<span><%= translated("Preview Mode") %></span>
<select phx-change="change_style_preview_mode" name="mode">
<select phx-change="change_style_preview_mode" phx-target={@myself} name="mode">
<option value="auto" selected={@style_editor.preview_mode == "auto"}><%= translated("Auto") %></option>
<option value="light" selected={@style_editor.preview_mode == "light"}><%= translated("Light") %></option>
<option value="dark" selected={@style_editor.preview_mode == "dark"}><%= translated("Dark") %></option>
</select>
</label>
<button class="primary" type="button" phx-click="apply_style_theme" disabled={@style_editor.selected_theme == @style_editor.applied_theme}><%= translated("Apply Theme") %></button>
<button class="primary" type="button" phx-click="apply_style_theme" phx-target={@myself} disabled={@style_editor.selected_theme == @style_editor.applied_theme}><%= translated("Apply Theme") %></button>
</div>
<div class="style-preview-container">