From 5df3c47ddbb97eea77196f0ed152e71779c9d72d Mon Sep 17 00:00:00 2001 From: Kayla Firestack Date: Mon, 2 Feb 2026 11:23:24 -0500 Subject: [PATCH 1/3] test: assert error messages present when export has no routes or services and changeset has errors --- .../trainsformer_export_section_test.exs | 22 ++++++++++++++++++ ...ips,no-stop-times,no-multi-route-trips.zip | Bin 0 -> 692 bytes 2 files changed, 22 insertions(+) create mode 100644 test/support/fixtures/trainsformer/invalid,reasons=no-trips,no-stop-times,no-multi-route-trips.zip diff --git a/test/integration/disruptionsv2/trainsformer_export_section_test.exs b/test/integration/disruptionsv2/trainsformer_export_section_test.exs index 48c9e4ac1..466f20206 100644 --- a/test/integration/disruptionsv2/trainsformer_export_section_test.exs +++ b/test/integration/disruptionsv2/trainsformer_export_section_test.exs @@ -217,6 +217,28 @@ defmodule Arrow.Integration.Disruptionsv2.TrainsformerExportSectionTest do ) end + feature "reports errors about missing service ids and route ids", %{session: session} do + disruption = disruption_v2_fixture(%{mode: :commuter_rail}) + + session + |> visit("/disruptions/#{disruption.id}") + |> click(text("Upload Trainsformer export")) + |> assert_text("Upload Trainsformer .zip") + |> attach_file(file_field("trainsformer_export", visible: false), + path: + "test/support/fixtures/trainsformer/invalid,reasons=no-trips,no-stop-times,no-multi-route-trips.zip" + ) + |> assert_text( + "Successfully imported export invalid,reasons=no-trips,no-stop-times,no-multi-route-trips.zip!" + ) + |> assert_text("Export must contain at least one Service ID") + |> assert_text("Export must contain at least one route") + |> click(Query.css("#save-export-button")) + |> assert_text("Export must contain at least one Service ID") + |> assert_text("Export must contain at least one route") + |> assert_has(Query.css("#save-export-button")) + end + feature "can cancel uploading a Trainsformer export", %{session: session} do disruption = disruption_v2_fixture(%{mode: :commuter_rail}) diff --git a/test/support/fixtures/trainsformer/invalid,reasons=no-trips,no-stop-times,no-multi-route-trips.zip b/test/support/fixtures/trainsformer/invalid,reasons=no-trips,no-stop-times,no-multi-route-trips.zip new file mode 100644 index 0000000000000000000000000000000000000000..b5693855af5f235c8da19774568aec61b72f61a9 GIT binary patch literal 692 zcmWIWW@h1H00E24kZ8u%PunGcY!DV@kYUIz%_+%@FUl`1NsTWl$}A|>E2$_64dG;9 zzLWDUdb5dj*?!LY;I zDW?3}q2?qYuLp?vfYub3H$Z6X;UZ zKu~*7lZhn|Y`X&a8Webr{FUWx+7eXzwx)-vJlD&AQ*%h;+O7O24V;BVc{k(&ycwC~m~q7rD4rM?7=d_6 zBZ!3~zE~mgg%(#3qjALwveD^4qtW9CXeKC*@R*4ie#mAD0$q(1nn2?~p^0G}D;vmP O%s}`LNG||+l>q>NuHmEr literal 0 HcmV?d00001 From 5ebc43e0b0533d4f4622e04aa788f126433b6e79 Mon Sep 17 00:00:00 2001 From: Kayla Firestack Date: Mon, 2 Feb 2026 09:19:37 -0500 Subject: [PATCH 2/3] fix: convert "manual" service ids error into changeset error --- lib/arrow/trainsformer/export.ex | 12 +++++-- lib/arrow_web/components/core_components.ex | 31 +++++++++++++++++++ .../edit_trainsformer_export_form.ex | 31 +++++++------------ 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/lib/arrow/trainsformer/export.ex b/lib/arrow/trainsformer/export.ex index 1e3d124d1..0f7d51fa5 100644 --- a/lib/arrow/trainsformer/export.ex +++ b/lib/arrow/trainsformer/export.ex @@ -26,8 +26,16 @@ defmodule Arrow.Trainsformer.Export do def changeset(export, attrs) do export |> cast(attrs, [:s3_path, :disruption_id, :name]) - |> cast_assoc(:services, with: &Service.changeset/2, required: true) - |> cast_assoc(:routes, with: &Route.changeset/2, required: true) + |> cast_assoc(:services, + with: &Service.changeset/2, + required: true, + required_message: "Export must contain at least one Service ID" + ) + |> cast_assoc(:routes, + with: &Route.changeset/2, + required: true, + required_message: "Export must contain at least one route" + ) |> validate_required([:s3_path]) |> assoc_constraint(:disruption) end diff --git a/lib/arrow_web/components/core_components.ex b/lib/arrow_web/components/core_components.ex index 37e9a98a6..d4bf84459 100644 --- a/lib/arrow_web/components/core_components.ex +++ b/lib/arrow_web/components/core_components.ex @@ -452,6 +452,37 @@ defmodule ArrowWeb.CoreComponents do """ end + @doc """ + Utility for showing errors on a `Phoenix.HTML.Form` + + Takes a form field as `field` and renders [`<.error>`](`error/1`) for each error + + <.errors field={@form[:services]} /> + + Can specify `always_show` to always show errors regardless of if the + field is considered "used" by `Phoenix.Component` + + <.errors field={@form[:services]} always_show /> + """ + attr :field, Phoenix.HTML.FormField, + doc: "a form field struct retrieved from the form, for example: `@form[:email]`" + + attr :always_show, :boolean, + default: false, + doc: "a flag that forces any errors to be displayed regardless of it's used state" + + def errors(%{field: field, always_show: show?} = assigns) do + errors = + if(Phoenix.Component.used_input?(field) or show?, do: field.errors, else: []) + + assigns = + assign(assigns, errors: Enum.map(errors, &translate_error(&1))) + + ~H""" + <.error :for={msg <- @errors} class="d-block alert alert-danger">{msg} + """ + end + def custom_normalize_value("text", value) when is_map(value) do iodata = Jason.encode_to_iodata!(value) Phoenix.HTML.Form.normalize_value("text", iodata) diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index fde05df77..c73e2fd75 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -141,6 +141,7 @@ defmodule ArrowWeb.EditTrainsformerExportForm do <% end %> + <.errors field={@form[:routes]} always_show />
Service ID @@ -250,6 +251,7 @@ defmodule ArrowWeb.EditTrainsformerExportForm do
+ <.errors field={@form[:services]} always_show /> @@ -537,26 +539,17 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end defp update_export(export_params, socket) do - imported_services = - for {key, value} <- export_params["services"], - into: %{}, - do: {key, value} + export = Trainsformer.get_export!(socket.assigns.export.id) - if imported_services == %{} do - {:noreply, assign(socket, error: "You must import at least one service")} - else - export = Trainsformer.get_export!(socket.assigns.export.id) - - case Trainsformer.update_export(export, export_params) do - {:ok, _} -> - {:noreply, - socket - |> push_patch(to: "/disruptions/#{socket.assigns.disruption.id}") - |> put_flash(:info, "Trainsformer service schedules updated successfully!")} - - {:error, %Ecto.Changeset{} = changeset} -> - {:noreply, assign(socket, form: to_form(changeset))} - end + case Trainsformer.update_export(export, export_params) do + {:ok, _} -> + {:noreply, + socket + |> push_patch(to: "/disruptions/#{socket.assigns.disruption.id}") + |> put_flash(:info, "Trainsformer service schedules updated successfully!")} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, form: to_form(changeset))} end end From efd54a03552b5a585ae8356971cf61d0631952af Mon Sep 17 00:00:00 2001 From: Kayla Firestack Date: Mon, 2 Feb 2026 11:24:27 -0500 Subject: [PATCH 3/3] cleanup: remove unnessicary `services` processing in `create_export` --- lib/arrow_web/components/edit_trainsformer_export_form.ex | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/arrow_web/components/edit_trainsformer_export_form.ex b/lib/arrow_web/components/edit_trainsformer_export_form.ex index c73e2fd75..32f6097b7 100644 --- a/lib/arrow_web/components/edit_trainsformer_export_form.ex +++ b/lib/arrow_web/components/edit_trainsformer_export_form.ex @@ -554,11 +554,6 @@ defmodule ArrowWeb.EditTrainsformerExportForm do end defp create_export(export_params, socket) do - imported_services = - for {key, value} <- export_params["services"], - into: %{}, - do: {key, value} - export_params = Map.put(export_params, "routes", socket.assigns.uploaded_file_routes) with {:ok, s3_path} <- @@ -571,8 +566,7 @@ defmodule ArrowWeb.EditTrainsformerExportForm do Trainsformer.create_export(%{ export_params | "s3_path" => s3_path, - "name" => socket.assigns.uploaded_file_name, - "services" => imported_services + "name" => socket.assigns.uploaded_file_name }) do {:noreply, socket