diff --git a/src/channel/index.ts b/src/channel/index.ts index 458cc226..5d2a7dec 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -297,6 +297,11 @@ class Channel implements Channel { vscode.commands.executeCommand(COMMANDS.RUN_TEST) return + case 'EDITOR_SYNC_PROGRESS': + // update progress when a level is deemed complete in the client + await this.context.progress.syncProgress(action.payload.progress) + return + default: logger(`No match for action type: ${actionType}`) return diff --git a/src/channel/state/Progress.ts b/src/channel/state/Progress.ts index 839614cb..4b143d8d 100644 --- a/src/channel/state/Progress.ts +++ b/src/channel/state/Progress.ts @@ -39,6 +39,10 @@ class Progress { public reset = () => { this.set(defaultValue) } + public syncProgress = (progress: T.Progress): T.Progress => { + const next = { ...this.value, ...progress } + return this.set(next) + } public setStepComplete = (tutorial: TT.Tutorial, stepId: string): T.Progress => { const next = this.value // mark step complete diff --git a/web-app/src/containers/Tutorial/components/Level.tsx b/web-app/src/containers/Tutorial/components/Level.tsx index bf782377..38f7a8bd 100644 --- a/web-app/src/containers/Tutorial/components/Level.tsx +++ b/web-app/src/containers/Tutorial/components/Level.tsx @@ -2,6 +2,8 @@ import * as React from 'react' import * as T from 'typings' import * as TT from 'typings/tutorial' import { css, jsx } from '@emotion/core' +import { Dropdown } from '@alifd/next' +import Icon from '../../../components/Icon' import Button from '../../../components/Button' import Markdown from '../../../components/Markdown' import ProcessMessages from '../../../components/ProcessMessages' @@ -22,12 +24,19 @@ const styles = { paddingBottom: '5rem', }, header: { + display: 'flex' as 'flex', + alignItems: 'center', + justifyContent: 'space-between', height: '2rem', backgroundColor: '#EBEBEB', fontSize: '1rem', lineHeight: '1rem', padding: '10px 1rem', }, + learn: { + textDecoration: 'none', + color: 'inherit', + }, text: { padding: '0rem 1rem', paddingBottom: '1rem', @@ -77,18 +86,34 @@ const styles = { } interface Props { - level: TT.Level & { status: T.ProgressStatus; index: number; steps: Array } + menu: any + steps: Array + title: string + index: number + content: string + status: 'COMPLETE' | 'ACTIVE' | 'INCOMPLETE' processes: T.ProcessEvent[] testStatus: T.TestStatus | null onContinue(): void onLoadSolution(): void } -const Level = ({ level, onContinue, onLoadSolution, processes, testStatus }: Props) => { +const Level = ({ + menu, + steps, + title, + content, + index, + status, + onContinue, + onLoadSolution, + processes, + testStatus, +}: Props) => { // @ts-ignore - let currentStep = level.steps.findIndex((s) => s.status === 'ACTIVE') + let currentStep = steps.findIndex((s) => s.status === 'ACTIVE') if (currentStep === -1) { - currentStep = level.steps.length + currentStep = steps.length } const pageBottomRef = React.useRef(null) @@ -103,18 +128,27 @@ const Level = ({ level, onContinue, onLoadSolution, processes, testStatus }: Pro
- Learn + + Learn + + } + triggerType="click" + > + {menu} +
-

{level.title}

- {level.content || ''} +

{title}

+ {content || ''}
- {level.steps.length ? ( + {steps.length ? (
Tasks
- {level.steps.map((step: (TT.Step & { status: T.ProgressStatus }) | null, index: number) => { + {steps.map((step: (TT.Step & { status: T.ProgressStatus }) | null, index: number) => { if (!step) { return null } @@ -146,17 +180,17 @@ const Level = ({ level, onContinue, onLoadSolution, processes, testStatus }: Pro
- {typeof level.index === 'number' ? `${level.index + 1}. ` : ''} - {level.title} + {typeof index === 'number' ? `${index + 1}. ` : ''} + {title} - {level.status === 'COMPLETE' || !level.steps.length ? ( + {status === 'COMPLETE' || !steps.length ? ( ) : ( - {currentStep} of {level.steps.length} tasks + {currentStep} of {steps.length} tasks )} diff --git a/web-app/src/containers/Tutorial/index.tsx b/web-app/src/containers/Tutorial/index.tsx index 0d49e7ad..c7e21201 100644 --- a/web-app/src/containers/Tutorial/index.tsx +++ b/web-app/src/containers/Tutorial/index.tsx @@ -1,8 +1,11 @@ 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 Level from './components/Level' +import logger from '../../services/logger' interface PageProps { context: T.MachineContext @@ -15,6 +18,9 @@ const TutorialPage = (props: PageProps) => { const tutorial = selectors.currentTutorial(props.context) const levelData: TT.Level = selectors.currentLevel(props.context) + const [title, setTitle] = React.useState(levelData.title) + const [content, setContent] = React.useState(levelData.content) + const onContinue = (): void => { props.send({ type: 'LEVEL_NEXT', @@ -28,29 +34,62 @@ const TutorialPage = (props: PageProps) => { props.send({ type: 'STEP_SOLUTION_LOAD' }) } - const level: TT.Level & { - status: T.ProgressStatus - index: number - steps: Array - } = { - ...levelData, - index: tutorial.levels.findIndex((l: TT.Level) => l.id === position.levelId), - status: progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE', - steps: levelData.steps.map((step: TT.Step) => { - // label step status for step component - let status: T.ProgressStatus = 'INCOMPLETE' - if (progress.steps[step.id]) { - status = 'COMPLETE' - } else if (step.id === position.stepId) { - status = 'ACTIVE' - } - return { ...step, status } - }), + const steps = levelData.steps.map((step: TT.Step) => { + // label step status for step component + let status: T.ProgressStatus = 'INCOMPLETE' + if (progress.steps[step.id]) { + status = 'COMPLETE' + } else if (step.id === position.stepId) { + status = 'ACTIVE' + } + 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 ( l.id === position.levelId)} + steps={steps} + status={progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE'} onContinue={onContinue} onLoadSolution={onLoadSolution} processes={processes} diff --git a/web-app/src/environment.ts b/web-app/src/environment.ts index e5e18633..d997cb9c 100644 --- a/web-app/src/environment.ts +++ b/web-app/src/environment.ts @@ -9,7 +9,6 @@ for (const required of requiredKeys) { export const DEBUG: boolean = (process.env.REACT_APP_DEBUG || '').toLowerCase() === 'true' export const VERSION: string = process.env.VERSION || 'unknown' export const NODE_ENV: string = process.env.NODE_ENV || 'development' -export const LOG: boolean = - (process.env.REACT_APP_LOG || '').toLowerCase() === 'true' && process.env.NODE_ENV !== 'production' +export const LOG: boolean = (process.env.REACT_APP_LOG || '').toLowerCase() === 'true' export const TUTORIAL_LIST_URL: string = process.env.REACT_APP_TUTORIAL_LIST_URL || '' export const SENTRY_DSN: string | null = process.env.REACT_APP_SENTRY_DSN || null diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts index 5770abf2..e3121018 100644 --- a/web-app/src/services/state/actions/editor.ts +++ b/web-app/src/services/state/actions/editor.ts @@ -74,6 +74,14 @@ export default (editorSend: any) => ({ }) } }, + syncLevelProgress(context: CR.MachineContext): void { + editorSend({ + type: 'EDITOR_SYNC_PROGRESS', + payload: { + progress: context.progress, + }, + }) + }, clearStorage(): void { editorSend({ type: 'TUTORIAL_CLEAR' }) }, diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index c928c8e4..4441a968 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -207,13 +207,12 @@ export const createMachine = (options: any) => { target: 'Normal', actions: ['loadStep'], }, - LEVEL_COMPLETE: { - target: 'LevelComplete', - actions: ['updateLevelProgress'], - }, + LEVEL_COMPLETE: 'LevelComplete', }, }, LevelComplete: { + onEntry: ['updateLevelProgress'], + onExit: ['syncLevelProgress'], on: { LEVEL_NEXT: { target: '#tutorial-load-next', diff --git a/web-app/stories/Level.stories.tsx b/web-app/stories/Level.stories.tsx index 26607d3c..999a79f4 100644 --- a/web-app/stories/Level.stories.tsx +++ b/web-app/stories/Level.stories.tsx @@ -6,6 +6,8 @@ import * as T from '../../typings' import * as TT from '../../typings/tutorial' import Level from '../src/containers/Tutorial/components/Level' import SideBarDecorator from './utils/SideBarDecorator' +import { Menu } from '@alifd/next' +import Icon from '../src/components/Icon' type ModifiedLevel = TT.Level & { status: T.ProgressStatus @@ -13,6 +15,19 @@ type ModifiedLevel = TT.Level & { steps: Array } +const menu = ( + + {[{ id: '1', title: 'First' }].map((level: TT.Level) => { + const icon = + return ( + + {icon}   {level.title} + + ) + })} + +) + storiesOf('Level', module) .addDecorator(SideBarDecorator) .addDecorator(withKnobs) @@ -24,7 +39,7 @@ storiesOf('Level', module) description: 'A summary of the level', content: 'Some content here in markdown', setup: null, - status: 'ACTIVE', + status: 'ACTIVE' as 'ACTIVE', steps: [ { id: 'L1:S1', @@ -72,7 +87,12 @@ storiesOf('Level', module) } return (