From 4fee1a6333e7bca2efe704ef8921db850e6ebec1 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 3 May 2026 11:32:43 +0200 Subject: [PATCH] chore: converted post editor to live component --- lib/bds/desktop/shell_live.ex | 193 ++- lib/bds/desktop/shell_live/cli_sync.ex | 23 - lib/bds/desktop/shell_live/index.html.heex | 4 +- lib/bds/desktop/shell_live/post_editor.ex | 1143 +++++++++-------- .../post_editor_html/post_editor.html.heex | 34 +- test/bds/desktop/shell_live_test.exs | 35 +- test/bds/ui/shell_test.exs | 4 +- 7 files changed, 757 insertions(+), 679 deletions(-) diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index 2ee3e24..c85e4dd 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -160,17 +160,9 @@ defmodule BDS.Desktop.ShellLive do |> assign(:titlebar_menu_item_index, nil) |> assign(:tab_meta, %{}) |> assign(:project_menu_open, false) - |> assign(:sidebar_filters_by_view, %{}) - |> assign(:sidebar_filter_panels, %{}) - |> assign(:post_editor_drafts, %{}) - |> assign(:post_editor_active_languages, %{}) - |> assign(:post_editor_tag_queries, %{}) - |> assign(:post_editor_category_queries, %{}) - |> assign(:post_editor_quick_actions_open, %{}) - |> assign(:post_editor_modes, %{}) - |> assign(:post_editor_expanded, %{}) - |> assign(:post_editor_save_states, %{}) - |> assign(:media_editor_drafts, %{}) + |> assign(:sidebar_filters_by_view, %{}) + |> assign(:sidebar_filter_panels, %{}) + |> assign(:media_editor_drafts, %{}) |> assign(:media_editor_quick_actions_open, %{}) |> assign(:media_editor_post_pickers_open, %{}) |> assign(:media_editor_post_picker_queries, %{}) @@ -339,79 +331,6 @@ defmodule BDS.Desktop.ShellLive do {:noreply, reload_shell(socket, workbench)} end - def handle_event("change_post_editor", %{"post_editor" => params}, socket) do - {:noreply, PostEditor.update(socket, params, &reload_shell/2)} - end - - def handle_event("save_post_editor", %{"id" => post_id}, socket) do - {:noreply, - PostEditor.persist_socket(socket, post_id, :save, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("publish_post_editor", %{"id" => post_id}, socket) do - {:noreply, - PostEditor.persist_socket(socket, post_id, :publish, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("discard_post_editor", %{"id" => post_id}, socket) do - {:noreply, - PostEditor.discard_socket(socket, post_id, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("delete_post_editor", %{"id" => post_id}, socket) do - {:noreply, PostEditor.delete_socket(socket, post_id, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("set_post_editor_mode", %{"id" => post_id, "mode" => mode}, socket) do - {:noreply, PostEditor.set_mode(socket, post_id, mode, &reload_shell/2)} - end - - def handle_event("toggle_post_metadata", %{"id" => post_id}, socket) do - {:noreply, PostEditor.toggle_section(socket, post_id, :metadata, &reload_shell/2)} - end - - def handle_event("toggle_post_excerpt", %{"id" => post_id}, socket) do - {:noreply, PostEditor.toggle_section(socket, post_id, :excerpt, &reload_shell/2)} - end - - def handle_event( - "select_post_editor_language", - %{"id" => post_id, "language" => language}, - socket - ) do - {:noreply, PostEditor.select_language(socket, post_id, language, &reload_shell/2)} - end - - def handle_event("toggle_post_editor_quick_actions", %{"id" => post_id}, socket) do - {:noreply, PostEditor.toggle_quick_actions(socket, post_id, &reload_shell/2)} - end - - def handle_event("detect_post_editor_language", %{"id" => post_id}, socket) do - {:noreply, - PostEditor.detect_language(socket, post_id, &reload_shell/2, &append_output_entry/5)} - end - - def handle_event("add_post_editor_tag", %{"id" => post_id, "tag" => tag}, socket) do - {:noreply, PostEditor.add_list_value(socket, post_id, :tags, tag, &reload_shell/2)} - end - - def handle_event("remove_post_editor_tag", %{"id" => post_id, "tag" => tag}, socket) do - {:noreply, PostEditor.remove_list_value(socket, post_id, :tags, tag, &reload_shell/2)} - end - - def handle_event("add_post_editor_category", %{"id" => post_id, "category" => category}, socket) do - {:noreply, PostEditor.add_list_value(socket, post_id, :categories, category, &reload_shell/2)} - end - - def handle_event( - "remove_post_editor_category", - %{"id" => post_id, "category" => category}, - socket - ) do - {:noreply, - PostEditor.remove_list_value(socket, post_id, :categories, category, &reload_shell/2)} - end - def handle_event("change_media_editor", %{"media_editor" => params}, socket) do {:noreply, MediaEditor.update(socket, params, &reload_shell/2)} end @@ -769,11 +688,8 @@ defmodule BDS.Desktop.ShellLive do socket = case socket.assigns[:current_tab] do %{type: :post, id: post_id} when kind in ["ai_suggestions", "language_picker"] -> - assign( - socket, - :post_editor_quick_actions_open, - Map.put(socket.assigns.post_editor_quick_actions_open, post_id, false) - ) + send_update(__MODULE__.PostEditor, id: "post-editor-#{post_id}", action: :close_quick_actions) + socket %{type: :media, id: media_id} when kind in ["ai_suggestions", "language_picker", "confirm_delete"] -> @@ -868,12 +784,8 @@ defmodule BDS.Desktop.ShellLive do socket result -> - PostEditor.insert_content( - socket, - post_id, - ShellOverlayComponents.markdown_link(result.title, result.canonical_url), - &reload_shell/2 - ) + send(self(), {:post_editor_insert_content, post_id, ShellOverlayComponents.markdown_link(result.title, result.canonical_url)}) + socket end {%{kind: :insert_media}, %{type: :post, id: post_id}} -> @@ -889,7 +801,8 @@ defmodule BDS.Desktop.ShellLive do "[#{result.original_name}](bds-media://#{result.media_id})" end - PostEditor.insert_content(socket, post_id, syntax, &reload_shell/2) + send(self(), {:post_editor_insert_content, post_id, syntax}) + socket end _other -> @@ -913,11 +826,11 @@ defmodule BDS.Desktop.ShellLive do end if details do - PostEditor.insert_content(socket, post_id, details, &reload_shell/2) - else - socket + send(self(), {:post_editor_insert_content, post_id, details}) end + socket + _other -> socket end @@ -931,7 +844,8 @@ defmodule BDS.Desktop.ShellLive do socket = case {socket.assigns[:shell_overlay], current_tab} do {%{kind: :language_picker}, %{type: :post, id: post_id}} -> - PostEditor.translate(socket, post_id, code, &reload_shell/2, &append_output_entry/5) + send(self(), {:post_editor_translate, post_id, code}) + socket {%{kind: :language_picker}, %{type: :media, id: media_id}} -> MediaEditor.translate(socket, media_id, code, &reload_shell/2, &append_output_entry/5) @@ -952,13 +866,8 @@ defmodule BDS.Desktop.ShellLive do execute_sidebar_delete(socket, route, id) {%{kind: :ai_suggestions} = overlay, %{type: :post, id: post_id}} -> - PostEditor.apply_ai_suggestions( - socket, - post_id, - Overlay.selected_ai_fields(overlay), - &reload_shell/2, - &append_output_entry/5 - ) + send(self(), {:post_editor_apply_ai_suggestions, post_id, Overlay.selected_ai_fields(overlay)}) + socket {%{kind: :ai_suggestions} = overlay, %{type: :media, id: media_id}} -> MediaEditor.apply_ai_suggestions( @@ -1272,6 +1181,47 @@ defmodule BDS.Desktop.ShellLive do {:noreply, append_output_entry(socket, title, message, nil, level)} end + def handle_info({:post_editor_output, title, message, level}, socket) do + {:noreply, append_output_entry(socket, title, message, nil, level)} + end + + def handle_info({:post_editor_dirty, post_id, dirty?}, socket) do + workbench = + if dirty? do + BDS.UI.Workbench.mark_dirty(socket.assigns.workbench, :post, post_id) + else + BDS.UI.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) 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) 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) 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) do + send_update(PostEditor, id: "post-editor-#{post_id}", + action: :apply_ai_suggestions, + fields: fields + ) + + {:noreply, socket} + end + def handle_info(:reload_shell, socket) do {:noreply, reload_shell(socket, socket.assigns.workbench)} end @@ -1348,7 +1298,6 @@ defmodule BDS.Desktop.ShellLive do |> assign(:menu_groups, socket.assigns[:menu_groups] || TitlebarMenu.groups()) |> assign(:titlebar_menu_item_index, socket.assigns[:titlebar_menu_item_index]) |> assign(:current_tab, current_tab(workbench)) - |> assign_post_editor() |> assign_media_editor() |> assign_chat_editor() |> assign_import_editor() @@ -1394,10 +1343,6 @@ defmodule BDS.Desktop.ShellLive do Enum.find(tabs, &(&1.type == type and &1.id == id)) end - defp assign_post_editor(socket) do - PostEditor.assign_socket(socket) - end - defp assign_media_editor(socket) do MediaEditor.assign_socket(socket) end @@ -1562,7 +1507,8 @@ defmodule BDS.Desktop.ShellLive do defp shell_command?(action), do: not is_nil(shell_command_atom(action)) defp save_current_tab(%{assigns: %{current_tab: %{type: :post, id: post_id}}} = socket) do - PostEditor.persist_socket(socket, post_id, :save, &reload_shell/2, &append_output_entry/5) + send_update(PostEditor, id: "post-editor-#{post_id}", action: :save) + socket end defp save_current_tab(%{assigns: %{current_tab: %{type: :media, id: media_id}}} = socket) do @@ -1597,7 +1543,8 @@ defmodule BDS.Desktop.ShellLive do defp save_current_tab(socket), do: reload_shell(socket, socket.assigns.workbench) defp publish_current_tab(%{assigns: %{current_tab: %{type: :post, id: post_id}}} = socket) do - PostEditor.persist_socket(socket, post_id, :publish, &reload_shell/2, &append_output_entry/5) + send_update(PostEditor, id: "post-editor-#{post_id}", action: :publish) + socket end defp publish_current_tab(socket), do: reload_shell(socket, socket.assigns.workbench) @@ -1657,9 +1604,21 @@ defmodule BDS.Desktop.ShellLive do defp execute_sidebar_delete(socket, route, id) do case route do "post" -> - socket - |> assign(:shell_overlay, nil) - |> PostEditor.delete_socket(id, &reload_shell/2, &append_output_entry/5) + case Posts.delete_post(id) do + {:ok, :deleted} -> + workbench = BDS.UI.Workbench.close_tab(socket.assigns.workbench, :post, id) + + socket + |> assign(:shell_overlay, nil) + |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:post, id})) + |> reload_shell(workbench) + + {:error, reason} -> + socket + |> assign(:shell_overlay, nil) + |> append_output_entry(sidebar_delete_title("post"), inspect(reason), nil, "error") + |> reload_shell(socket.assigns.workbench) + end "media" -> socket diff --git a/lib/bds/desktop/shell_live/cli_sync.ex b/lib/bds/desktop/shell_live/cli_sync.ex index 6d9b43c..26a3046 100644 --- a/lib/bds/desktop/shell_live/cli_sync.ex +++ b/lib/bds/desktop/shell_live/cli_sync.ex @@ -46,29 +46,6 @@ defmodule BDS.Desktop.ShellLive.CliSync do |> assign(:workbench, workbench) |> assign(:shell_overlay, nil) |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:post, post_id})) - |> assign(:post_editor_drafts, Map.delete(socket.assigns.post_editor_drafts, post_id)) - |> assign( - :post_editor_active_languages, - Map.delete(socket.assigns.post_editor_active_languages, post_id) - ) - |> assign( - :post_editor_tag_queries, - Map.delete(socket.assigns.post_editor_tag_queries, post_id) - ) - |> assign( - :post_editor_category_queries, - Map.delete(socket.assigns.post_editor_category_queries, post_id) - ) - |> assign( - :post_editor_quick_actions_open, - Map.delete(socket.assigns.post_editor_quick_actions_open, post_id) - ) - |> assign(:post_editor_modes, Map.delete(socket.assigns.post_editor_modes, post_id)) - |> assign(:post_editor_expanded, Map.delete(socket.assigns.post_editor_expanded, post_id)) - |> assign( - :post_editor_save_states, - Map.delete(socket.assigns.post_editor_save_states, post_id) - ) {socket, workbench} end diff --git a/lib/bds/desktop/shell_live/index.html.heex b/lib/bds/desktop/shell_live/index.html.heex index 26eaea1..edd7083 100644 --- a/lib/bds/desktop/shell_live/index.html.heex +++ b/lib/bds/desktop/shell_live/index.html.heex @@ -382,8 +382,8 @@ <% else %> <%= cond do %> - <% @current_tab.type == :post and @post_editor -> %> - + <% @current_tab.type == :post -> %> + <.live_component module={PostEditor} id={"post-editor-#{@current_tab.id}"} current_tab={@current_tab} offline_mode={@offline_mode} /> <% @current_tab.type == :media and @media_editor -> %> diff --git a/lib/bds/desktop/shell_live/post_editor.ex b/lib/bds/desktop/shell_live/post_editor.ex index aebf950..6778fdf 100644 --- a/lib/bds/desktop/shell_live/post_editor.ex +++ b/lib/bds/desktop/shell_live/post_editor.ex @@ -1,32 +1,26 @@ defmodule BDS.Desktop.ShellLive.PostEditor do @moduledoc false - use Phoenix.Component + use Phoenix.LiveComponent alias BDS.{AI, Posts, Preview} alias BDS.Desktop.ShellData alias BDS.Desktop.ShellLive.PostEditor.{DraftManagement, ListValues, Persistence, PostMetadata} + alias BDS.Desktop.UILocale alias BDS.Posts.Post alias BDS.Tags - alias BDS.UI.Workbench import DraftManagement, only: [ current_draft: 4, - delete_nested_map: 3, editing_canonical_language?: 3, - maybe_update_draft: 7, normalize_language: 2, normalize_mode: 1, normalize_params: 3, persisted_form: 3, - put_draft_field: 6, - put_nested_map: 4, - put_query_state: 4, - query_value: 3, + persisted_form: 4, record_status: 1, record_title: 2, - reload_with_assigned_workbench: 2, save_state_for_action: 1, toggled_sections: 3 ] @@ -76,548 +70,301 @@ defmodule BDS.Desktop.ShellLive.PostEditor do embed_templates("post_editor_html/*") - @spec assign_socket(term()) :: term() - def assign_socket(socket) do - assigns = - Map.put( - socket.assigns, - :project_metadata, - project_metadata(socket.assigns.projects.active_project_id) - ) + @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() - assign(socket, :post_editor, build(assigns)) + {:ok, socket} end - @spec update(term(), term(), term()) :: term() - def update(socket, params, reload) do - case socket.assigns.current_tab do - %{type: :post, id: post_id} -> - case Posts.get_post(post_id) do - nil -> - socket + def update(%{action: :publish} = assigns, socket) do + socket = + socket + |> assign(Map.drop(assigns, [:action])) + |> do_publish() - %Post{} = post -> - metadata = project_metadata(post.project_id) - canonical_language = canonical_language(post, metadata) + {:ok, socket} + end - current_language = - Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language) + def update(%{action: :close_quick_actions} = assigns, socket) do + socket = + socket + |> assign(Map.drop(assigns, [:action])) + |> assign(:quick_actions_open?, false) + |> build_data() - requested_language = normalize_language(Map.get(params, "language"), current_language) + {:ok, socket} + end - next_language = - if current_language == canonical_language do - requested_language - else - current_language - end + def update(%{action: :insert_content, content: content} = assigns, socket) do + socket = + socket + |> assign(Map.drop(assigns, [:action, :content])) + |> Phoenix.LiveView.push_event("post-editor-insert-content", %{ + id: socket.assigns.post_id, + content: content + }) + |> assign(:shell_overlay, nil) - draft = normalize_params(params, current_language, next_language) - current = current_draft(socket.assigns, post, metadata, next_language) - dirty? = draft != current + {:ok, socket} + end - socket - |> put_query_state(post_id, :tags, Map.get(params, "tag_query", "")) - |> put_query_state(post_id, :categories, Map.get(params, "category_query", "")) - |> maybe_update_draft(post_id, post, current_language, next_language, draft, dirty?) - |> reload_with_assigned_workbench(reload) - end + def update(%{action: :translate, language: language} = assigns, socket) do + socket = + socket + |> assign(Map.drop(assigns, [:action, :language])) + |> do_translate(language) - _other -> - socket + {:ok, socket} + end + + def update(%{action: :apply_ai_suggestions, fields: fields} = assigns, socket) do + socket = + socket + |> assign(Map.drop(assigns, [:action, :fields])) + |> do_apply_ai_suggestions(fields) + + {:ok, socket} + end + + def update(assigns, socket) do + socket = + socket + |> assign(assigns) + |> ensure_state() + |> build_data() + + {:ok, socket} + end + + @spec render(map()) :: Phoenix.LiveView.Rendered.t() + @impl true + def render(%{post_editor: nil} = assigns), do: ~H"
" + + def render(assigns) do + post_editor(assigns) + end + + @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) :: + {:noreply, Phoenix.LiveView.Socket.t()} + @impl true + def handle_event("change_post_editor", %{"post_editor" => params}, socket) do + post_id = socket.assigns.post_id + current_language = socket.assigns.active_language + metadata = socket.assigns.project_metadata + post = socket.assigns.post + + requested_language = normalize_language(Map.get(params, "language"), current_language) + + next_language = + if current_language == socket.assigns.canonical_language do + requested_language + else + current_language + end + + draft = normalize_params(params, current_language, next_language) + current = component_current_draft(socket, post, metadata, next_language) + dirty? = draft != current + was_dirty? = socket.assigns.dirty? + + socket = + socket + |> assign(:tag_query, Map.get(params, "tag_query", "")) + |> assign(:category_query, Map.get(params, "category_query", "")) + |> maybe_update_component_draft(next_language, draft) + |> assign(:dirty?, dirty?) + |> build_data() + + if dirty? != was_dirty? do + notify_parent({:post_editor_dirty, post_id, dirty?}) end + + {:noreply, socket} end - @spec persist_socket(term(), term(), term(), term(), term()) :: term() - def persist_socket(socket, post_id, action, reload, append_output) do - case Posts.get_post(post_id) do - nil -> - socket - - %Post{} = post -> - metadata = project_metadata(post.project_id) - canonical_language = canonical_language(post, metadata) - - active_language = - Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language) - - draft = current_draft(socket.assigns, post, metadata, active_language) - - case persist(post, draft, active_language, metadata, action) do - {:ok, record} -> - workbench = Workbench.clear_dirty(socket.assigns.workbench, :post, post_id) - normalized_form = persisted_form(Posts.get_post!(post_id), metadata, active_language) - - socket - |> assign(:workbench, workbench) - |> assign( - :post_editor_drafts, - put_nested_map( - socket.assigns.post_editor_drafts, - post_id, - active_language, - normalized_form - ) - ) - |> assign( - :post_editor_save_states, - Map.put( - socket.assigns.post_editor_save_states, - post_id, - save_state_for_action(action) - ) - ) - |> assign( - :tab_meta, - Map.put(socket.assigns.tab_meta, {:post, post_id}, %{ - title: record_title(record, Posts.get_post!(post_id)), - subtitle: Atom.to_string(record_status(record)) - }) - ) - |> reload.(workbench) - - {:error, reason} -> - socket - |> append_output.(translated("Post"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end + def handle_event("save_post_editor", _params, socket) do + {:noreply, do_save(socket)} end - @spec discard_socket(term(), term(), term(), term()) :: term() - def discard_socket(socket, post_id, reload, append_output) do - case Posts.get_post(post_id) do - nil -> - socket - - %Post{} = post -> - metadata = project_metadata(post.project_id) - canonical_language = canonical_language(post, metadata) - - active_language = - Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language) - - case discard(post, active_language, metadata) do - {:ok, restored_post} -> - workbench = Workbench.clear_dirty(socket.assigns.workbench, :post, post_id) - - socket - |> assign(:workbench, workbench) - |> assign( - :post_editor_drafts, - delete_nested_map(socket.assigns.post_editor_drafts, post_id, active_language) - ) - |> assign( - :post_editor_save_states, - Map.put(socket.assigns.post_editor_save_states, post_id, :discarded) - ) - |> assign( - :tab_meta, - Map.put(socket.assigns.tab_meta, {:post, post_id}, %{ - title: restored_post.title || restored_post.slug || restored_post.id, - subtitle: Atom.to_string(restored_post.status || :draft) - }) - ) - |> reload.(workbench) - - {:error, reason} -> - socket - |> append_output.(translated("Post"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end + def handle_event("publish_post_editor", _params, socket) do + {:noreply, do_publish(socket)} end - @spec delete_socket(term(), term(), term(), term()) :: term() - def delete_socket(socket, post_id, reload, append_output) do - case Posts.delete_post(post_id) do - {:ok, :deleted} -> - workbench = Workbench.close_tab(socket.assigns.workbench, :post, post_id) - - socket - |> assign(:tab_meta, Map.delete(socket.assigns.tab_meta, {:post, post_id})) - |> assign(:post_editor_drafts, Map.delete(socket.assigns.post_editor_drafts, post_id)) - |> assign( - :post_editor_active_languages, - Map.delete(socket.assigns.post_editor_active_languages, post_id) - ) - |> assign( - :post_editor_tag_queries, - Map.delete(socket.assigns.post_editor_tag_queries, post_id) - ) - |> assign( - :post_editor_category_queries, - Map.delete(socket.assigns.post_editor_category_queries, post_id) - ) - |> assign( - :post_editor_quick_actions_open, - Map.delete(socket.assigns.post_editor_quick_actions_open, post_id) - ) - |> assign(:post_editor_modes, Map.delete(socket.assigns.post_editor_modes, post_id)) - |> assign(:post_editor_expanded, Map.delete(socket.assigns.post_editor_expanded, post_id)) - |> assign( - :post_editor_save_states, - Map.delete(socket.assigns.post_editor_save_states, post_id) - ) - |> reload.(workbench) - - {:error, reason} -> - socket - |> append_output.(translated("Post"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end + def handle_event("discard_post_editor", _params, socket) do + {:noreply, do_discard(socket)} end - @spec set_mode(term(), term(), term(), term()) :: term() - def set_mode(socket, post_id, mode, reload) do - workbench = socket.assigns.workbench + def handle_event("delete_post_editor", _params, socket) do + {:noreply, do_delete(socket)} + end + + def handle_event("set_post_editor_mode", %{"mode" => mode}, socket) do normalized_mode = normalize_mode(mode) if normalized_mode == :preview do - case Posts.get_post(post_id) do - %Post{} = post -> - _ = Preview.ensure_preview(post.project_id) - - _other -> - :ok + case socket.assigns.post do + %Post{} = post -> _ = Preview.ensure_preview(post.project_id) + _other -> :ok end end - socket - |> assign( - :post_editor_modes, - Map.put(socket.assigns.post_editor_modes, post_id, normalized_mode) - ) - |> reload.(workbench) - end - - @spec toggle_section(term(), term(), term(), term()) :: term() - def toggle_section(socket, post_id, section, reload) when section in [:metadata, :excerpt] do - workbench = socket.assigns.workbench - - socket - |> assign( - :post_editor_expanded, - Map.put( - socket.assigns.post_editor_expanded, - post_id, - toggled_sections(socket.assigns.post_editor_expanded, post_id, section) - ) - ) - |> reload.(workbench) - end - - @spec select_language(term(), term(), term(), term()) :: term() - def select_language(socket, post_id, language, reload) do - workbench = socket.assigns.workbench - - socket - |> assign( - :post_editor_active_languages, - Map.put( - socket.assigns.post_editor_active_languages, - post_id, - normalize_language(language, language) - ) - ) - |> reload.(workbench) - end - - @spec toggle_quick_actions(term(), term(), term()) :: term() - def toggle_quick_actions(socket, post_id, reload) do - workbench = socket.assigns.workbench - - socket - |> assign( - :post_editor_quick_actions_open, - Map.update(socket.assigns.post_editor_quick_actions_open, post_id, true, &(!&1)) - ) - |> reload.(workbench) - end - - @spec detect_language(term(), term(), term(), term()) :: term() - def detect_language(socket, post_id, reload, append_output) do - if Map.get(socket.assigns, :offline_mode, true) do + socket = socket - |> append_output.( - translated("Detect Language"), - translated("Automatic AI actions stay gated by airplane mode."), - nil, - "info" - ) - |> reload.(socket.assigns.workbench) - else - case Posts.get_post(post_id) do - nil -> - socket + |> assign(:mode, normalized_mode) + |> build_data() - %Post{} = post -> - metadata = project_metadata(post.project_id) - canonical_language = canonical_language(post, metadata) - - active_language = - Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language) - - draft = current_draft(socket.assigns, post, metadata, active_language) - text = Enum.join([Map.get(draft, "title", ""), Map.get(draft, "content", "")], "\n\n") - - case AI.detect_language(text) do - {:ok, %{language_code: language_code}} - when is_binary(language_code) and language_code != "" -> - socket - |> put_draft_field( - post_id, - post, - active_language, - "language", - normalize_language(language_code, canonical_language) - ) - |> reload_with_assigned_workbench(reload) - - {:error, reason} -> - socket - |> append_output.(translated("Detect Language"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - - _other -> - socket - |> append_output.( - translated("Detect Language"), - translated("Language detection failed."), - nil, - "error" - ) - |> reload.(socket.assigns.workbench) - end - end - end + {:noreply, socket} end - @spec translate(term(), term(), term(), term(), term()) :: term() - def translate(socket, post_id, language, reload, append_output) do - if Map.get(socket.assigns, :offline_mode, true) do + def handle_event("toggle_post_metadata", _params, socket) do + socket = socket - |> append_output.( - translated("Translate"), - translated("Automatic AI actions stay gated by airplane mode."), - nil, - "info" - ) - |> reload.(socket.assigns.workbench) - else - normalized_language = normalize_language(language, "") + |> assign(:expanded, toggled_sections(socket.assigns.expanded, socket.assigns.post_id, :metadata)) + |> build_data() - case AI.translate_post(post_id, normalized_language) do - {:ok, translation} -> - with {:ok, _saved_translation} <- - Posts.upsert_post_translation(post_id, normalized_language, %{ - title: translation.title, - excerpt: translation.excerpt, - content: translation.content - }) do - socket - |> assign( - :post_editor_active_languages, - Map.put(socket.assigns.post_editor_active_languages, post_id, normalized_language) - ) - |> assign( - :post_editor_drafts, - delete_nested_map(socket.assigns.post_editor_drafts, post_id, normalized_language) - ) - |> assign( - :post_editor_quick_actions_open, - Map.put(socket.assigns.post_editor_quick_actions_open, post_id, false) - ) - |> reload.(socket.assigns.workbench) - else - {:error, reason} -> - socket - |> append_output.(translated("Translate"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end + {:noreply, socket} + end - {:error, reason} -> - socket - |> append_output.(translated("Translate"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) + def handle_event("toggle_post_excerpt", _params, socket) do + socket = + socket + |> assign(:expanded, toggled_sections(socket.assigns.expanded, socket.assigns.post_id, :excerpt)) + |> build_data() + + {:noreply, socket} + end + + def handle_event("select_post_editor_language", %{"language" => language}, socket) do + socket = + socket + |> assign(:active_language, normalize_language(language, language)) + |> build_data() + + {:noreply, socket} + end + + def handle_event("toggle_post_editor_quick_actions", _params, socket) do + socket = + socket + |> assign(:quick_actions_open?, not socket.assigns.quick_actions_open?) + |> build_data() + + {:noreply, socket} + end + + def handle_event("detect_post_editor_language", _params, socket) do + {:noreply, do_detect_language(socket)} + end + + def handle_event("add_post_editor_tag", %{"tag" => tag}, socket) do + {:noreply, do_add_list_value(socket, :tags, tag)} + end + + def handle_event("remove_post_editor_tag", %{"tag" => tag}, socket) do + {:noreply, do_remove_list_value(socket, :tags, tag)} + end + + def handle_event("add_post_editor_category", %{"category" => category}, socket) do + {:noreply, do_add_list_value(socket, :categories, category)} + end + + def handle_event("remove_post_editor_category", %{"category" => category}, socket) do + {:noreply, do_remove_list_value(socket, :categories, category)} + end + + def handle_event("insert_content", %{"content" => content}, socket) do + socket = + socket + |> Phoenix.LiveView.push_event("post-editor-insert-content", %{ + id: socket.assigns.post_id, + content: content + }) + |> assign(:shell_overlay, nil) + + {:noreply, socket} + end + + def handle_event("close_quick_actions", _params, socket) do + socket = + socket + |> assign(:quick_actions_open?, false) + |> build_data() + + {:noreply, socket} + end + + defp component_current_draft(socket, post, metadata, active_language) do + persisted = persisted_form(post, metadata, active_language) + Map.get(socket.assigns.drafts, active_language, persisted) + end + + defp ensure_state(socket) do + post_id = socket.assigns.current_tab.id + post = Posts.get_post(post_id) + metadata = project_metadata(post && post.project_id) + canonical = if post, do: canonical_language(post, metadata), else: "en" + + defaults = %{ + post_id: post_id, + post: post, + project_metadata: metadata, + canonical_language: canonical, + active_language: canonical, + drafts: %{}, + tag_query: "", + category_query: "", + quick_actions_open?: false, + mode: :markdown, + expanded: %{metadata: post && blank?(post.title), excerpt: post && not blank?(post.excerpt)}, + save_state: :idle, + dirty?: false + } + + 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) end - @spec apply_ai_suggestions(term(), term(), term(), term(), term()) :: term() - def apply_ai_suggestions(socket, post_id, fields, reload, append_output) do - case Posts.get_post(post_id) do + defp build_data(socket) do + case socket.assigns.post do nil -> - socket - - %Post{} -> - attrs = - fields - |> Enum.reduce(%{}, fn field, acc -> - case field.key do - "title" -> Map.put(acc, :title, blank_to_nil(field.suggested_value)) - "excerpt" -> Map.put(acc, :excerpt, blank_to_nil(field.suggested_value)) - "slug" -> Map.put(acc, :slug, blank_to_nil(field.suggested_value)) - _other -> acc - end - end) - - if map_size(attrs) == 0 do - socket |> assign(:shell_overlay, nil) - else - case Posts.update_post(post_id, attrs) do - {:ok, updated_post} -> - metadata = project_metadata(updated_post.project_id) - - active_language = - Map.get( - socket.assigns.post_editor_active_languages, - post_id, - canonical_language(updated_post, metadata) - ) - - refreshed_form = persisted_form(updated_post, metadata, active_language) - - socket - |> assign( - :post_editor_drafts, - put_nested_map( - socket.assigns.post_editor_drafts, - post_id, - active_language, - refreshed_form - ) - ) - |> assign( - :post_editor_save_states, - Map.put(socket.assigns.post_editor_save_states, post_id, :dirty) - ) - |> assign(:shell_overlay, nil) - |> reload.(socket.assigns.workbench) - - {:error, reason} -> - socket - |> append_output.(translated("AI Suggestions"), inspect(reason), nil, "error") - |> reload.(socket.assigns.workbench) - end - end - end - end - - @spec insert_content(term(), term(), term(), term()) :: term() - def insert_content(socket, post_id, snippet, reload) do - socket - |> Phoenix.LiveView.push_event("post-editor-insert-content", %{id: post_id, content: snippet}) - |> assign(:shell_overlay, nil) - |> reload.(socket.assigns.workbench) - end - - @spec add_list_value(term(), term(), term(), term(), term()) :: term() - def add_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do - case Posts.get_post(post_id) do - nil -> - socket + assign(socket, :post_editor, nil) %Post{} = post -> - metadata = project_metadata(post.project_id) - canonical_language = canonical_language(post, metadata) - - active_language = - Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language) - - draft = current_draft(socket.assigns, post, metadata, active_language) - normalized = normalize_list_entry(value) - - if normalized == "" do - socket - else - ensure_list_value(post.project_id, kind, normalized) - - updated = - draft - |> Map.get(field_key(kind), "") - |> csv_to_list() - |> Kernel.++([normalized]) - |> Enum.uniq() - |> Enum.join(", ") - - socket - |> put_query_state(post_id, kind, "") - |> put_draft_field(post_id, post, active_language, field_key(kind), updated) - |> reload_with_assigned_workbench(reload) - end - end - end - - @spec remove_list_value(term(), term(), term(), term(), term()) :: term() - def remove_list_value(socket, post_id, kind, value, reload) when kind in [:tags, :categories] do - case Posts.get_post(post_id) do - nil -> - socket - - %Post{} = post -> - metadata = project_metadata(post.project_id) - canonical_language = canonical_language(post, metadata) - - active_language = - Map.get(socket.assigns.post_editor_active_languages, post_id, canonical_language) - - draft = current_draft(socket.assigns, post, metadata, active_language) - - updated = - draft - |> Map.get(field_key(kind), "") - |> csv_to_list() - |> Enum.reject(&(&1 == value)) - |> Enum.join(", ") - - socket - |> put_draft_field(post_id, post, active_language, field_key(kind), updated) - |> reload_with_assigned_workbench(reload) - end - end - - @spec build(term()) :: term() - def build(%{current_tab: %{type: :post, id: post_id}} = assigns) do - case Posts.get_post(post_id) do - nil -> - nil - - %Post{} = post -> - metadata = assigned_project_metadata(assigns) - canonical_language = canonical_language(post, metadata) - - active_language = - Map.get(assigns.post_editor_active_languages, post.id, canonical_language) - + metadata = socket.assigns.project_metadata + canonical_language = socket.assigns.canonical_language + active_language = socket.assigns.active_language translations = translations(post.id) - persisted = DraftManagement.persisted_form(post, metadata, active_language, translations) + persisted = persisted_form(post, metadata, active_language, translations) form = - assigns.post_editor_drafts - |> Map.get(post.id, %{}) + socket.assigns.drafts |> Map.get(active_language, persisted) - expanded = - Map.get(assigns.post_editor_expanded, post.id, %{ - metadata: blank?(post.title), - excerpt: not blank?(post.excerpt) - }) - + expanded = socket.assigns.expanded current_translation = Map.get(translations, active_language) - %{ + data = %{ id: post.id, display_title: display_title(form["title"], post.slug, post.id), subtitle: nil, slug: post.slug || post.id, status: post.status, - dirty?: Workbench.dirty?(assigns.workbench, :post, post.id), - save_state: Map.get(assigns.post_editor_save_states, post.id, :idle), - quick_actions_open?: Map.get(assigns.post_editor_quick_actions_open, post.id, false), + dirty?: socket.assigns.dirty?, + save_state: socket.assigns.save_state, + quick_actions_open?: socket.assigns.quick_actions_open?, metadata_expanded: Map.get(expanded, :metadata, false), excerpt_expanded: Map.get(expanded, :excerpt, false), - mode: Map.get(assigns.post_editor_modes, post.id, :markdown), + mode: socket.assigns.mode, editing_canonical?: editing_canonical_language?(translations, active_language, canonical_language), can_publish?: post.status == :draft, @@ -635,20 +382,20 @@ defmodule BDS.Desktop.ShellLive.PostEditor do tag_options: Tags.list_tags(post.project_id), tag_values: tag_values(form), tag_chips: tag_chips(form, Tags.list_tags(post.project_id)), - tag_query: query_value(assigns, :tags, post.id), + tag_query: socket.assigns.tag_query, tag_query_addable?: query_addable?( - query_value(assigns, :tags, post.id), + socket.assigns.tag_query, tag_values(form), Tags.list_tags(post.project_id), fn option -> option.name end ), category_values: category_values(form), - category_query: query_value(assigns, :categories, post.id), + category_query: socket.assigns.category_query, category_options: metadata.categories || [], category_query_addable?: query_addable?( - query_value(assigns, :categories, post.id), + socket.assigns.category_query, category_values(form), metadata.categories || [], & &1 @@ -657,13 +404,13 @@ defmodule BDS.Desktop.ShellLive.PostEditor do tag_suggestions( form, Tags.list_tags(post.project_id), - query_value(assigns, :tags, post.id) + socket.assigns.tag_query ), category_suggestions: category_suggestions( form, metadata.categories || [], - query_value(assigns, :categories, post.id) + socket.assigns.category_query ), gallery_count: gallery_count(form), preview_url: @@ -671,7 +418,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do post, active_language, canonical_language, - Map.get(assigns.post_editor_modes, post.id, :markdown) + socket.assigns.mode ), translation_flags: translation_flags(post, canonical_language, active_language, translations), @@ -679,10 +426,388 @@ defmodule BDS.Desktop.ShellLive.PostEditor do post_links: post_links(post.id), footer: footer(post, current_translation, active_language, canonical_language) } + + assign(socket, :post_editor, data) end end - def build(_assigns), do: nil + defp do_save(socket) do + post = socket.assigns.post + + case post do + nil -> + socket + + %Post{} = post -> + metadata = socket.assigns.project_metadata + active_language = socket.assigns.active_language + draft = component_current_draft(socket, post, metadata, active_language) + + case persist(post, draft, active_language, metadata, :save) do + {:ok, record} -> + refreshed_post = Posts.get_post!(post.id) + _refreshed_form = persisted_form(refreshed_post, metadata, active_language) + + socket = + socket + |> assign(:post, refreshed_post) + |> assign(:drafts, Map.delete(socket.assigns.drafts, active_language)) + |> assign(:save_state, save_state_for_action(:save)) + |> 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_parent({:post_editor_dirty, post.id, false}) + notify_output(socket, translated("Post"), translated("Post saved")) + socket + + {:error, reason} -> + notify_output(socket, translated("Post"), inspect(reason), "error") + |> build_data() + end + end + end + + defp do_publish(socket) do + post = socket.assigns.post + + case post do + nil -> + socket + + %Post{} = post -> + metadata = socket.assigns.project_metadata + active_language = socket.assigns.active_language + draft = component_current_draft(socket, post, metadata, active_language) + + case persist(post, draft, active_language, metadata, :publish) do + {:ok, record} -> + refreshed_post = Posts.get_post!(post.id) + _refreshed_form = persisted_form(refreshed_post, metadata, active_language) + + socket = + socket + |> assign(:post, refreshed_post) + |> assign(:drafts, Map.delete(socket.assigns.drafts, active_language)) + |> assign(:save_state, save_state_for_action(:publish)) + |> 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_parent({:post_editor_dirty, post.id, false}) + notify_output(socket, translated("Post"), translated("Post published")) + socket + + {:error, reason} -> + notify_output(socket, translated("Post"), inspect(reason), "error") + |> build_data() + end + end + end + + defp do_discard(socket) do + post = socket.assigns.post + + case post do + nil -> + socket + + %Post{} = post -> + metadata = socket.assigns.project_metadata + active_language = socket.assigns.active_language + + case discard(post, active_language, metadata) do + {:ok, restored_post} -> + socket = + socket + |> assign(:post, restored_post) + |> assign(:drafts, Map.delete(socket.assigns.drafts, active_language)) + |> assign(:save_state, :discarded) + |> assign(:dirty?, false) + |> build_data() + + notify_parent( + {:post_editor_tab_meta, post.id, + restored_post.title || restored_post.slug || restored_post.id, + Atom.to_string(restored_post.status || :draft)} + ) + + notify_parent({:post_editor_dirty, post.id, false}) + socket + + {:error, reason} -> + notify_output(socket, translated("Post"), inspect(reason), "error") + |> build_data() + end + end + end + + defp do_delete(socket) do + post_id = socket.assigns.post_id + + case Posts.delete_post(post_id) do + {:ok, :deleted} -> + notify_parent({:close_tab, :post, post_id}) + socket + + {:error, reason} -> + notify_output(socket, translated("Post"), inspect(reason), "error") + |> build_data() + end + end + + defp do_detect_language(socket) do + if Map.get(socket.assigns, :offline_mode, true) do + notify_output( + socket, + translated("Detect Language"), + translated("Automatic AI actions stay gated by airplane mode."), + "info" + ) + |> build_data() + else + post = socket.assigns.post + + case post do + nil -> + socket + + %Post{} = post -> + metadata = socket.assigns.project_metadata + active_language = socket.assigns.active_language + draft = component_current_draft(socket, post, metadata, active_language) + text = Enum.join([Map.get(draft, "title", ""), Map.get(draft, "content", "")], "\n\n") + + case AI.detect_language(text) do + {:ok, %{language_code: language_code}} + when is_binary(language_code) and language_code != "" -> + socket + |> put_component_draft_field("language", normalize_language(language_code, socket.assigns.canonical_language)) + |> build_data() + + {:error, reason} -> + notify_output(socket, translated("Detect Language"), inspect(reason), "error") + |> build_data() + + _other -> + notify_output( + socket, + translated("Detect Language"), + translated("Language detection failed."), + "error" + ) + |> build_data() + end + end + end + end + + defp do_translate(socket, language) do + if Map.get(socket.assigns, :offline_mode, true) do + notify_output( + socket, + translated("Translate"), + translated("Automatic AI actions stay gated by airplane mode."), + "info" + ) + |> build_data() + else + post_id = socket.assigns.post_id + normalized_language = normalize_language(language, "") + + case AI.translate_post(post_id, normalized_language) do + {:ok, translation} -> + with {:ok, _saved_translation} <- + Posts.upsert_post_translation(post_id, normalized_language, %{ + title: translation.title, + excerpt: translation.excerpt, + content: translation.content + }) do + socket = + socket + |> assign(:active_language, normalized_language) + |> assign(:drafts, Map.delete(socket.assigns.drafts, normalized_language)) + |> assign(:quick_actions_open?, false) + |> build_data() + + notify_parent({:post_editor_dirty, post_id, false}) + socket + else + {:error, reason} -> + notify_output(socket, translated("Translate"), inspect(reason), "error") + |> build_data() + end + + {:error, reason} -> + notify_output(socket, translated("Translate"), inspect(reason), "error") + |> build_data() + end + end + end + + defp do_apply_ai_suggestions(socket, fields) do + post_id = socket.assigns.post_id + + case Posts.get_post(post_id) do + nil -> + socket + + %Post{} = _post -> + attrs = + fields + |> Enum.reduce(%{}, fn field, acc -> + case field.key do + "title" -> Map.put(acc, :title, blank_to_nil(field.suggested_value)) + "excerpt" -> Map.put(acc, :excerpt, blank_to_nil(field.suggested_value)) + "slug" -> Map.put(acc, :slug, blank_to_nil(field.suggested_value)) + _other -> acc + end + end) + + if map_size(attrs) == 0 do + assign(socket, :shell_overlay, nil) + else + case Posts.update_post(post_id, attrs) do + {:ok, updated_post} -> + metadata = project_metadata(updated_post.project_id) + active_language = socket.assigns.active_language + refreshed_form = persisted_form(updated_post, metadata, active_language) + + socket = + socket + |> assign(:post, updated_post) + |> assign(:project_metadata, metadata) + |> assign(:drafts, Map.put(socket.assigns.drafts, active_language, refreshed_form)) + |> assign(:save_state, :dirty) + |> assign(:dirty?, true) + |> assign(:shell_overlay, nil) + |> build_data() + + notify_parent({:post_editor_dirty, post_id, true}) + socket + + {:error, reason} -> + notify_output(socket, translated("AI Suggestions"), inspect(reason), "error") + |> build_data() + end + end + end + end + + defp do_add_list_value(socket, kind, value) do + post = socket.assigns.post + + case post do + nil -> + socket + + %Post{} = post -> + metadata = socket.assigns.project_metadata + active_language = socket.assigns.active_language + draft = component_current_draft(socket, post, metadata, active_language) + normalized = normalize_list_entry(value) + + if normalized == "" do + socket + else + ensure_list_value(post.project_id, kind, normalized) + + updated = + draft + |> Map.get(field_key(kind), "") + |> csv_to_list() + |> Kernel.++([normalized]) + |> Enum.uniq() + |> Enum.join(", ") + + socket = + socket + |> assign_query(kind, "") + |> put_component_draft_field(field_key(kind), updated) + |> build_data() + + notify_parent({:post_editor_dirty, socket.assigns.post_id, true}) + assign(socket, :dirty?, true) + end + end + end + + defp do_remove_list_value(socket, kind, value) do + post = socket.assigns.post + + case post do + nil -> + socket + + %Post{} = post -> + metadata = socket.assigns.project_metadata + active_language = socket.assigns.active_language + draft = component_current_draft(socket, post, metadata, active_language) + + updated = + draft + |> Map.get(field_key(kind), "") + |> csv_to_list() + |> Enum.reject(&(&1 == value)) + |> Enum.join(", ") + + socket = + socket + |> put_component_draft_field(field_key(kind), updated) + |> build_data() + + notify_parent({:post_editor_dirty, socket.assigns.post_id, true}) + assign(socket, :dirty?, true) + end + end + + defp maybe_update_component_draft(socket, next_language, draft) do + current_language = socket.assigns.active_language + + cond do + current_language == next_language -> + assign(socket, :drafts, Map.put(socket.assigns.drafts, next_language, draft)) + + Map.has_key?(socket.assigns.drafts, current_language) -> + socket + |> assign(:active_language, next_language) + |> assign(:drafts, Map.put(socket.assigns.drafts, next_language, draft)) + + true -> + socket + |> assign(:active_language, next_language) + |> assign(:drafts, Map.put(%{}, next_language, draft)) + end + end + + defp put_component_draft_field(socket, field, value) do + active_language = socket.assigns.active_language + post = socket.assigns.post + metadata = socket.assigns.project_metadata + draft = current_draft(socket.assigns, post, metadata, active_language) + updated = Map.put(draft, field, value) + assign(socket, :drafts, Map.put(socket.assigns.drafts, active_language, updated)) + end + + 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}) + socket + end @spec post_status_label(term()) :: term() def post_status_label(status), do: ShellData.dashboard_status_label(status) @@ -700,7 +825,5 @@ defmodule BDS.Desktop.ShellLive.PostEditor do @spec translated(term(), term()) :: term() def translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) - - defp assigned_project_metadata(assigns), do: Map.get(assigns, :project_metadata, %{}) + do: ShellData.translate(text, bindings, UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex b/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex index 9438ae6..2172843 100644 --- a/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex +++ b/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex @@ -22,7 +22,7 @@ class="secondary quick-actions-btn" type="button" phx-click="toggle_post_editor_quick_actions" - phx-value-id={@post_editor.id} + phx-target={@myself} > <%= translated("Quick Actions") %> @@ -66,26 +66,26 @@ <%= if @post_editor.can_publish? do %> - <% end %> <%= if @post_editor.can_publish? do %> - <% end %> <%= if @post_editor.can_delete? do %> - <% end %> -
+ \ No newline at end of file + diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index 2d94fe3..a0e02ca 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -863,6 +863,7 @@ defmodule BDS.Desktop.ShellLiveTest do |> render_change() _html = render_hook(view, "native_menu_action", %{"action" => "save"}) + _html = render(view) saved_post = Posts.get_post!(post.id) assert saved_post.title == "Saved Through Menu" @@ -2096,26 +2097,35 @@ defmodule BDS.Desktop.ShellLiveTest do refute html =~ "gallery-button" refute html =~ "Desktop workbench content routed through the Elixir shell." - html = render_click(view, "toggle_post_editor_quick_actions", %{"id" => post.id}) + html = + view + |> element("[data-testid='post-editor'] .quick-actions-btn") + |> render_click() assert html =~ "quick-actions-menu" assert html =~ "quick-action-item" assert html =~ "quick-actions-divider" - html = render_click(view, "set_post_editor_mode", %{"id" => post.id, "mode" => "preview"}) + html = + view + |> element("[phx-click='set_post_editor_mode'][phx-value-mode='preview']") + |> render_click() assert html =~ ~s(data-testid="post-editor-preview") assert html =~ "editor-preview-frame" refute html =~ ~s(data-testid="post-editor-content") - html = render_click(view, "set_post_editor_mode", %{"id" => post.id, "mode" => "markdown"}) + html = + view + |> element("[phx-click='set_post_editor_mode'][phx-value-mode='markdown']") + |> render_click() assert html =~ ~s(data-testid="post-editor-content") assert html =~ ~s(data-monaco-language="markdown-with-macros") assert html =~ ~s(phx-hook="MonacoEditor") refute html =~ "post-editor-markdown-highlight" - html = + _html = view |> form("[data-testid='post-editor-form']", %{ post_editor: %{ @@ -2131,10 +2141,12 @@ defmodule BDS.Desktop.ShellLiveTest do }) |> render_change() + html = render(view) assert html =~ ~s(class="tab active dirty") assert html =~ "Updated Shell Post" - _html = render_click(view, "save_post_editor", %{"id" => post.id}) + _html = render_hook(view, "native_menu_action", %{"action" => "save"}) + _html = render(view) saved_post = Posts.get_post!(post.id) assert saved_post.title == "Updated Shell Post" @@ -2145,7 +2157,10 @@ defmodule BDS.Desktop.ShellLiveTest do assert saved_post.author == "Ada Lovelace" assert saved_post.language == "de" - html = render_click(view, "publish_post_editor", %{"id" => post.id}) + html = + view + |> element("[data-testid='post-publish-button']") + |> render_click() assert html =~ ~s(data-testid="post-status-badge") assert html =~ ~s(data-testid="post-delete-button") @@ -2169,10 +2184,14 @@ defmodule BDS.Desktop.ShellLiveTest do }) |> render_change() - _html = render_click(view, "save_post_editor", %{"id" => post.id}) + _html = render_hook(view, "native_menu_action", %{"action" => "save"}) + _html = render(view) assert Posts.get_post!(post.id).status == :draft - html = render_click(view, "discard_post_editor", %{"id" => post.id}) + html = + view + |> element("[data-testid='post-discard-button']") + |> render_click() discarded_post = Posts.get_post!(post.id) assert html =~ "Updated Shell Post" diff --git a/test/bds/ui/shell_test.exs b/test/bds/ui/shell_test.exs index c43666e..1bc3116 100644 --- a/test/bds/ui/shell_test.exs +++ b/test/bds/ui/shell_test.exs @@ -379,8 +379,8 @@ defmodule BDS.UI.ShellTest do "/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex" ) - assert template =~ "⚡<\/span>\s*<%= translated\("Quick Actions"\) %><\/span>/s,