defmodule BDS.Desktop.ShellLive.ImportEditor do @moduledoc false use Phoenix.LiveComponent alias BDS.{AI, ImportAnalysis, ImportDefinitions, ImportExecution} alias BDS.Desktop.{FilePicker, FolderPicker, ShellData} alias BDS.Desktop.ShellLive.Notify alias BDS.Desktop.ShellLive.ImportEditor.{ AnalysisState, ConflictResolution, ProgressTracking, TaxonomyEditing } import AnalysisState, only: [ default_analysis_state: 0, default_sections: 0, detail_items: 2, importable_counts: 1, translate_phase: 1 ] import ProgressTracking, only: [ default_execution_state: 0, execution_progress_width: 1, format_eta: 1, translate_execution_phase: 1 ] use Gettext, backend: BDS.Gettext import TaxonomyEditing, only: [ existing_taxonomy_terms: 1, taxonomy_item_editing?: 3, taxonomy_mapping_tooltip: 1, taxonomy_pill_class: 1 ] # ── LiveComponent lifecycle ──────────────────────────────────────────────── @spec update(map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} @impl true def update(%{action: :finish_analysis, ref: ref, result: result}, socket) do socket = case socket.assigns.analysis_state do %{ref: ^ref} = _state -> do_finish_analysis(socket, result) _other -> socket end {:ok, build_data(socket)} end def update(%{action: :finish_execution, ref: ref, result: result}, socket) do socket = case socket.assigns.execution_state do %{ref: ^ref} = _state -> do_finish_execution(socket, result) _other -> socket end {:ok, build_data(socket)} end def update(%{action: :note_analysis_progress, step: step, detail: detail}, socket) do socket = socket |> assign( :analysis_state, Map.merge(socket.assigns.analysis_state, %{loading: true, step: step, detail: detail}) ) |> build_data() {:ok, socket} end def update( %{ action: :note_execution_progress, phase: phase, current: current, total: total, detail: detail }, socket ) do {detail_text, eta} = ProgressTracking.decompose_progress_detail(detail) socket = socket |> assign( :execution_state, Map.merge(socket.assigns.execution_state, %{ is_executing: true, phase: translate_execution_phase(phase), current: current, total: total, detail: detail_text, eta: eta }) ) |> build_data() {: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(assigns) do import_editor = %{ id: assigns.definition_id, definition_name: assigns.definition_name, uploads_folder_path: assigns.uploads_folder_path, wxr_file_path: assigns.wxr_file_path, report: assigns.report, taxonomy_terms: assigns.taxonomy_terms, taxonomy_edit: assigns.taxonomy_edit, analysis_state: assigns.analysis_state, execution_state: assigns.execution_state, importable_counts: assigns.importable_counts, sections: assigns.sections, selected_model: assigns.selected_model, selected_model_label: assigns.selected_model_label, model_selector_open?: assigns.model_selector_open?, available_models: assigns.available_models, offline?: assigns.offline_mode?, is_loading: assigns.analysis_state.loading } assigns = Map.put(assigns, :import_editor, import_editor) import_editor(assigns) end # ── Event handlers ───────────────────────────────────────────────────────── @spec handle_event(String.t(), map(), Phoenix.LiveView.Socket.t()) :: {:noreply, Phoenix.LiveView.Socket.t()} @impl true def handle_event("change_import_editor_definition", %{"import_definition" => params}, socket) do definition_id = socket.assigns.definition_id socket = case ImportDefinitions.update_definition(definition_id, %{name: Map.get(params, "name", "")}) do {:ok, _definition} -> build_data(socket) _other -> build_data(socket) end {:noreply, socket} end def handle_event("select_import_uploads_folder", _params, socket) do definition_id = socket.assigns.definition_id socket = case FolderPicker.choose_directory(dgettext("ui", "Uploads Folder")) do {:ok, uploads_folder_path} -> {:ok, _definition} = ImportDefinitions.update_definition(definition_id, %{ uploads_folder_path: uploads_folder_path }) build_data(socket) :cancel -> build_data(socket) {:error, %{message: message}} -> notify_output(dgettext("ui", "Import"), message, "error") build_data(socket) end {:noreply, socket} end def handle_event("select_import_wxr_file", _params, socket) do definition_id = socket.assigns.definition_id project_id = socket.assigns.project_id socket = case FilePicker.choose_file(dgettext("ui", "WXR File")) do {:ok, wxr_file_path} -> {:ok, definition} = ImportDefinitions.update_definition(definition_id, %{ wxr_file_path: wxr_file_path, last_analysis_result: nil }) parent = self() task = Task.Supervisor.async_nolink(BDS.Tasks.TaskSupervisor, fn -> ImportAnalysis.analyze_wxr( project_id, wxr_file_path, definition.uploads_folder_path, on_progress: fn step, detail -> send(parent, {:import_analysis_progress, translate_phase(step), detail}) end ) end) :ok = allow_repo_sandbox(task.pid) socket |> assign(:analysis_state, %{ loading: true, step: dgettext("ui", "Analyzing WXR file..."), detail: Path.basename(wxr_file_path), file_path: wxr_file_path, ref: task.ref }) |> assign(:execution_state, default_execution_state()) |> build_data() :cancel -> build_data(socket) {:error, %{message: message}} -> notify_output(dgettext("ui", "Import"), message, "error") build_data(socket) end {:noreply, socket} end def handle_event("execute_import_editor", _params, socket) do definition_id = socket.assigns.definition_id project_id = socket.assigns.project_id socket = with %{} = definition <- ImportDefinitions.get_definition(definition_id), %{} = report <- ImportDefinitions.decode_analysis_result(definition) do default_author = AnalysisState.default_author(project_id) counts = importable_counts(report) if counts.total == 0 do build_data(socket) else parent = self() task = Task.Supervisor.async_nolink(BDS.Tasks.TaskSupervisor, fn -> ImportExecution.execute_import(project_id, report, uploads_folder_path: definition.uploads_folder_path, default_author: default_author, on_progress: fn phase, current, total, detail -> send( parent, {:import_execution_progress, phase, current, total, detail} ) end ) end) :ok = AnalysisState.allow_repo_sandbox(task.pid) progress_phase = translate_execution_phase("posts") socket |> assign(:execution_state, %{ is_executing: true, completed: false, error: nil, count: counts.total, result: nil, phase: progress_phase, current: 0, total: counts.total, detail: nil, eta: nil, ref: task.ref }) |> build_data() end else _other -> build_data(socket) end {:noreply, socket} end def handle_event("change_import_conflict_resolution", params, socket) do definition_id = socket.assigns.definition_id socket = with %{} = definition <- ImportDefinitions.get_definition(definition_id), %{} = report <- ImportDefinitions.decode_analysis_result(definition), updated_report <- ConflictResolution.update_conflict_resolution( report, Map.get(params, "item_type"), Map.get(params, "item_name"), Map.get(params, "resolution") ), {:ok, _definition} <- ImportDefinitions.update_definition(definition_id, %{ last_analysis_result: updated_report }) do build_data(socket) else _other -> build_data(socket) end {:noreply, socket} end def handle_event( "start_import_taxonomy_edit", %{"type" => type, "name" => name, "mapped_to" => mapped_to}, socket ) do socket = socket |> assign(:taxonomy_edit, %{ type: type, name: name, value: mapped_to |> to_string() |> blank_to_nil() }) |> build_data() {:noreply, socket} end def handle_event("save_import_taxonomy_edit", params, socket) do definition_id = socket.assigns.definition_id project_id = socket.assigns.project_id socket = with %{} = definition <- ImportDefinitions.get_definition(definition_id), %{} = report <- ImportDefinitions.decode_analysis_result(definition), type <- Map.get(params, "type"), name <- Map.get(params, "name"), mapped_to <- Map.get(params, "mapped_to"), normalized_value <- TaxonomyEditing.normalize_taxonomy_mapping_value(project_id, type, mapped_to), updated_report <- TaxonomyEditing.update_taxonomy_mapping(report, type, name, normalized_value), {:ok, _definition} <- ImportDefinitions.update_definition(definition_id, %{ last_analysis_result: updated_report }) do socket |> assign(:taxonomy_edit, nil) |> build_data() else _other -> socket |> assign(:taxonomy_edit, nil) |> build_data() end {:noreply, socket} end def handle_event("cancel_import_taxonomy_edit", _params, socket) do {:noreply, assign(socket, :taxonomy_edit, nil) |> build_data()} end def handle_event("clear_import_taxonomy_mapping", %{"type" => type, "name" => name}, socket) do definition_id = socket.assigns.definition_id project_id = socket.assigns.project_id socket = with %{} = definition <- ImportDefinitions.get_definition(definition_id), %{} = report <- ImportDefinitions.decode_analysis_result(definition), normalized_value <- TaxonomyEditing.normalize_taxonomy_mapping_value(project_id, type, ""), updated_report <- TaxonomyEditing.update_taxonomy_mapping(report, type, name, normalized_value), {:ok, _definition} <- ImportDefinitions.update_definition(definition_id, %{ last_analysis_result: updated_report }) do socket |> assign(:taxonomy_edit, nil) |> build_data() else _other -> socket |> assign(:taxonomy_edit, nil) |> build_data() end {:noreply, socket} end def handle_event("toggle_import_section", %{"section" => section}, socket) do section_atom = BDS.BoundedAtoms.import_section(section) socket = if section_atom do next_sections = Map.update!(socket.assigns.sections, section_atom, &(!&1)) assign(socket, :sections, next_sections) |> build_data() else build_data(socket) end {:noreply, socket} end def handle_event("toggle_import_ai_model_selector", _params, socket) do {:noreply, assign(socket, :model_selector_open?, not socket.assigns.model_selector_open?) |> build_data()} end def handle_event("select_import_ai_model", %{"model" => model_id}, socket) do socket = socket |> assign(:selected_model, model_id) |> assign(:model_selector_open?, false) |> build_data() {:noreply, socket} end def handle_event("analyze_import_taxonomy_ai", _params, socket) do definition_id = socket.assigns.definition_id project_id = socket.assigns.project_id socket = with %{} = definition <- ImportDefinitions.get_definition(definition_id), %{} = report <- ImportDefinitions.decode_analysis_result(definition) do if socket.assigns.offline_mode? do notify_output( dgettext("ui", "Import"), BDS.Gettext.lgettext( socket.assigns[:page_language] || ShellData.ui_language(), "ui", "Automatic AI actions stay gated by airplane mode." ), "info" ) build_data(socket) else taxonomy_terms = existing_taxonomy_terms(project_id) import_terms = %{ categories: Enum.map(Map.get(report.items, :categories, []), & &1.name), tags: Enum.map(Map.get(report.items, :tags, []), & &1.name) } opts = if socket.assigns.selected_model do [model: socket.assigns.selected_model] else [] end case AI.analyze_import_taxonomy(import_terms, taxonomy_terms, opts) do {:ok, analysis} -> updated_report = TaxonomyEditing.apply_taxonomy_mappings(report, analysis) {:ok, _definition} = ImportDefinitions.update_definition(definition_id, %{ last_analysis_result: updated_report }) mapped_count = TaxonomyEditing.auto_mapped_count(report, updated_report) notify_output( dgettext("ui", "Import"), dgettext("ui", "%{count} mapped", count: mapped_count), "info" ) build_data(socket) {:error, reason} -> notify_output(dgettext("ui", "Import"), inspect(reason), "error") build_data(socket) end end else _other -> build_data(socket) end {:noreply, socket} end # ── handle_info for async tasks ──────────────────────────────────────────── @spec handle_info( {:import_analysis_progress, atom() | String.t(), String.t()}, Phoenix.LiveView.Socket.t() ) :: {:noreply, Phoenix.LiveView.Socket.t()} def handle_info({:import_analysis_progress, step, detail}, socket) do socket = socket |> assign( :analysis_state, Map.merge(socket.assigns.analysis_state, %{loading: true, step: step, detail: detail}) ) |> build_data() {:noreply, socket} end def handle_info( {:import_execution_progress, phase, current, total, detail}, socket ) do {detail_text, eta} = ProgressTracking.decompose_progress_detail(detail) socket = socket |> assign( :execution_state, Map.merge(socket.assigns.execution_state, %{ is_executing: true, phase: translate_execution_phase(phase), current: current, total: total, detail: detail_text, eta: eta }) ) |> build_data() {:noreply, socket} end def handle_info({ref, result}, socket) when is_reference(ref) do Process.demonitor(ref, [:flush]) socket = cond do match?(%{ref: ^ref}, socket.assigns.analysis_state) -> do_finish_analysis(socket, result) match?(%{ref: ^ref}, socket.assigns.execution_state) -> do_finish_execution(socket, result) true -> socket end {:noreply, build_data(socket)} end def handle_info({:DOWN, ref, :process, _pid, reason}, socket) when is_reference(ref) do socket = cond do match?(%{ref: ^ref}, socket.assigns.analysis_state) and reason not in [:normal, :shutdown] -> message = if is_binary(reason), do: reason, else: inspect(reason) socket |> assign(:analysis_state, default_analysis_state()) |> notify_output(dgettext("ui", "Import"), message, "error") match?(%{ref: ^ref}, socket.assigns.execution_state) and reason not in [:normal, :shutdown] -> message = if is_binary(reason), do: reason, else: inspect(reason) socket |> assign( :execution_state, Map.merge(socket.assigns.execution_state, %{ is_executing: false, completed: false, error: message, ref: nil }) ) |> notify_output(dgettext("ui", "Import"), message, "error") true -> socket end {:noreply, build_data(socket)} end # ── State helpers ────────────────────────────────────────────────────────── defp ensure_state(socket) do definition_id = socket.assigns.current_tab.id defaults = %{ definition_id: definition_id, analysis_state: default_analysis_state(), execution_state: default_execution_state(), sections: default_sections(), taxonomy_edit: nil, model_selector_open?: false, selected_model: nil, project_id: socket.assigns[:project_id], offline_mode?: socket.assigns[:offline_mode] || true } 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 defp build_data(socket) do definition_id = socket.assigns.definition_id case ImportDefinitions.get_definition(definition_id) do nil -> assign(socket, :report, nil) definition -> report = ImportDefinitions.decode_analysis_result(definition) taxonomy_terms = existing_taxonomy_terms(socket.assigns.project_id) selected_model = resolve_selected_model(socket) available_models = AI.available_chat_models(selected_model) socket |> assign(:definition_name, definition.name) |> assign(:uploads_folder_path, definition.uploads_folder_path) |> assign(:wxr_file_path, definition.wxr_file_path) |> assign(:report, report) |> assign(:taxonomy_terms, taxonomy_terms) |> assign(:importable_counts, importable_counts(report)) |> assign(:selected_model, selected_model) |> assign(:selected_model_label, selected_model_label(selected_model, available_models)) |> assign(:available_models, available_models) |> maybe_update_tab_meta(definition.name) end end defp maybe_update_tab_meta(socket, name) do title = name || dgettext("ui", "Untitled Import") Notify.tab_meta(:import, socket.assigns.definition_id, title, dgettext( "ui", "Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported." ) ) socket end defp resolve_selected_model(socket) do socket.assigns.selected_model || preferred_model(socket) end defp preferred_model(socket) do preference_key = if socket.assigns.offline_mode?, do: :airplane_chat, else: :chat case AI.get_model_preference(preference_key) do {:ok, model} when is_binary(model) and model != "" -> model _other -> nil end end defp selected_model_label(nil, []), do: dgettext("ui", "Analyze with...") defp selected_model_label(nil, [model | _rest]), do: model.name || model.id defp selected_model_label(model_id, available_models) do case Enum.find(available_models, &(&1.id == model_id)) do nil -> model_id model -> model.name || model.id end end # ── Task finishers ───────────────────────────────────────────────────────── defp do_finish_analysis(socket, result) do analysis_state = socket.assigns.analysis_state definition_id = socket.assigns.definition_id socket = assign(socket, :analysis_state, default_analysis_state()) case result do {:ok, report} -> attrs = %{ wxr_file_path: analysis_state.file_path, last_analysis_result: report } |> maybe_put(:name, AnalysisState.suggested_definition_name(report)) case ImportDefinitions.update_definition(definition_id, attrs) do {:ok, _definition} -> socket {:error, reason} -> notify_output(socket, dgettext("ui", "Import"), inspect(reason), "error") end {:error, %{message: message}} -> notify_output(socket, dgettext("ui", "Import"), message, "error") {:error, reason} -> notify_output(socket, dgettext("ui", "Import"), inspect(reason), "error") end end defp do_finish_execution(socket, result) do previous_state = socket.assigns.execution_state socket = case result do {:ok, _execution_result} -> socket |> assign(:execution_state, %{ previous_state | is_executing: false, completed: true, error: nil, current: previous_state.total, detail: nil, ref: nil }) |> notify_output( dgettext("ui", "Import"), dgettext("ui", "Import completed successfully!", count: previous_state.count), "info" ) {:error, %{message: message}} -> socket |> assign(:execution_state, %{ previous_state | is_executing: false, completed: false, error: message, ref: nil }) |> notify_output(dgettext("ui", "Import"), message, "error") {:error, reason} -> message = inspect(reason) socket |> assign(:execution_state, %{ previous_state | is_executing: false, completed: false, error: message, ref: nil }) |> notify_output(dgettext("ui", "Import"), message, "error") end # Allow DB connections to settle before rebuilding Process.sleep(20) socket end # ── Rendering (existing function components) ─────────────────────────────── attr(:import_editor, :map, required: true) @spec import_editor(map()) :: Phoenix.LiveView.Rendered.t() def import_editor(assigns) do assigns = assigns |> assign(:report, Map.get(assigns.import_editor, :report)) |> assign( :analysis_state, Map.get(assigns.import_editor, :analysis_state, default_analysis_state()) ) |> assign(:execution_state, Map.get(assigns.import_editor, :execution_state)) |> assign( :counts, Map.get(assigns.import_editor, :importable_counts, %{ total: 0, tags: 0, posts: 0, media: 0, pages: 0 }) ) |> assign(:sections, Map.get(assigns.import_editor, :sections, default_sections())) |> assign(:detail_posts, detail_items(Map.get(assigns.import_editor, :report), :posts)) |> assign(:detail_pages, detail_items(Map.get(assigns.import_editor, :report), :pages)) |> assign(:detail_media, detail_items(Map.get(assigns.import_editor, :report), :media)) |> assign( :post_conflicts, Enum.filter( detail_items(Map.get(assigns.import_editor, :report), :posts), &(&1.status == "conflict") ) ) |> assign( :page_conflicts, Enum.filter( detail_items(Map.get(assigns.import_editor, :report), :pages), &(&1.status == "conflict") ) ) |> assign( :post_items, Enum.filter( detail_items(Map.get(assigns.import_editor, :report), :posts), &(Map.get(&1, :post_type, "post") == "post") ) ) |> assign( :other_items, Enum.reject( detail_items(Map.get(assigns.import_editor, :report), :posts), &(Map.get(&1, :post_type, "post") == "post") ) ) ~H"""

