refactor(rover): /rover-locations as paginated table with status filter
Reuses the LiveTable pattern from /contacts and /users so the page gets sort, search, and pagination. Adds an in-page Ideal/Off Limits filter.
This commit is contained in:
parent
6c5d066121
commit
7eafa4c71a
2 changed files with 269 additions and 157 deletions
|
|
@ -1,38 +1,81 @@
|
||||||
defmodule MicrowavepropWeb.RoverLocationsLive do
|
defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Globally-shared list of rover-friendly (or off-limits) parking
|
Globally-shared list of rover-friendly (or off-limits) parking
|
||||||
locations. Everyone sees the list; only logged-in users can add or
|
locations. Everyone sees the table; only logged-in users can add or
|
||||||
edit entries (and only the creator can edit/delete their own).
|
edit entries (and only the creator can edit/delete their own).
|
||||||
"""
|
"""
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Rover.Location
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
alias Microwaveprop.Accounts.User
|
alias Microwaveprop.Accounts.User
|
||||||
alias Microwaveprop.Radio.Maidenhead
|
alias Microwaveprop.Radio.Maidenhead
|
||||||
|
alias Microwaveprop.Repo
|
||||||
alias Microwaveprop.Rover
|
alias Microwaveprop.Rover
|
||||||
alias Microwaveprop.Rover.Location
|
alias Microwaveprop.Rover.Location
|
||||||
|
|
||||||
|
@valid_status_filters ~w(ideal off_limits)
|
||||||
|
|
||||||
|
def table_options do
|
||||||
|
%{sorting: %{default_sort: [inserted_at: :desc]}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def fields do
|
||||||
|
[
|
||||||
|
id: %{label: "ID", hidden: true},
|
||||||
|
user_id: %{label: "Owner", hidden: true},
|
||||||
|
status: %{label: "Status", sortable: true, renderer: &status_cell/1},
|
||||||
|
lat: %{label: "Location", sortable: true, renderer: &location_cell/2},
|
||||||
|
lon: %{label: "Lon", hidden: true},
|
||||||
|
notes: %{label: "Notes", searchable: true, renderer: ¬es_cell/1},
|
||||||
|
inserted_at: %{label: "Added (UTC)", sortable: true, renderer: &format_ts/1}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def filters, do: []
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
# Called by LiveTable to get the base queryable. Filters by status when
|
||||||
|
# the user picks one in the in-page select; nil means show all.
|
||||||
|
def visible_query_provider(status_filter) do
|
||||||
|
base = from(l in Location, as: :resource)
|
||||||
|
|
||||||
|
case status_filter do
|
||||||
|
"ideal" -> from(l in base, where: l.status == :ideal)
|
||||||
|
"off_limits" -> from(l in base, where: l.status == :off_limits)
|
||||||
|
_ -> base
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
{:ok,
|
{:ok,
|
||||||
assign(socket,
|
socket
|
||||||
|
|> assign(
|
||||||
page_title: "Rover Locations",
|
page_title: "Rover Locations",
|
||||||
locations: Rover.list_locations(),
|
|
||||||
form: nil,
|
form: nil,
|
||||||
editing_id: nil,
|
editing_id: nil,
|
||||||
expanded_id: nil
|
status_filter: nil
|
||||||
)}
|
)
|
||||||
|
|> assign(:data_provider, {__MODULE__, :visible_query_provider, [nil]})}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("toggle_map", %{"id" => id}, socket) do
|
def handle_event("filter_status", %{"value" => value}, socket) do
|
||||||
new_id = if socket.assigns.expanded_id == id, do: nil, else: id
|
status = parse_status(value)
|
||||||
{:noreply, assign(socket, expanded_id: new_id)}
|
data_provider = {__MODULE__, :visible_query_provider, [status]}
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:status_filter, status)
|
||||||
|
|> assign(:data_provider, data_provider)
|
||||||
|
|> reload()}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("new", _params, socket) do
|
def handle_event("new", _params, socket) do
|
||||||
if user = current_user(socket) do
|
if user = current_user(socket) do
|
||||||
form = blank_form(user)
|
{:noreply, assign(socket, form: blank_form(user), editing_id: nil)}
|
||||||
{:noreply, assign(socket, form: form, editing_id: nil)}
|
|
||||||
else
|
else
|
||||||
{:noreply, put_flash(socket, :error, "Sign in to add a location.")}
|
{:noreply, put_flash(socket, :error, "Sign in to add a location.")}
|
||||||
end
|
end
|
||||||
|
|
@ -41,7 +84,7 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
def handle_event("edit", %{"id" => id}, socket) do
|
def handle_event("edit", %{"id" => id}, socket) do
|
||||||
case current_user(socket) do
|
case current_user(socket) do
|
||||||
%User{id: user_id} ->
|
%User{id: user_id} ->
|
||||||
case Enum.find(socket.assigns.locations, &(&1.id == id)) do
|
case Repo.get(Location, id) do
|
||||||
%Location{user_id: ^user_id} = loc ->
|
%Location{user_id: ^user_id} = loc ->
|
||||||
cs = Location.changeset(loc, %{})
|
cs = Location.changeset(loc, %{})
|
||||||
{:noreply, assign(socket, form: to_form(cs), editing_id: id)}
|
{:noreply, assign(socket, form: to_form(cs), editing_id: id)}
|
||||||
|
|
@ -87,8 +130,8 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(locations: Rover.list_locations())
|
|> put_flash(:info, "Location removed.")
|
||||||
|> put_flash(:info, "Location removed.")}
|
|> reload()}
|
||||||
|
|
||||||
{:error, :not_found} ->
|
{:error, :not_found} ->
|
||||||
{:noreply, put_flash(socket, :error, "You can only delete your own locations.")}
|
{:noreply, put_flash(socket, :error, "You can only delete your own locations.")}
|
||||||
|
|
@ -104,8 +147,9 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
{:ok, _loc} ->
|
{:ok, _loc} ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(form: nil, editing_id: nil, locations: Rover.list_locations())
|
|> assign(form: nil, editing_id: nil)
|
||||||
|> put_flash(:info, "Location added.")}
|
|> put_flash(:info, "Location added.")
|
||||||
|
|> reload()}
|
||||||
|
|
||||||
{:error, cs} ->
|
{:error, cs} ->
|
||||||
{:noreply, assign(socket, form: to_form(cs))}
|
{:noreply, assign(socket, form: to_form(cs))}
|
||||||
|
|
@ -117,8 +161,9 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
{:ok, _loc} ->
|
{:ok, _loc} ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(form: nil, editing_id: nil, locations: Rover.list_locations())
|
|> assign(form: nil, editing_id: nil)
|
||||||
|> put_flash(:info, "Location updated.")}
|
|> put_flash(:info, "Location updated.")
|
||||||
|
|> reload()}
|
||||||
|
|
||||||
{:error, :not_found} ->
|
{:error, :not_found} ->
|
||||||
{:noreply, put_flash(socket, :error, "You can only edit your own locations.")}
|
{:noreply, put_flash(socket, :error, "You can only edit your own locations.")}
|
||||||
|
|
@ -128,14 +173,34 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp location_for_edit(socket) do
|
defp location_for_edit(%{assigns: %{editing_id: id}}) when is_binary(id) do
|
||||||
Enum.find(socket.assigns.locations, &(&1.id == socket.assigns.editing_id)) || %Location{}
|
Repo.get(Location, id) || %Location{}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp location_for_edit(_), do: %Location{}
|
||||||
|
|
||||||
defp blank_form(_user) do
|
defp blank_form(_user) do
|
||||||
%Location{} |> Location.changeset(%{}) |> to_form()
|
%Location{} |> Location.changeset(%{}) |> to_form()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Re-runs the LiveTable fetch with the current data_provider + options.
|
||||||
|
# Cheaper and simpler than push_patch — preserves search/sort/page state
|
||||||
|
# without round-tripping through the URL.
|
||||||
|
defp reload(socket) do
|
||||||
|
options = socket.assigns.options
|
||||||
|
data_provider = socket.assigns.data_provider
|
||||||
|
table_options = get_merged_table_options()
|
||||||
|
|
||||||
|
{resources, updated_options} = fetch_resources(options, data_provider)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign_to_socket(resources, table_options)
|
||||||
|
|> assign(:options, updated_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_status(value) when value in @valid_status_filters, do: value
|
||||||
|
defp parse_status(_), do: nil
|
||||||
|
|
||||||
defp current_user(%Phoenix.LiveView.Socket{assigns: assigns}), do: scope_user(assigns)
|
defp current_user(%Phoenix.LiveView.Socket{assigns: assigns}), do: scope_user(assigns)
|
||||||
defp current_user(assigns) when is_map(assigns), do: scope_user(assigns)
|
defp current_user(assigns) when is_map(assigns), do: scope_user(assigns)
|
||||||
|
|
||||||
|
|
@ -146,157 +211,180 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp can_edit?(_assigns, %Location{user_id: nil}), do: false
|
defp owner?(_scope, %{user_id: nil}), do: false
|
||||||
|
|
||||||
defp can_edit?(assigns, %Location{user_id: uid}) do
|
defp owner?(scope, %{user_id: uid}) do
|
||||||
case current_user(assigns) do
|
case scope_user(%{current_scope: scope}) do
|
||||||
%User{id: ^uid} -> true
|
%User{id: ^uid} -> true
|
||||||
_ -> false
|
_ -> false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp status_label(:ideal), do: "Ideal"
|
defp owner?(_, _), do: false
|
||||||
defp status_label(:off_limits), do: "Off Limits"
|
|
||||||
|
|
||||||
defp status_class(:ideal), do: "badge badge-success"
|
defp actions_for(scope) do
|
||||||
defp status_class(:off_limits), do: "badge badge-error"
|
[
|
||||||
|
buttons: fn %{record: record} ->
|
||||||
|
row_actions(record, scope)
|
||||||
|
end
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp row_actions(record, scope) do
|
||||||
|
assigns = %{record: record, owner?: owner?(scope, record)}
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div :if={@owner?} class="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="edit"
|
||||||
|
phx-value-id={@record.id}
|
||||||
|
class="btn btn-xs btn-ghost"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="delete"
|
||||||
|
phx-value-id={@record.id}
|
||||||
|
data-confirm="Delete this location?"
|
||||||
|
class="btn btn-xs btn-ghost text-error"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_cell(:ideal) do
|
||||||
|
assigns = %{}
|
||||||
|
~H|<span class="badge badge-success">Ideal</span>|
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_cell(:off_limits) do
|
||||||
|
assigns = %{}
|
||||||
|
~H|<span class="badge badge-error">Off Limits</span>|
|
||||||
|
end
|
||||||
|
|
||||||
|
defp status_cell(_), do: ""
|
||||||
|
|
||||||
|
defp location_cell(_value, %{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do
|
||||||
|
grid = Maidenhead.from_latlon(lat, lon, 10)
|
||||||
|
assigns = %{lat: lat, lon: lon, grid: grid}
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-mono text-sm font-semibold">{@grid}</span>
|
||||||
|
<span class="font-mono text-xs text-base-content/60">
|
||||||
|
{Float.round(@lat, 5)}, {Float.round(@lon, 5)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp location_cell(_, _), do: ""
|
||||||
|
|
||||||
|
defp notes_cell(nil), do: ""
|
||||||
|
defp notes_cell(""), do: ""
|
||||||
|
|
||||||
|
defp notes_cell(notes) when is_binary(notes) do
|
||||||
|
assigns = %{notes: notes}
|
||||||
|
~H|<span class="whitespace-pre-line">{@notes}</span>|
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_ts(nil), do: ""
|
||||||
|
defp format_ts(%DateTime{} = dt), do: Calendar.strftime(dt, "%Y-%m-%d")
|
||||||
|
defp format_ts(%NaiveDateTime{} = dt), do: Calendar.strftime(dt, "%Y-%m-%d")
|
||||||
|
defp format_ts(other), do: to_string(other)
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
<Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-5xl">
|
||||||
<div class="max-w-5xl mx-auto px-4 py-6">
|
<.header>
|
||||||
<div class="flex items-center justify-between mb-4">
|
Rover Locations
|
||||||
<div>
|
<:subtitle>Community-shared parking spots for portable rover ops.</:subtitle>
|
||||||
<h1 class="text-2xl font-semibold">Rover Locations</h1>
|
<:actions>
|
||||||
<p class="text-sm text-base-content/60">
|
|
||||||
Community-shared parking spots for portable rover ops.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
phx-click="new"
|
phx-click="new"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary"
|
||||||
disabled={is_nil(current_user(assigns))}
|
disabled={is_nil(current_user(assigns))}
|
||||||
>
|
>
|
||||||
+ Add location
|
<.icon name="hero-plus" class="w-5 h-5" /> Add location
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</:actions>
|
||||||
|
</.header>
|
||||||
|
|
||||||
<p :if={is_nil(current_user(assigns))} class="alert alert-info text-sm mb-4">
|
<p :if={is_nil(current_user(assigns))} class="alert alert-info text-sm mb-4">
|
||||||
<a href={~p"/users/log-in"} class="link link-primary">Sign in</a> to add or edit locations.
|
<a href={~p"/users/log-in"} class="link link-primary">Sign in</a> to add or edit locations.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div :if={@form} class="card bg-base-200 p-4 mb-4">
|
<div :if={@form} class="card bg-base-200 p-4 mb-4">
|
||||||
<h2 class="text-lg font-semibold mb-2">
|
<h2 class="text-lg font-semibold mb-2">
|
||||||
{if @editing_id, do: "Edit location", else: "New location"}
|
{if @editing_id, do: "Edit location", else: "New location"}
|
||||||
</h2>
|
</h2>
|
||||||
<.form for={@form} phx-change="validate" phx-submit="save" id="location-form">
|
<.form for={@form} phx-change="validate" phx-submit="save" id="location-form">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
<.input
|
|
||||||
field={@form[:lat]}
|
|
||||||
type="number"
|
|
||||||
step="any"
|
|
||||||
label="Latitude"
|
|
||||||
placeholder="32.5"
|
|
||||||
/>
|
|
||||||
<.input
|
|
||||||
field={@form[:lon]}
|
|
||||||
type="number"
|
|
||||||
step="any"
|
|
||||||
label="Longitude"
|
|
||||||
placeholder="-97.5"
|
|
||||||
/>
|
|
||||||
<.input
|
|
||||||
field={@form[:status]}
|
|
||||||
type="select"
|
|
||||||
label="Status"
|
|
||||||
options={[{"Ideal", :ideal}, {"Off Limits", :off_limits}]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<.input
|
<.input
|
||||||
field={@form[:notes]}
|
field={@form[:lat]}
|
||||||
type="textarea"
|
type="number"
|
||||||
label="Notes"
|
step="any"
|
||||||
rows="3"
|
label="Latitude"
|
||||||
placeholder="Trail access, line-of-sight notes, contact info, etc."
|
placeholder="32.5"
|
||||||
/>
|
/>
|
||||||
<div class="flex gap-2 mt-3">
|
<.input
|
||||||
<button type="submit" class="btn btn-primary btn-sm">
|
field={@form[:lon]}
|
||||||
{if @editing_id, do: "Save", else: "Add"}
|
type="number"
|
||||||
</button>
|
step="any"
|
||||||
<button type="button" phx-click="cancel" class="btn btn-ghost btn-sm">
|
label="Longitude"
|
||||||
Cancel
|
placeholder="-97.5"
|
||||||
</button>
|
/>
|
||||||
</div>
|
<.input
|
||||||
</.form>
|
field={@form[:status]}
|
||||||
</div>
|
type="select"
|
||||||
|
label="Status"
|
||||||
<div :if={@locations == []} class="text-base-content/60 text-sm py-8 text-center">
|
options={[{"Ideal", :ideal}, {"Off Limits", :off_limits}]}
|
||||||
No locations yet. Be the first to share one.
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<.input
|
||||||
<ul :if={@locations != []} class="space-y-3">
|
field={@form[:notes]}
|
||||||
<li
|
type="textarea"
|
||||||
:for={loc <- @locations}
|
label="Notes"
|
||||||
id={"location-#{loc.id}"}
|
rows="3"
|
||||||
class="card bg-base-100 border border-base-300 p-4"
|
placeholder="Trail access, line-of-sight notes, contact info, etc."
|
||||||
>
|
/>
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex gap-2 mt-3">
|
||||||
<div
|
<button type="submit" class="btn btn-primary btn-sm">
|
||||||
class="flex-1 min-w-0 cursor-pointer"
|
{if @editing_id, do: "Save", else: "Add"}
|
||||||
phx-click="toggle_map"
|
</button>
|
||||||
phx-value-id={loc.id}
|
<button type="button" phx-click="cancel" class="btn btn-ghost btn-sm">
|
||||||
>
|
Cancel
|
||||||
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
</button>
|
||||||
<span class={status_class(loc.status)}>{status_label(loc.status)}</span>
|
</div>
|
||||||
<span class="font-mono text-sm font-semibold">
|
</.form>
|
||||||
{Maidenhead.from_latlon(loc.lat, loc.lon, 10)}
|
|
||||||
</span>
|
|
||||||
<span class="font-mono text-xs text-base-content/60">
|
|
||||||
{Float.round(loc.lat, 5)}, {Float.round(loc.lon, 5)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p :if={loc.notes && loc.notes != ""} class="text-sm whitespace-pre-line">
|
|
||||||
{loc.notes}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-base-content/50 mt-1">
|
|
||||||
Added {Calendar.strftime(loc.inserted_at, "%Y-%m-%d")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div :if={can_edit?(assigns, loc)} class="flex gap-2 shrink-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
phx-click="edit"
|
|
||||||
phx-value-id={loc.id}
|
|
||||||
class="btn btn-ghost btn-xs"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
phx-click="delete"
|
|
||||||
phx-value-id={loc.id}
|
|
||||||
data-confirm="Delete this location?"
|
|
||||||
class="btn btn-ghost btn-xs text-error"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
:if={@expanded_id == loc.id}
|
|
||||||
id={"location-map-#{loc.id}"}
|
|
||||||
phx-hook="LocationMap"
|
|
||||||
phx-update="ignore"
|
|
||||||
data-lat={loc.lat}
|
|
||||||
data-lon={loc.lon}
|
|
||||||
class="mt-3 h-80 rounded border border-base-300 z-0"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<form phx-change="filter_status" class="mb-4 flex items-center gap-2">
|
||||||
|
<label for="status-filter" class="text-sm font-medium">Show:</label>
|
||||||
|
<select
|
||||||
|
id="status-filter"
|
||||||
|
name="value"
|
||||||
|
class="select select-sm select-bordered"
|
||||||
|
>
|
||||||
|
<option value="all" selected={is_nil(@status_filter)}>All</option>
|
||||||
|
<option value="ideal" selected={@status_filter == "ideal"}>Ideal</option>
|
||||||
|
<option value="off_limits" selected={@status_filter == "off_limits"}>Off Limits</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<.live_table
|
||||||
|
fields={fields()}
|
||||||
|
filters={filters()}
|
||||||
|
options={@options}
|
||||||
|
streams={@streams}
|
||||||
|
actions={actions_for(@current_scope)}
|
||||||
|
/>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,32 @@ defmodule MicrowavepropWeb.RoverLocationsLiveTest do
|
||||||
assert html =~ ~s(disabled)
|
assert html =~ ~s(disabled)
|
||||||
assert html =~ "Add location"
|
assert html =~ "Add location"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "status filter narrows the visible rows", %{conn: conn} do
|
||||||
|
user = Microwaveprop.AccountsFixtures.user_fixture()
|
||||||
|
{:ok, _} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :ideal, notes: "ideal-spot"})
|
||||||
|
{:ok, _} = Rover.create_location(user, %{lat: 33.0, lon: -98.0, status: :off_limits, notes: "no-go-spot"})
|
||||||
|
|
||||||
|
{:ok, lv, html} = live(conn, ~p"/rover-locations")
|
||||||
|
assert html =~ "ideal-spot"
|
||||||
|
assert html =~ "no-go-spot"
|
||||||
|
|
||||||
|
html =
|
||||||
|
lv
|
||||||
|
|> form("form[phx-change=filter_status]", %{"value" => "ideal"})
|
||||||
|
|> render_change()
|
||||||
|
|
||||||
|
assert html =~ "ideal-spot"
|
||||||
|
refute html =~ "no-go-spot"
|
||||||
|
|
||||||
|
html =
|
||||||
|
lv
|
||||||
|
|> form("form[phx-change=filter_status]", %{"value" => "off_limits"})
|
||||||
|
|> render_change()
|
||||||
|
|
||||||
|
refute html =~ "ideal-spot"
|
||||||
|
assert html =~ "no-go-spot"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "logged-in user" do
|
describe "logged-in user" do
|
||||||
|
|
@ -41,7 +67,7 @@ defmodule MicrowavepropWeb.RoverLocationsLiveTest do
|
||||||
test "can add a new location", %{conn: conn} do
|
test "can add a new location", %{conn: conn} do
|
||||||
{:ok, lv, _html} = live(conn, ~p"/rover-locations")
|
{:ok, lv, _html} = live(conn, ~p"/rover-locations")
|
||||||
|
|
||||||
lv |> element("button", "+ Add location") |> render_click()
|
lv |> element("button", "Add location") |> render_click()
|
||||||
|
|
||||||
lv
|
lv
|
||||||
|> form("#location-form",
|
|> form("#location-form",
|
||||||
|
|
@ -53,19 +79,17 @@ defmodule MicrowavepropWeb.RoverLocationsLiveTest do
|
||||||
assert html =~ "test note"
|
assert html =~ "test note"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can edit and delete only their own location", %{conn: conn, user: user} do
|
test "edit/delete buttons appear only on the owner's row", %{conn: conn, user: user} do
|
||||||
other = Microwaveprop.AccountsFixtures.user_fixture()
|
other = Microwaveprop.AccountsFixtures.user_fixture()
|
||||||
{:ok, mine} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :ideal})
|
{:ok, mine} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :ideal})
|
||||||
{:ok, theirs} = Rover.create_location(other, %{lat: 33.0, lon: -98.0, status: :off_limits})
|
{:ok, theirs} = Rover.create_location(other, %{lat: 33.0, lon: -98.0, status: :off_limits})
|
||||||
|
|
||||||
{:ok, lv, html} = live(conn, ~p"/rover-locations")
|
{:ok, lv, _html} = live(conn, ~p"/rover-locations")
|
||||||
|
|
||||||
assert html =~ "location-#{mine.id}"
|
assert has_element?(lv, "button[phx-click=edit][phx-value-id='#{mine.id}']", "Edit")
|
||||||
assert html =~ "location-#{theirs.id}"
|
assert has_element?(lv, "button[phx-click=delete][phx-value-id='#{mine.id}']", "Delete")
|
||||||
|
refute has_element?(lv, "button[phx-click=edit][phx-value-id='#{theirs.id}']", "Edit")
|
||||||
# Edit/Delete buttons rendered for owned location only.
|
refute has_element?(lv, "button[phx-click=delete][phx-value-id='#{theirs.id}']", "Delete")
|
||||||
assert has_element?(lv, "#location-#{mine.id} button", "Edit")
|
|
||||||
refute has_element?(lv, "#location-#{theirs.id} button", "Edit")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue