Skip to content

Commit

Permalink
feat: Open the detour modal from a notification
Browse files Browse the repository at this point in the history
  • Loading branch information
joshlarson committed Sep 27, 2024
1 parent 20eaec5 commit 394e0a0
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 43 deletions.
160 changes: 117 additions & 43 deletions assets/src/components/notificationCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode } from "react"
import React, { ReactNode, useCallback, useState } from "react"
import { useRoute, useRoutes } from "../contexts/routesContext"
import {
BlockWaiverNotification,
Expand All @@ -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,
Expand All @@ -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 : []
)
Expand All @@ -43,52 +57,112 @@ export const NotificationCard = ({
return null
}

const onClose = () => {
setShowDetourModal(false)
}
const isUnread = notification.state === "unread"
return (
<CardReadable
currentTime={currentTime}
title={<>{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}
>
<CardBody>{description(notification, routes, routeAtCreation)}</CardBody>
{isBlockWaiverNotification(notification) && (
<CardProperties
properties={[
{
label: "Run",
value:
notification.content.runIds.length > 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,
},
]}
<>
<CardReadable
currentTime={currentTime}
title={<>{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}
>
<CardBody>
{description(notification, routes, routeAtCreation)}
</CardBody>
{isBlockWaiverNotification(notification) && (
<CardProperties
properties={[
{
label: "Run",
value:
notification.content.runIds.length > 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,
},
]}
/>
)}
</CardReadable>
{detourId && (
<DetourNotificationModal
show={showDetourModal}
detourId={detourId}
onClose={onClose}
/>
)}
</CardReadable>
</>
)
}

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 (
<DetourModal
onClose={onClose}
show={show}
originalRoute={{}}
key={detourId ?? ""}
{...(stateOfDetourModal ? { snapshot: stateOfDetourModal } : {})}
/>
)
}

Expand Down
98 changes: 98 additions & 0 deletions assets/tests/components/notificationCard.openDetour.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<RoutesProvider routes={routes}>
<NotificationCard
notification={n}
currentTime={new Date()}
openVPPForCurrentVehicle={() => {}}
/>
</RoutesProvider>
)

// 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()
})
})
21 changes: 21 additions & 0 deletions assets/tests/factories/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BlockWaiverReason,
BridgeLoweredNotification,
BridgeRaisedNotification,
DetourNotification,
Notification,
NotificationType,
} from "../../src/realtime"
Expand Down Expand Up @@ -84,3 +85,23 @@ export const bridgeLoweredNotificationFactory = Factory.define<
state: "unread",
content: bridgeLoweredNotificationContentFactory.build(),
}))

const detourActivatedNotificationContentFactory =
Factory.define<DetourNotification>(() => ({
$type: NotificationType.Detour,
status: "activated",
detourId: 79,
headsign: "79 - Wooded Mountains",
route: "79",
direction: "Inbound",
origin: "Maple Knoll",
}))

export const detourActivatedNotificationFactory = Factory.define<
Notification<DetourNotification>
>(({ sequence }) => ({
id: sequence.toString(),
createdAt: new Date(),
state: "unread",
content: detourActivatedNotificationContentFactory.build(),
}))

0 comments on commit 394e0a0

Please sign in to comment.