<%= dgettext("ui", "Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported.") %>

<%= @import_editor.uploads_folder_path || dgettext("ui", "No folder selected") %>
<%= @import_editor.wxr_file_path || dgettext("ui", "Select a file to analyze") %>
<%= if @import_editor.is_loading do %>
<%= @analysis_state.step || dgettext("ui", "Analyzing WXR file...") %>
<%= if present?(@analysis_state.detail) do %>
<%= @analysis_state.detail %>
<% end %>
<% end %> <%= if not is_nil(@report) and not @import_editor.is_loading do %>
<%= dgettext("ui", "Site") %> <%= get_in(@report, [:site_info, :title]) || dgettext("ui", "Untitled") %>
<%= dgettext("ui", "URL") %> <%= get_in(@report, [:site_info, :url]) || dgettext("ui", "N/A") %>
<%= dgettext("ui", "Language") %> <%= get_in(@report, [:site_info, :language]) || dgettext("ui", "N/A") %>
<%= dgettext("ui", "File") %> <%= @import_editor.wxr_file_path |> to_string() |> Path.basename() %>
<.stat_card label={dgettext("ui", "posts")} stats={@report.post_stats} /> <%= if Map.get(@report, :other_stats) && Map.get(@report.other_stats, :total, 0) > 0 do %> <.other_stat_card label={dgettext("ui", "Other")} stats={@report.other_stats} /> <% end %> <.stat_card label={dgettext("ui", "pages")} stats={@report.page_stats} /> <.media_stat_card label={dgettext("ui", "media")} stats={@report.media_stats} /> <.taxonomy_stat_card label={dgettext("ui", "Categories")} stats={@report.category_stats} /> <.taxonomy_stat_card label={dgettext("ui", "Tags")} stats={@report.tag_stats} />
<%= if Enum.any?(Map.get(@report, :date_distribution, [])) do %>

