diff --git a/assets/js/components/Dashboard/AlertDetails.tsx b/assets/js/components/Dashboard/AlertDetails.tsx index f445e7cb1..c4734d351 100644 --- a/assets/js/components/Dashboard/AlertDetails.tsx +++ b/assets/js/components/Dashboard/AlertDetails.tsx @@ -28,7 +28,7 @@ const AlertDetails: ComponentType = () => { const navigate = useNavigate(); - const foundAlert = alerts.find((alert) => alert.id === id); + const foundAlert = (alerts ?? []).find((alert) => alert.id === id); useEffect(() => { if (foundAlert) { setSelectedAlert(foundAlert); @@ -37,9 +37,17 @@ const AlertDetails: ComponentType = () => { const validAlertId = id && allAPIAlertIds.includes(id) ? id : undefined; + if (!alerts || !places) { + return null; + } + const alertPlaces = placesWithSelectedAlert( + selectedAlert, + places, + screensByAlertMap, + ); return selectedAlert ? ( // Define a new ContextProvider so state is not saved to Context used on the PlacesPage. - +
@@ -69,14 +77,10 @@ const AlertDetails: ComponentType = () => {
)} diff --git a/assets/js/components/Dashboard/AlertsPage.tsx b/assets/js/components/Dashboard/AlertsPage.tsx index c5b808ee9..809f97ce3 100644 --- a/assets/js/components/Dashboard/AlertsPage.tsx +++ b/assets/js/components/Dashboard/AlertsPage.tsx @@ -26,6 +26,10 @@ import moment from "moment-timezone"; const AlertsPage: ComponentType = () => { const { places, alerts, screensByAlertMap } = useScreenplayState(); + if (!places || !alerts) { + return null; + } + const alertsWithPlaces = alerts.filter( (alert) => screensByAlertMap[alert.id], ); diff --git a/assets/js/components/Dashboard/Dashboard.tsx b/assets/js/components/Dashboard/Dashboard.tsx index b05a2853b..f22a1b06a 100644 --- a/assets/js/components/Dashboard/Dashboard.tsx +++ b/assets/js/components/Dashboard/Dashboard.tsx @@ -114,7 +114,7 @@ const Dashboard: ComponentType = () => { alerts: newAlerts, screens_by_alert: screensByAlertMap, } = alertsData; - findAndSetBannerAlert(alerts, newAlerts); + findAndSetBannerAlert(alerts ?? [], newAlerts); setAlerts(newAlerts, allAPIalertIds, screensByAlertMap); } }; diff --git a/assets/js/components/Dashboard/PlacesPage.tsx b/assets/js/components/Dashboard/PlacesPage.tsx index 6cf28b890..14eca989e 100644 --- a/assets/js/components/Dashboard/PlacesPage.tsx +++ b/assets/js/components/Dashboard/PlacesPage.tsx @@ -18,6 +18,7 @@ import { import { DirectionID } from "Models/direction_id"; import { usePrevious } from "Hooks/usePrevious"; import { sortByStationOrder } from "../../util"; +import fp from "lodash/fp"; const getSortLabel = ( modeLineFilterValue: { label: string }, @@ -36,6 +37,9 @@ const getSortLabel = ( const PlacesPage: ComponentType = () => { const { places } = useScreenplayState(); + if (!places) { + return null; + } return (
Places
@@ -48,16 +52,16 @@ const PlacesPage: ComponentType = () => { interface PlacesListProps { places: Place[]; - noModeFilter?: boolean; isAlertPlacesList?: boolean; showAnimationForNewPlaces?: boolean; + showLineMap?: boolean; } const PlacesList: ComponentType = ({ places, - noModeFilter, isAlertPlacesList, showAnimationForNewPlaces, + showLineMap = true, }: PlacesListProps) => { // ascending/southbound/westbound = 0, descending/northbound/eastbound = 1 const { @@ -175,6 +179,11 @@ const PlacesList: ComponentType = ({ statusFilterValue !== STATUSES[0] || screenTypeFilterValue !== SCREEN_TYPES[0]; + const allPlaceRoutes = fp.uniq(places.flatMap(({ routes }) => routes)); + const modeOptions = MODES_AND_LINES.filter(({ ids }) => { + return ids[0] === "All" || fp.intersection(ids, allPlaceRoutes).length > 0; + }); + return ( <> @@ -187,14 +196,12 @@ const PlacesList: ComponentType = ({ /> - {!noModeFilter && ( - handleSelectModeOrLine(value)} - selectedValue={modeLineFilterValue} - className="modes-and-lines" - /> - )} + handleSelectModeOrLine(value)} + selectedValue={modeLineFilterValue} + className="modes-and-lines" + /> = ({ } activeEventKeys={activeEventKeys} sortDirection={sortDirection} - filteredLine={isOnlyFilteredByRoute ? getFilteredLine() : null} + filteredLine={ + showLineMap && isOnlyFilteredByRoute ? getFilteredLine() : null + } className={isFiltered || isAlertPlacesList ? "filtered" : ""} /> ); diff --git a/assets/js/hooks/usePlacesWithPaEss.ts b/assets/js/hooks/usePlacesWithPaEss.ts index c176bc3f6..f7b760f40 100644 --- a/assets/js/hooks/usePlacesWithPaEss.ts +++ b/assets/js/hooks/usePlacesWithPaEss.ts @@ -6,7 +6,7 @@ export const usePlacesWithPaEss = () => { const { places } = useScreenplayState(); return useMemo( () => - places + (places ?? []) .map((place) => ({ ...place, screens: place.screens.filter((screen) => screen.type === "pa_ess"), diff --git a/assets/js/hooks/useScreenplayContext.tsx b/assets/js/hooks/useScreenplayContext.tsx index f0ffecc9a..b04b4b952 100644 --- a/assets/js/hooks/useScreenplayContext.tsx +++ b/assets/js/hooks/useScreenplayContext.tsx @@ -16,6 +16,7 @@ import { BannerAlert } from "../components/Dashboard/AlertBanner"; import { ActionOutcomeToastProps } from "../components/Dashboard/ActionOutcomeToast"; import useSWR, { KeyedMutator } from "swr"; import { getSuppressedPredictions } from "Utils/api"; +import fp from "lodash/fp"; interface Props { children: React.ReactNode; @@ -28,9 +29,9 @@ interface FilterValue { } interface ScreenplayState { - places: Place[]; + places?: Place[]; lineStops: LineStop[]; - alerts: Alert[]; + alerts?: Alert[]; allAPIAlertIds: string[]; screensByAlertMap: ScreensByAlert; bannerAlert?: BannerAlert; @@ -55,9 +56,9 @@ const [useScreenplayState, ScreenplayStateProvider] = createGenericContext(); const ScreenplayStateContainer = ({ children }: Props) => { - const [places, setPlaces] = useState([]); + const [places, setPlaces] = useState(); const [lineStops, setLineStops] = useState([]); - const [alerts, setAlerts] = useState([]); + const [alerts, setAlerts] = useState(); const [allAPIAlertIds, setAllAPIAlertIds] = useState([]); const [screensByAlertMap, setScreensByAlertMap] = useState( {}, @@ -156,10 +157,30 @@ interface PlacesListState { const [usePlacesListState, PlacesListStateProvider] = createGenericContext(); -const PlacesListStateContainer = ({ children }: Props) => { +interface PlacesListStateContainerProps { + children: React.ReactNode; + places?: Place[]; +} + +const PlacesListStateContainer = ({ + children, + places, +}: PlacesListStateContainerProps) => { const [sortDirection, setSortDirection] = useState(0); const [modeLineFilterValue, setModeLineFilterValue] = useState( - PLACES_PAGE_MODES_AND_LINES[0], + () => { + return places && places.length > 0 + ? fp + .reverse(PLACES_PAGE_MODES_AND_LINES) + .find( + ({ ids }) => + ids[0] === "All" || + places.every( + (place) => fp.intersection(ids, place.routes).length > 0, + ), + )! + : PLACES_PAGE_MODES_AND_LINES[0]; + }, ); const [screenTypeFilterValue, setScreenTypeFilterValue] = useState(SCREEN_TYPES[0]); diff --git a/assets/js/util.ts b/assets/js/util.ts index 10ae3f26f..29e37c52d 100644 --- a/assets/js/util.ts +++ b/assets/js/util.ts @@ -122,7 +122,7 @@ export const formatEffect = (effect: string) => { // Filters out screens that don't have the alert, then filters out places with empty // screens array export const placesWithSelectedAlert = ( - alert: Alert | null, + alert: Alert | undefined, places: Place[], screensByAlertMap: ScreensByAlert, ) => { diff --git a/assets/tests/components/alertsPage.test.tsx b/assets/tests/components/alertsPage.test.tsx index b5878409a..d207bcfcb 100644 --- a/assets/tests/components/alertsPage.test.tsx +++ b/assets/tests/components/alertsPage.test.tsx @@ -5,9 +5,12 @@ import { renderWithScreenplayProvider } from "../utils/renderWithScreenplayProvi describe("Alerts Page", () => { describe("filtering", () => { - test("filters places by mode and route", async () => { + beforeEach(async () => { renderWithScreenplayProvider(); + await screen.findAllByRole("button", { name: "All MODES" }); + }); + test("filters places by mode and route", async () => { expect(await screen.findByTestId("1")).toBeInTheDocument(); // Verify alerts not present on a screen are not visible expect(screen.queryByTestId("5")).toBeNull(); @@ -33,8 +36,6 @@ describe("Alerts Page", () => { }); test("filters places by screen type", async () => { - renderWithScreenplayProvider(); - fireEvent.click(screen.getByRole("button", { name: "All SCREEN TYPES" })); fireEvent.click( await screen.findByRole("button", { name: "Bus Shelter" }), diff --git a/assets/tests/components/placesPage.test.tsx b/assets/tests/components/placesPage.test.tsx index 02595ba01..a011afa30 100644 --- a/assets/tests/components/placesPage.test.tsx +++ b/assets/tests/components/placesPage.test.tsx @@ -5,9 +5,12 @@ import { renderWithScreenplayProvider } from "../utils/renderWithScreenplayProvi describe("PlacesPage", () => { describe("filtering", () => { - test("filters places by screen type", async () => { + beforeEach(async () => { renderWithScreenplayProvider(); + await screen.findAllByRole("button", { name: "All MODES" }); + }); + test("filters places by screen type", async () => { fireEvent.click(screen.getByRole("button", { name: "All SCREEN TYPES" })); fireEvent.click(await screen.findByRole("button", { name: "DUP" })); expect(await screen.findByText("Davis")).toBeInTheDocument(); @@ -36,8 +39,6 @@ describe("PlacesPage", () => { }); test("filters places by mode and route", async () => { - renderWithScreenplayProvider(); - fireEvent.click(screen.getByRole("button", { name: "All MODES" })); fireEvent.click(await screen.findByRole("button", { name: "Blue Line" })); expect(await screen.findByText("WONDERLAND")).toBeInTheDocument(); @@ -64,8 +65,6 @@ describe("PlacesPage", () => { }); test("adds `filtered` class to PlaceRow when filtered", async () => { - renderWithScreenplayProvider(); - fireEvent.click(screen.getByRole("button", { name: "All MODES" })); fireEvent.click(await screen.findByRole("button", { name: "Blue Line" })); expect( @@ -74,8 +73,6 @@ describe("PlacesPage", () => { }); test("reset button clears filters", async () => { - renderWithScreenplayProvider(); - fireEvent.click(screen.getByRole("button", { name: "All MODES" })); fireEvent.click(await screen.findByRole("button", { name: "Blue Line" })); fireEvent.click( @@ -86,9 +83,12 @@ describe("PlacesPage", () => { }); describe("sorting", () => { - test("sort label changes depending on filter selected", async () => { + beforeEach(async () => { renderWithScreenplayProvider(); + await screen.findAllByRole("button", { name: "All MODES" }); + }); + test("sort label changes depending on filter selected", async () => { expect(screen.getByTestId("sort-label").textContent?.trim()).toBe("ABC"); fireEvent.click(screen.getByRole("button", { name: "All MODES" })); fireEvent.click(await screen.findByRole("button", { name: "Blue Line" })); @@ -110,8 +110,6 @@ describe("PlacesPage", () => { }); test("sort label changes when clicked", async () => { - renderWithScreenplayProvider(); - expect(screen.getByTestId("sort-label").textContent?.trim()).toBe("ABC"); fireEvent.click(screen.getByTestId("sort-label")); expect( @@ -140,8 +138,6 @@ describe("PlacesPage", () => { }); test("sort order changes for RL", async () => { - renderWithScreenplayProvider(); - fireEvent.click(screen.getByRole("button", { name: "All MODES" })); fireEvent.click(await screen.findByRole("button", { name: "Red Line" })); expect( @@ -159,8 +155,6 @@ describe("PlacesPage", () => { }); test("sort order changes for OL", async () => { - renderWithScreenplayProvider(); - fireEvent.click(screen.getByRole("button", { name: "All MODES" })); fireEvent.click(screen.getByRole("button", { name: "Orange Line" })); expect( @@ -190,8 +184,6 @@ describe("PlacesPage", () => { }); test("sort order changes for GL", async () => { - renderWithScreenplayProvider(); - fireEvent.click(screen.getByRole("button", { name: "All MODES" })); fireEvent.click(screen.getByRole("button", { name: "Green Line" })); @@ -240,8 +232,6 @@ describe("PlacesPage", () => { }); test("sort order changes for BL", async () => { - renderWithScreenplayProvider(); - fireEvent.click(screen.getByRole("button", { name: "All MODES" })); fireEvent.click(screen.getByRole("button", { name: "Blue Line" }));