From ddd672827d15191040a9473bd8d801c7c8678484 Mon Sep 17 00:00:00 2001 From: Kayla Firestack Date: Tue, 11 Jun 2024 14:10:24 -0400 Subject: [PATCH] feat(ts/hooks/useDetour): control waypoints with state machine (#2607) Co-authored-by: Josh Larson --- assets/src/hooks/useDetour.ts | 80 ++++++++-------- assets/src/models/createDetourMachine.ts | 116 ++++++++++++++++++++++- 2 files changed, 151 insertions(+), 45 deletions(-) diff --git a/assets/src/hooks/useDetour.ts b/assets/src/hooks/useDetour.ts index c3613aa65..56f9b4e3a 100644 --- a/assets/src/hooks/useDetour.ts +++ b/assets/src/hooks/useDetour.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react" +import { useCallback, useMemo } from "react" import { ShapePoint } from "../schedule" import { fetchDetourDirections, fetchFinishedDetour } from "../api" @@ -29,19 +29,31 @@ export const useDetour = (input: CreateDetourMachineInput) => { input, }) - const { routePattern } = snapshot.context - - const [startPoint, setStartPoint] = useState(null) - const [endPoint, setEndPoint] = useState(null) - const [waypoints, setWaypoints] = useState([]) + const { routePattern, startPoint, endPoint, waypoints } = snapshot.context + const allPoints = useMemo(() => { + if (!startPoint) { + return [] + } else if (!endPoint) { + return [startPoint].concat(waypoints) + } else { + return [startPoint].concat(waypoints).concat([endPoint]) + } + }, [startPoint, waypoints, endPoint]) const { result: finishedDetour } = useApiCall({ apiCall: useCallback(async () => { + /* Until we have "typegen" in XState, + * we need to validate these exist for typescript + * + * > [Warning] XState Typegen does not fully support XState v5 yet. However, + * > strongly-typed machines can still be achieved without Typegen. + * > -- https://stately.ai/docs/migration#use-typestypegen-instead-of-tstypes + */ if (routePattern && startPoint && endPoint) { return fetchFinishedDetour(routePattern.id, startPoint, endPoint) - } else { - return null } + + return null }, [startPoint, endPoint, routePattern]), }) @@ -50,15 +62,7 @@ export const useDetour = (input: CreateDetourMachineInput) => { longitude: startPoint?.lon, }) - const detourShape = useDetourDirections( - useMemo( - () => - [startPoint, ...waypoints, endPoint].filter( - (v): v is ShapePoint => !!v - ), - [startPoint, waypoints, endPoint] - ) ?? [] - ) + const detourShape = useDetourDirections(allPoints) const coordinates = detourShape.result && isOk(detourShape.result) @@ -76,38 +80,32 @@ export const useDetour = (input: CreateDetourMachineInput) => { }) } - const canAddWaypoint = () => startPoint !== null && endPoint === null + const canAddWaypoint = () => + snapshot.can({ + type: "detour.edit.place-waypoint", + location: { lat: 0, lon: 0 }, + }) + const addWaypoint = canAddWaypoint() - ? (p: ShapePoint) => { - setWaypoints((positions) => [...positions, p]) + ? (location: ShapePoint) => { + send({ type: "detour.edit.place-waypoint", location }) } : undefined - const addConnectionPoint = (point: ShapePoint) => { - if (startPoint === null) { - setStartPoint(point) - } else if (endPoint === null) { - setEndPoint(point) - } - } + const addConnectionPoint = (point: ShapePoint) => + send({ + type: "detour.edit.place-waypoint-on-route", + location: point, + }) - const canUndo = - startPoint !== null && snapshot.matches({ "Detour Drawing": "Editing" }) + const canUndo = snapshot.can({ type: "detour.edit.undo" }) const undo = () => { - if (endPoint !== null) { - setEndPoint(null) - } else if (waypoints.length > 0) { - setWaypoints((positions) => positions.slice(0, positions.length - 1)) - } else if (startPoint !== null) { - setStartPoint(null) - } + send({ type: "detour.edit.undo" }) } const clear = () => { - setEndPoint(null) - setStartPoint(null) - setWaypoints([]) + send({ type: "detour.edit.clear-detour" }) } const finishDetour = () => { @@ -208,7 +206,9 @@ export const useDetour = (input: CreateDetourMachineInput) => { clear, /** When present, puts this detour in "finished mode" */ - finishDetour: endPoint !== null ? finishDetour : undefined, + finishDetour: snapshot.can({ type: "detour.edit.done" }) + ? finishDetour + : undefined, /** When present, puts this detour in "edit mode" */ editDetour, } diff --git a/assets/src/models/createDetourMachine.ts b/assets/src/models/createDetourMachine.ts index 821820a0a..dbed2af7d 100644 --- a/assets/src/models/createDetourMachine.ts +++ b/assets/src/models/createDetourMachine.ts @@ -1,4 +1,5 @@ import { setup, assign, fromPromise, ActorLogicFrom, InputFrom } from "xstate" +import { ShapePoint } from "../schedule" import { Route, RouteId, RoutePattern } from "../schedule" import { fetchRoutePatterns } from "../api" @@ -9,6 +10,10 @@ export const createDetourMachine = setup({ routePattern?: RoutePattern routePatterns?: RoutePattern[] + + waypoints: ShapePoint[] + startPoint: ShapePoint | undefined + endPoint: ShapePoint | undefined } input: @@ -29,8 +34,6 @@ export const createDetourMachine = setup({ } events: - | { type: "detour.edit.done" } - | { type: "detour.edit.resume" } | { type: "detour.route-pattern.open" } | { type: "detour.route-pattern.done" } | { type: "detour.route-pattern.delete-route" } @@ -39,6 +42,13 @@ export const createDetourMachine = setup({ type: "detour.route-pattern.select-pattern" routePattern: RoutePattern } + | { type: "detour.edit.done" } + | { type: "detour.edit.resume" } + | { type: "detour.edit.clear-detour" } + | { type: "detour.edit.place-waypoint-on-route"; location: ShapePoint } + | { type: "detour.edit.place-waypoint"; location: ShapePoint } + | { type: "detour.edit.undo" } + | { type: "detour.share.copy-detour"; detourText: string } }, actors: { "fetch-route-patterns": fromPromise< @@ -63,11 +73,40 @@ export const createDetourMachine = setup({ "set.route-id": assign({ route: (_, params: { route: Route }) => params.route, }), + "detour.add-start-point": assign({ + startPoint: (_, params: { location: ShapePoint }) => params.location, + }), + "detour.remove-start-point": assign({ + startPoint: undefined, + }), + "detour.add-waypoint": assign({ + waypoints: ({ context }, params: { location: ShapePoint }) => [ + ...context.waypoints, + params.location, + ], + }), + "detour.remove-last-waypoint": assign({ + waypoints: ({ context }) => context.waypoints.slice(0, -1), + }), + "detour.add-end-point": assign({ + endPoint: (_, params: { location: ShapePoint }) => params.location, + }), + "detour.remove-end-point": assign({ + endPoint: undefined, + }), + "detour.clear": assign({ + startPoint: undefined, + waypoints: [], + endPoint: undefined, + }), }, }).createMachine({ id: "Detours Machine", context: ({ input }) => ({ ...input, + waypoints: [], + startPoint: undefined, + endPoint: undefined, }), initial: "Detour Drawing", @@ -176,19 +215,86 @@ export const createDetourMachine = setup({ }, }, Editing: { + initial: "Pick Start Point", on: { "detour.route-pattern.open": { target: "Pick Route Pattern", }, - "detour.edit.done": { - target: "Share Detour", + "detour.edit.clear-detour": { + target: ".Pick Start Point", + actions: "detour.clear", }, }, + states: { + "Pick Start Point": { + on: { + "detour.edit.place-waypoint-on-route": { + target: "Place Waypoint", + actions: { + type: "detour.add-start-point", + params: ({ event: { location } }) => ({ + location, + }), + }, + }, + }, + }, + "Place Waypoint": { + on: { + "detour.edit.place-waypoint": { + actions: { + type: "detour.add-waypoint", + params: ({ event: { location } }) => ({ + location, + }), + }, + }, + "detour.edit.place-waypoint-on-route": { + target: "Finished Drawing", + actions: { + type: "detour.add-end-point", + params: ({ event: { location } }) => ({ + location, + }), + }, + }, + "detour.edit.undo": [ + { + guard: ({ context }) => context.waypoints.length === 0, + actions: "detour.remove-start-point", + target: "Pick Start Point", + }, + { + actions: "detour.remove-last-waypoint", + target: "Place Waypoint", + }, + ], + }, + }, + "Finished Drawing": { + on: { + "detour.edit.undo": { + actions: "detour.remove-end-point", + target: "Place Waypoint", + }, + "detour.edit.done": { + target: "Done", + }, + }, + }, + Done: { + type: "final", + }, + }, + + onDone: { + target: "Share Detour", + }, }, "Share Detour": { on: { "detour.edit.resume": { - target: "Editing", + target: "Editing.Finished Drawing", }, }, },