neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Fix editing a foci: crash on the show page, and a lost place
11a1dc3 · Gabriel Morell · 2026-08-12 14:27
Message
{commit_body(@commit)}
Files changed
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/form_component.ex
+4
−0
@@ -23,6 +23,10 @@ defmodule RoomSanctumWeb.FociLive.FormComponent do
| 23 | 23 | :ok, |
| 24 | 24 | socket |
| 25 | 25 | |> assign(assigns) |
| 26 | + # validate and save both write this over the params, so without it a | |
| 27 | + # foci edited by any other field loses the point it is a foci for. | |
| 28 | + # assign_new, so a place picked off the map is not reset by a re-render. | |
| 29 | + |> assign_new(:place, fn -> foci.place end) | |
| 26 | 30 | |> assign_form(changeset) |
| 27 | 31 | } |
| 28 | 32 | end |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/form_component.html.heex
+5
−0
@@ -38,6 +38,11 @@
| 38 | 38 | </div> |
| 39 | 39 | </div> |
| 40 | 40 | <div class="form-control"> |
| 41 | + <%!-- place has no input of its own, so its error would | |
| 42 | + otherwise go nowhere and the save look like a no-op --%> | |
| 43 | + <.error :for={msg <- Enum.map(@form[:place].errors, &RoomSanctumWeb.CoreComponents.translate_error/1)}> | |
| 44 | + Pick a place on the map -- <%= msg %> | |
| 45 | + </.error> | |
| 41 | 46 | <label class="block text-sm font-semibold leading-6 text-zinc-800"> |
| 42 | 47 | <div id="structural-shim" phx-update="ignore"> |
| 43 | 48 | <div id="map" style="height: 400px; width: 600px" phx-hook="mkMap" data-latlng={@foci |>getlatlng} class="rounded-sm"></div> |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/form_component_coords.ex
+2
−0
@@ -32,6 +32,8 @@ defmodule RoomSanctumWeb.FociLive.FormComponentCoords do
| 32 | 32 | :ok, |
| 33 | 33 | socket |
| 34 | 34 | |> assign(assigns) |
| 35 | + # see FormComponent: validate and save write this over the params | |
| 36 | + |> assign_new(:place, fn -> foci.place end) | |
| 35 | 37 | |> assign(:latitude, lat) |
| 36 | 38 | |> assign(:longitude, lng) |
| 37 | 39 | |> assign(:coord_input, format_coords(lat, lng)) |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/show.ex
+1
−1
@@ -1,5 +1,5 @@
| 1 | 1 | defmodule RoomSanctumWeb.FociLive.Show do |
| 2 | − use RoomSanctumWeb, :live_view | |
| 2 | + use RoomSanctumWeb, :live_view_a | |
| 3 | 3 | |
| 4 | 4 | alias RoomSanctum.Configuration |
| 5 | 5 |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_foci_live/show.html.heex
+1
−0
@@ -11,6 +11,7 @@
| 11 | 11 | action={@live_action} |
| 12 | 12 | foci={@foci} |
| 13 | 13 | patch={Routes.foci_show_path(@socket, :show, @foci)} |
| 14 | + current_user={@current_user} | |
| 14 | 15 | /> |
| 15 | 16 | </.modal> |
| 16 | 17 | <div class="mx-4 py-2"> |
added
apps/room_sanctum/test/room_sanctum_web/live/foci_edit_test.exs
+145
−0
@@ -0,0 +1,145 @@
| 1 | +defmodule RoomSanctumWeb.FociEditTest do | |
| 2 | + @moduledoc """ | |
| 3 | + Editing a foci, from both places that offer it. | |
| 4 | + | |
| 5 | + Two faults met on the same keystroke: the show page mounts without a | |
| 6 | + current_user, which the form component reads on every validate, and both | |
| 7 | + form components overwrite `place` with an assign that is only set once the | |
| 8 | + map (or the coordinate box) has been touched -- so a rename dropped the | |
| 9 | + point the foci is for. | |
| 10 | + """ | |
| 11 | + use RoomSanctumWeb.ConnCase | |
| 12 | + | |
| 13 | + import Phoenix.LiveViewTest | |
| 14 | + | |
| 15 | + alias RoomSanctum.{Accounts, Configuration} | |
| 16 | + | |
| 17 | + @sfo %Geo.Point{coordinates: {-122.3841977119446, 37.62257668960213}, srid: 4326} | |
| 18 | + | |
| 19 | + setup %{conn: conn} do | |
| 20 | + {:ok, user} = | |
| 21 | + Accounts.register_user(%{ | |
| 22 | + email: "foci#{System.unique_integer([:positive])}@example.com", | |
| 23 | + password: "hello world!hello world!" | |
| 24 | + }) | |
| 25 | + | |
| 26 | + {:ok, foci} = Configuration.create_foci(%{name: "SFO", user_id: user.id, place: @sfo}) | |
| 27 | + | |
| 28 | + %{conn: log_in_user(conn, user), user: user, foci: foci} | |
| 29 | + end | |
| 30 | + | |
| 31 | + describe "from the foci's own page" do | |
| 32 | + test "typing in a field does not take the page down", ctx do | |
| 33 | + {:ok, live, _html} = live(ctx.conn, Routes.foci_show_path(ctx.conn, :edit, ctx.foci)) | |
| 34 | + | |
| 35 | + # the component reads socket.assigns.current_user, which this page | |
| 36 | + # never assigned -- a KeyError on the first keystroke | |
| 37 | + html = | |
| 38 | + live | |
| 39 | + |> form("#foci-form", foci: %{name: "SFO2"}) | |
| 40 | + |> render_change() | |
| 41 | + | |
| 42 | + assert html =~ "SFO2" | |
| 43 | + end | |
| 44 | + | |
| 45 | + test "renaming keeps the place it is a foci for", ctx do | |
| 46 | + {:ok, live, _html} = live(ctx.conn, Routes.foci_show_path(ctx.conn, :edit, ctx.foci)) | |
| 47 | + | |
| 48 | + live |> form("#foci-form", foci: %{name: "SFO2"}) |> render_submit() | |
| 49 | + | |
| 50 | + reloaded = Configuration.get_foci!(ctx.foci.id) | |
| 51 | + assert reloaded.name == "SFO2" | |
| 52 | + assert reloaded.place == @sfo | |
| 53 | + end | |
| 54 | + | |
| 55 | + test "the place survives a validate before the save", ctx do | |
| 56 | + {:ok, live, _html} = live(ctx.conn, Routes.foci_show_path(ctx.conn, :edit, ctx.foci)) | |
| 57 | + | |
| 58 | + form = form(live, "#foci-form", foci: %{name: "SFO2"}) | |
| 59 | + render_change(form) | |
| 60 | + render_submit(form) | |
| 61 | + | |
| 62 | + assert Configuration.get_foci!(ctx.foci.id).place == @sfo | |
| 63 | + end | |
| 64 | + end | |
| 65 | + | |
| 66 | + describe "from the foci list" do | |
| 67 | + test "renaming keeps the place", ctx do | |
| 68 | + {:ok, live, _html} = live(ctx.conn, Routes.foci_index_path(ctx.conn, :edit, ctx.foci)) | |
| 69 | + | |
| 70 | + live |> form("#foci-form", foci: %{name: "SFO2"}) |> render_submit() | |
| 71 | + | |
| 72 | + reloaded = Configuration.get_foci!(ctx.foci.id) | |
| 73 | + assert reloaded.name == "SFO2" | |
| 74 | + assert reloaded.place == @sfo | |
| 75 | + end | |
| 76 | + | |
| 77 | + test "the coordinate editor keeps the place too", ctx do | |
| 78 | + {:ok, live, _html} = live(ctx.conn, Routes.foci_index_path(ctx.conn, :edit_coords, ctx.foci)) | |
| 79 | + | |
| 80 | + live |> form("#foci-form", foci: %{name: "SFO2"}) |> render_submit() | |
| 81 | + | |
| 82 | + reloaded = Configuration.get_foci!(ctx.foci.id) | |
| 83 | + assert reloaded.name == "SFO2" | |
| 84 | + assert reloaded.place == @sfo | |
| 85 | + end | |
| 86 | + | |
| 87 | + test "a new foci still needs a place picked", ctx do | |
| 88 | + {:ok, live, _html} = live(ctx.conn, Routes.foci_index_path(ctx.conn, :new)) | |
| 89 | + | |
| 90 | + html = live |> form("#foci-form", foci: %{name: "Nowhere"}) |> render_submit() | |
| 91 | + | |
| 92 | + # place has no input, so the error had nowhere to render and the save | |
| 93 | + # looked like a no-op | |
| 94 | + assert html =~ "Pick a place on the map" | |
| 95 | + assert Configuration.list_focis({:user, ctx.user.id}) |> length() == 1 | |
| 96 | + end | |
| 97 | + end | |
| 98 | + | |
| 99 | + describe "picking a new place" do | |
| 100 | + # The map hook pushes map-update to #foci-form, which LiveViewTest cannot | |
| 101 | + # drive (the form carries phx-target, not phx-hook), so this drives the | |
| 102 | + # component directly. The point is that the fallback is assign_new: a | |
| 103 | + # place clicked on the map must survive the next update/2. | |
| 104 | + alias RoomSanctumWeb.FociLive.FormComponent | |
| 105 | + | |
| 106 | + defp socket_for(foci) do | |
| 107 | + {:ok, socket} = | |
| 108 | + FormComponent.update( | |
| 109 | + %{foci: foci, id: foci.id, title: "Modify Foci", action: :edit, patch: "/"}, | |
| 110 | + %Phoenix.LiveView.Socket{assigns: %{__changed__: %{}}} | |
| 111 | + ) | |
| 112 | + | |
| 113 | + socket | |
| 114 | + end | |
| 115 | + | |
| 116 | + test "the stored place is what the form falls back to", ctx do | |
| 117 | + assert socket_for(ctx.foci).assigns.place == @sfo | |
| 118 | + end | |
| 119 | + | |
| 120 | + test "a place clicked on the map is not reset by the next re-render", ctx do | |
| 121 | + {:noreply, socket} = | |
| 122 | + FormComponent.handle_event( | |
| 123 | + "map-update", | |
| 124 | + %{"latlng" => %{"lat" => 42.3601, "lng" => -71.0589}}, | |
| 125 | + socket_for(ctx.foci) | |
| 126 | + ) | |
| 127 | + | |
| 128 | + assert socket.assigns.place.coordinates == {-71.0589, 42.3601} | |
| 129 | + | |
| 130 | + # the parent re-rendering the component must not undo the pick | |
| 131 | + {:ok, socket} = | |
| 132 | + FormComponent.update(%{foci: ctx.foci, id: ctx.foci.id}, socket) | |
| 133 | + | |
| 134 | + assert socket.assigns.place.coordinates == {-71.0589, 42.3601} | |
| 135 | + end | |
| 136 | + end | |
| 137 | + | |
| 138 | + describe "the edit page itself" do | |
| 139 | + test "renders with the foci's name in the form", ctx do | |
| 140 | + {:ok, _live, html} = live(ctx.conn, Routes.foci_show_path(ctx.conn, :edit, ctx.foci)) | |
| 141 | + | |
| 142 | + assert html =~ ~s(value="SFO") | |
| 143 | + end | |
| 144 | + end | |
| 145 | +end |
Parents: 26943c7