From 6c7fde6b9549a1b5ea63124898a3c09f93fffe34 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 3 May 2026 09:03:54 +0200 Subject: [PATCH] changed tags editor into live component --- lib/bds/desktop/shell_live.ex | 62 +--- lib/bds/desktop/shell_live/index.html.heex | 4 +- lib/bds/desktop/shell_live/tags_editor.ex | 322 ++++++++++-------- .../tags_editor_html/tags_editor.html.heex | 20 +- test/bds/desktop/shell_live_test.exs | 27 +- 5 files changed, 233 insertions(+), 202 deletions(-) diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index 168547e..80a1159 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -180,13 +180,9 @@ defmodule BDS.Desktop.ShellLive do |> 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(:tags_editor_selected, []) - |> assign(:tags_editor_new_tag, %{"name" => "", "color" => ""}) - |> assign(:tags_editor_edit_draft, %{}) - |> assign(:tags_editor_merge_target, "") - |> assign(:script_editor_drafts, %{}) + |> assign(:style_editor_theme, nil) + |> assign(:style_editor_preview_mode, "auto") + |> assign(:script_editor_drafts, %{}) |> assign(:template_editor_drafts, %{}) |> assign(:chat_editor_inputs, %{}) |> assign(:chat_model_selectors_open, %{}) @@ -682,42 +678,6 @@ defmodule BDS.Desktop.ShellLive do {:noreply, MenuEditor.handle_keydown(socket, key, &reload_shell/2)} end - def handle_event("toggle_tag_selection", %{"name" => tag_name}, socket) do - {:noreply, TagsEditor.toggle_selection(socket, tag_name, &reload_shell/2)} - end - - def handle_event("change_new_tag_editor", %{"new_tag" => params}, socket) do - {:noreply, TagsEditor.update_new_tag(socket, params, &reload_shell/2)} - end - - def handle_event("create_tag_editor", _params, socket) do - {:noreply, TagsEditor.create_tag(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("change_edit_tag_editor", %{"edit_tag" => params}, socket) do - {:noreply, TagsEditor.update_edit_tag(socket, params, &reload_shell/2)} - end - - def handle_event("save_tag_editor", _params, socket) do - {:noreply, TagsEditor.save_tag(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("delete_tag_editor", _params, socket) do - {:noreply, TagsEditor.delete_selected(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("change_merge_target", %{"target" => target}, socket) do - {:noreply, TagsEditor.update_merge_target(socket, target, &reload_shell/2)} - end - - def handle_event("merge_tags_editor", _params, socket) do - {:noreply, TagsEditor.merge_selected(socket, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("sync_tags_editor", _params, socket) do - {:noreply, TagsEditor.sync(socket, &reload_shell/2, &append_output_entry/5)} - end - def handle_event("change_script_editor", %{"script_editor" => params}, socket) do {:noreply, CodeEntityEditor.update_script(socket, params, &reload_shell/2)} end @@ -1483,6 +1443,14 @@ defmodule BDS.Desktop.ShellLive do end end + def handle_info({:tags_editor_output, title, message, level}, socket) do + {:noreply, append_output_entry(socket, title, message, nil, level)} + end + + def handle_info(:tags_changed, socket) do + {:noreply, reload_shell(socket, socket.assigns.workbench)} + end + @impl true def render(assigns) do UILocale.put(assigns.page_language) @@ -1555,7 +1523,6 @@ defmodule BDS.Desktop.ShellLive do |> assign_media_editor() |> assign_settings_editor() |> assign_menu_editor() - |> assign_tags_editor() |> assign_code_entity_editor() |> assign_chat_editor() |> assign_import_editor() @@ -1617,10 +1584,6 @@ defmodule BDS.Desktop.ShellLive do MenuEditor.assign_socket(socket) end - defp assign_tags_editor(socket) do - TagsEditor.assign_socket(socket) - end - defp assign_code_entity_editor(socket) do CodeEntityEditor.assign_socket(socket) end @@ -1801,7 +1764,8 @@ defmodule BDS.Desktop.ShellLive do end defp save_current_tab(%{assigns: %{current_tab: %{type: :tags}}} = socket) do - TagsEditor.save_tag(socket, &reload_shell/2, &append_output_entry/5) + send_update(TagsEditor, id: "tags-editor", action: :save) + socket end defp save_current_tab(%{assigns: %{current_tab: %{type: :scripts}}} = socket) do diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex index f6b6de0..7838ef5 100644 --- a/lib/bds/desktop/shell_live/index.html.heex +++ b/lib/bds/desktop/shell_live/index.html.heex @@ -397,8 +397,8 @@ <% @current_tab.type == :menu_editor and @menu_editor -> %> - <% @current_tab.type == :tags and @tags_editor -> %> - + <% @current_tab.type == :tags and @current_project -> %> + <.live_component module={TagsEditor} id="tags-editor" project_id={@current_project.id} current_tab={@current_tab} tab_meta={@tab_meta} /> <% @current_tab.type == :scripts and @script_editor -> %> diff --git a/lib/bds/desktop/shell_live/tags_editor.ex b/lib/bds/desktop/shell_live/tags_editor.ex index d480c22..b175727 100644 --- a/lib/bds/desktop/shell_live/tags_editor.ex +++ b/lib/bds/desktop/shell_live/tags_editor.ex @@ -1,12 +1,13 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do @moduledoc false - use Phoenix.Component + use Phoenix.LiveComponent import Ecto.Query - alias BDS.Desktop.ShellData alias BDS.{Repo, Tags} + alias BDS.Desktop.ShellData + alias BDS.Desktop.UILocale alias BDS.Posts.Post alias BDS.Tags.Tag alias BDS.Templates.Template @@ -15,14 +16,37 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do @tags_sections ~w(cloud manage merge) - @spec assign_socket(term()) :: term() - def assign_socket(socket) do - assign(socket, :tags_editor, build(socket.assigns)) + @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} + @impl true + def update(%{action: :save} = assigns, socket) do + socket = + socket + |> assign(Map.drop(assigns, [:action])) + |> do_save() + + {:ok, socket} end - @spec toggle_selection(term(), term(), term()) :: term() - def toggle_selection(socket, tag_name, reload) do - selected = Map.get(socket.assigns, :tags_editor_selected, []) + def update(assigns, socket) do + socket = + socket + |> assign(assigns) + |> load_data() + + {:ok, socket} + end + + @spec render(map()) :: Phoenix.LiveView.Rendered.t() + @impl true + def render(assigns) do + tags_editor(assigns) + end + + @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) :: + {:noreply, Phoenix.LiveView.Socket.t()} + @impl true + def handle_event("toggle_tag_selection", %{"name" => tag_name}, socket) do + selected = socket.assigns.tags_editor.selected next_selected = if tag_name in selected do @@ -31,26 +55,27 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do selected ++ [tag_name] end - socket - |> assign(:tags_editor_selected, next_selected) - |> maybe_seed_edit_draft(next_selected) - |> reload.(socket.assigns.workbench) + socket = + socket + |> put_in_tags_editor([:selected], next_selected) + |> maybe_seed_edit_draft(socket.assigns.project_id, next_selected) + + {:noreply, socket} end - @spec update_new_tag(term(), term(), term()) :: term() - def update_new_tag(socket, params, reload) do - socket - |> assign(:tags_editor_new_tag, %{ - "name" => Map.get(params, "name", ""), - "color" => Map.get(params, "color", "") - }) - |> reload.(socket.assigns.workbench) + def handle_event("change_new_tag_editor", %{"new_tag" => params}, socket) do + tags_editor = + Map.put(socket.assigns.tags_editor, :new_tag, %{ + "name" => Map.get(params, "name", ""), + "color" => Map.get(params, "color", "") + }) + + {:noreply, assign(socket, :tags_editor, tags_editor)} end - @spec create_tag(term(), term(), term()) :: term() - def create_tag(socket, reload, append_output) do - project_id = socket.assigns.projects.active_project_id - draft = Map.get(socket.assigns, :tags_editor_new_tag, %{}) + def handle_event("create_tag_editor", _params, socket) do + project_id = socket.assigns.project_id + draft = socket.assigns.tags_editor.new_tag case Tags.create_tag(%{ project_id: project_id, @@ -58,115 +83,83 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do color: blank_to_nil(Map.get(draft, "color")) }) do {:ok, _tag} -> + notify_parent(:tags_changed) + socket - |> assign(:tags_editor_new_tag, %{"name" => "", "color" => ""}) - |> reload.(socket.assigns.workbench) + |> put_in_tags_editor([:new_tag], %{"name" => "", "color" => ""}) + |> load_data() + |> noreply() {:error, reason} -> - socket - |> append_output.(translated("Tags"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) + notify_output(translated("Tags"), inspect(reason), "error") + {:noreply, socket} end end - @spec update_edit_tag(term(), term(), term()) :: term() - def update_edit_tag(socket, params, reload) do - socket - |> assign(:tags_editor_edit_draft, %{ - "name" => Map.get(params, "name", ""), - "color" => Map.get(params, "color", ""), - "post_template_slug" => Map.get(params, "post_template_slug", "") - }) - |> reload.(socket.assigns.workbench) + def handle_event("change_edit_tag_editor", %{"edit_tag" => params}, socket) do + tags_editor = + Map.put(socket.assigns.tags_editor, :edit_draft, %{ + "name" => Map.get(params, "name", ""), + "color" => Map.get(params, "color", ""), + "post_template_slug" => Map.get(params, "post_template_slug", "") + }) + + {:noreply, assign(socket, :tags_editor, tags_editor)} end - @spec save_tag(term(), term(), term()) :: term() - def save_tag(socket, reload, append_output) do - selected = Map.get(socket.assigns, :tags_editor_selected, []) - draft = Map.get(socket.assigns, :tags_editor_edit_draft, %{}) + def handle_event("save_tag_editor", _params, socket) do + {:noreply, do_save(socket)} + end - case selected do + def handle_event("delete_tag_editor", _params, socket) do + case socket.assigns.tags_editor.selected do [tag_name] -> case Repo.get_by(Tag, - project_id: socket.assigns.projects.active_project_id, + project_id: socket.assigns.project_id, name: tag_name ) do nil -> - reload.(socket, socket.assigns.workbench) - - %Tag{} = tag -> - with {:ok, _updated_tag} <- - Tags.update_tag(tag.id, %{ - color: blank_to_nil(Map.get(draft, "color")), - post_template_slug: blank_to_nil(Map.get(draft, "post_template_slug")) - }), - {:ok, renamed_tag} <- maybe_rename_tag(tag, Map.get(draft, "name", tag.name)) do - socket - |> assign(:tags_editor_selected, [renamed_tag.name]) - |> maybe_seed_edit_draft([renamed_tag.name]) - |> reload.(socket.assigns.workbench) - else - {:error, reason} -> - socket - |> append_output.(translated("Tags"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - - _other -> - reload.(socket, socket.assigns.workbench) - end - end - - @spec delete_selected(term(), term(), term()) :: term() - def delete_selected(socket, reload, append_output) do - case Map.get(socket.assigns, :tags_editor_selected, []) do - [tag_name] -> - case Repo.get_by(Tag, - project_id: socket.assigns.projects.active_project_id, - name: tag_name - ) do - nil -> - reload.(socket, socket.assigns.workbench) + {:noreply, socket} %Tag{} = tag -> case Tags.delete_tag(tag.id) do {:ok, _deleted} -> + notify_parent(:tags_changed) + socket - |> assign(:tags_editor_selected, []) - |> assign(:tags_editor_edit_draft, %{}) - |> reload.(socket.assigns.workbench) + |> put_in_tags_editor([:selected], []) + |> put_in_tags_editor([:edit_draft], %{}) + |> load_data() + |> noreply() {:error, reason} -> - socket - |> append_output.(translated("Tags"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) + notify_output(translated("Tags"), inspect(reason), "error") + {:noreply, socket} end end _other -> - reload.(socket, socket.assigns.workbench) + {:noreply, socket} end end - @spec update_merge_target(term(), term(), term()) :: term() - def update_merge_target(socket, target, reload) do - socket - |> assign(:tags_editor_merge_target, to_string(target || "")) - |> reload.(socket.assigns.workbench) + def handle_event("change_merge_target", %{"target" => target}, socket) do + tags_editor = + Map.put(socket.assigns.tags_editor, :merge_target, to_string(target || "")) + + {:noreply, assign(socket, :tags_editor, tags_editor)} end - @spec merge_selected(term(), term(), term()) :: term() - def merge_selected(socket, reload, append_output) do - selected = Map.get(socket.assigns, :tags_editor_selected, []) - target_name = Map.get(socket.assigns, :tags_editor_merge_target, "") + def handle_event("merge_tags_editor", _params, socket) do + selected = socket.assigns.tags_editor.selected + target_name = socket.assigns.tags_editor.merge_target cond do length(selected) < 2 or target_name == "" -> - reload.(socket, socket.assigns.workbench) + {:noreply, socket} true -> - project_id = socket.assigns.projects.active_project_id + project_id = socket.assigns.project_id tags = Repo.all( @@ -178,51 +171,90 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do case target do nil -> - reload.(socket, socket.assigns.workbench) + {:noreply, socket} _target -> case Tags.merge_tags(Enum.map(sources, & &1.id), target.id) do {:ok, _merged} -> + notify_parent(:tags_changed) + socket - |> assign(:tags_editor_selected, [target.name]) - |> assign(:tags_editor_merge_target, target.name) - |> maybe_seed_edit_draft([target.name]) - |> reload.(socket.assigns.workbench) + |> put_in_tags_editor([:selected], [target.name]) + |> put_in_tags_editor([:merge_target], target.name) + |> maybe_seed_edit_draft(project_id, [target.name]) + |> load_data() + |> noreply() {:error, reason} -> - socket - |> append_output.(translated("Tags"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) + notify_output(translated("Tags"), inspect(reason), "error") + {:noreply, socket} end end end end - @spec sync(term(), term(), term()) :: term() - def sync(socket, reload, append_output) do - case Tags.sync_tags_from_posts(socket.assigns.projects.active_project_id) do - {:ok, _tags} -> reload.(socket, socket.assigns.workbench) + def handle_event("sync_tags_editor", _params, socket) do + case Tags.sync_tags_from_posts(socket.assigns.project_id) do + {:ok, _tags} -> + notify_parent(:tags_changed) + {:noreply, load_data(socket)} + {:error, reason} -> - socket - |> append_output.(translated("Tags"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) + notify_output(translated("Tags"), inspect(reason), "error") + {:noreply, socket} end end - @spec build(term()) :: term() - def build(%{current_tab: %{type: :tags}} = assigns) do - project_id = assigns.projects.active_project_id + defp do_save(socket) do + selected = socket.assigns.tags_editor.selected + draft = socket.assigns.tags_editor.edit_draft + project_id = socket.assigns.project_id + + case selected do + [tag_name] -> + case Repo.get_by(Tag, project_id: project_id, name: tag_name) do + nil -> + socket + + %Tag{} = tag -> + with {:ok, _updated_tag} <- + Tags.update_tag(tag.id, %{ + color: blank_to_nil(Map.get(draft, "color")), + post_template_slug: blank_to_nil(Map.get(draft, "post_template_slug")) + }), + {:ok, renamed_tag} <- maybe_rename_tag(tag, Map.get(draft, "name", tag.name)) do + notify_parent(:tags_changed) + + socket + |> put_in_tags_editor([:selected], [renamed_tag.name]) + |> maybe_seed_edit_draft(project_id, [renamed_tag.name]) + |> load_data() + else + {:error, reason} -> + notify_output(translated("Tags"), inspect(reason), "error") + socket + end + end + + _other -> + socket + end + end + + defp load_data(socket) do + project_id = socket.assigns.project_id tags = Repo.all(from tag in Tag, where: tag.project_id == ^project_id, order_by: [asc: tag.name]) counts = tag_counts(project_id) - selected = Map.get(assigns, :tags_editor_selected, []) + selected = Map.get(socket.assigns, :tags_editor, %{}) |> Map.get(:selected, []) edit_tag = if length(selected) == 1, do: Enum.find(tags, &(&1.name == hd(selected))), else: nil - edit_draft = Map.get(assigns, :tags_editor_edit_draft, edit_draft(edit_tag)) + edit_draft = + Map.get(socket.assigns, :tags_editor, %{}) |> Map.get(:edit_draft, edit_draft(edit_tag)) templates = Repo.all( @@ -232,27 +264,39 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do select: %{slug: template.slug, title: template.title} ) - selected_section = current_tags_section(assigns) + selected_section = current_tags_section(socket.assigns) - %{ + data = %{ tags: Enum.map(tags, fn tag -> %{name: tag.name, color: tag.color, count: Map.get(counts, tag.name, 0)} end), selected: selected, - new_tag: Map.get(assigns, :tags_editor_new_tag, %{"name" => "", "color" => ""}), + new_tag: + Map.get(socket.assigns, :tags_editor, %{}) |> Map.get(:new_tag, %{"name" => "", "color" => ""}), edit_draft: edit_draft, templates: templates, - merge_target: Map.get(assigns, :tags_editor_merge_target, List.first(selected) || ""), + merge_target: + Map.get(socket.assigns, :tags_editor, %{}) |> Map.get(:merge_target, List.first(selected) || ""), selected_section: selected_section } + + assign(socket, :tags_editor, data) end - def build(_assigns), do: nil + defp put_in_tags_editor(socket, [key], value) do + assign(socket, :tags_editor, Map.put(socket.assigns.tags_editor, key, value)) + end - @spec translated(term(), term()) :: term() - def translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) + defp noreply(socket), do: {:noreply, socket} + + defp notify_parent(message) do + send(self(), message) + end + + defp notify_output(title, message, level) do + send(self(), {:tags_editor_output, title, message, level}) + end @spec tag_font_size(term(), term()) :: term() def tag_font_size(count, counts) do @@ -274,14 +318,18 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do |> Enum.join("; ") end - defp maybe_seed_edit_draft(socket, [tag_name]) do - case Repo.get_by(Tag, project_id: socket.assigns.projects.active_project_id, name: tag_name) do - %Tag{} = tag -> assign(socket, :tags_editor_edit_draft, edit_draft(tag)) - _other -> assign(socket, :tags_editor_edit_draft, %{}) + defp maybe_seed_edit_draft(socket, project_id, [tag_name]) do + case Repo.get_by(Tag, project_id: project_id, name: tag_name) do + %Tag{} = tag -> + put_in_tags_editor(socket, [:edit_draft], edit_draft(tag)) + + _other -> + put_in_tags_editor(socket, [:edit_draft], %{}) end end - defp maybe_seed_edit_draft(socket, _selected), do: assign(socket, :tags_editor_edit_draft, %{}) + defp maybe_seed_edit_draft(socket, _project_id, _selected), + do: put_in_tags_editor(socket, [:edit_draft], %{}) defp edit_draft(nil), do: %{} @@ -314,13 +362,12 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do end end - defp current_tab_meta(assigns) do - case Map.get(assigns, :current_tab) do - %{type: type, id: id} -> Map.get(assigns[:tab_meta] || %{}, {type, id}, %{}) - _other -> %{} - end + defp current_tab_meta(%{current_tab: %{type: type, id: id}, tab_meta: tab_meta}) do + Map.get(tab_meta || %{}, {type, id}, %{}) end + defp current_tab_meta(_assigns), do: %{} + defp tag_counts(project_id) do Repo.all(from post in Post, where: post.project_id == ^project_id, select: post.tags) |> List.flatten() @@ -336,4 +383,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do trimmed -> trimmed end end + + defp translated(text, bindings \\ %{}), + do: ShellData.translate(text, bindings, UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex b/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex index fb285ce..17888a4 100644 --- a/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex +++ b/lib/bds/desktop/shell_live/tags_editor_html/tags_editor.html.heex @@ -18,12 +18,12 @@ <%= if Enum.empty?(@tags_editor.tags) do %>

<%= translated("No tags found") %>

- +
<% else %>
<%= for tag <- @tags_editor.tags do %> - <% end %> @@ -35,16 +35,16 @@

<%= translated("Create / Edit") %>

-
+
- +
<%= if @tags_editor.edit_draft != %{} do %> -
+
@@ -54,8 +54,8 @@ <% end %> - - + +
<% end %> @@ -67,12 +67,12 @@
- <%= for tag_name <- @tags_editor.selected do %> <% end %> - +
@@ -81,7 +81,7 @@

<%= translated("Sync") %>

- +
diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index 3e69be3..418bf45 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -368,17 +368,34 @@ defmodule BDS.Desktop.ShellLiveTest do |> element("[data-testid='sidebar-open-item'][data-item-id='tags-cloud']") |> render_click() - html = render_click(view, "sync_tags_editor", %{}) + html = + view + |> element("#tags-section-sync button[phx-click='sync_tags_editor']") + |> render_click() assert Enum.map(Tags.list_tags(project.id), & &1.name) == ["Alpha", "Beta"] assert html =~ "Alpha" assert html =~ "Beta" - _html = render_click(view, "toggle_tag_selection", %{"name" => "Alpha"}) - _html = render_click(view, "toggle_tag_selection", %{"name" => "Beta"}) - _html = render_change(view, "change_merge_target", %{"target" => "Alpha"}) + _html = + view + |> element("#tags-editor-shell button[phx-click='toggle_tag_selection'][phx-value-name='Alpha']") + |> render_click() - html = render_click(view, "merge_tags_editor", %{}) + _html = + view + |> element("#tags-editor-shell button[phx-click='toggle_tag_selection'][phx-value-name='Beta']") + |> render_click() + + _html = + view + |> element("#tags-editor-shell select[phx-change='change_merge_target']") + |> render_change(%{"target" => "Alpha"}) + + html = + view + |> element("#tags-editor-shell button[phx-click='merge_tags_editor']") + |> render_click() assert Enum.map(Tags.list_tags(project.id), & &1.name) == ["Alpha"] assert Repo.get!(Post, post.id).tags == ["Alpha"]