diff --git a/assets/src/components/notificationCard.tsx b/assets/src/components/notificationCard.tsx
index ce4022f8a..b96f6205c 100644
--- a/assets/src/components/notificationCard.tsx
+++ b/assets/src/components/notificationCard.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode } from "react"
+import React, { ReactNode, useCallback, useState } from "react"
import { useRoute, useRoutes } from "../contexts/routesContext"
import {
BlockWaiverNotification,
@@ -13,6 +13,13 @@ import { CardBody, CardProperties, CardReadable } from "./card"
import { fullStoryEvent } from "../helpers/fullStory"
import { RoutePill } from "./routePill"
import inTestGroup, { TestGroups } from "../userInTestGroup"
+import { useApiCall } from "../hooks/useApiCall"
+import { fetchDetour } from "../api"
+import { createDetourMachine } from "../models/createDetourMachine"
+import { isValidSnapshot } from "../util/isValidSnapshot"
+import { isErr } from "../util/result"
+import { DetourModal } from "./detours/detourModal"
+import { DetourId } from "../models/detoursList"
export const NotificationCard = ({
notification,
@@ -27,6 +34,13 @@ export const NotificationCard = ({
hideLatestNotification?: () => void
noFocusOrHover?: boolean
}) => {
+ const [showDetourModal, setShowDetourModal] = useState(false)
+
+ const detourId =
+ notification.content.$type === NotificationType.Detour
+ ? notification.content.detourId
+ : undefined
+
const routes = useRoutes(
isBlockWaiverNotification(notification) ? notification.content.routeIds : []
)
@@ -43,52 +57,112 @@ export const NotificationCard = ({
return null
}
+ const onClose = () => {
+ setShowDetourModal(false)
+ }
const isUnread = notification.state === "unread"
return (
- {title(notification)}>}
- style="kiwi"
- isActive={isUnread}
- openCallback={() => {
- openVPPForCurrentVehicle(notification)
-
- if (hideLatestNotification) {
- hideLatestNotification()
- }
-
- if (notification.content.$type === NotificationType.BridgeMovement) {
- fullStoryEvent("User clicked Chelsea Bridge Notification", {})
- }
- }}
- closeCallback={hideLatestNotification}
- time={notification.createdAt}
- noFocusOrHover={noFocusOrHover}
- >
- {description(notification, routes, routeAtCreation)}
- {isBlockWaiverNotification(notification) && (
- 0
- ? notification.content.runIds.join(", ")
- : null,
- },
- {
- label: "Operator",
- value:
- notification.content.operatorName !== null &&
- notification.content.operatorId !== null
- ? `${notification.content.operatorName} #${notification.content.operatorId}`
- : null,
- sensitive: true,
- },
- ]}
+ <>
+ {title(notification)}>}
+ style="kiwi"
+ isActive={isUnread}
+ openCallback={() => {
+ if (notification.content.$type === NotificationType.Detour) {
+ setShowDetourModal(true)
+ } else {
+ openVPPForCurrentVehicle(notification)
+
+ if (hideLatestNotification) {
+ hideLatestNotification()
+ }
+
+ if (
+ notification.content.$type === NotificationType.BridgeMovement
+ ) {
+ fullStoryEvent("User clicked Chelsea Bridge Notification", {})
+ }
+ }
+ }}
+ closeCallback={hideLatestNotification}
+ time={notification.createdAt}
+ noFocusOrHover={noFocusOrHover}
+ >
+
+ {description(notification, routes, routeAtCreation)}
+
+ {isBlockWaiverNotification(notification) && (
+ 0
+ ? notification.content.runIds.join(", ")
+ : null,
+ },
+ {
+ label: "Operator",
+ value:
+ notification.content.operatorName !== null &&
+ notification.content.operatorId !== null
+ ? `${notification.content.operatorName} #${notification.content.operatorId}`
+ : null,
+ sensitive: true,
+ },
+ ]}
+ />
+ )}
+
+ {detourId && (
+
)}
-
+ >
+ )
+}
+
+const DetourNotificationModal = ({
+ detourId,
+ show,
+ onClose,
+}: {
+ detourId: DetourId
+ show: boolean
+ onClose: () => void
+}) => {
+ const { result: stateOfDetourModal } = useApiCall({
+ apiCall: useCallback(async () => {
+ if (detourId === undefined) {
+ return undefined
+ }
+ const detourResponse = await fetchDetour(detourId)
+ if (isErr(detourResponse)) {
+ return undefined
+ }
+ const snapshot = isValidSnapshot(
+ createDetourMachine,
+ detourResponse.ok.state
+ )
+ if (isErr(snapshot)) {
+ return undefined
+ }
+ return snapshot.ok
+ }, [detourId]),
+ })
+
+ return (
+
)
}
diff --git a/assets/tests/components/notificationCard.openDetour.test.tsx b/assets/tests/components/notificationCard.openDetour.test.tsx
new file mode 100644
index 000000000..8256b39bb
--- /dev/null
+++ b/assets/tests/components/notificationCard.openDetour.test.tsx
@@ -0,0 +1,98 @@
+import { jest, describe, test, expect } from "@jest/globals"
+import "@testing-library/jest-dom/jest-globals"
+import React from "react"
+import { render, screen } from "@testing-library/react"
+import { NotificationCard } from "../../src/components/notificationCard"
+import { detourActivatedNotificationFactory } from "../factories/notification"
+import routeFactory from "../factories/route"
+import userEvent from "@testing-library/user-event"
+import { RoutesProvider } from "../../src/contexts/routesContext"
+import { fetchDetour, fetchDetours } from "../../src/api"
+import { Ok } from "../../src/util/result"
+import { detourListFactory } from "../factories/detourListFactory"
+import { createActor } from "xstate"
+import { createDetourMachine } from "../../src/models/createDetourMachine"
+import { originalRouteFactory } from "../factories/originalRouteFactory"
+import { shapePointFactory } from "../factories/shapePointFactory"
+import getTestGroups from "../../src/userTestGroups"
+import { TestGroups } from "../../src/userInTestGroup"
+
+jest.mock("../../src/api")
+jest.mock("../../src/helpers/fullStory")
+jest.mock("../../src/userTestGroups")
+
+const routes = [
+ routeFactory.build({
+ id: "route1",
+ name: "r1",
+ }),
+ routeFactory.build({
+ id: "route2",
+ name: "r2",
+ }),
+ routeFactory.build({
+ id: "route3",
+ name: "r3",
+ }),
+]
+
+describe("NotificationCard", () => {
+ test("renders detour details modal to match mocked fetchDetour", async () => {
+ jest
+ .mocked(getTestGroups)
+ .mockReturnValue([TestGroups.DetoursPilot, TestGroups.DetoursList])
+
+ jest.mocked(fetchDetours).mockResolvedValue(Ok(detourListFactory.build()))
+
+ // Stub out a detour machine, and start a detour-in-progress
+ const machine = createActor(createDetourMachine, {
+ input: originalRouteFactory.build(),
+ }).start()
+ machine.send({
+ type: "detour.edit.place-waypoint-on-route",
+ location: shapePointFactory.build(),
+ })
+ machine.send({
+ type: "detour.edit.place-waypoint",
+ location: shapePointFactory.build(),
+ })
+ machine.send({
+ type: "detour.edit.place-waypoint-on-route",
+ location: shapePointFactory.build(),
+ })
+ machine.send({ type: "detour.edit.done" })
+
+ const snapshot = machine.getPersistedSnapshot()
+ machine.stop()
+
+ // Return the state of the machine as the fetchDetour mocked value,
+ // even if it doesn't match the detour clicked
+ jest.mocked(fetchDetour).mockResolvedValue(
+ Ok({
+ updatedAt: 1726147775,
+ author: "fake@email.com",
+ state: snapshot,
+ })
+ )
+
+ const n = detourActivatedNotificationFactory.build()
+
+ render(
+
+ {}}
+ />
+
+ )
+
+ // await userEvent.click(screen.getByText(/run1/))
+ await userEvent.click(screen.getByRole("button", { name: /Detour/ }))
+ // Render modal based on mocked value, which is a detour-in-progress
+
+ expect(
+ screen.getByRole("heading", { name: "Share Detour Details" })
+ ).toBeVisible()
+ })
+})