diff --git a/CHANGELOG.md b/CHANGELOG.md index adbe6d9f..79da8400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,8 +94,10 @@ Resulting in a folder structure like the following: ## [0.4.0] -- Navigate through text content from previous levels. +- Want to look back at a previous lesson's content? Navigate through text content from previous levels by clicking the "Learn" dropdown. ![traverse content](./docs/images/traverse-content.png) -- Fixes progress navigation bug when no steps in a level +- Continue an incomplete tutorial started in the same workspace. Choose the "continue" path from the start screen. Progress is stored in local storage in the workspace. + +![continue tutorial](./docs/images/continue-tutorial.png) diff --git a/docs/images/continue-tutorial.png b/docs/images/continue-tutorial.png new file mode 100644 index 00000000..b6a31d9d Binary files /dev/null and b/docs/images/continue-tutorial.png differ diff --git a/src/channel/index.ts b/src/channel/index.ts index 5d2a7dec..b9734d64 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -75,24 +75,22 @@ class Channel implements Channel { // continue from tutorial from local storage const tutorial: TT.Tutorial | null = this.context.tutorial.get() - // new tutorial - this.send({ type: 'START_NEW_TUTORIAL', payload: { env } }) - return - - // disable continue until fixed - - // // set tutorial - // const { position, progress } = await this.context.setTutorial(this.workspaceState, tutorial) + // no stored tutorial, must start new tutorial + if (!tutorial || !tutorial.id) { + this.send({ type: 'START_NEW_TUTORIAL', payload: { env } }) + return + } - // if (progress.complete) { - // // tutorial is already complete - // this.send({ type: 'TUTORIAL_ALREADY_COMPLETE', payload: { env } }) - // return - // } - // // communicate to client the tutorial & stepProgress state - // this.send({ type: 'LOAD_STORED_TUTORIAL', payload: { env, tutorial, progress, position } }) + // load continued tutorial position & progress + const { position, progress } = await this.context.setTutorial(this.workspaceState, tutorial) - // return + if (progress.complete) { + // tutorial is already complete + this.send({ type: 'TUTORIAL_ALREADY_COMPLETE', payload: { env } }) + return + } + // communicate to client the tutorial & stepProgress state + this.send({ type: 'LOAD_STORED_TUTORIAL', payload: { env, tutorial, progress, position } }) } catch (e) { const error = { type: 'UnknownError', diff --git a/web-app/src/components/Router/index.tsx b/web-app/src/components/Router/index.tsx index 56be6dad..8dda5bce 100644 --- a/web-app/src/components/Router/index.tsx +++ b/web-app/src/components/Router/index.tsx @@ -17,7 +17,7 @@ declare let acquireVsCodeApi: any const editor = acquireVsCodeApi() const editorSend = (action: T.Action) => { - logger(`CLIENT TO EXT: "${action.type}"`) + logger(`TO EXT: "${action.type}"`) return editor.postMessage(action) } @@ -25,6 +25,11 @@ const editorSend = (action: T.Action) => { const useRouter = (): Output => { const [state, send] = useMachine(createMachine({ editorSend })) + const sendWithLog = (action: T.Action): void => { + logger(`SEND: ${action.type}`, action) + send(action) + } + logger(`STATE: ${JSON.stringify(state.value)}`) // event bus listener @@ -38,8 +43,7 @@ const useRouter = (): Output => { if (action.source) { return } - logger(`CLIENT RECEIVED: "${action.type}"`) - send(action) + sendWithLog(action) } window.addEventListener(listener, handler) return () => { @@ -74,7 +78,7 @@ const useRouter = (): Output => { return { context: state.context, - send, + send: sendWithLog, Router, Route, } diff --git a/web-app/src/containers/Start/index.tsx b/web-app/src/containers/Start/index.tsx index 257f25c4..922bfed7 100644 --- a/web-app/src/containers/Start/index.tsx +++ b/web-app/src/containers/Start/index.tsx @@ -86,7 +86,11 @@ interface ContainerProps { const StartPageContainer = ({ context, send }: ContainerProps) => { const tutorial = context.tutorial || undefined return ( - send('CONTINUE_TUTORIAL')} onNew={() => send('NEW_TUTORIAL')} tutorial={tutorial} /> + send({ type: 'CONTINUE_TUTORIAL' })} + onNew={() => send({ type: 'NEW_TUTORIAL' })} + tutorial={tutorial} + /> ) } diff --git a/web-app/src/containers/Tutorial/ContentMenu.tsx b/web-app/src/containers/Tutorial/ContentMenu.tsx new file mode 100644 index 00000000..69e3329e --- /dev/null +++ b/web-app/src/containers/Tutorial/ContentMenu.tsx @@ -0,0 +1,52 @@ +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' + +interface Props { + tutorial: TT.Tutorial + position: T.Position + progress: T.Progress + setTitle: (title: string) => void + setContent: (content: string) => void +} + +const ContentMenu = ({ tutorial, position, progress, setTitle, setContent }: Props) => { + const setMenuContent = (levelId: string) => { + const selectedLevel: TT.Level | undefined = tutorial.levels.find((l: TT.Level) => l.id === levelId) + if (selectedLevel) { + setTitle(selectedLevel.title) + setContent(selectedLevel.content) + } + } + return ( + + {tutorial.levels.map((level: TT.Level) => { + const isCurrent = level.id === position.levelId + const isComplete = progress.levels[level.id] + let icon + let disabled = false + + if (isComplete) { + // completed icon + icon = + } else if (isCurrent) { + // current icon` + icon = + } else { + // upcoming + disabled = true + icon = + } + return ( + setMenuContent(level.id)}> + {icon}   {level.title} + + ) + })} + + ) +} + +export default ContentMenu diff --git a/web-app/src/containers/Tutorial/index.tsx b/web-app/src/containers/Tutorial/index.tsx index c7e21201..4c04f660 100644 --- a/web-app/src/containers/Tutorial/index.tsx +++ b/web-app/src/containers/Tutorial/index.tsx @@ -1,11 +1,9 @@ import * as React from 'react' import * as T from 'typings' import * as TT from 'typings/tutorial' -import { Menu } from '@alifd/next' import * as selectors from '../../services/selectors' -import Icon from '../../components/Icon' +import ContentMenu from './ContentMenu' import Level from './components/Level' -import logger from '../../services/logger' interface PageProps { context: T.MachineContext @@ -23,9 +21,9 @@ const TutorialPage = (props: PageProps) => { const onContinue = (): void => { props.send({ - type: 'LEVEL_NEXT', + type: 'NEXT_LEVEL', payload: { - LevelId: position.levelId, + levelId: position.levelId, }, }) } @@ -45,48 +43,19 @@ const TutorialPage = (props: PageProps) => { return { ...step, status } }) - const setMenuContent = (levelId: string) => { - const selectedLevel: TT.Level | undefined = tutorial.levels.find((l: TT.Level) => l.id === levelId) - if (selectedLevel) { - setTitle(selectedLevel.title) - setContent(selectedLevel.content) - } - } - - const menu = ( - - {tutorial.levels.map((level: TT.Level) => { - const isCurrent = level.id === position.levelId - logger('progress', progress) - const isComplete = progress.levels[level.id] - let icon - let disabled = false - - if (isComplete) { - // completed icon - icon = - } else if (isCurrent) { - // current icon` - icon = - } else { - // upcoming - disabled = true - icon = - } - return ( - setMenuContent(level.id)}> - {icon}   {level.title} - - ) - })} - - ) - return ( + } index={tutorial.levels.findIndex((l: TT.Level) => l.id === position.levelId)} steps={steps} status={progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE'} diff --git a/web-app/src/services/state/actions/context.ts b/web-app/src/services/state/actions/context.ts index e179effe..6c6a25f9 100644 --- a/web-app/src/services/state/actions/context.ts +++ b/web-app/src/services/state/actions/context.ts @@ -3,6 +3,7 @@ import * as TT from 'typings/tutorial' import { assign, send, ActionFunctionMap } from 'xstate' import * as selectors from '../../selectors' import onError from '../../../services/sentry/onError' +import logger from '../../../services/logger' const contextActions: ActionFunctionMap = { // @ts-ignore @@ -15,25 +16,20 @@ const contextActions: ActionFunctionMap = { }, }), // @ts-ignore - storeContinuedTutorial: assign({ - env: (context: T.MachineContext, event: T.MachineEvent) => { - return { + loadContinuedTutorial: assign((context: T.MachineContext, event: T.MachineEvent): any => { + return { + env: { ...context.env, ...event.payload.env, - } - }, - tutorial: (context: T.MachineContext, event: T.MachineEvent) => { - return event.payload.tutorial - }, - progress: (context: T.MachineContext, event: T.MachineEvent) => { - return event.payload.progress - }, - position: (context: T.MachineContext, event: T.MachineEvent) => { - return event.payload.position - }, + }, + tutorial: event.payload.tutorial, + progress: event.payload.progress, + position: event.payload.position, + } }), + // @ts-ignore - startNewTutorial: assign({ + initProgressPosition: assign({ position: (context: T.MachineContext, event: T.MachineEvent): any => { const position: T.Position = selectors.initialPosition(context) return position @@ -119,8 +115,7 @@ const contextActions: ActionFunctionMap = { // @ts-ignore updatePosition: assign({ position: (context: T.MachineContext, event: T.MachineEvent): any => { - const { position } = event.payload - return position + return event.payload }, }), loadNext: send( @@ -140,7 +135,7 @@ const contextActions: ActionFunctionMap = { // NEXT STEP if (hasNextStep) { const nextPosition = { ...position, stepId: steps[stepIndex + 1].id } - return { type: 'NEXT_STEP', payload: { position: nextPosition } } + return { type: 'NEXT_STEP', payload: nextPosition } } // has next level? @@ -164,7 +159,7 @@ const contextActions: ActionFunctionMap = { levelId: nextLevel.id, stepId: nextLevel.steps[0].id, } - return { type: 'NEXT_LEVEL', payload: { position: nextPosition } } + return { type: 'NEXT_LEVEL', payload: nextPosition } } // COMPLETED @@ -230,8 +225,9 @@ const contextActions: ActionFunctionMap = { error: (): any => null, }), // @ts-ignore - checkEmptySteps: send((context: T.MachineContext) => { + checkLevelCompleted: send((context: T.MachineContext) => { // no step id indicates no steps to complete + logger(context.position) return { type: context.position.stepId === null ? 'START_COMPLETED_LEVEL' : 'START_LEVEL', } diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index 4441a968..8b91d813 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -64,7 +64,7 @@ export const createMachine = (options: any) => { }, LOAD_STORED_TUTORIAL: { target: 'Start', - actions: ['storeContinuedTutorial'], + actions: ['loadContinuedTutorial'], }, START_NEW_TUTORIAL: { target: 'Start', @@ -97,7 +97,7 @@ export const createMachine = (options: any) => { on: { NEW_TUTORIAL: 'ValidateSetup', CONTINUE_TUTORIAL: { - target: '#tutorial-level', + target: '#tutorial', actions: ['continueConfig'], }, CONTINUE_FAILED: { @@ -127,7 +127,7 @@ export const createMachine = (options: any) => { }, }, StartTutorial: { - onEntry: ['startNewTutorial'], + onEntry: ['initProgressPosition'], after: { 0: '#tutorial', }, @@ -157,7 +157,7 @@ export const createMachine = (options: any) => { initial: 'Load', states: { Load: { - onEntry: ['loadLevel', 'loadStep', 'checkEmptySteps'], + onEntry: ['loadLevel', 'loadStep', 'checkLevelCompleted'], on: { START_LEVEL: 'Normal', START_COMPLETED_LEVEL: 'LevelComplete', @@ -214,9 +214,9 @@ export const createMachine = (options: any) => { onEntry: ['updateLevelProgress'], onExit: ['syncLevelProgress'], on: { - LEVEL_NEXT: { + NEXT_LEVEL: { target: '#tutorial-load-next', - actions: ['testClear'], + actions: ['testClear', 'updatePosition'], }, }, },