From 606340e1c79fef0c5ee22f9659271623660bbb97 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 12 Apr 2026 17:18:47 -0500 Subject: [PATCH] Convert /admin/contact-edits list to live_table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pending-edits list on the admin review page now sorts through live_table. Columns for contact, submitted-by user, changed-fields summary, and submitted timestamp are rendered via 2-arity custom renderers so the Ecto-preloaded :contact and :user associations stay accessible — live_table's data_provider path skips select_columns so the full struct (with preloads) reaches the renderer. The review drawer, diff table, approve/reject flow, and note textarea all stay as-is; after approve/reject the page push_patches back to its current path so live_table re-runs handle_params and the list refreshes. Pending count in the header now reads Radio.pending_edit_count/0 instead of length(@edits) since we no longer hold the full list in socket state. --- lib/microwaveprop/radio.ex | 13 ++ .../live/admin/contact_edit_live.ex | 125 ++++++++++-------- 2 files changed, 83 insertions(+), 55 deletions(-) diff --git a/lib/microwaveprop/radio.ex b/lib/microwaveprop/radio.ex index 68517d55..6829c0e9 100644 --- a/lib/microwaveprop/radio.ex +++ b/lib/microwaveprop/radio.ex @@ -732,6 +732,19 @@ defmodule Microwaveprop.Radio do |> Repo.all() end + @doc """ + Base query for pending contact edits with `:user` and `:contact` + preloaded. Used as a `live_table` `data_provider` so sort/paginate + can be applied on top. + """ + @spec pending_edits_query() :: Ecto.Query.t() + def pending_edits_query do + from e in ContactEdit, + as: :resource, + where: e.status == :pending, + preload: [:user, :contact] + end + @spec list_contact_edits(Ecto.UUID.t()) :: [ContactEdit.t()] def list_contact_edits(contact_id) do ContactEdit diff --git a/lib/microwaveprop_web/live/admin/contact_edit_live.ex b/lib/microwaveprop_web/live/admin/contact_edit_live.ex index d54a4e92..6af3b86c 100644 --- a/lib/microwaveprop_web/live/admin/contact_edit_live.ex +++ b/lib/microwaveprop_web/live/admin/contact_edit_live.ex @@ -1,21 +1,50 @@ defmodule MicrowavepropWeb.Admin.ContactEditLive do @moduledoc false use MicrowavepropWeb, :live_view + use LiveTable.LiveResource, schema: Microwaveprop.Radio.ContactEdit alias Microwaveprop.Radio alias Microwaveprop.Radio.EditNotifier + def fields do + [ + id: %{label: "ID", hidden: true}, + contact_id: %{label: "Contact", renderer: &contact_cell/2}, + user_id: %{label: "Submitted By", renderer: &user_cell/2}, + proposed_changes: %{label: "Fields Changed", renderer: &changed_fields_cell/1}, + inserted_at: %{label: "Submitted", sortable: true, renderer: &format_ts/1} + ] + end + + def filters, do: [] + + def actions do + [ + review: fn %{record: edit} -> + assigns = %{edit: edit} + + ~H""" + + """ + end + ] + end + @impl true def mount(_params, _session, socket) do - edits = Radio.list_pending_edits() - {:ok, - assign(socket, - page_title: "Review Contact Edits", - edits: edits, - reviewing: nil, - admin_note: "" - )} + socket + |> assign(:page_title, "Review Contact Edits") + |> assign(:reviewing, nil) + |> assign(:admin_note, "") + |> assign(:pending_count, Radio.pending_edit_count()) + |> assign(:data_provider, {Radio, :pending_edits_query, []})} end @impl true @@ -43,8 +72,9 @@ defmodule MicrowavepropWeb.Admin.ContactEditLive do {:noreply, socket - |> assign(reviewing: nil, admin_note: "", edits: Radio.list_pending_edits()) - |> put_flash(:info, "Edit approved and applied.")} + |> assign(reviewing: nil, admin_note: "", pending_count: Radio.pending_edit_count()) + |> put_flash(:info, "Edit approved and applied.") + |> push_patch(to: "/" <> (socket.assigns[:current_path] || "admin/contact-edits"))} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to approve edit.")} @@ -62,8 +92,9 @@ defmodule MicrowavepropWeb.Admin.ContactEditLive do {:noreply, socket - |> assign(reviewing: nil, admin_note: "", edits: Radio.list_pending_edits()) - |> put_flash(:info, "Edit rejected.")} + |> assign(reviewing: nil, admin_note: "", pending_count: Radio.pending_edit_count()) + |> put_flash(:info, "Edit rejected.") + |> push_patch(to: "/" <> (socket.assigns[:current_path] || "admin/contact-edits"))} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to reject edit.")} @@ -114,13 +145,32 @@ defmodule MicrowavepropWeb.Admin.ContactEditLive do defp format_proposed("qso_timestamp", val) when is_binary(val), do: val defp format_proposed(_field, val), do: to_string(val) + defp contact_cell(_value, %{contact: contact}) when not is_nil(contact) do + assigns = %{contact: contact} + + ~H""" + <.link navigate={~p"/contacts/#{@contact.id}"} class="link link-primary"> + {@contact.station1} / {@contact.station2} + +
{format_band(@contact.band)}
+ """ + end + + defp contact_cell(_value, _record), do: "—" + + defp user_cell(_value, %{user: %{callsign: callsign}}), do: callsign + defp user_cell(_value, _record), do: "—" + + defp changed_fields_cell(changes) when is_map(changes), do: changed_fields_summary(changes) + defp changed_fields_cell(_), do: "—" + @impl true def render(assigns) do ~H""" <.header> Review Contact Edits - <:subtitle>{length(@edits)} pending + <:subtitle>{@pending_count} pending <:actions> <.link navigate={~p"/admin/backfill"} class="btn btn-sm btn-ghost"> <.icon name="hero-arrow-left" class="w-4 h-4" /> Admin @@ -192,48 +242,13 @@ defmodule MicrowavepropWeb.Admin.ContactEditLive do <% end %> - <%= if @edits == [] do %> -
- <.icon name="hero-check-circle" class="w-12 h-12 mx-auto mb-2" /> -

No pending edits to review.

-
- <% else %> -
- - - - - - - - - - - - <%= for edit <- @edits do %> - - - - - - - - <% end %> - -
ContactSubmitted ByFields ChangedSubmitted
- <.link navigate={~p"/contacts/#{edit.contact.id}"} class="link link-primary"> - {edit.contact.station1} / {edit.contact.station2} - -
- {format_band(edit.contact.band)} -
-
{edit.user.callsign}{changed_fields_summary(edit.proposed_changes)}{format_ts(edit.inserted_at)} - -
-
- <% end %> + <.live_table + fields={fields()} + filters={filters()} + options={@options} + streams={@streams} + actions={actions()} + />
""" end