defmodule BDS.Desktop.ShellLive.MenuEditor do @moduledoc false use Phoenix.LiveComponent use Gettext, backend: BDS.Gettext alias BDS.Desktop.ShellLive.MenuEditor.{ DraftManagement, PageCategory, State, TreeOps, TreePredicates } embed_templates("menu_editor_html/*") @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} @impl true def update(%{action: :save} = assigns, socket) do socket = assign(socket, Map.drop(assigns, [:action])) socket = do_save(socket) {:ok, socket} end def update(assigns, socket) do socket = socket |> assign(assigns) |> ensure_project_assigns() |> build_data() {:ok, socket} end @spec render(map()) :: Phoenix.LiveView.Rendered.t() @impl true def render(assigns) do menu_editor(assigns) end @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) :: {:noreply, Phoenix.LiveView.Socket.t()} @impl true def handle_event("menu_editor_select_item", %{"item_id" => item_id}, socket) do socket = socket |> State.update_state(fn state -> %{state | selected_id: item_id} end) |> build_data() {:noreply, socket} end def handle_event("change_menu_editor_entry", %{"menu_editor_entry" => params}, socket) do query = Map.get(params, "query", "") socket = socket |> State.update_state(fn state -> put_in(state, [:draft, :query], query) end) |> build_data() {:noreply, socket} end def handle_event("submit_menu_editor_entry", _params, socket) do socket = case DraftManagement.current_draft(socket.assigns) do %{type: :page} -> socket |> State.update_state(&DraftManagement.finalize_submenu_draft/1) |> build_data() %{type: :category} -> socket |> DraftManagement.confirm_category_draft(&State.update_state/2) |> build_data() _other -> socket end {:noreply, socket} end def handle_event("cancel_menu_editor_entry", _params, socket) do socket = socket |> State.update_state(&DraftManagement.cancel_draft/1) |> build_data() {:noreply, socket} end def handle_event("select_menu_editor_page", %{"post_id" => post_id}, socket) do socket = case PageCategory.page_post(socket.assigns.projects.active_project_id, post_id) do nil -> socket post -> socket |> State.update_state(&DraftManagement.assign_page_to_draft(&1, post)) |> build_data() end {:noreply, socket} end def handle_event("select_menu_editor_category", %{"name" => name}, socket) do project_id = socket.assigns.projects.active_project_id socket = case Enum.find(PageCategory.category_options(project_id), &(&1.name == name)) do nil -> socket category -> socket |> State.update_state(&DraftManagement.assign_category_to_draft(&1, category)) |> build_data() end {:noreply, socket} end def handle_event("menu_editor_toolbar_action", %{"action" => action}, socket) do socket = handle_toolbar_action(socket, action) {:noreply, socket} end def handle_event( "menu_editor_drop_item", %{ "drag_item_id" => drag_item_id, "target_item_id" => target_item_id, "position" => position }, socket ) do socket = socket |> State.update_state(&TreeOps.drop_selected(&1, drag_item_id, target_item_id, position)) |> build_data() {:noreply, socket} end def handle_event("menu_editor_keydown", %{"key" => "Escape"}, socket) do socket = socket |> State.update_state(&DraftManagement.cancel_draft/1) |> build_data() {:noreply, socket} end def handle_event("menu_editor_keydown", _params, socket) do {:noreply, socket} end defp handle_toolbar_action(socket, "add-entry") do socket |> State.update_state(&DraftManagement.start_page_draft/1) |> build_data() end defp handle_toolbar_action(socket, "add-category-archive") do socket |> State.update_state(&DraftManagement.start_category_draft/1) |> build_data() end defp handle_toolbar_action(socket, "save") do do_save(socket) end defp handle_toolbar_action(socket, "move-up") do socket |> State.update_state(&TreeOps.move_selected(&1, :up)) |> build_data() end defp handle_toolbar_action(socket, "move-down") do socket |> State.update_state(&TreeOps.move_selected(&1, :down)) |> build_data() end defp handle_toolbar_action(socket, "indent") do socket |> State.update_state(&TreeOps.indent_selected/1) |> build_data() end defp handle_toolbar_action(socket, "unindent") do socket |> State.update_state(&TreeOps.unindent_selected/1) |> build_data() end defp handle_toolbar_action(socket, "delete") do socket |> State.update_state(&TreeOps.delete_selected/1) |> build_data() end defp handle_toolbar_action(socket, _other), do: socket defp do_save(socket) do state = socket.assigns.menu_editor_state {:ok, _menu} = BDS.Menu.update_menu( state.project_id, Enum.map(state.items, &TreeOps.persisted_item/1) ) notify_output(dgettext("ui", "Blog Menu"), dgettext("ui", "Blog menu saved"), "info") socket |> build_data() end defp ensure_project_assigns(socket) do project_id = socket.assigns.project_id if Map.has_key?(socket.assigns, :projects) do socket else assign(socket, :projects, %{active_project_id: project_id}) end end defp build_data(socket) do tab_id = socket.assigns.current_tab.id state = State.ensure_state(socket.assigns) menu_editor = State.build(socket.assigns, state) tab_meta = Map.put(socket.assigns.tab_meta, {:menu_editor, tab_id}, %{ title: dgettext("ui", "Blog Menu"), subtitle: dgettext( "ui", "Manage the central blog navigation outline and save it to meta/menu.opml." ) }) socket |> assign(:menu_editor_state, state) |> assign(:menu_editor, menu_editor) |> assign(:tab_meta, tab_meta) end defp notify_output(title, message, level) do send(self(), {:menu_editor_output, title, message, level}) end attr(:menu_editor, :map, required: true) @spec menu_editor(term()) :: term() def menu_editor(assigns) attr(:items, :list, required: true) attr(:menu_editor, :map, required: true) attr(:depth, :integer, required: true) attr(:myself, :any, required: true) @spec menu_tree_level(term()) :: term() def menu_tree_level(assigns) do ~H""" <%= for item <- @items do %> <% end %> """ end attr(:kind, :atom, required: true) @spec kind_icon(term()) :: term() def kind_icon(assigns) do ~H""" <%= case @kind do %> <% :home -> %> <% :page -> %> <% :category_archive -> %> <% _other -> %> <% end %> """ end @spec row_label(term(), term()) :: term() def row_label(item, category_titles) do if item.kind == :category_archive do Map.get(category_titles || %{}, item.slug, item.label) else item.label end end @spec kind_label(term()) :: term() def kind_label(:home), do: dgettext("ui", "Home") def kind_label(:page), do: dgettext("ui", "Page") def kind_label(:category_archive), do: dgettext("ui", "Category Archive") def kind_label(:submenu), do: dgettext("ui", "Submenu") defdelegate draft_item?(menu_editor, item_id), to: TreePredicates @spec editing_title(term()) :: term() def editing_title(%{draft: %{type: :category}}), do: dgettext("ui", "Add Category Archive") def editing_title(_menu_editor), do: dgettext("ui", "Select Page") @spec editing_hint(term()) :: term() def editing_hint(%{draft: %{type: :category}}), do: dgettext("ui", "Select an existing category or press Enter to create a new archive entry") def editing_hint(_menu_editor), do: dgettext("ui", "Select a page below or press Enter to create a submenu") @spec editing_placeholder(term()) :: term() def editing_placeholder(%{draft: %{type: :category}}), do: dgettext("ui", "Type a category name") def editing_placeholder(_menu_editor), do: dgettext("ui", "Type a page title or submenu label") end