<%= dgettext("ui", "Date Distribution") %>

<%= for row <- @report.date_distribution do %>
<%= row.year %>
<%= row.post_count %> / <%= row.media_count %>
<% end %>
<% end %> <%= if @execution_state.is_executing do %>

<%= dgettext("ui", "Importing...") %>

<%= @execution_state.phase || dgettext("ui", "Starting...") %> <%= if present?(@execution_state.detail) do %> <%= @execution_state.detail %> <% end %> <%= @execution_state.current || 0 %> / <%= @execution_state.total || @counts.total %> <%= if eta = format_eta(Map.get(@execution_state, :eta)) do %> <%= eta %> <% end %>
<% end %> <%= if not @execution_state.is_executing and not @execution_state.completed do %>
<%= dgettext("ui", "Ready to import:") %> <%= if @counts.tags > 0 do %><%= @counts.tags %> <%= dgettext("ui", "tags/categories") %><% end %> <%= if @counts.posts > 0 do %><%= @counts.posts %> <%= dgettext("ui", "posts") %><% end %> <%= if @counts.media > 0 do %><%= @counts.media %> <%= dgettext("ui", "media") %><% end %> <%= if @counts.pages > 0 do %><%= @counts.pages %> <%= dgettext("ui", "pages") %><% end %>
<% end %> <%= if @execution_state.completed do %>
<%= dgettext("ui", "Import completed successfully!", count: @execution_state.count || @counts.total) %>
<% end %> <%= if present?(@execution_state.error) do %>
<%= dgettext("ui", "Import failed: %{error}", error: @execution_state.error) %>
<% end %> <%= if Enum.any?(@post_conflicts) do %> <.conflict_section title={dgettext("ui", "Post Slug Conflicts")} items={@post_conflicts} expanded={@sections.post_conflicts} section="post_conflicts" myself={@myself} /> <% end %> <%= if Enum.any?(@page_conflicts) do %> <.conflict_section title={dgettext("ui", "Page Slug Conflicts")} items={@page_conflicts} expanded={@sections.page_conflicts} section="page_conflicts" myself={@myself} /> <% end %> <%= if Enum.any?(@post_items) do %> <.post_detail_section title={dgettext("ui", "Posts (%{count})", count: length(@post_items))} items={@post_items} expanded={@sections.posts} section="posts" myself={@myself} /> <% end %> <%= if Enum.any?(@other_items) do %> <.post_detail_section title={dgettext("ui", "Other (%{count})", count: length(@other_items))} items={@other_items} expanded={@sections.other} section="other" show_type={true} myself={@myself} /> <% end %> <%= if Enum.any?(@detail_pages) do %> <.post_detail_section title={dgettext("ui", "Pages (%{count})", count: length(@detail_pages))} items={@detail_pages} expanded={@sections.pages} section="pages" myself={@myself} /> <% end %> <%= if Enum.any?(@detail_media) do %> <.media_detail_section title={dgettext("ui", "Media (%{count})", count: length(@detail_media))} items={@detail_media} expanded={@sections.media} section="media" myself={@myself} /> <% end %> <%= if Enum.any?(Map.get(@report.items, :categories, [])) or Enum.any?(Map.get(@report.items, :tags, [])) do %>
<%= if @sections.taxonomy do %>
<%= if @import_editor.model_selector_open? do %>
<%= for model <- @import_editor.available_models do %> <% end %>
<% end %>
<%= dgettext("ui", "AI will suggest mappings from new to existing items to avoid duplicates") %>
<.taxonomy_group title={dgettext("ui", "Categories")} items={Map.get(@report.items, :categories, [])} suggestions={Map.get(@import_editor.taxonomy_terms, :categories, [])} edit={@import_editor.taxonomy_edit} type="categories" myself={@myself} /> <.taxonomy_group title={dgettext("ui", "Tags")} items={Map.get(@report.items, :tags, [])} suggestions={Map.get(@import_editor.taxonomy_terms, :tags, [])} edit={@import_editor.taxonomy_edit} type="tags" myself={@myself} />
<% end %>
<% end %> <% macros = Map.get(@report, :macros, %{}) %> <%= if Enum.any?(Map.get(macros, :discovered, [])) do %>
<%= if @sections.macros do %>
<%= dgettext("ui", "%{count} mapped", count: macros.mapped_count || 0) %> <%= dgettext("ui", "%{count} unmapped", count: macros.unmapped_count || 0) %>
<%= for macro <- macros.discovered do %>
<%= macro.name %> <%= if macro.mapped, do: dgettext("ui", "Mapped"), else: dgettext("ui", "Unknown") %> <%= dgettext("ui", "%{count} uses", count: macro.total_count) %>
<%= if Enum.any?(Map.get(macro, :usages, [])) do %>
<%= for usage <- macro.usages do %>
<%= if Enum.any?(Map.get(usage, :params, %{})) do %> <%= for {k, v} <- usage.params do %> <%= k %>=<%= v %> <% end %> <% else %> <%= dgettext("ui", "(no parameters)") %> <% end %> <%= dgettext("ui", "%{count} uses", count: usage.count) %>
<% end %>
<% end %> <%= if Enum.any?(Map.get(macro, :post_slugs, [])) do %>
<%= dgettext("ui", "Used in: %{items}%{more}", items: Enum.join(Enum.take(macro.post_slugs, 5), ", "), more: (if length(macro.post_slugs) > 5, do: dgettext("ui", ", +%{count} more", count: length(macro.post_slugs) - 5), else: "")) %>
<% end %>
<% end %>
<% end %>
<% end %> <% else %> <%= if @import_editor.is_loading do %> <% else %>

