Fix the vision form, and let it pick queries by name

c065e44 · Gabriel Morell · 2026-09-01 15:32

43 files +859 -114
Message
{commit_body(@commit)}

Files changed

modified apps/room_hermes/lib/room_hermes_web/views/error_view.ex
+6 −4
@@ -7,10 +7,12 @@ defmodule RoomHermesWeb.ErrorView do
7 7 # "Internal Server Error"
8 8 # end
9 9
10 − # By default, Phoenix returns the status message from
11 − # the template name. For example, "404.html" becomes
12 − # "Not Found".
13 − def template_not_found(template, _assigns) do
10 + # By default, Phoenix returns the status message from the template name. For
11 + # example, "404.html" becomes "Not Found".
12 + #
13 + # render/2 rather than template_not_found/2, which was Phoenix 1.6's fallback
14 + # and is not on the path the endpoint takes -- see RoomSanctumWeb.ErrorView.
15 + def render(template, _assigns) do
14 16 Phoenix.Controller.status_message_from_template(template)
15 17 end
16 18 end
modified apps/room_sanctum/lib/room_sanctum/configuration/vision.ex
+19 −0
@@ -26,6 +26,25 @@ defmodule RoomSanctum.Configuration.Vision do
26 26 |> foreign_key_constraint(:user_id)
27 27 |> validate_required([:name, :user_id])
28 28 |> validate_queries_have_valid_ids()
29 + |> validate_queries_are_distinct()
30 + end
31 +
32 + # A query belongs on a vision once. Twice is never deliberate -- the same
33 + # arrivals drawn twice on the board and two identical markers on the map --
34 + # and the form now takes the queries that are spoken for out of every other
35 + # row's picker, so reaching here means something got past that.
36 + defp validate_queries_are_distinct(changeset) do
37 + ids =
38 + changeset
39 + |> get_field(:queries)
40 + |> List.wrap()
41 + |> Enum.map(fn query -> query.data && Map.get(query.data, :query) end)
42 + |> Enum.reject(&is_nil/1)
43 +
44 + case ids -- Enum.uniq(ids) do
45 + [] -> changeset
46 + _repeated -> add_error(changeset, :queries, "each query can only be on a vision once")
47 + end
29 48 end
30 49
31 50 def meta_changeset(meta, attrs \\ %{}) do
modified apps/room_sanctum/lib/room_sanctum_web/components/core_components.ex
+18 −15
@@ -139,13 +139,13 @@ defmodule RoomSanctumWeb.CoreComponents do
139 139 {@rest}
140 140 >
141 141 <p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
142 − <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
143 − <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
142 + <.icon :if={@kind == :info} name="fa-circle-info" class="h-4 w-4" />
143 + <.icon :if={@kind == :error} name="fa-circle-exclamation" class="h-4 w-4" />
144 144 <%= @title %>
145 145 </p>
146 146 <p class="mt-2 text-sm leading-5"><%= msg %></p>
147 147 <button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
148 − <.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
148 + <.icon name="fa-xmark" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
149 149 </button>
150 150 </div>
151 151 """
@@ -172,7 +172,7 @@ defmodule RoomSanctumWeb.CoreComponents do
172 172 phx-connected={hide("#disconnected")}
173 173 hidden
174 174 >
175 − Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
175 + Attempting to reconnect <.icon name="fa-arrows-rotate" class="ml-1 h-3 w-3 animate-spin" />
176 176 </.flash>
177 177 """
178 178 end
@@ -295,6 +295,7 @@ defmodule RoomSanctumWeb.CoreComponents do
295 295 attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
296 296
297 297 attr :checked_value, :string, doc: "the currently checked value"
298 +
298 299 attr :rest, :global,
299 300 include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
300 301 multiple pattern placeholder readonly required rows size step)
@@ -375,7 +376,6 @@ defmodule RoomSanctumWeb.CoreComponents do
375 376
376 377 # checked={(@checked_value == opt) || (@field.value == String.to_atom(opt))}
377 378
378 −
379 379 # A card grid instead of a <select>: same single form field, but every choice
380 380 # is visible at once with room for a line explaining what it pulls in.
381 381 # Options are {value, label, description} tuples; the icon is looked up from
@@ -601,7 +601,7 @@ defmodule RoomSanctumWeb.CoreComponents do
601 601 def error(assigns) do
602 602 ~H"""
603 603 <p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600 phx-no-feedback:hidden">
604 − <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
604 + <.icon name="fa-circle-exclamation" class="mt-0.5 h-5 w-5 flex-none" />
605 605 <%= render_slot(@inner_block) %>
606 606 </p>
607 607 """
@@ -752,7 +752,7 @@ defmodule RoomSanctumWeb.CoreComponents do
752 752 navigate={@navigate}
753 753 class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
754 754 >
755 − <.icon name="hero-arrow-left-solid" class="h-3 w-3" />
755 + <.icon name="fa-arrow-left" class="h-3 w-3" />
756 756 <%= render_slot(@inner_block) %>
757 757 </.link>
758 758 </div>
@@ -767,22 +767,26 @@ defmodule RoomSanctumWeb.CoreComponents do
767 767 be applied by using the `-solid` and `-mini` suffix.
768 768
769 769 You can customize the size and colors of the icons by setting
770 − width, height, and background color classes.
770 + size and colour classes.
771 771
772 − Icons are extracted from your `assets/vendor/heroicons` directory and bundled
773 − within your compiled app.css by the plugin in your `assets/tailwind.config.js`.
772 + Font Awesome, which is what this app loads: `<.icon name="fa-trash" />`. The
773 + generated code this replaced took heroicon names -- `hero-trash` -- and drew
774 + them as `<span class="hero-trash">`, which needs a heroicons dependency and a
775 + Tailwind plugin to become an SVG. This app has never had either, so every
776 + icon rendered as an empty span: the vision form's add and remove buttons, the
777 + flash messages' close and status marks, all of them.
774 778
775 779 ## Examples
776 780
777 − <.icon name="hero-x-mark-solid" />
778 − <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
781 + <.icon name="fa-xmark" />
782 + <.icon name="fa-arrows-rotate" class="ml-1 w-3 h-3 animate-spin" />
779 783 """
780 784 attr :name, :string, required: true
781 785 attr :class, :string, default: nil
782 786
783 − def icon(%{name: "hero-" <> _} = assigns) do
787 + def icon(assigns) do
784 788 ~H"""
785 − <span class={[@name, @class]} />
789 + <span class={["fa-solid fa-fw", @name, @class]} />
786 790 """
787 791 end
788 792
@@ -915,5 +919,4 @@ defmodule RoomSanctumWeb.CoreComponents do
915 919 <i :if={@tint not in [nil, ""]} class={"fa-solid fa-circle text-#{@tint}-500 #{@class}"}></i>
916 920 """
917 921 end
918 −
919 922 end
modified apps/room_sanctum/lib/room_sanctum_web/components/layouts/root_public.html.heex
+1 −0
@@ -4,6 +4,7 @@
4 4 <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6 6 <%= csrf_meta_tag() %>
7 + <.basemap_meta />
7 8 <%= live_title_tag assigns[:page_title] || "RoomSanctum", suffix: " · Apollos Crib" %>
8 9 <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
9 10 <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
modified apps/room_sanctum/lib/room_sanctum_web/components/layouts/root_public_afterdark.html.heex
+1 −0
@@ -4,6 +4,7 @@
4 4 <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6 6 <%= csrf_meta_tag() %>
7 + <.basemap_meta />
7 8 <%= live_title_tag assigns[:page_title] || "RoomSanctum", suffix: " · Apollos Crib" %>
8 9 <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
9 10 <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>
modified apps/room_sanctum/lib/room_sanctum_web/components/layouts/root_public_her.html.heex
+1 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/components/layouts/root_public_inky.html.heex
+1 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/components/layouts/root_public_map.html.heex
+19 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/components/query_geospatial_map.ex
+10 −1

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/cfg_pythiae_live/public_map.ex
+92 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/cfg_pythiae_live/public_map.html.heex
+27 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_pythiae_live/show.html.heex
+2 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/form_component.ex
+196 −88

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/live/cfg_vision_live/form_component.html.heex
+2 −2

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/router.ex
+5 −0

Click to load diff…

modified apps/room_sanctum/lib/room_sanctum_web/views/error_view.ex
+8 −4

Click to load diff…

added apps/room_sanctum/test/room_sanctum/vision_distinct_queries_test.exs
+44 −0

Click to load diff…

added apps/room_sanctum/test/room_sanctum_web/components/icon_test.exs
+38 −0

Click to load diff…

added apps/room_sanctum/test/room_sanctum_web/live/pythiae_public_map_test.exs
+69 −0

Click to load diff…

added apps/room_sanctum/test/room_sanctum_web/live/vision_form_queries_test.exs
+288 −0

Click to load diff…

modified apps/room_sanctum/test/room_sanctum_web/views/error_view_test.exs
+12 −0

Click to load diff…

Parents: 31a0a77