Files
bDS2/lib/bds/desktop/shell_live/menu_editor.ex

444 lines
14 KiB
Elixir

defmodule BDS.Desktop.ShellLive.MenuEditor do
@moduledoc false
use Phoenix.LiveComponent
alias BDS.Desktop.ShellData
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(translated("menuEditor.tabTitle"), translated("menuEditor.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: translated("menuEditor.tabTitle"),
subtitle: translated("menuEditor.description")
})
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 %>
<li class="menu-editor-tree-item">
<% editing? = draft_item?(@menu_editor, item.item_id) %>
<% selected? = item.item_id == @menu_editor.selected_id %>
<div
class={[
"menu-editor-row",
if(selected?, do: "is-selected"),
if(editing?, do: "is-editing")
]}
data-testid="menu-editor-row"
data-menu-item-id={item.item_id}
data-menu-kind={item.kind}
data-menu-label={row_label(item, @menu_editor.category_titles)}
data-menu-can-drop-inside={to_string(item.kind == :submenu)}
data-selected={to_string(selected?)}
phx-click={unless(editing?, do: "menu_editor_select_item")}
phx-value-item_id={unless(editing?, do: item.item_id)}
phx-target={@myself}
style={"--menu-editor-depth: #{@depth};"}
>
<span class="menu-editor-row-handle" data-menu-drag-handle="true" title={translated("menuEditor.dragHandle")}>⋮⋮</span>
<span class="menu-editor-row-kind" title={kind_label(item.kind)} aria-label={kind_label(item.kind)}>
<.kind_icon kind={item.kind} />
</span>
<%= if editing? do %>
<div class="menu-editor-row-title is-editing">
<form
class="menu-editor-entry-form"
data-testid="menu-editor-entry-form"
phx-change="change_menu_editor_entry"
phx-submit="submit_menu_editor_entry"
phx-target={@myself}
>
<input
class="menu-editor-inline-input"
type="text"
name="menu_editor_entry[query]"
value={@menu_editor.draft_query}
placeholder={editing_placeholder(@menu_editor)}
autocomplete="off"
/>
<div class="menu-editor-inline-search">
<div class="menu-editor-inline-search-head">
<div>
<strong><%= editing_title(@menu_editor) %></strong>
<span><%= editing_hint(@menu_editor) %></span>
</div>
<div class="menu-editor-inline-actions">
<%= if @menu_editor.draft.type == :page do %>
<button class="menu-editor-inline-action" data-testid="menu-editor-create-submenu" type="submit">
<%= translated("menuEditor.addSubmenu") %>
</button>
<% else %>
<button class="menu-editor-inline-action" type="submit">
<%= translated("menuEditor.addCategoryArchive") %>
</button>
<% end %>
<button class="menu-editor-inline-action" type="button" phx-click="cancel_menu_editor_entry" phx-target={@myself}>
<%= translated("Cancel") %>
</button>
</div>
</div>
<%= if @menu_editor.draft.type == :page do %>
<div class="menu-editor-picker-list">
<%= if @menu_editor.filtered_pages == [] do %>
<div class="menu-editor-picker-state"><%= translated("menuEditor.pagePicker.empty") %></div>
<% else %>
<%= for post <- @menu_editor.filtered_pages do %>
<button
class="menu-editor-picker-item"
type="button"
phx-click="select_menu_editor_page"
phx-value-post_id={post.id}
phx-target={@myself}
>
<span><%= post.title %></span>
<small><%= post.slug %></small>
</button>
<% end %>
<% end %>
</div>
<% else %>
<div class="menu-editor-picker-list">
<%= if @menu_editor.filtered_categories == [] do %>
<div class="menu-editor-picker-state"><%= translated("menuEditor.categoryPicker.empty") %></div>
<% else %>
<%= for category <- @menu_editor.filtered_categories do %>
<button
class="menu-editor-picker-item"
type="button"
phx-click="select_menu_editor_category"
phx-value-name={category.name}
phx-target={@myself}
>
<span><%= category.title %></span>
<small><%= category.name %></small>
</button>
<% end %>
<% end %>
</div>
<% end %>
</div>
</form>
</div>
<% else %>
<span class="menu-editor-row-title"><%= row_label(item, @menu_editor.category_titles) %></span>
<% end %>
</div>
<%= if item.children != [] do %>
<ul class="menu-editor-tree-level">
<.menu_tree_level items={item.children} menu_editor={@menu_editor} depth={@depth + 1} myself={@myself} />
</ul>
<% end %>
</li>
<% end %>
"""
end
attr(:kind, :atom, required: true)
@spec kind_icon(term()) :: term()
def kind_icon(assigns) do
~H"""
<%= case @kind do %>
<% :home -> %>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 2 2 7v7h4V9h4v5h4V7L8 2z" /></svg>
<% :page -> %>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M3 2h7l3 3v9H3V2zm7 1.5V6h2.5L10 3.5z" /></svg>
<% :category_archive -> %>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 3h12v3H2V3zm1 4h10v6H3V7zm2 1v1h6V8H5z" /></svg>
<% _other -> %>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 3h12v2H2V3zm0 4h12v2H2V7zm0 4h12v2H2v-2z" /></svg>
<% end %>
"""
end
@spec translated(term(), term()) :: term()
def translated(text, bindings \\ %{}),
do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current())
@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: translated("menuEditor.type.home")
def kind_label(:page), do: translated("menuEditor.type.page")
def kind_label(:category_archive), do: translated("menuEditor.type.categoryArchive")
def kind_label(:submenu), do: translated("menuEditor.type.submenu")
defdelegate draft_item?(menu_editor, item_id), to: TreePredicates
@spec editing_title(term()) :: term()
def editing_title(%{draft: %{type: :category}}), do: translated("menuEditor.addCategoryArchive")
def editing_title(_menu_editor), do: translated("menuEditor.pagePicker.title")
@spec editing_hint(term()) :: term()
def editing_hint(%{draft: %{type: :category}}), do: translated("menuEditor.categoryPicker.hint")
def editing_hint(_menu_editor), do: translated("menuEditor.createHint")
@spec editing_placeholder(term()) :: term()
def editing_placeholder(%{draft: %{type: :category}}),
do: translated("menuEditor.newCategoryPlaceholder")
def editing_placeholder(_menu_editor), do: translated("menuEditor.newEntryPlaceholder")
end