<%= dgettext("ui", "Select a WordPress export file to begin analysis.") %>

<% end %> <% end %>
""" end attr(:title, :string, required: true) attr(:items, :list, required: true) attr(:expanded, :boolean, required: true) attr(:section, :string, required: true) attr(:myself, :any, required: true) @spec conflict_section(map()) :: Phoenix.LiveView.Rendered.t() def conflict_section(assigns) do ~H"""
<%= if @expanded do %> <%= for item <- @items do %> <% end %>
<%= dgettext("ui", "Slug") %> <%= dgettext("ui", "New Entry (WXR)") %> <%= dgettext("ui", "Existing Entry") %> <%= dgettext("ui", "Resolution") %>
<%= Map.get(item, :slug) %> <%= Map.get(item, :title) %> <%= Map.get(item, :existing_title) || dgettext("ui", "--") %>
<% end %>
""" end attr(:title, :string, required: true) attr(:items, :list, required: true) attr(:expanded, :boolean, required: true) attr(:section, :string, required: true) attr(:show_type, :boolean, default: false) attr(:myself, :any, required: true) @spec post_detail_section(map()) :: Phoenix.LiveView.Rendered.t() def post_detail_section(assigns) do ~H"""
<%= if @expanded do %> <%= if @show_type do %> <% end %> <%= for item <- @items do %> <%= if @show_type do %> <% end %> <% end %>
<%= dgettext("ui", "Status") %><%= dgettext("ui", "Type") %><%= dgettext("ui", "Title") %> <%= dgettext("ui", "Slug") %> <%= dgettext("ui", "Categories") %> <%= dgettext("ui", "WP Status") %> <%= dgettext("ui", "Existing Match") %>
<%= item.status %><%= Map.get(item, :post_type, Map.get(item, :item_type)) %><%= Map.get(item, :title) %> <%= Map.get(item, :slug) %> <%= joined_or_none(Map.get(item, :categories)) %> <%= Map.get(item, :wp_status) || dgettext("ui", "--") %> <%= Map.get(item, :existing_title) || dgettext("ui", "--") %>
<% end %>
""" end attr(:title, :string, required: true) attr(:items, :list, required: true) attr(:expanded, :boolean, required: true) attr(:section, :string, required: true) attr(:myself, :any, required: true) @spec media_detail_section(map()) :: Phoenix.LiveView.Rendered.t() def media_detail_section(assigns) do ~H"""
<%= if @expanded do %> <%= for item <- @items do %> <% end %>
<%= dgettext("ui", "Status") %> <%= dgettext("ui", "Filename") %> <%= dgettext("ui", "Type") %> <%= dgettext("ui", "Path") %> <%= dgettext("ui", "Existing Match") %>
<%= item.status %> <%= Map.get(item, :filename) %> <%= Map.get(item, :mime_type) || dgettext("ui", "--") %> <%= Map.get(item, :relative_path) %> <%= Map.get(item, :existing_title) || dgettext("ui", "--") %>
<% end %>
""" end attr(:label, :string, required: true) attr(:stats, :map, required: true) @spec stat_card(map()) :: Phoenix.LiveView.Rendered.t() def stat_card(assigns) do ~H"""

