diff --git a/src/actions/onRunReset.ts b/src/actions/onRunReset.ts index 745c9123..cac47dfc 100644 --- a/src/actions/onRunReset.ts +++ b/src/actions/onRunReset.ts @@ -3,15 +3,21 @@ import * as TT from 'typings/tutorial' import Context from '../services/context/context' import { exec } from '../services/node' import reset from '../services/reset' -import getLastCommitHash from '../services/reset/lastHash' +import getCommitHashByPosition from '../services/reset/lastHash' -const onRunReset = async (context: Context) => { +type ResetAction = { + type: 'LATEST' | 'POSITION' + position?: T.Position +} + +// reset to the start of the last test +const onRunReset = async (action: ResetAction, context: Context) => { // reset to timeline const tutorial: TT.Tutorial | null = context.tutorial.get() - const position: T.Position = context.position.get() + const position: T.Position = action.position ? action.position : context.position.get() // get last pass commit - const hash = getLastCommitHash(position, tutorial?.levels || []) + const hash: string = getCommitHashByPosition(position, tutorial) const branch = tutorial?.config.repo.branch diff --git a/src/channel.ts b/src/channel.ts index 4e21f8c6..287a9691 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -79,8 +79,11 @@ class Channel implements Channel { case 'EDITOR_RUN_TEST': actions.onRunTest(action) return - case 'EDITOR_RUN_RESET': - actions.onRunReset(this.context) + case 'EDITOR_RUN_RESET_LATEST': + actions.onRunReset({ type: 'LATEST' }, this.context) + return + case 'EDITOR_RUN_RESET_POSITION': + actions.onRunReset({ type: 'POSITION', position: action.payload.position }, this.context) return default: logger(`No match for action type: ${actionType}`) diff --git a/src/services/reset/lastHash.test.ts b/src/services/reset/lastHash.test.ts index 445f1c9e..230635e2 100644 --- a/src/services/reset/lastHash.test.ts +++ b/src/services/reset/lastHash.test.ts @@ -5,52 +5,83 @@ import getLastCommitHash from './lastHash' describe('lastHash', () => { it('should grab the last passing hash from a step', () => { const position: T.Position = { levelId: '1', stepId: '1.2' } - const levels: TT.Level[] = [ - { - id: '1', - title: '', - summary: '', - content: '', - steps: [ - { - id: '1.1', - content: '', - setup: { commits: ['abcdef1'] }, - }, - { - id: '1.2', - content: '', - setup: { commits: ['abcdef2'] }, - }, - ], - }, - ] - const result = getLastCommitHash(position, levels) + // @ts-ignore + const tutorial: TT.Tutorial = { + levels: [ + { + id: '1', + title: '', + summary: '', + content: '', + steps: [ + { + id: '1.1', + content: '', + setup: { commits: ['abcdef1'] }, + }, + { + id: '1.2', + content: '', + setup: { commits: ['abcdef2'] }, + }, + ], + }, + ], + } + const result = getLastCommitHash(position, tutorial) expect(result).toBe('abcdef2') }) it('should grab the last passing hash from a step with several commits', () => { const position: T.Position = { levelId: '1', stepId: '1.2' } - const levels: TT.Level[] = [ - { - id: '1', - title: '', - summary: '', - content: '', - steps: [ - { - id: '1.1', - content: '', - setup: { commits: ['abcdef1'] }, - }, - { - id: '1.2', - content: '', - setup: { commits: ['abcdef2', 'abcdef3'] }, + // @ts-ignore + const tutorial: TT.Tutorial = { + levels: [ + { + id: '1', + title: '', + summary: '', + content: '', + steps: [ + { + id: '1.1', + content: '', + setup: { commits: ['abcdef1'] }, + }, + { + id: '1.2', + content: '', + setup: { commits: ['abcdef2', 'abcdef3'] }, + }, + ], + }, + ], + } + const result = getLastCommitHash(position, tutorial) + expect(result).toBe('abcdef3') + }) + it('should grab the last passing hash when level has no steps', () => { + const position: T.Position = { levelId: '1', stepId: null } + // @ts-ignore + const tutorial: TT.Tutorial = { + config: { + // @ts-ignore + testRunner: { + setup: { + commits: ['abcdef2', 'abcdef3'], }, - ], + }, }, - ] - const result = getLastCommitHash(position, levels) + levels: [ + { + id: '1', + title: '', + summary: '', + content: '', + steps: [], + }, + ], + } + const result = getLastCommitHash(position, tutorial) expect(result).toBe('abcdef3') }) }) diff --git a/src/services/reset/lastHash.ts b/src/services/reset/lastHash.ts index afe0c1e7..3fe75e3c 100644 --- a/src/services/reset/lastHash.ts +++ b/src/services/reset/lastHash.ts @@ -1,14 +1,42 @@ import * as TT from '../../../typings/tutorial' import * as T from '../../../typings' -const getLastCommitHash = (position: T.Position, levels: TT.Level[]) => { +const getLastCommitHash = (position: T.Position, tutorial: TT.Tutorial | null) => { + if (!tutorial) { + throw new Error('No tutorial found') + } + const { levels } = tutorial // get previous position const { levelId, stepId } = position - const level: TT.Level | undefined = levels.find((l) => levelId === l.id) + let level: TT.Level | undefined = levels.find((l) => levelId === l.id) if (!level) { throw new Error(`No level found matching ${levelId}`) } + + // handle a level with no steps + if (!level.steps || !level.steps.length) { + if (level.setup && level.setup.commits) { + // return level commit + const levelCommits = level.setup.commits + return levelCommits[levelCommits.length - 1] + } else { + // is there a previous level? + // @ts-ignore + const levelIndex = levels.findIndex((l: TT.Level) => level.id === l.id) + if (levelIndex > 0) { + level = levels[levelIndex - 1] + } else { + // use init commit + const configCommits = tutorial.config.testRunner.setup?.commits + if (!configCommits) { + throw new Error('No commits found to reset back to') + } + return configCommits[configCommits.length - 1] + } + } + } + const step = level.steps.find((s) => stepId === s.id) if (!step) { throw new Error(`No step found matching ${stepId}`) diff --git a/web-app/src/containers/Tutorial/containers/Review.tsx b/web-app/src/containers/Tutorial/containers/Review.tsx index c07bbcd5..30789307 100644 --- a/web-app/src/containers/Tutorial/containers/Review.tsx +++ b/web-app/src/containers/Tutorial/containers/Review.tsx @@ -1,12 +1,15 @@ import * as React from 'react' import * as T from 'typings' -import { Switch } from '@alifd/next' -import Steps from '../components/Steps' +import { Button, Icon } from '@alifd/next' +import Step from '../components/Step' +import Hints from '../components/Hints' import Content from '../components/Content' import { Theme } from '../../../styles/theme' +import AdminContext from '../../../services/admin/context' interface Props { levels: T.LevelUI[] + onResetToPosition(position: T.Position): void } const styles = { @@ -36,28 +39,88 @@ const styles = { fontSize: '70%', }, levels: {}, + steps: { + padding: '1rem 1rem', + }, + adminNav: { + position: 'absolute' as 'absolute', + right: '1rem', + lineHeight: '16px', + }, } const ReviewPage = (props: Props) => { - const [stepVisibility, setStepVisibility] = React.useState(true) + const { + state: { adminMode }, + } = React.useContext(AdminContext) + const show = (status: T.ProgressStatus): boolean => { + return adminMode || status !== 'INCOMPLETE' + } return (
Review
-
- Show steps  - setStepVisibility(checked)} /> -
- {props.levels.map((level: T.LevelUI, index: number) => ( - <> - - {stepVisibility ? : null} - {index < props.levels.length - 1 ?
: null} - - ))} + {props.levels.map((level: T.LevelUI, index: number) => + show(level.status) ? ( +
+ {adminMode && ( +
+ +
+ )} + + +
+ {level.steps.map((step: T.StepUI) => { + if (!step) { + return null + } + return show(step.status) ? ( +
+ {adminMode && ( +
+ +
+ )} + + +
+ ) : null + })} +
+ + {index < props.levels.length - 1 ?
: null} +
+ ) : null, + )}
) diff --git a/web-app/src/containers/Tutorial/formatLevels.ts b/web-app/src/containers/Tutorial/formatLevels.ts index 89c93de9..90557746 100644 --- a/web-app/src/containers/Tutorial/formatLevels.ts +++ b/web-app/src/containers/Tutorial/formatLevels.ts @@ -32,17 +32,24 @@ const formatLevels = ({ progress, position, levels, testStatus }: Input): Output const currentLevel = levels[levelIndex] + let stepIndex = currentLevel.steps.findIndex((s: TT.Step) => s.id === position.stepId) + if (stepIndex === -1) { + stepIndex = levels[levelIndex].steps.length + } + const levelUI: T.LevelUI = { ...currentLevel, status: progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE', - steps: currentLevel.steps.map((step: TT.Step) => { + steps: currentLevel.steps.map((step: TT.Step, index) => { // label step status for step component let status: T.ProgressStatus = 'INCOMPLETE' let subtasks - if (progress.steps[step.id]) { + if (index < stepIndex || (index === stepIndex && progress.steps[step.id])) { status = 'COMPLETE' - } else if (step.id === position.stepId) { + } else if (index === stepIndex) { status = 'ACTIVE' + } else { + status = 'INCOMPLETE' } if (step.subtasks && step.subtasks) { const testSummaries = Object.keys(testStatus?.summary || {}) @@ -95,10 +102,6 @@ const formatLevels = ({ progress, position, levels, testStatus }: Input): Output const levelsUI: T.LevelUI[] = [...completed, levelUI, ...incompleted] - let stepIndex = levelUI.steps.findIndex((s: T.StepUI) => s.status === 'ACTIVE') - if (stepIndex === -1) { - stepIndex = levels[levelIndex].steps.length - } return { level: levelUI, levels: levelsUI, levelIndex, stepIndex } } diff --git a/web-app/src/containers/Tutorial/index.tsx b/web-app/src/containers/Tutorial/index.tsx index 0face25c..96385c33 100644 --- a/web-app/src/containers/Tutorial/index.tsx +++ b/web-app/src/containers/Tutorial/index.tsx @@ -119,6 +119,10 @@ const TutorialPage = (props: PageProps) => { props.send({ type: 'RUN_RESET' }) } + const onResetToPosition = (position: T.Position): void => { + props.send({ type: 'RUN_RESET_TO_POSITION', payload: { position } }) + } + const [menuVisible, setMenuVisible] = React.useState(false) const [page, setPage] = React.useState<'about' | 'level' | 'review' | 'settings'>('level') @@ -150,7 +154,7 @@ const TutorialPage = (props: PageProps) => { )} - {page === 'review' && } + {page === 'review' && } {/* {page === 'settings' && } */} diff --git a/web-app/src/environment.ts b/web-app/src/environment.ts index f2054d88..24969112 100644 --- a/web-app/src/environment.ts +++ b/web-app/src/environment.ts @@ -16,5 +16,4 @@ export const TUTORIAL_LIST_URL: string = process.env.REACT_APP_TUTORIAL_LIST_URL export const DISPLAY_RUN_TEST_BUTTON = (process.env.CODEROAD_DISPLAY_RUN_TEST_BUTTON || 'true').toLowerCase() !== 'false' // default true -export const ADMIN_MODE = false -// (process.env.CODEROAD_ADMIN_MODE || process.env.STORYBOOK_ADMIN_MODE || '').toLowerCase() === 'true' // default false +export const ADMIN_MODE = (process.env.CODEROAD_ADMIN_MODE || '').toLowerCase() === 'true' // default false diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts index 225e3e09..5fae0b9f 100644 --- a/web-app/src/services/state/actions/editor.ts +++ b/web-app/src/services/state/actions/editor.ts @@ -1,5 +1,6 @@ import * as T from 'typings' import * as TT from 'typings/tutorial' +import { assign } from 'xstate' import * as selectors from '../../selectors' export default (editorSend: any) => ({ @@ -117,9 +118,19 @@ export default (editorSend: any) => ({ payload: { position: context.position }, }) }, - runReset() { + runReset(): void { editorSend({ - type: 'EDITOR_RUN_RESET', + type: 'EDITOR_RUN_RESET_LATEST', }) }, + // @ts-ignore + runResetToPosition: assign({ + position: (context: T.MachineContext, event: T.MachineEvent) => { + editorSend({ + type: 'EDITOR_RUN_RESET_POSITION', + payload: event.payload, + }) + return event.payload.position + }, + }), }) diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index 6af6f8a9..284de98b 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -171,6 +171,9 @@ export const createMachine = (options: any) => { RUN_RESET: { actions: ['runReset'], }, + RUN_RESET_TO_POSITION: { + actions: ['runResetToPosition'], + }, KEY_PRESS_ENTER: { actions: ['runTest'], },