Fix editing a foci: crash on the show page, and a lost place

11a1dc3 · Gabriel Morell · 2026-08-12 14:27

15 files +158 -1
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