<%= @label %>

<%= total_stats(@stats) %>
<%= if @stats.new_count > 0 do %><%= @stats.new_count %> <%= dgettext("ui", "new") %><% end %> <%= if @stats.update_count > 0 do %><%= @stats.update_count %> <%= dgettext("ui", "update") %><% end %> <%= if @stats.conflict_count > 0 do %><%= @stats.conflict_count %> <%= dgettext("ui", "conflict") %><% end %> <%= if @stats.duplicate_count > 0 do %><%= @stats.duplicate_count %> <%= dgettext("ui", "duplicate") %><% end %>
""" end attr(:label, :string, required: true) attr(:stats, :map, required: true) @spec other_stat_card(map()) :: Phoenix.LiveView.Rendered.t() def other_stat_card(assigns) do ~H"""

<%= @label %>

<%= Map.get(@stats, :total, 0) %>
<%= for type <- Map.get(@stats, :types, []) do %> <%= type %> <% end %>
""" end attr(:label, :string, required: true) attr(:stats, :map, required: true) @spec media_stat_card(map()) :: Phoenix.LiveView.Rendered.t() def media_stat_card(assigns) do ~H"""

<%= @label %>

<%= total_media_stats(@stats) %>
<%= if @stats.new_count > 0 do %><%= @stats.new_count %> <%= dgettext("ui", "new") %><% end %> <%= if @stats.update_count > 0 do %><%= @stats.update_count %> <%= dgettext("ui", "update") %><% end %> <%= if @stats.conflict_count > 0 do %><%= @stats.conflict_count %> <%= dgettext("ui", "conflict") %><% end %> <%= if @stats.duplicate_count > 0 do %><%= @stats.duplicate_count %> <%= dgettext("ui", "duplicate") %><% end %> <%= if @stats.missing_count > 0 do %><%= @stats.missing_count %> <%= dgettext("ui", "missing") %><% end %>
""" end attr(:label, :string, required: true) attr(:stats, :map, required: true) @spec taxonomy_stat_card(map()) :: Phoenix.LiveView.Rendered.t() def taxonomy_stat_card(assigns) do ~H"""

