Match the add-to buttons to the visions, and let a query be unpinned

395f406 · Gabriel Morell · 2026-08-11 17:57

12 files +121 -8
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_query_live/show.ex
+25 −0
@@ -268,6 +268,31 @@ defmodule RoomSanctumWeb.QueryLive.Show do
268 268 end
269 269 end
270 270
271 + def handle_event("remove-from", %{"vision" => vision}, socket) do
272 + Process.send_after(self(), :update_sec, 200)
273 +
274 + vision = Configuration.get_vision!(vision)
275 + query_id = String.to_integer(socket.assigns.query_id)
276 +
277 + # Both halves again: drop every embed pointing at this query -- there may
278 + # be more than one, since re-pinning used to duplicate them -- and the id.
279 + queries =
280 + vision.queries
281 + |> Poison.encode!()
282 + |> Poison.decode!()
283 + |> Enum.reject(fn q -> to_string(get_in(q, ["data", "query"])) == to_string(query_id) end)
284 +
285 + query_ids = (vision.query_ids || []) -- [query_id]
286 +
287 + case Configuration.update_vision_ni(vision, %{queries: queries, query_ids: query_ids}) do
288 + {:ok, _vision} ->
289 + {:noreply, socket}
290 +
291 + {:error, _changeset} ->
292 + {:noreply, put_flash(socket, :error, "Could not remove this from #{vision.name}")}
293 + end
294 + end
295 +
271 296 # A vision records its queries twice: an embedded list that renders, and an
272 297 # id array every "is this query in a vision" lookup reads. Both have to move.
273 298 defp pin_query(vision, socket) do
modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_query_live/show.html.heex
+22 −8
@@ -151,18 +151,32 @@
151 151 <% end %>
152 152 </h3>
153 153 <%= for v <- @visions do %>
154 − <%= live_patch to: Routes.vision_show_path(@socket, :show, v.id), class: "btn btn-primary gap-2 float-left mx-2" do %>
155 − <%= v.name %>
156 − <% end %>
154 + <div class="flex gap-2 mx-2">
155 + <%= live_patch to: Routes.vision_show_path(@socket, :show, v.id), class: "btn btn-primary gap-2 grow" do %>
156 + <%= v.name %>
157 + <% end %>
158 + <%!-- Only while the panel is open, so the usual view of "the
159 + visions this query is in" stays a row of plain links. --%>
160 + <button
161 + :if={@avail_sel}
162 + type="button"
163 + class="btn btn-error btn-square shrink-0"
164 + phx-click="remove-from"
165 + phx-value-vision={v.id}
166 + title={"Remove from #{v.name}"}
167 + >
168 + <i class="fa-solid fa-minus"></i>
169 + </button>
170 + </div>
157 171 <% end%>
158 172 <%= if @avail_sel do %>
159 173 <%= if @avail_visions != [] do %>
160 174 <h4 class="text-xl text-accent font-bold">Add to</h4>
161 − <div class="flex flex-wrap gap-2">
162 − <%= for v <- @avail_visions do %>
163 − <div class="btn btn-secondary gap-2" phx-click="add-to" phx-value-vision={v.id}><%= v.name %></div>
164 − <% end %>
165 − </div>
175 + <%!-- Direct children of the card body, so they stretch the same
176 + way the visions this query already belongs to do. --%>
177 + <%= for v <- @avail_visions do %>
178 + <div class="btn btn-secondary gap-2 float-left mx-2" phx-click="add-to" phx-value-vision={v.id}><%= v.name %></div>
179 + <% end %>
166 180 <% end %>
167 181
168 182 <h4 class="text-xl text-accent font-bold mt-4">Or a new one</h4>
modified apps/room_sanctum/test/room_sanctum_web/live/add_to_vision_test.exs
+74 −0
@@ -180,6 +180,80 @@ defmodule RoomSanctumWeb.AddToVisionTest do
180 180 end
181 181 end
182 182
183 + describe "taking a query back out" do
184 + defp remove(conn, query, vision) do
185 + {:ok, live, _html} = live(conn, Routes.query_show_path(conn, :show, query))
186 + send(live.pid, :update_sec)
187 + _ = render(live)
188 + render_click(live, "remove-from", %{"vision" => to_string(vision.id)})
189 + Process.sleep(50)
190 + Configuration.get_vision!(vision.id)
191 + end
192 +
193 + test "drops the query from both halves", ctx do
194 + q = query(ctx, "AAPL")
195 + add(ctx.conn, q, ctx.vision)
196 +
197 + vision = remove(ctx.conn, q, ctx.vision)
198 +
199 + assert vision.query_ids == []
200 + assert vision.queries == []
201 + end
202 +
203 + test "the query page stops listing the vision", ctx do
204 + q = query(ctx, "AAPL")
205 + add(ctx.conn, q, ctx.vision)
206 + assert length(Configuration.get_visions(:query, to_string(q.id))) == 1
207 +
208 + remove(ctx.conn, q, ctx.vision)
209 +
210 + assert Configuration.get_visions(:query, to_string(q.id)) == []
211 + end
212 +
213 + test "the vision is offered again afterwards", ctx do
214 + q = query(ctx, "AAPL")
215 + add(ctx.conn, q, ctx.vision)
216 + assert Configuration.get_visions_nv(:query, to_string(q.id)) == []
217 +
218 + remove(ctx.conn, q, ctx.vision)
219 +
220 + assert length(Configuration.get_visions_nv(:query, to_string(q.id))) == 1
221 + end
222 +
223 + test "the other queries in that vision are left alone", ctx do
224 + a = query(ctx, "AAPL")
225 + b = query(ctx, "MSFT")
226 + add(ctx.conn, a, ctx.vision)
227 + add(ctx.conn, b, ctx.vision)
228 +
229 + vision = remove(ctx.conn, a, ctx.vision)
230 +
231 + assert vision.query_ids == [b.id]
232 + assert length(vision.queries) == 1
233 + end
234 +
235 + test "removing something already gone is not an error", ctx do
236 + q = query(ctx, "AAPL")
237 +
238 + vision = remove(ctx.conn, q, ctx.vision)
239 +
240 + assert vision.query_ids in [nil, []]
241 + end
242 +
243 + test "the button only appears while the panel is open", ctx do
244 + q = query(ctx, "AAPL")
245 + add(ctx.conn, q, ctx.vision)
246 +
247 + {:ok, live, _html} = live(ctx.conn, Routes.query_show_path(ctx.conn, :show, q))
248 + send(live.pid, :update_sec)
249 + closed = render(live)
250 + refute closed =~ ~s(phx-click="remove-from")
251 +
252 + open = render_click(live, "toggle-sel", %{})
253 + assert open =~ ~s(phx-click="remove-from")
254 + end
255 + end
256 +
183 257 defp conn_for(ctx), do: ctx.conn
184 258
185 259 test "a third query still lands", ctx do

Parents: 8665053