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

feat: Show "Depart at" buttons and show the correct itinerary on click #2238

Merged
merged 16 commits into from
Dec 3, 2024

Conversation

joshlarson
Copy link
Contributor

@joshlarson joshlarson commented Nov 26, 2024

Adds these buttons, which toggle between different versions of the same itinerary.

Screenshot 2024-11-27 at 4 34 29 PM

Summary of changes

Asana Ticket: Trip Planner Preview | "Depart at" buttons


General checks

  • Are the changes organized into self-contained commits with descriptive and well-formatted commit messages? This is a good practice that can facilitate easier reviews.
  • Testing. Do the changes include relevant passing updates to tests? This includes updating screenshots. Preferably tests are run locally to verify that there are no test failures created by these changes, before opening a PR.
  • Tech debt. Have you checked for tech debt you can address in the area you're working in? This can be a good time to address small issues, or create Asana tickets for larger issues.

New UI, or substantial UI changes

  • Cross-browser compatibility is less of an issue now that we're no longer supporting IE, but changes still need to work as expected in Safari, Chrome, and Firefox.
  • Are interactive elements accessible? This includes at minimum having relevant keyboard interactions and visible focus, but can also include verification with screen reader testing.
  • Other accessibility checks such as sufficient color constrast, or whether the layout holds up at 200% zoom level.

New endpoints, or non-trivial changes to current endpoints

  • Have we load-tested any new pages or internal API endpoints that will receive significant traffic? See load testing docs
  • If this change involves routes, does it work correctly with pertinent "unusual" routes such as the combined Green Line, Silver Line, Foxboro commuter rail, and single-direction bus routes like the 170?

@joshlarson joshlarson marked this pull request as ready for review November 27, 2024 21:33
@joshlarson joshlarson requested a review from a team as a code owner November 27, 2024 21:33
@joshlarson joshlarson enabled auto-merge (squash) November 27, 2024 21:41
@joshlarson joshlarson disabled auto-merge November 27, 2024 21:41
@joshlarson joshlarson enabled auto-merge (squash) November 27, 2024 21:41
Copy link
Contributor

@anthonyshull anthonyshull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would probably be better as a function component since it has so little state. I'm just nervous about having state in multiple places...sometimes in the live view, sometimes in live components. With things like the map which can potentially have a lot of state, needs a lot of interactivity, and is very self-contained, it makes more sense to make it a live component. It would probably help to have some kind of rule-of-thumb about the number of attributes and callbacks that make a live component make sense. I don't think this component meets a minimal criteria.