<%= @label %>

<%= @stats.existing_count + @stats.mapped_count + @stats.new_count %>
<%= if @stats.existing_count > 0 do %><%= @stats.existing_count %> <%= dgettext("ui", "existing") %><% end %> <%= if @stats.mapped_count > 0 do %><%= @stats.mapped_count %> <%= dgettext("ui", "mapped") %><% end %> <%= if @stats.new_count > 0 do %><%= @stats.new_count %> <%= dgettext("ui", "new") %><% end %>
""" end attr(:title, :string, required: true) attr(:items, :list, required: true) attr(:suggestions, :list, required: true) attr(:edit, :map, default: nil) attr(:type, :string, required: true) attr(:myself, :any, required: true) @spec taxonomy_group(map()) :: Phoenix.LiveView.Rendered.t() def taxonomy_group(assigns) do ~H"""

<%= @title %>

<%= for term <- @suggestions do %> <% end %>
<%= for item <- @items do %> <%= if taxonomy_item_editing?(@edit, @type, item.name) do %>
<%= item.name %> <%= if present?(item.mapped_to) do %> <% end %>
<% else %>
<%= if item.exists_in_project do %> <%= item.name %> <% else %> <% end %> <%= if present?(item.mapped_to) do %> <% end %>
<% end %> <% end %>
""" end # ── Private helpers ─────────────────────────────────────────────────────── defp notify_output(socket, title, message, level \\ "info") do Notify.output(title, message, level) socket end defp allow_repo_sandbox(pid) when is_pid(pid) do if Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) do try do Ecto.Adapters.SQL.Sandbox.allow(BDS.Repo, self(), pid) rescue _error -> :ok end else :ok end :ok end defp joined_or_none(values) when is_list(values) and values != [], do: Enum.join(values, ", ") defp joined_or_none(_values), do: dgettext("ui", "--") defp status_badge_class(status), do: ["status-badge", status] defp distribution_width(value, rows, key) do max_value = rows |> Enum.map(&Map.get(&1, key, 0)) |> Enum.max(fn -> 1 end) max(8, value / max(max_value, 1) * 100) end defp total_stats(stats), do: stats.new_count + stats.update_count + stats.conflict_count + stats.duplicate_count defp total_media_stats(stats), do: total_stats(stats) + stats.missing_count defp conflict_resolution_selected?(item, "ignore") do Map.get(item, :resolution, "ignore") in ["ignore", "skip"] end defp conflict_resolution_selected?(item, "overwrite") do Map.get(item, :resolution) in ["overwrite", "merge"] end defp present?(value), do: value not in [nil, ""] defp blank?(value), do: value in [nil, ""] defp blank_to_nil(""), do: nil defp blank_to_nil(value), do: value defp maybe_put(map, _key, nil), do: map defp maybe_put(map, key, value), do: Map.put(map, key, value) end