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 (