type="button"
class={[
"border border-brand-primary rounded px-2.5 py-1.5 mr-2 text-brand-primary text-lg",
"hover:bg-brand-primary-lightest #{@background_class}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just do:

"hover:bg-brand-primary-lightest",
@background_class

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, you might even want to do:

"hover:bg-brand-primary-lightest bg-transparent",
@active && "bg-brand-primary-lightest"

That allows you to remove two lines of code.

Comment on lines 213 to 214
update_trip_details(base_itinerary, trip_id: "trip_id_1", start_time: ~T[09:26:00]),
update_trip_details(base_itinerary, trip_id: "trip_id_2", start_time: ~T[10:46:00])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try to avoid magic numbers, names, times, etc. in tests. Reading this test, I wonder what is special about these two times. If we're just testing that the text in the button reflects the time we pass it, we can assign a random time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're testing that the later time is the one that appears, we can create a random time for the first then add time to that, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went the way I did here because I prefer not to have logic in tests, since that makes it harder to see exactly what the test is testing. In this case, the logic would be the conversion from ~T[09:26:00] into the string "9:26AM".

I do agree that having ~T[09:26:00] and "9:26AM" be closer to each other in the test file would make them feel less magic. Lemme try a quick refactor, and then let's talk if we still don't love that.

Comment on lines 12 to 16
def stub_otp_results(itineraries) do
expect(OpenTripPlannerClient.Mock, :plan, fn _ ->
{:ok, %OpenTripPlannerClient.Plan{itineraries: itineraries}}
end)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should probably be private functions since they won't be used anywhere outside of this file.

Comment on lines 19 to 25
# Uhhh the OTP factory will generate with any route_type value but our
# parsing will break with unexpected route types
itineraries =
OtpFactory.build_list(3, :itinerary)
|> Enum.map(&limit_route_types/1)

stub_otp_results(itineraries)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we already do this in a factory?


If not, we should probably fix the factory rather than add that code here (since it applies to all code that uses the OtpFactory and not just this test).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we do this in a factory (I didn't add this - I just moved it), and I do think we should. I was thinking that fixing that could be a follow-up PR, so that the depart-at buttons can still go-live.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine as long as we do it later. I know the factory does this, but it also parses it. Might be worth trying to use the factory directly:

expect(OpenTripPlannerClient.Mock, :plan, fn _ ->
{:ok, %OpenTripPlannerClient.Plan{itineraries: []}}
end)
stub_otp_results([])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you're only using this in one place, I would just write this as an expectation in this test. That would allow you to remove one of the two functions above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stub_otp_results is called in two places - once here and once in stub_populated_otp_results. I guess I could inline it in both places, but I wanted to avoid duplicating 👇

expect(OpenTripPlannerClient.Mock, :plan, fn _ ->
  {:ok, %OpenTripPlannerClient.Plan{itineraries: itineraries}}
end)

@joshlarson
Copy link
Contributor Author

I think this would probably be better as a function component since it has so little state. I'm just nervous about having state in multiple places...sometimes in the live view, sometimes in live components.

I'd love to talk about this more, since I actually found this easier to reason about by having the state located closer to where that state is used. I got confused by having the state managed by the live view, but then having the interaction still be facilitated by the buttons within the details component.

I could sort of see having the state still live at the live view level, and get managed via callback, but I think that the itinerary_details component would still need to be a live component in order to use callbacks.

@anthonyshull
Copy link
Contributor

I think this would probably be better as a function component since it has so little state. I'm just nervous about having state in multiple places...sometimes in the live view, sometimes in live components.

I'd love to talk about this more, since I actually found this easier to reason about by having the state located closer to where that state is used. I got confused by having the state managed by the live view, but then having the interaction still be facilitated by the buttons within the details component.

I could sort of see having the state still live at the live view level, and get managed via callback, but I think that the itinerary_details component would still need to be a live component in order to use callbacks.

If you don't capture clicks with target = myself then they'll go to the parent. That way the logic for the entire live view is in a single place instead of in multiple places. It gives us a nice pattern of expectation that attributes flow down and events bubble up. From the docs:

Generally speaking, you should prefer functional components over live components, as they are a simpler abstraction, with a smaller surface area. The use case for live components only arises when there is a need for encapsulating both event handling and additional state.

@anthonyshull
Copy link
Contributor

In metro, the only reason the map and datepicker are live components is because they have to interact with external libraries and manage state in JS. Even the map, when clicked, actually sends the event to the parent live view. So, if you were going to add a pin where the user clicked the live view would add that pin in a callback.

https://github.com/mbta/mbta_metro/blob/ad3b265264dd57f4e4284ae0a010d6869b732047/lib/mbta_metro/live/map.ex#L14

@joshlarson
Copy link
Contributor Author

Talked offline with the following conclusion:

  • We'll make ItineraryDetail into a function component as part of this PR.
  • Fast-follow PR to make TripPlannerResultsSection into a function component as well, with state managed exclusively by the TripPlanner live view.

{:ok,
socket
|> assign(:expanded_itinerary_index, nil)
|> assign(:selected_itinerary_detail_index, 0)}
Copy link
Contributor Author

@joshlarson joshlarson Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per this comment, both of these bits of state are going to get moved up to the TripPlanner level in a follow-up PR.

Copy link
Collaborator

@thecristen thecristen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the feedback!

@thecristen thecristen disabled auto-merge December 3, 2024 17:09
@thecristen thecristen merged commit 5ab1881 into main Dec 3, 2024
16 of 17 checks passed
@thecristen thecristen deleted the jdl/depart-at-buttons branch December 3, 2024 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants