Skip to content

Commit

Permalink
chore(TripPlan.InputForm): change modes type from enum to map (#2192)
Browse files Browse the repository at this point in the history
  • Loading branch information
thecristen authored Oct 10, 2024
1 parent b4e4a81 commit 092eb65
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 98 deletions.
135 changes: 127 additions & 8 deletions lib/dotcom/trip_plan/input_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ defmodule Dotcom.TripPlan.InputForm do

alias OpenTripPlannerClient.PlanParams

@valid_modes [:RAIL, :SUBWAY, :BUS, :FERRY]
@time_types [:now, :leave_at, :arrive_by]
@time_types ~W(now leave_at arrive_by)a

@error_messages %{
from: "Please specify an origin location.",
Expand All @@ -25,14 +24,19 @@ defmodule Dotcom.TripPlan.InputForm do
typed_embedded_schema do
embeds_one(:from, __MODULE__.Location)
embeds_one(:to, __MODULE__.Location)
embeds_one(:modes, __MODULE__.Modes)
field(:datetime_type, Ecto.Enum, values: @time_types)
field(:datetime, :naive_datetime)
field(:modes, {:array, Ecto.Enum}, values: @valid_modes)
field(:wheelchair, :boolean, default: true)
end

def time_types, do: @time_types
def valid_modes, do: @valid_modes

def initial_modes do
__MODULE__.Modes.fields()
|> Enum.map(&{Atom.to_string(&1), "true"})
|> Map.new()
end

def to_params(%__MODULE__{
from: from,
Expand All @@ -48,7 +52,7 @@ defmodule Dotcom.TripPlan.InputForm do
arriveBy: datetime_type == :arrive_by,
date: PlanParams.to_date_param(datetime),
time: PlanParams.to_time_param(datetime),
transportModes: PlanParams.to_modes_param(modes),
transportModes: __MODULE__.Modes.selected_mode_keys(modes) |> PlanParams.to_modes_param(),
wheelchair: wheelchair
}
|> PlanParams.new()
Expand All @@ -60,9 +64,10 @@ defmodule Dotcom.TripPlan.InputForm do

def changeset(form, params) do
form
|> cast(params, [:datetime_type, :datetime, :modes, :wheelchair])
|> cast(params, [:datetime_type, :datetime, :wheelchair])
|> cast_embed(:from, required: true)
|> cast_embed(:to, required: true)
|> cast_embed(:modes, required: true)
end

def validate_params(params) do
Expand All @@ -72,10 +77,11 @@ defmodule Dotcom.TripPlan.InputForm do
|> update_change(:to, &update_location_change/1)
|> validate_required(:from, message: error_message(:from))
|> validate_required(:to, message: error_message(:to))
|> validate_required([:datetime_type, :modes, :wheelchair])
|> validate_required(:modes, message: error_message(:modes))
|> validate_required([:datetime_type, :wheelchair])
|> validate_same_locations()
|> validate_length(:modes, min: 1, message: error_message(:modes))
|> validate_chosen_datetime()
|> validate_modes()
end

# make the parent field blank if the location isn't valid
Expand All @@ -96,6 +102,16 @@ defmodule Dotcom.TripPlan.InputForm do
end
end

defp validate_modes(changeset) do
case get_change(changeset, :modes) do
%Ecto.Changeset{valid?: false} ->
add_error(changeset, :modes, error_message(:modes))

_ ->
changeset
end
end

defp validate_chosen_datetime(changeset) do
case get_field(changeset, :datetime_type) do
:now ->
Expand Down Expand Up @@ -164,4 +180,107 @@ defmodule Dotcom.TripPlan.InputForm do
end
end
end

defmodule Modes do
@moduledoc """
Represents the set of modes to be selected for a trip plan, additionally
validating that at least one mode is selected. Also provides helper
functions for rendering in forms.
"""

use TypedEctoSchema

alias Ecto.Changeset
alias OpenTripPlannerClient.PlanParams

@primary_key false
typed_embedded_schema do
field(:RAIL, :boolean, default: true)
field(:SUBWAY, :boolean, default: true)
field(:BUS, :boolean, default: true)
field(:FERRY, :boolean, default: true)
end

def fields, do: __MODULE__.__schema__(:fields)

def changeset(modes, params) do
modes
|> cast(params, fields())
|> validate_at_least_one()
end

defp validate_at_least_one(changeset) do
if Enum.all?(fields(), &(get_change(changeset, &1) == false)) do
add_error(changeset, :FERRY, "")
else
changeset
end
end

@doc """
Translates a mode atom into a short string.
"""
@spec mode_label(PlanParams.mode_t()) :: String.t()
def mode_label(:RAIL), do: "Commuter rail"
def mode_label(mode), do: Phoenix.Naming.humanize(mode)

@spec selected_mode_keys(__MODULE__.t()) :: [PlanParams.mode_t()]
def selected_mode_keys(%__MODULE__{} = modes) do
modes
|> Map.from_struct()
|> Enum.reject(&(elem(&1, 1) == false))
|> Enum.map(&elem(&1, 0))
end

@doc """
Summarizes the selected mode values into a single short string.
"""
@spec selected_modes(Changeset.t() | __MODULE__.t() | [PlanParams.mode_t()]) :: String.t()
def selected_modes(%Changeset{} = modes_changeset) do
modes_changeset
|> Changeset.apply_changes()
|> selected_modes()
end

def selected_modes(%__MODULE__{} = modes) do
modes
|> selected_mode_keys()
|> selected_modes()
end

def selected_modes([]), do: "No transit modes selected"
def selected_modes([mode]), do: mode_name(mode) <> " Only"

def selected_modes(modes) do
if fields() -- modes == [] do
"All modes"
else
fields()
|> Enum.filter(&(&1 in modes))
|> summarized_modes()
end
end

defp summarized_modes([mode1, mode2]) do
mode_name(mode1) <> " and " <> mode_name(mode2)
end

defp summarized_modes(modes) do
modes
|> Enum.map(&mode_name/1)
|> Enum.intersperse(", ")
|> List.insert_at(-2, "and ")
|> Enum.join("")
end

defp mode_name(mode) do
case mode do
:RAIL -> :commuter_rail
:SUBWAY -> :subway
:BUS -> :bus
:FERRY -> :ferry
end
|> DotcomWeb.ViewHelpers.mode_name()
end
end
end
91 changes: 11 additions & 80 deletions lib/dotcom_web/components/live_components/trip_planner_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ defmodule DotcomWeb.Components.LiveComponents.TripPlannerForm do
use DotcomWeb, :live_component

import DotcomWeb.ViewHelpers, only: [svg: 1]
import Phoenix.HTML.Form, only: [input_name: 2, input_value: 2, input_id: 2]

import MbtaMetro.Components.Feedback
import MbtaMetro.Components.InputGroup
import Phoenix.HTML.Form, only: [input_name: 2, input_value: 2, input_id: 2]

alias Dotcom.TripPlan.{InputForm, OpenTripPlanner}
alias Dotcom.TripPlan.{InputForm, InputForm.Modes, OpenTripPlanner}

@all_modes [:RAIL, :SUBWAY, :BUS, :FERRY]
@form_defaults %{
"datetime_type" => :now,
"datetime" => NaiveDateTime.local_now(),
"modes" => @all_modes,
"modes" => InputForm.initial_modes(),
"wheelchair" => true
}

Expand Down Expand Up @@ -130,45 +128,18 @@ defmodule DotcomWeb.Components.LiveComponents.TripPlannerForm do
<.fieldset legend="Modes">
<.accordion>
<:heading>
<%= selected_modes(input_value(@form, :modes)) %>
<%= Modes.selected_modes(input_value(f, :modes)) %>
</:heading>
<:content>
<div class="flex flex-col gap-1">
<input
type="checkbox"
class="peer sr-only"
name={input_name(@form, :modes) <> "[]"}
value=""
checked="true"
/>
<label
:for={
{mode_name, mode_value} <- [
{"Commuter Rail", :RAIL},
{"Subway", :SUBWAY},
{"Bus", :BUS},
{"Ferry", :FERRY}
]
}
for={input_id(@form, :modes) <> "_#{mode_value}"}
class="rounded border-solid border-2 border-transparent hover:bg-zinc-100 has-[:checked]:font-semibold py-1 px-2 mb-0"
>
<input
<div class="flex flex-col gap-05 px-2">
<.inputs_for :let={f} field={f[:modes]}>
<.input
:for={subfield <- Modes.fields()}
type="checkbox"
class="shrink-0 mr-2 rounded w-6 h-6 border-blue-500 rounded border-solid border-2 focus:border-blue-700 checked:border-blue-700 checked:bg-blue-700"
id={input_id(@form, :modes) <> "_#{mode_value}"}
name={input_name(@form, :modes) <> "[]"}
value={mode_value}
checked={
if(input_value(@form, :modes),
do:
mode_value in input_value(@form, :modes) ||
"#{mode_value}" in input_value(@form, :modes)
)
}
field={f[subfield]}
label={Modes.mode_label(subfield)}
/>
<%= mode_name %>
</label>
</.inputs_for>
</div>
</:content>
<:extra :if={used_input?(f[:modes])}>
Expand Down Expand Up @@ -231,44 +202,4 @@ defmodule DotcomWeb.Components.LiveComponents.TripPlannerForm do
_ = on_submit.(result)
result
end

defp mode_atom(mode) do
case mode do
:RAIL -> :commuter_rail
:SUBWAY -> :subway
:BUS -> :bus
:FERRY -> :ferry
other when is_binary(other) and other != "" -> String.to_atom(other)
_ -> :unknown
end
end

defp mode_name(mode) do
case mode_atom(mode) do
:unknown ->
""

other ->
DotcomWeb.ViewHelpers.mode_name(other)
end
end

defp selected_modes(modes) when modes == @all_modes do
"All modes"
end

defp selected_modes([]), do: "No transit modes selected"
defp selected_modes(nil), do: "No transit modes selected"

defp selected_modes([mode]), do: mode_name(mode) <> " Only"
defp selected_modes([mode1, mode2]), do: mode_name(mode1) <> " and " <> mode_name(mode2)

defp selected_modes(modes) do
modes
|> Enum.map(&mode_name/1)
|> Enum.reject(&(&1 == ""))
|> Enum.intersperse(", ")
|> List.insert_at(-2, "and ")
|> Enum.join("")
end
end
21 changes: 16 additions & 5 deletions lib/dotcom_web/live/trip_planner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule DotcomWeb.Live.TripPlanner do
use DotcomWeb, :live_view

alias DotcomWeb.Components.LiveComponents.TripPlannerForm
alias Dotcom.TripPlan.ItineraryGroups
alias Dotcom.TripPlan.{InputForm.Modes, ItineraryGroups}

import DotcomWeb.Components.TripPlanner.ItineraryGroup, only: [itinerary_group: 1]

Expand Down Expand Up @@ -38,10 +38,21 @@ defmodule DotcomWeb.Live.TripPlanner do
on_submit={fn data -> send(self(), {:updated_form, data}) end}
/>
<section>
<p :if={@submitted_values && @groups} class="text-lg text-emerald-700">
<%= Enum.count(@groups) %> ways to get from <%= @submitted_values.from.name %> to <%= @submitted_values.to.name %>, using <%= inspect(
@submitted_values.modes
) %>
<p :if={@submitted_values} class="text-xl">
Planning trips from <strong><%= @submitted_values.from.name %></strong>
to <strong><%= @submitted_values.to.name %></strong>
<br /> using <strong><%= Modes.selected_modes(@submitted_values.modes) %></strong>,
<strong>
<%= if @submitted_values.datetime_type == :arrive_by, do: "Arriving by", else: "Leaving" %> <%= @submitted_values.datetime
|> Timex.format!("{Mfull} {D}, {h12}:{m} {AM}") %>
</strong>
</p>
<p :if={@submitted_values && @groups} class="text-xl text-emerald-600">
Found
<strong>
<%= Enum.count(@groups) %> <%= Inflex.inflect("way", Enum.count(@groups)) %>
</strong>
to go.
</p>
</section>
<section class="flex w-full border border-solid border-slate-400">
Expand Down
Loading

0 comments on commit 092eb65

Please sign in to comment.