Skip to content

Fix/hints #380

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

Merged
merged 6 commits into from
Jul 1, 2020
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
2 changes: 1 addition & 1 deletion typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export interface MachineStateSchema {
}
Tutorial: {
states: {
LoadNext: {}
Level: {
states: {
Load: {}
Expand All @@ -86,6 +85,7 @@ export interface MachineStateSchema {
TestFail: {}
StepNext: {}
LevelComplete: {}
LoadNext: {}
}
}
Completed: {}
Expand Down
5 changes: 4 additions & 1 deletion web-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import * as React from 'react'
import { ConfigProvider } from '@alifd/next'
import enUS from '@alifd/next/lib/locale/en-us'
import ErrorBoundary from './components/ErrorBoundary'
import Workspace from './components/Workspace'
import Routes from './Routes'

const App = () => (
<ConfigProvider locale={enUS}>
<ErrorBoundary>
<Routes />
<Workspace>
<Routes />
</Workspace>
</ErrorBoundary>
</ConfigProvider>
)
Expand Down
70 changes: 34 additions & 36 deletions web-app/src/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,52 @@
import * as React from 'react'
import useRouter from './components/Router'
import Workspace from './components/Workspace'
import useStateMachine from './services/state/useStateMachine'
import { Router, Route } from './components/Router'
import ErrorView from './components/Error'
import LoadingPage from './containers/Loading'
import StartPage from './containers/Start'
import SelectTutorialPage from './containers/SelectTutorial'
import CompletedPage from './containers/Tutorial/CompletedPage'
import TutorialPage from './containers/Tutorial'

/*
* NOTE: due to a lack of URLs and a dependency on xstate
* we have to implement a custom router here
*/
const Routes = () => {
const { context, send, Router, Route } = useRouter()
const { context, route, send } = useStateMachine()

// TODO: handle only full page errors
if (context.error) {
return (
<Workspace>
<ErrorView send={send} error={context.error} />
</Workspace>
)
return <ErrorView send={send} error={context.error} />
}

return (
<Workspace>
<Router>
{/* Setup */}
<Route path={['Setup.Startup', 'Setup.ValidateSetup']}>
<LoadingPage text="Launching..." processes={context.processes} />
</Route>
<Route path="Setup.Start">
<StartPage send={send} context={context} />
</Route>
<Route path="Setup.SelectTutorial">
<SelectTutorialPage send={send} context={context} />
</Route>
<Route path={['Setup.SetupNewTutorial', 'Setup.StartTutorial']}>
<LoadingPage text="Configuring tutorial..." />
</Route>
{/* Tutorial */}
<Route path={['Tutorial.LoadNext', 'Tutorial.Level.Load']}>
<LoadingPage text="Loading Level..." processes={context.processes} />
</Route>
<Route path="Tutorial.Level">
<TutorialPage send={send} context={context} />
</Route>
{/* Completed */}
<Route path="Tutorial.Completed">
<CompletedPage context={context} />
</Route>
</Router>
</Workspace>
<Router route={route}>
{/* Setup */}
<Route paths={{ Setup: { Startup: true, ValidateSetup: true } }}>
<LoadingPage text="Launching..." processes={context.processes} />
</Route>
<Route paths={{ Setup: { Start: true } }}>
<StartPage send={send} context={context} />
</Route>
<Route paths={{ Setup: { SelectTutorial: true } }}>
<SelectTutorialPage send={send} context={context} />
</Route>
<Route paths={{ Setup: { SetupNewTutorial: true, StartTutorial: true } }}>
<LoadingPage text="Configuring tutorial..." />
</Route>
{/* Tutorial */}
<Route paths={{ Tutorial: { Level: { Load: true } } }}>
<LoadingPage text="Loading Level..." processes={context.processes} />
</Route>
<Route paths={{ Tutorial: { Level: true } }}>
<TutorialPage send={send} context={context} />
</Route>
{/* Completed */}
<Route paths={{ Tutorial: { Completed: true } }}>
<CompletedPage context={context} />
</Route>
</Router>
)
}

Expand Down
8 changes: 0 additions & 8 deletions web-app/src/components/Router/Route.tsx

This file was deleted.

115 changes: 41 additions & 74 deletions web-app/src/components/Router/index.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,54 @@
import * as React from 'react'
import * as T from 'typings'
import { createMachine } from '../../services/state/machine'
import { useMachine } from '../../services/xstate-react'
import Route from './Route'
import onError from '../../services/sentry/onError'
import logger from '../../services/logger'

interface Output {
context: T.MachineContext
send: (action: any) => void
Router: any
Route: any
interface RouterProps {
children: React.ReactChildren | React.ReactChildren[]
route: string
}

declare let acquireVsCodeApi: any

const editor = acquireVsCodeApi()
const editorSend = (action: T.Action) => {
logger(`TO EXT: "${action.type}"`)
return editor.postMessage(action)
}

// router finds first state match of <Route path='' />
const useRouter = (): Output => {
const [state, send] = useMachine<T.MachineContext, any>(createMachine({ editorSend }))

const sendWithLog = (action: T.Action): void => {
logger(`SEND: ${action.type}`, action)
send(action)
}

logger(`STATE: ${JSON.stringify(state.value)}`)

// event bus listener
React.useEffect(() => {
const listener = 'message'
// propograte channel event to state machine
const handler = (event: any) => {
// NOTE: must call event.data, cannot destructure. VSCode acts odd
const action = event.data
// ignore browser events from plugins
if (action.source) {
return
// check if a route string (eg. 'a.b.c')
// matches a paths object ({ a: { b: { c: true }}})
const matches = (route: string, paths: object): boolean => {
const keys: string[] = route.split('.')
let current: any = paths || {}
// if the key throws, there is no match
for (const key of keys) {
const next = current[key]
if (next) {
// exit early if property value is true
if (next === true) {
return true
}
sendWithLog(action)
}
window.addEventListener(listener, handler)
return () => {
window.removeEventListener(listener, handler)
current = next
continue
} else {
return false
}
}, [])
}
return true
}

const Router = ({ children }: any) => {
const childArray = React.Children.toArray(children)
for (const child of childArray) {
// match path
// @ts-ignore
const { path } = child.props
let pathMatch
if (typeof path === 'string') {
pathMatch = state.matches(path)
} else if (Array.isArray(path)) {
pathMatch = path.some((p) => state.matches(p))
} else {
throw new Error(`Invalid route path ${JSON.stringify(path)}`)
}
if (pathMatch) {
// @ts-ignore
return child.props.children
}
export const Router = ({ children, route }: RouterProps) => {
// @ts-ignore may accept string as well as element
const childArray: React.ReactElement[] = React.Children.toArray(children)
for (const child of childArray) {
// match path
const { paths } = child.props
let pathMatch = matches(route, paths)

if (pathMatch) {
return child.props.children
}
const message = `No Route matches for ${JSON.stringify(state)}`
onError(new Error(message))
console.warn(message)
return null
}
const message = `No Route matches for "${JSON.stringify(route)}"`
onError(new Error(message))
console.warn(message)
return null
}

return {
context: state.context,
send: sendWithLog,
Router,
Route,
}
interface RouteProps {
children: any
paths: object
}

export default useRouter
export const Route = ({ children }: RouteProps) => children
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import * as T from 'typings'
import * as TT from 'typings/tutorial'
import { Menu } from '@alifd/next'
import Icon from '../../components/Icon'
import Icon from '../../../components/Icon'

interface Props {
tutorial: TT.Tutorial
Expand Down
12 changes: 7 additions & 5 deletions web-app/src/containers/Tutorial/components/Hints.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,24 @@ const styles = {

interface Props {
hints: string[]
hintIndex: number
setHintIndex(value: number): void
}

const Hints = (props: Props) => {
const [hintIndex, setHintIndex] = React.useState(-1)
const isFinalHint = props.hints.length - 1 === hintIndex
const isFinalHint = props.hints.length - 1 === props.hintIndex
const nextHint = () => {
if (!isFinalHint) {
setHintIndex((currentHintIndex) => currentHintIndex + 1)
if (isFinalHint) {
return
}
props.setHintIndex(props.hintIndex + 1)
}
return (
<div style={styles.hints}>
<div style={styles.hintList}>
{/* only show revealed hints */}
{props.hints.map((h, i) => {
return i <= hintIndex ? (
return i <= props.hintIndex ? (
<div key={i} style={styles.hint}>
<Markdown>{h}</Markdown>
</div>
Expand Down
Loading