Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(TripPlan.InputForm): change modes type from enum to map #2192

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading