fix(rover-planning): suppress fresh-station error + tolerate integer JSONB values
- add_station no longer seeds `"input" => ""` in params, so used_input?/1 treats the new row as untouched and hides the validate_required error until the user types. - Show page's format_distance/format_db/format_meters helpers coerce with `* 1.0` because JSONB returns bare zeros as integers, which crashed Float.round/2 (FunctionClauseError on /rover-planning/:id render). - Lat/lon row uses items-start + mt-6 trash so the inputs stay aligned even when the callsign cell grows from validation feedback.
This commit is contained in:
parent
eca463add1
commit
cff0c6775d
3 changed files with 67 additions and 9 deletions
|
|
@ -91,9 +91,12 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Form do
|
|||
stations = Map.get(params, "stations", %{})
|
||||
next_index = next_station_index(stations)
|
||||
|
||||
# Don't include "input" in the new station's params — `used_input?/1`
|
||||
# treats any field present in params as "used" and would render the
|
||||
# `validate_required([:input])` error before the user has typed
|
||||
# anything. Leaving the key out keeps the row pristine until edited.
|
||||
stations =
|
||||
Map.put(stations, Integer.to_string(next_index), %{
|
||||
"input" => "",
|
||||
"position" => Integer.to_string(next_index)
|
||||
})
|
||||
|
||||
|
|
@ -247,14 +250,20 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Form do
|
|||
|
||||
<.inputs_for :let={s} field={@form[:stations]}>
|
||||
<div class="card bg-base-200 p-3 mb-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<%!--
|
||||
`items-start` keeps the lat/lon row top-aligned even when the
|
||||
callsign cell grows (e.g. an error message under the input).
|
||||
The trash button uses `mt-6` to sit beside the input — matches
|
||||
the label height so it visually aligns with the input field.
|
||||
--%>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 items-start">
|
||||
<.input
|
||||
field={s[:input]}
|
||||
type="text"
|
||||
label="Callsign / grid / lat,lon"
|
||||
placeholder="W5LUA or EM13qc or 32.91, -97.06"
|
||||
/>
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="flex-1">
|
||||
<.input field={s[:lat]} type="number" step="any" label="Lat" />
|
||||
</div>
|
||||
|
|
@ -265,7 +274,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Form do
|
|||
type="button"
|
||||
phx-click="remove_station"
|
||||
phx-value-index={s.index}
|
||||
class="btn btn-ghost btn-sm text-error mb-2"
|
||||
class="btn btn-ghost btn-sm text-error mt-6"
|
||||
title="Remove station"
|
||||
>
|
||||
<.icon name="hero-trash" class="w-4 h-4" />
|
||||
|
|
|
|||
|
|
@ -124,12 +124,18 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
|||
%{total: total, complete: complete, pending: pending, failed: failed}
|
||||
end
|
||||
|
||||
defp format_distance(km) when is_number(km), do: "#{Float.round(km, 1)} km / #{Float.round(km * 0.621371, 1)} mi"
|
||||
# JSONB round-trips bare zeros as integers (e.g. `0` not `0.0`), and
|
||||
# `Float.round/2` only accepts floats. Coerce with `* 1.0` everywhere
|
||||
# we round a value that came out of a `:map` column.
|
||||
defp format_distance(km) when is_number(km), do: "#{Float.round(km * 1.0, 1)} km / #{Float.round(km * 0.621371, 1)} mi"
|
||||
|
||||
defp format_distance(_), do: "—"
|
||||
|
||||
defp format_db(nil), do: "—"
|
||||
defp format_db(value) when is_number(value), do: "#{Float.round(value, 1)} dB"
|
||||
defp format_db(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} dB"
|
||||
|
||||
defp format_meters(nil), do: "—"
|
||||
defp format_meters(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} m"
|
||||
|
||||
defp rover_label(%{rover_location: %{lat: lat, lon: lon}}) when is_number(lat) and is_number(lon) do
|
||||
grid = Maidenhead.from_latlon(lat, lon, 6)
|
||||
|
|
@ -243,9 +249,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
|||
{if path.result, do: format_distance(path.result["distance_km"]), else: "—"}
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
{if path.result,
|
||||
do: "#{Float.round(path.result["min_clearance_m"] || 0.0, 1)} m",
|
||||
else: "—"}
|
||||
{if path.result, do: format_meters(path.result["min_clearance_m"]), else: "—"}
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
{if path.result, do: format_db(path.result["diffraction_db"]), else: "—"}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
|
|||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Microwaveprop.AccountsFixtures
|
||||
alias Microwaveprop.Repo
|
||||
alias Microwaveprop.Rover
|
||||
alias Microwaveprop.RoverPlanning
|
||||
alias Microwaveprop.RoverPlanning.Path
|
||||
alias Microwaveprop.Terrain.ElevationClient
|
||||
|
||||
setup do
|
||||
|
|
@ -69,6 +71,34 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
|
|||
assert html =~ "Path profiles"
|
||||
end
|
||||
|
||||
test "renders integer path-result values without crashing", %{conn: conn} do
|
||||
# JSONB round-trips bare zeros as integers (e.g. `0` not `0.0`), so the
|
||||
# show page MUST tolerate non-float numerics in `result`. Previously
|
||||
# `Float.round(value, n)` crashed with FunctionClauseError when value
|
||||
# came back as integer 0.
|
||||
user = AccountsFixtures.user_fixture()
|
||||
mission = create_mission(user, "Integer result mission")
|
||||
|
||||
[path | _] = Repo.all(Path)
|
||||
|
||||
{:ok, _} =
|
||||
path
|
||||
|> Ecto.Changeset.change(%{
|
||||
status: :complete,
|
||||
result: %{
|
||||
"distance_km" => 0,
|
||||
"min_clearance_m" => 0,
|
||||
"diffraction_db" => 0,
|
||||
"verdict" => "clear"
|
||||
}
|
||||
})
|
||||
|> Repo.update()
|
||||
|
||||
{:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
|
||||
assert html =~ "Integer result mission"
|
||||
assert html =~ "Clear"
|
||||
end
|
||||
|
||||
test "redirects when mission missing", %{conn: conn} do
|
||||
missing = Ecto.UUID.generate()
|
||||
|
||||
|
|
@ -83,6 +113,21 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
|
|||
live(conn, ~p"/rover-planning/new")
|
||||
end
|
||||
|
||||
test "clicking 'Add station' does not show 'can't be blank' on the new row", %{conn: conn} do
|
||||
user = AccountsFixtures.user_fixture()
|
||||
conn = log_in_user(conn, user)
|
||||
{:ok, lv, _html} = live(conn, ~p"/rover-planning/new")
|
||||
|
||||
html =
|
||||
lv
|
||||
|> element("button[phx-click='add_station']")
|
||||
|> render_click()
|
||||
|
||||
# The new (untouched) station must not render a validation error until
|
||||
# the user actually interacts with its input.
|
||||
refute html =~ "can't be blank"
|
||||
end
|
||||
|
||||
test "typing into the form does not flip the only_known_good checkbox", %{conn: conn} do
|
||||
user = AccountsFixtures.user_fixture()
|
||||
conn = log_in_user(conn, user)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue