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: Open the detour modal from a notification #2835

Merged
merged 6 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions assets/src/components/detourListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,14 @@ export const DetourListPage = () => {
<DetourModal
onClose={onCloseDetour}
show
originalRoute={{}}
key={detourId ?? ""}
{...(detour
? {
snapshot: detour.state,
author: detour.author,
updatedAt: detour.updatedAt,
}
: {})}
: { originalRoute: {} })}
/>
)}
</div>
Expand Down
159 changes: 117 additions & 42 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 @@ -14,6 +14,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 @@ -28,6 +35,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 @@ -44,55 +58,116 @@ export const NotificationCard = ({
return null
}

const onCloseDetour = () => {
setShowDetourModal(false)
}
const isUnread = notification.state === "unread"
return (
<CardReadable
currentTime={currentTime}
title={<>{title(notification)}</>}
style="kiwi"
isActive={isUnread}
openCallback={() => {
openVPPForCurrentVehicle(notification)

if (hideLatestNotification) {
hideLatestNotification()
}
<>
<CardReadable
currentTime={currentTime}
title={<>{title(notification)}</>}
style="kiwi"
isActive={isUnread}
openCallback={() => {
if (notification.content.$type === NotificationType.Detour) {
setShowDetourModal(true)
} else {
openVPPForCurrentVehicle(notification)

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,
},
]}
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>
{showDetourModal && detourId && (
<DetourNotificationModal
show
detourId={detourId}
onClose={onCloseDetour}
/>
)}
</CardReadable>
</>
)
}

const DetourNotificationModal = ({
detourId,
show,
onClose,
}: {
detourId: DetourId
show: boolean
onClose: () => void
}) => {
const { result: detour } = 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 detourResponse.ok
}, [detourId]),
})

return detour ? (
<DetourModal
onClose={onClose}
show={show}
key={detourId ?? ""}
snapshot={detour.state}
author={detour.author}
updatedAt={detour.updatedAt}
/>
) : null
}

export const title = (notification: Notification) => {
switch (notification.content.$type) {
case NotificationType.BlockWaiver: {
Expand Down
66 changes: 66 additions & 0 deletions assets/tests/components/notificationCard.openDetour.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 getTestGroups from "../../src/userTestGroups"
import { TestGroups } from "../../src/userInTestGroup"
import { detourStateMachineFactory } from "../factories/detourStateMachineFactory"

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()))
jest
.mocked(fetchDetour)
.mockResolvedValue(Ok(detourStateMachineFactory.build()))

const n = detourActivatedNotificationFactory.build()

render(
<RoutesProvider routes={routes}>
<NotificationCard
notification={n}
currentTime={new Date()}
openVPPForCurrentVehicle={() => {}}
/>
</RoutesProvider>
)

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()
})
})
9 changes: 9 additions & 0 deletions assets/tests/components/notificationCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@ import { RoutesProvider } from "../../src/contexts/routesContext"
import { fullStoryEvent } from "../../src/helpers/fullStory"
import getTestGroups from "../../src/userTestGroups"
import { TestGroups } from "../../src/userInTestGroup"
import { fetchDetour, fetchDetours } from "../../src/api"
import { Ok } from "../../src/util/result"
import { detourStateMachineFactory } from "../factories/detourStateMachineFactory"
import { detourListFactory } from "../factories/detourListFactory"

jest.mock("../../src/api")
jest.mock("../../src/helpers/fullStory")
jest.mock("../../src/userTestGroups")

beforeEach(() => {
jest.mocked(getTestGroups).mockReturnValue([TestGroups.DetoursList])
jest.mocked(fetchDetours).mockResolvedValue(Ok(detourListFactory.build()))
jest
.mocked(fetchDetour)
.mockResolvedValue(Ok(detourStateMachineFactory.build()))
})

const routes = [
Expand Down
35 changes: 35 additions & 0 deletions assets/tests/factories/detourStateMachineFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Factory } from "fishery"
import { createActor } from "xstate"
import { createDetourMachine } from "../../src/models/createDetourMachine"
import { originalRouteFactory } from "./originalRouteFactory"
import { shapePointFactory } from "./shapePointFactory"
import { DetourWithState } from "../../src/models/detour"

export const detourStateMachineFactory = Factory.define<DetourWithState>(() => {
// 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 {
updatedAt: 1724866392,
author: "fake@email.com",
state: snapshot,
}
})
Loading