From fab734d3340439bf75f8e62f929d2c514ba9187d Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 10:27:51 +0000 Subject: [PATCH 01/10] Added an embed mode which works via the URL, not any data in our db --- src/App.tsx | 41 ++++++++++++++++++- src/CanvasView.tsx | 86 +++++++++++++++----------------------- src/embedContext.ts | 10 +++-- src/pages/_app.tsx | 15 ++++++- src/pages/view-only.tsx | 91 +++++++++++++++++++++++++++++++++++++++++ src/types.ts | 5 +++ src/utils.ts | 19 +++++---- 7 files changed, 199 insertions(+), 68 deletions(-) create mode 100644 src/pages/view-only.tsx diff --git a/src/App.tsx b/src/App.tsx index cfffc708..382deedb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import { MachineNameChooserModal } from './MachineNameChooserModal'; import { PaletteProvider } from './PaletteContext'; import { paletteMachine } from './paletteMachine'; import { PanelsView } from './PanelsView'; -import { SimulationProvider } from './SimulationContext'; +import { SimulationProvider, useSimulationMode } from './SimulationContext'; import { simulationMachine } from './simulationMachine'; import { getSourceActor, useSourceRegistryData } from './sourceMachine'; import { theme } from './theme'; @@ -21,6 +21,7 @@ import { useInterpretCanvas } from './useInterpretCanvas'; import router, { useRouter } from 'next/router'; import { parseEmbedQuery, withoutEmbedQueryParams } from './utils'; import { registryLinks } from './registryLinks'; +import { canZoom, canZoomIn, canZoomOut } from './canvasMachine'; const defaultHeadProps = { title: 'XState Visualizer', @@ -88,6 +89,7 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) { const paletteService = useInterpret(paletteMachine); // don't use `devTools: true` here as it would freeze your browser const simService = useInterpret(simulationMachine); + const machine = useSelector(simService, (state) => { return state.context.currentSessionId ? state.context.serviceDataMap[state.context.currentSessionId!]?.machine @@ -120,6 +122,32 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) { embed, }); + const shouldEnableZoomOutButton = useSelector( + canvasService, + (state) => canZoom(embed) && canZoomOut(state.context), + ); + + const shouldEnableZoomInButton = useSelector( + canvasService, + (state) => canZoom(embed) && canZoomIn(state.context), + ); + + const canShowWelcomeMessage = sourceState.hasTag('canShowWelcomeMessage'); + + const showControls = useMemo( + () => !embed?.isEmbedded || embed.controls, + [embed], + ); + + const showZoomButtonsInEmbed = useMemo( + () => !embed?.isEmbedded || (embed.controls && embed.zoom), + [embed], + ); + const showPanButtonInEmbed = useMemo( + () => !embed?.isEmbedded || (embed.controls && embed.pan), + [embed], + ); + // This is because we're doing loads of things on client side anyway if (!isOnClientSide()) return ; @@ -142,7 +170,16 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) { > {!(embed?.isEmbedded && embed.mode === EmbedMode.Panels) && ( - + )} diff --git a/src/CanvasView.tsx b/src/CanvasView.tsx index ed43da9d..e613a6e6 100644 --- a/src/CanvasView.tsx +++ b/src/CanvasView.tsx @@ -1,8 +1,8 @@ import { AddIcon, MinusIcon, - RepeatIcon, QuestionOutlineIcon, + RepeatIcon, } from '@chakra-ui/icons'; import { Box, @@ -19,78 +19,60 @@ import { VStack, } from '@chakra-ui/react'; import { useSelector } from '@xstate/react'; -import xstatePkgJson from 'xstate/package.json'; import React, { useMemo } from 'react'; +import xstatePkgJson from 'xstate/package.json'; import { CanvasContainer } from './CanvasContainer'; import { useCanvas } from './CanvasContext'; -import { canZoom, canZoomIn, canZoomOut } from './canvasMachine'; +import { CanvasHeader } from './CanvasHeader'; import { toDirectedGraph } from './directedGraph'; import { Graph } from './Graph'; -import { useSimulation, useSimulationMode } from './SimulationContext'; -import { CanvasHeader } from './CanvasHeader'; -import { Overlay } from './Overlay'; -import { useEmbed } from './embedContext'; import { CompressIcon, HandIcon } from './Icons'; -import { useSourceActor } from './sourceMachine'; +import { Overlay } from './Overlay'; +import { useSimulation, useSimulationMode } from './SimulationContext'; import { WelcomeArea } from './WelcomeArea'; -export const CanvasView: React.FC = () => { +export const CanvasView = (props: { + shouldEnableZoomOutButton?: boolean; + shouldEnableZoomInButton?: boolean; + canShowWelcomeMessage?: boolean; + showControls?: boolean; + showZoomButtonsInEmbed?: boolean; + showPanButtonInEmbed?: boolean; + isEmbedded?: boolean; + hideHeader: boolean; +}) => { // TODO: refactor this so an event can be explicitly sent to a machine // it isn't straightforward to do at the moment cause the target machine lives in a child component const [panModeEnabled, setPanModeEnabled] = React.useState(false); - const embed = useEmbed(); - const simService = useSimulation(); const canvasService = useCanvas(); - const [sourceState] = useSourceActor(); + + const simService = useSimulation(); + const machine = useSelector(simService, (state) => { return state.context.currentSessionId ? state.context.serviceDataMap[state.context.currentSessionId!]?.machine : undefined; }); - const isLayoutPending = useSelector(simService, (state) => - state.hasTag('layoutPending'), - ); - const isEmpty = useSelector(simService, (state) => state.hasTag('empty')); - const digraph = useMemo( - () => (machine ? toDirectedGraph(machine) : undefined), - [machine], - ); - - const shouldEnableZoomOutButton = useSelector( - canvasService, - (state) => canZoom(embed) && canZoomOut(state.context), - ); - - const shouldEnableZoomInButton = useSelector( - canvasService, - (state) => canZoom(embed) && canZoomIn(state.context), - ); const simulationMode = useSimulationMode(); - const canShowWelcomeMessage = sourceState.hasTag('canShowWelcomeMessage'); - - const showControls = useMemo( - () => !embed?.isEmbedded || embed.controls, - [embed], + const digraph = useMemo( + () => (machine ? toDirectedGraph(machine) : undefined), + [machine], ); - const showZoomButtonsInEmbed = useMemo( - () => !embed?.isEmbedded || (embed.controls && embed.zoom), - [embed], - ); - const showPanButtonInEmbed = useMemo( - () => !embed?.isEmbedded || (embed.controls && embed.pan), - [embed], + const isLayoutPending = useSelector(simService, (state) => + state.hasTag('layoutPending'), ); + const isEmpty = useSelector(simService, (state) => state.hasTag('empty')); return ( - {!embed?.isEmbedded && ( + {!props.hideHeader && ( @@ -107,10 +89,10 @@ export const CanvasView: React.FC = () => { )} - {isEmpty && canShowWelcomeMessage && } + {isEmpty && props.canShowWelcomeMessage && } - {showControls && ( + {props.showControls && ( { data-testid="controls" > - {showZoomButtonsInEmbed && ( + {props.showZoomButtonsInEmbed && ( <> } - disabled={!shouldEnableZoomOutButton} + disabled={!props.shouldEnableZoomOutButton} onClick={() => canvasService.send('ZOOM.OUT')} variant="secondary" /> @@ -140,7 +122,7 @@ export const CanvasView: React.FC = () => { aria-label="Zoom in" title="Zoom in" icon={} - disabled={!shouldEnableZoomInButton} + disabled={!props.shouldEnableZoomInButton} onClick={() => canvasService.send('ZOOM.IN')} variant="secondary" /> @@ -153,7 +135,7 @@ export const CanvasView: React.FC = () => { onClick={() => canvasService.send('FIT_TO_CONTENT')} variant="secondary" /> - {!embed?.isEmbedded && ( + {!props.isEmbedded && ( { /> )} - {showPanButtonInEmbed && ( + {props.showPanButtonInEmbed && ( } @@ -184,7 +166,7 @@ export const CanvasView: React.FC = () => { RESET )} - {!embed?.isEmbedded && ( + {!props.isEmbedded && ( ('Embed'); +const EmbedReactContext = createContext(null as EmbedContext); + +export const EmbedProvider = EmbedReactContext.Provider; + +export const useEmbed = () => useContext(EmbedReactContext); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index c80d15d5..ee72a95d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -14,6 +14,7 @@ import '../InvokeViz.scss'; import '../monacoPatch'; import '../StateNodeViz.scss'; import '../TransitionViz.scss'; +import { NextComponentWithMeta } from '../types'; // import { isOnClientSide } from '../isOnClientSide'; @@ -32,7 +33,7 @@ if ( }); } -const MyApp = ({ pageProps, Component }: AppProps) => { +const AuthWrapper = ({ pageProps, Component }: AppProps) => { const router = useRouter(); const authService = useInterpret( @@ -50,4 +51,16 @@ const MyApp = ({ pageProps, Component }: AppProps) => { ); }; +const MyApp = ( + props: AppProps & { + Component: NextComponentWithMeta; + }, +) => { + if (props.Component.preventAuth) { + return ; + } + + return ; +}; + export default MyApp; diff --git a/src/pages/view-only.tsx b/src/pages/view-only.tsx new file mode 100644 index 00000000..05ecc769 --- /dev/null +++ b/src/pages/view-only.tsx @@ -0,0 +1,91 @@ +import { Box, ChakraProvider } from '@chakra-ui/react'; +import { useInterpret } from '@xstate/react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import React, { useEffect, useMemo } from 'react'; +import { createMachine } from 'xstate'; +import { CanvasProvider } from '../CanvasContext'; +import { CanvasView } from '../CanvasView'; +import { EmbedProvider } from '../embedContext'; +import { SimulationProvider } from '../SimulationContext'; +import { simulationMachine } from '../simulationMachine'; +import { theme } from '../theme'; +import { NextComponentWithMeta } from '../types'; +import { useInterpretCanvas } from '../useInterpretCanvas'; +import { parseEmbedQuery, withoutEmbedQueryParams } from '../utils'; + +const machine = createMachine({ + initial: 'wow', + states: { + wow: { + on: { + NEXT: { + target: 'new', + }, + }, + }, + new: {}, + }, +}); + +const ViewOnlyPage: NextComponentWithMeta = () => { + const canvasService = useInterpretCanvas({ + sourceID: null, + }); + const simulationService = useInterpret(simulationMachine); + const router = useRouter(); + + useEffect(() => { + simulationService.send({ + type: 'MACHINES.REGISTER', + machines: [machine], + }); + }, []); + + const embed = useMemo( + () => ({ + ...parseEmbedQuery(router.query), + isEmbedded: true, + originalUrl: withoutEmbedQueryParams(router.query), + }), + [router.query], + ); + return ( + <> + + + + + + + + + + + + + + + + ); +}; + +ViewOnlyPage.preventAuth = true; + +export default ViewOnlyPage; diff --git a/src/types.ts b/src/types.ts index 68df4587..89add870 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ import type { import { SourceFileFragment } from './graphql/SourceFileFragment.generated'; import { Model } from 'xstate/lib/model.types'; import type { editor } from 'monaco-editor'; +import { NextPage } from 'next'; export type AnyStateMachine = StateMachine; @@ -84,3 +85,7 @@ export interface Point { x: number; y: number; } + +export type NextComponentWithMeta = NextPage & { + preventAuth?: boolean; +}; diff --git a/src/utils.ts b/src/utils.ts index 75ee4093..4d9c918f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -214,17 +214,18 @@ export const DEFAULT_EMBED_PARAMS: ParsedEmbed = { zoom: false, controls: false, }; -export const parseEmbedQuery = (query?: NextRouter['query']): ParsedEmbed => { - const parsedEmbed = DEFAULT_EMBED_PARAMS; - const getQueryParamValue = (qParamValue: string | string[]) => { - return Array.isArray(qParamValue) ? qParamValue[0] : qParamValue; - }; +const getQueryParamValue = (qParamValue: string | string[]) => { + return Array.isArray(qParamValue) ? qParamValue[0] : qParamValue; +}; - const computeBooleanQParamValue = (qParamValue: string) => { - // Parse to number to treat "0" as false - return !!+qParamValue; - }; +const computeBooleanQParamValue = (qParamValue: string) => { + // Parse to number to treat "0" as false + return !!+qParamValue; +}; + +export const parseEmbedQuery = (query?: NextRouter['query']): ParsedEmbed => { + const parsedEmbed = DEFAULT_EMBED_PARAMS; if (query?.mode) { const parsedMode = getQueryParamValue(query?.mode); From 8697c338ae069b59046b8a071c8556909e7b3a91 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 10:59:28 +0000 Subject: [PATCH 02/10] Completed work --- src/App.tsx | 46 ++------------- src/CanvasView.tsx | 73 +++++++++++++----------- src/pages/view-only.tsx | 123 ++++++++++++++++++++++++++++------------ src/withReadyRouter.tsx | 25 ++++++++ 4 files changed, 159 insertions(+), 108 deletions(-) create mode 100644 src/withReadyRouter.tsx diff --git a/src/App.tsx b/src/App.tsx index 382deedb..a881df41 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,27 +1,26 @@ import { Box, ChakraProvider } from '@chakra-ui/react'; -import React, { useEffect, useMemo } from 'react'; import { useActor, useInterpret, useSelector } from '@xstate/react'; -import { useAuth } from './authContext'; +import router, { useRouter } from 'next/router'; +import React, { useEffect, useMemo } from 'react'; import { AppHead } from './AppHead'; +import { useAuth } from './authContext'; import { CanvasProvider } from './CanvasContext'; -import { EmbedProvider } from './embedContext'; import { CanvasView } from './CanvasView'; +import { EmbedProvider } from './embedContext'; import { isOnClientSide } from './isOnClientSide'; import { MachineNameChooserModal } from './MachineNameChooserModal'; import { PaletteProvider } from './PaletteContext'; import { paletteMachine } from './paletteMachine'; import { PanelsView } from './PanelsView'; -import { SimulationProvider, useSimulationMode } from './SimulationContext'; +import { registryLinks } from './registryLinks'; +import { SimulationProvider } from './SimulationContext'; import { simulationMachine } from './simulationMachine'; import { getSourceActor, useSourceRegistryData } from './sourceMachine'; import { theme } from './theme'; import { EditorThemeProvider } from './themeContext'; import { EmbedContext, EmbedMode } from './types'; import { useInterpretCanvas } from './useInterpretCanvas'; -import router, { useRouter } from 'next/router'; import { parseEmbedQuery, withoutEmbedQueryParams } from './utils'; -import { registryLinks } from './registryLinks'; -import { canZoom, canZoomIn, canZoomOut } from './canvasMachine'; const defaultHeadProps = { title: 'XState Visualizer', @@ -113,8 +112,6 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) { }); }, [machine?.id, sendToSourceService]); - // TODO: Subject to refactor into embedActor - const sourceID = sourceState!.context.sourceID; const canvasService = useInterpretCanvas({ @@ -122,32 +119,8 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) { embed, }); - const shouldEnableZoomOutButton = useSelector( - canvasService, - (state) => canZoom(embed) && canZoomOut(state.context), - ); - - const shouldEnableZoomInButton = useSelector( - canvasService, - (state) => canZoom(embed) && canZoomIn(state.context), - ); - const canShowWelcomeMessage = sourceState.hasTag('canShowWelcomeMessage'); - const showControls = useMemo( - () => !embed?.isEmbedded || embed.controls, - [embed], - ); - - const showZoomButtonsInEmbed = useMemo( - () => !embed?.isEmbedded || (embed.controls && embed.zoom), - [embed], - ); - const showPanButtonInEmbed = useMemo( - () => !embed?.isEmbedded || (embed.controls && embed.pan), - [embed], - ); - // This is because we're doing loads of things on client side anyway if (!isOnClientSide()) return ; @@ -171,14 +144,7 @@ function App({ isEmbedded = false }: { isEmbedded?: boolean }) { {!(embed?.isEmbedded && embed.mode === EmbedMode.Panels) && ( )} diff --git a/src/CanvasView.tsx b/src/CanvasView.tsx index e613a6e6..e603eb63 100644 --- a/src/CanvasView.tsx +++ b/src/CanvasView.tsx @@ -1,8 +1,8 @@ import { AddIcon, MinusIcon, - QuestionOutlineIcon, RepeatIcon, + QuestionOutlineIcon, } from '@chakra-ui/icons'; import { Box, @@ -19,60 +19,67 @@ import { VStack, } from '@chakra-ui/react'; import { useSelector } from '@xstate/react'; -import React, { useMemo } from 'react'; import xstatePkgJson from 'xstate/package.json'; +import React, { useMemo } from 'react'; import { CanvasContainer } from './CanvasContainer'; import { useCanvas } from './CanvasContext'; -import { CanvasHeader } from './CanvasHeader'; +import { canZoom, canZoomIn, canZoomOut } from './canvasMachine'; import { toDirectedGraph } from './directedGraph'; import { Graph } from './Graph'; -import { CompressIcon, HandIcon } from './Icons'; -import { Overlay } from './Overlay'; import { useSimulation, useSimulationMode } from './SimulationContext'; +import { CanvasHeader } from './CanvasHeader'; +import { Overlay } from './Overlay'; +import { useEmbed } from './embedContext'; +import { CompressIcon, HandIcon } from './Icons'; import { WelcomeArea } from './WelcomeArea'; -export const CanvasView = (props: { - shouldEnableZoomOutButton?: boolean; - shouldEnableZoomInButton?: boolean; - canShowWelcomeMessage?: boolean; - showControls?: boolean; - showZoomButtonsInEmbed?: boolean; - showPanButtonInEmbed?: boolean; - isEmbedded?: boolean; - hideHeader: boolean; -}) => { +export const CanvasView = (props: { canShowWelcomeMessage?: boolean }) => { // TODO: refactor this so an event can be explicitly sent to a machine // it isn't straightforward to do at the moment cause the target machine lives in a child component const [panModeEnabled, setPanModeEnabled] = React.useState(false); - const canvasService = useCanvas(); - + const embed = useEmbed(); const simService = useSimulation(); - + const canvasService = useCanvas(); const machine = useSelector(simService, (state) => { return state.context.currentSessionId ? state.context.serviceDataMap[state.context.currentSessionId!]?.machine : undefined; }); - - const simulationMode = useSimulationMode(); - + const isLayoutPending = useSelector(simService, (state) => + state.hasTag('layoutPending'), + ); + const isEmpty = useSelector(simService, (state) => state.hasTag('empty')); const digraph = useMemo( () => (machine ? toDirectedGraph(machine) : undefined), [machine], ); - const isLayoutPending = useSelector(simService, (state) => - state.hasTag('layoutPending'), + const shouldEnableZoomOutButton = useSelector( + canvasService, + (state) => canZoom(embed) && canZoomOut(state.context), ); - const isEmpty = useSelector(simService, (state) => state.hasTag('empty')); + + const shouldEnableZoomInButton = useSelector( + canvasService, + (state) => canZoom(embed) && canZoomIn(state.context), + ); + + const simulationMode = useSimulationMode(); + + const showControls = !embed?.isEmbedded || embed.controls; + + const showZoomButtonsInEmbed = + !embed?.isEmbedded || (embed.controls && embed.zoom); + const showPanButtonInEmbed = + !embed?.isEmbedded || (embed.controls && embed.pan); return ( - {!props.hideHeader && ( + {!embed?.isEmbedded && ( @@ -92,7 +99,7 @@ export const CanvasView = (props: { {isEmpty && props.canShowWelcomeMessage && } - {props.showControls && ( + {showControls && ( - {props.showZoomButtonsInEmbed && ( + {showZoomButtonsInEmbed && ( <> } - disabled={!props.shouldEnableZoomOutButton} + disabled={!shouldEnableZoomOutButton} onClick={() => canvasService.send('ZOOM.OUT')} variant="secondary" /> @@ -122,7 +129,7 @@ export const CanvasView = (props: { aria-label="Zoom in" title="Zoom in" icon={} - disabled={!props.shouldEnableZoomInButton} + disabled={!shouldEnableZoomInButton} onClick={() => canvasService.send('ZOOM.IN')} variant="secondary" /> @@ -135,7 +142,7 @@ export const CanvasView = (props: { onClick={() => canvasService.send('FIT_TO_CONTENT')} variant="secondary" /> - {!props.isEmbedded && ( + {!embed?.isEmbedded && ( )} - {props.showPanButtonInEmbed && ( + {showPanButtonInEmbed && ( } @@ -166,7 +173,7 @@ export const CanvasView = (props: { RESET )} - {!props.isEmbedded && ( + {!embed?.isEmbedded && ( { + if (!query.machine) { + throw new Error(); + } + + if (Array.isArray(query.machine)) { + throw new Error(); + } + + const lzResult = lzString.decompressFromEncodedURIComponent(query.machine); + + if (!lzResult) throw new Error(); + + const machineConfig = JSON.parse(lzResult); + + // Tests that the machine is valid + return createMachine(machineConfig); +}; + +const viewOnlyPageMachine = createMachine<{ + query: NextRouter['query']; +}>({ + initial: 'checkingIfMachineIsValid', states: { - wow: { - on: { - NEXT: { - target: 'new', + checkingIfMachineIsValid: { + invoke: { + src: async (context) => { + return parseMachineFromQuery(context.query); }, + onDone: { + target: 'valid', + }, + onError: { + target: 'notValid', + }, + }, + }, + notValid: { + type: 'final', + entry: (context, event) => { + console.error('Could not parse machine.', event); }, }, - new: {}, + valid: { + type: 'final', + entry: sendParent((context) => + simModel.events['MACHINES.REGISTER']([ + parseMachineFromQuery(context.query), + ]), + ), + }, }, }); -const ViewOnlyPage: NextComponentWithMeta = () => { +/** + * Displays a view-only page which can render a machine + * to the canvas from the URL + * + * Use this example URL: http://localhost:3000/viz/view-only?machine=N4IglgdmAuYIYBsQC4QHcD2aQBoQGdo5oBTfFUTbZYAXzwhOrttqA + * + * This is for loading OG images quickly, and for many other applications + * + * To create the machine hash, use the lzString.compressToEncodedURIComponent + * function on a JSON.stringified machine config. + * + * You can also use the typical embed controls + */ +const ViewOnlyPage = withReadyRouter(() => { const canvasService = useInterpretCanvas({ sourceID: null, }); const simulationService = useInterpret(simulationMachine); const router = useRouter(); - useEffect(() => { - simulationService.send({ - type: 'MACHINES.REGISTER', - machines: [machine], - }); - }, []); + useMachine(viewOnlyPageMachine, { + context: { + query: router.query, + }, + parent: simulationService, + }); const embed = useMemo( () => ({ @@ -50,11 +105,9 @@ const ViewOnlyPage: NextComponentWithMeta = () => { }), [router.query], ); + return ( <> - - - @@ -63,20 +116,9 @@ const ViewOnlyPage: NextComponentWithMeta = () => { data-testid="app" data-viz-theme="dark" as="main" - display="grid" - gridTemplateColumns="1fr auto" - gridTemplateAreas={`"canvas"`} height="100vh" > - + @@ -84,8 +126,19 @@ const ViewOnlyPage: NextComponentWithMeta = () => { ); +}); + +const ViewOnlyPageParent: NextComponentWithMeta = () => { + return ( + <> + + + + + + ); }; -ViewOnlyPage.preventAuth = true; +ViewOnlyPageParent.preventAuth = true; -export default ViewOnlyPage; +export default ViewOnlyPageParent; diff --git a/src/withReadyRouter.tsx b/src/withReadyRouter.tsx new file mode 100644 index 00000000..20963ac9 --- /dev/null +++ b/src/withReadyRouter.tsx @@ -0,0 +1,25 @@ +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; + +/** + * Ensures that Next's router is always ready (i.e. has + * query params loaded) + */ +export const withReadyRouter = (WrappedComponent: any) => { + WrappedComponent.displayName = `WithReadyRouter${WrappedComponent.displayName}`; + + const WithReadyRouter = () => { + const router = useRouter(); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + setIsReady(router.isReady); + }, [router.isReady]); + + if (!isReady) return null; + + return ; + }; + + return WithReadyRouter; +}; From 3ec5dbcb97bfcd2a07dadc8aec403ad8c9c38b0f Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 11:40:53 +0000 Subject: [PATCH 03/10] Update src/pages/view-only.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- src/pages/view-only.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/view-only.tsx b/src/pages/view-only.tsx index 6900b223..91601c5e 100644 --- a/src/pages/view-only.tsx +++ b/src/pages/view-only.tsx @@ -32,7 +32,11 @@ const parseMachineFromQuery = (query: NextRouter['query']) => { const machineConfig = JSON.parse(lzResult); // Tests that the machine is valid - return createMachine(machineConfig); + try { + return createMachine(machineConfig); + } catch { + throw new Error("decompressed `machine` couldn't be used to `createMachine`") + } }; const viewOnlyPageMachine = createMachine<{ From 170610bf478ca74438641b15bd425ad11a394151 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 11:40:59 +0000 Subject: [PATCH 04/10] Update src/pages/view-only.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- src/pages/view-only.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/view-only.tsx b/src/pages/view-only.tsx index 91601c5e..eda16f98 100644 --- a/src/pages/view-only.tsx +++ b/src/pages/view-only.tsx @@ -27,7 +27,7 @@ const parseMachineFromQuery = (query: NextRouter['query']) => { const lzResult = lzString.decompressFromEncodedURIComponent(query.machine); - if (!lzResult) throw new Error(); + if (!lzResult) throw new Error("`machine` query param couldn't be decompressed"); const machineConfig = JSON.parse(lzResult); From bddbdcaa388bab278f5136c24aadaba951b2db5e Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 11:41:04 +0000 Subject: [PATCH 05/10] Update src/pages/view-only.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- src/pages/view-only.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/view-only.tsx b/src/pages/view-only.tsx index eda16f98..02896973 100644 --- a/src/pages/view-only.tsx +++ b/src/pages/view-only.tsx @@ -18,7 +18,7 @@ import lzString from 'lz-string'; const parseMachineFromQuery = (query: NextRouter['query']) => { if (!query.machine) { - throw new Error(); + throw new Error("`machine` query param is required"); } if (Array.isArray(query.machine)) { From f8eab8e29bcb5a82a1ce7c26be282d5fea156326 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 11:41:09 +0000 Subject: [PATCH 06/10] Update src/pages/view-only.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- src/pages/view-only.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/view-only.tsx b/src/pages/view-only.tsx index 02896973..8c305b51 100644 --- a/src/pages/view-only.tsx +++ b/src/pages/view-only.tsx @@ -22,7 +22,7 @@ const parseMachineFromQuery = (query: NextRouter['query']) => { } if (Array.isArray(query.machine)) { - throw new Error(); + throw new Error("`machine` query param can't be an array"); } const lzResult = lzString.decompressFromEncodedURIComponent(query.machine); From 29e3bcb979b49ba3590136c20998b8d7c719fd9e Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 12:06:10 +0000 Subject: [PATCH 07/10] Update src/withReadyRouter.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- src/withReadyRouter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/withReadyRouter.tsx b/src/withReadyRouter.tsx index 20963ac9..723ab778 100644 --- a/src/withReadyRouter.tsx +++ b/src/withReadyRouter.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'; * query params loaded) */ export const withReadyRouter = (WrappedComponent: any) => { - WrappedComponent.displayName = `WithReadyRouter${WrappedComponent.displayName}`; + WrappedComponent.displayName = `WithReadyRouter(${WrappedComponent.displayName || WrappedComponent.name})`; const WithReadyRouter = () => { const router = useRouter(); From 38f195943aaa638e04d40b99b03e2b39f3a2624a Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 16:56:46 +0000 Subject: [PATCH 08/10] Empty commit for visualisation From 850025f2c527407878c5502d20346aeffbdc8ba7 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 17:00:20 +0000 Subject: [PATCH 09/10] Respond to feedback --- src/pages/view-only.tsx | 54 +++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/pages/view-only.tsx b/src/pages/view-only.tsx index 8c305b51..f18af358 100644 --- a/src/pages/view-only.tsx +++ b/src/pages/view-only.tsx @@ -15,10 +15,11 @@ import { useInterpretCanvas } from '../useInterpretCanvas'; import { parseEmbedQuery, withoutEmbedQueryParams } from '../utils'; import { withReadyRouter } from '../withReadyRouter'; import lzString from 'lz-string'; +import { AppHead } from '../AppHead'; const parseMachineFromQuery = (query: NextRouter['query']) => { if (!query.machine) { - throw new Error("`machine` query param is required"); + throw new Error('`machine` query param is required'); } if (Array.isArray(query.machine)) { @@ -27,7 +28,8 @@ const parseMachineFromQuery = (query: NextRouter['query']) => { const lzResult = lzString.decompressFromEncodedURIComponent(query.machine); - if (!lzResult) throw new Error("`machine` query param couldn't be decompressed"); + if (!lzResult) + throw new Error("`machine` query param couldn't be decompressed"); const machineConfig = JSON.parse(lzResult); @@ -35,7 +37,9 @@ const parseMachineFromQuery = (query: NextRouter['query']) => { try { return createMachine(machineConfig); } catch { - throw new Error("decompressed `machine` couldn't be used to `createMachine`") + throw new Error( + "decompressed `machine` couldn't be used to `createMachine`", + ); } }; @@ -111,33 +115,35 @@ const ViewOnlyPage = withReadyRouter(() => { ); return ( - <> - - - - - - - - - - - - + + + + + + + + + + + ); }); const ViewOnlyPageParent: NextComponentWithMeta = () => { return ( <> - - - + ); From 317bb3e5409f55bb66623e84c05f456a5a41d874 Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Mon, 29 Nov 2021 17:06:27 +0000 Subject: [PATCH 10/10] Empty commit for visualisation