defmodule BDS.Desktop.ShellLive.PanelRenderer do @moduledoc false use Phoenix.Component alias BDS.Desktop.ShellData alias BDS.Git alias BDS.Media alias BDS.Media.Media, as: MediaRecord alias BDS.PostLinks alias BDS.Posts alias BDS.Posts.Post alias BDS.UI.TaskGrouping use Gettext, backend: BDS.Gettext @doc "Render the active panel tab body." def render_panel_body(assigns) do case assigns.workbench.panel.active_tab do :tasks -> render_task_entries(assigns) :output -> render_output_entries(assigns) :post_links -> render_post_links(assigns) :git_log -> render_git_log(assigns) other -> render_generic_panel(assigns, other) end end @doc "Render the editor toolbar for the current tab." def render_editor_toolbar(assigns) do buttons = editor_toolbar_buttons(assigns.current_tab) assigns = assign(assigns, :editor_toolbar_buttons, buttons) ~H""" <%= if Enum.any?(@editor_toolbar_buttons) do %>
<%= for button <- @editor_toolbar_buttons do %> <% end %>
<% end %> """ end defp render_task_entries(assigns) do entries = TaskGrouping.build_task_entries(Map.get(assigns.task_status, :tasks, [])) collapsed = Map.get(assigns, :collapsed_task_groups) || MapSet.new() assigns = assigns |> assign(:task_entries, entries) |> assign(:collapsed_task_groups, collapsed) ~H""" <%= if Enum.empty?(@task_entries) do %>
<%= dgettext("ui", "Tasks") %> <%= dgettext("ui", "No background tasks running") %>
<% else %>
<%= for entry <- @task_entries do %> <%= case entry do %> <% {:single, task} -> %> {render_task_row(assigns, task, false)} <% {:group, group_id, group_name, tasks} -> %> {render_task_group(assigns, group_id, group_name, tasks)} <% end %> <% end %>
<% end %> """ end defp render_task_group(assigns, group_id, group_name, tasks) do summary = TaskGrouping.summarize_task_group(tasks) expanded = not MapSet.member?(assigns.collapsed_task_groups, group_id) assigns = assigns |> assign(:group_id, group_id) |> assign(:group_name, group_name) |> assign(:group_tasks, tasks) |> assign(:group_expanded, expanded) |> assign(:group_meta, task_group_meta(summary)) ~H"""
<%= if @group_expanded do %> <%= for task <- @group_tasks do %> {render_task_row(assigns, task, true)} <% end %> <% end %>
""" end defp render_task_row(assigns, task, is_child) do assigns = assigns |> assign(:task, task) |> assign(:task_child?, is_child) ~H"""
<%= @task.name %> <%= Map.get(@task, :status_label, @task.status |> to_string() |> String.capitalize()) %>
<%= @task.message || @task.group_name || "" %> <%= if is_number(@task.progress) do %>
<%= Map.get(@task, :progress_label, progress_percent(@task.progress)) %>
<% end %> <%= if @task.status == :running do %>
<% end %>
""" end # "NN% · X Running · Y Pending" — the breakdown only lists non-zero counts, # like the old app's summarizeTaskGroup rendering. defp task_group_meta(summary) do parts = [] |> maybe_append_breakdown(summary.running, dgettext("ui", "Running")) |> maybe_append_breakdown(summary.pending, dgettext("ui", "Pending")) suffix = if parts == [], do: "", else: " · " <> Enum.join(parts, " · ") progress_percent(summary.progress) <> suffix end defp maybe_append_breakdown(parts, count, _label) when count <= 0, do: parts defp maybe_append_breakdown(parts, count, label), do: parts ++ ["#{count} #{label}"] defp render_output_entries(assigns) do ~H""" <%= if Enum.empty?(@output_entries) do %>
<%= dgettext("ui", "Output") %> <%= dgettext("ui", "No shell output yet") %>
<% else %>
<%= for entry <- @output_entries do %>
<%= entry.title %> <%= entry.message %> <%= if present?(entry.details) do %> <%= entry.details %> <% end %>
<% end %>
<% end %> """ end defp render_post_links(assigns) do panel_links = assigns[:panel_post_links] || %{backlinks: [], outlinks: []} assigns = assigns |> assign(:backlinks, Map.get(panel_links, :backlinks, [])) |> assign(:outlinks, Map.get(panel_links, :outlinks, [])) ~H""" <%= if Enum.empty?(@backlinks) and Enum.empty?(@outlinks) do %>
<%= dgettext("ui", "Post Links") %> <%= dgettext("ui", "No post links yet") %>
<% else %>
<%= if Enum.any?(@backlinks) do %>
<%= dgettext("ui", "Backlinks") %>
<%= for entry <- @backlinks do %> <% end %> <% end %> <%= if Enum.any?(@outlinks) do %>
<%= dgettext("ui", "Links To") %>
<%= for entry <- @outlinks do %> <% end %> <% end %>
<% end %> """ end defp render_git_log(assigns) do entries = assigns[:panel_git_entries] || [] assigns = assign(assigns, :git_entries, entries) ~H""" <%= if Enum.empty?(@git_entries) do %>
<%= dgettext("ui", "Git Log") %> <%= dgettext("ui", "No git history yet") %>
<% else %>
<%= for entry <- @git_entries do %>
<%= short_commit_hash(entry.hash) %> <%= entry.subject || dgettext("ui", "No commit subject") %> <%= entry.hash %>
<% end %>
<% end %> """ end defp render_generic_panel(assigns, tab) do assigns = assign(assigns, :panel_label, ShellData.route_label(tab)) ~H"""
<%= @panel_label %> <%= dgettext("ui", "The shared lower panel is available for tasks, output, git details, and editor-specific diagnostics.") %>
""" end def fetch_post_link_entries(assigns) do case assigns.current_tab do %{type: :post, id: post_id} -> %{ backlinks: related_posts(PostLinks.list_incoming_links(post_id), :source_post_id), outlinks: related_posts(PostLinks.list_outgoing_links(post_id), :target_post_id) } _other -> %{backlinks: [], outlinks: []} end end defp related_posts(links, key) do Enum.map(links, fn link -> case Posts.get_post(Map.fetch!(link, key)) do %Post{} = post -> %{ id: post.id, title: post.title || post.slug || post.id, text: link.link_text || post.slug || post.id } _other -> nil end end) |> Enum.reject(&is_nil/1) end def fetch_git_log_entries(assigns) do case git_history_target(assigns.current_tab) do nil -> [] {project_id, file_path} -> case Git.file_history(project_id, file_path) do {:ok, %{commits: commits}} -> commits _other -> [] end end end defp git_history_target(%{type: :post, id: post_id}) do case Posts.get_post(post_id) do %Post{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] -> {project_id, file_path} _other -> nil end end defp git_history_target(%{type: :media, id: media_id}) do case Media.get_media(media_id) do %MediaRecord{project_id: project_id, file_path: file_path} when file_path not in [nil, ""] -> {project_id, file_path} _other -> nil end end defp git_history_target(_tab), do: nil def editor_toolbar_buttons(nil), do: [] def editor_toolbar_buttons(%{type: :post}) do [ %{kind: "ai_suggestions", label: "AI Suggestions", destructive: false}, %{kind: "insert_link", label: "Insert Link", destructive: false}, %{kind: "insert_media", label: "Insert Media", destructive: false}, %{kind: "language_picker", label: "Translate", destructive: false}, %{kind: "gallery", label: "Gallery", destructive: false} ] end def editor_toolbar_buttons(%{type: :media}) do [ %{kind: "ai_suggestions", label: "AI Suggestions", destructive: false}, %{kind: "language_picker", label: "Translate", destructive: false}, %{kind: "confirm_delete", label: "Delete Media", destructive: true} ] end def editor_toolbar_buttons(%{type: :tags}) do [ %{kind: "confirm_merge", label: "Merge Tags", destructive: false}, %{kind: "confirm_delete", label: "Delete Tag", destructive: true} ] end def editor_toolbar_buttons(_tab), do: [] defp short_commit_hash(hash) when is_binary(hash), do: String.slice(hash, 0, 7) defp short_commit_hash(_hash), do: "-------" # Only called inside the template's `is_number(task.progress)` guard. defp progress_percent(progress) when is_number(progress) do rounded = progress |> Kernel.*(100) |> Float.round(0) |> trunc() "#{rounded}%" end defp present?(value), do: BDS.Values.present?(value) end