diff --git a/package-lock.json b/package-lock.json index 724e64b8..5dc30df4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2038,9 +2038,9 @@ } }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.7.0-beta", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.0-beta.tgz", + "integrity": "sha512-4jyCX+IQamrPJxgkABPq9xf+hUN+GWHVxoj+oey1TadCPa4snQl1RKwUba+1dyzYCamwlCxKvZQ3TjyWLhMGBA==", "dev": true }, "uri-js": { diff --git a/package.json b/package.json index 8db06618..817ef359 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "prettier": "^1.18.2", "tslint": "^5.20.0", "tslint-config-prettier": "^1.18.0", - "typescript": "^3.6.4", + "typescript": "^3.7.0-beta", "vscode": "^1.1.36", "vscode-test": "^1.2.0" }, diff --git a/src/actions/runTest.ts b/src/actions/runTest.ts index d5fd5af6..f67b8c63 100644 --- a/src/actions/runTest.ts +++ b/src/actions/runTest.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode' import node from '../services/node' +// TODO: use tap parser to make it easier to support other test runners + // ensure only latest run_test action is taken let currentId = 0 @@ -35,12 +37,6 @@ async function runTest({onSuccess, onFail, onRun, onError}: Props): Promise => { // run commits - for (const commit of commits) { - await git.loadCommit(commit) + if (commits) { + for (const commit of commits) { + await git.loadCommit(commit) + } } // run command - await runCommands(commands) + if (commands) { + await runCommands(commands) + } // open files - for (const filePath of files) { - try { - // TODO: figure out why this does not work - // try { - // const absoluteFilePath = join(workspaceRoot.uri.path, filePath) - // const doc = await vscode.workspace.openTextDocument(absoluteFilePath) - // await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) - // // there are times when initialization leave the panel behind any files opened - // // ensure the panel is redrawn on the right side first - // // webview.createOrShow() - // } catch (error) { - // console.log(`Failed to open file ${filePath}`, error) - // } - const wr = vscode.workspace.rootPath - if (!wr) { - throw new Error('No workspace root path') + if (files) { + for (const filePath of files) { + try { + // TODO: figure out why this does not work + // try { + // const absoluteFilePath = join(workspaceRoot.uri.path, filePath) + // const doc = await vscode.workspace.openTextDocument(absoluteFilePath) + // await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) + // // there are times when initialization leave the panel behind any files opened + // // ensure the panel is redrawn on the right side first + // // webview.createOrShow() + // } catch (error) { + // console.log(`Failed to open file ${filePath}`, error) + // } + const wr = vscode.workspace.rootPath + if (!wr) { + throw new Error('No workspace root path') + } + const absoluteFilePath = join(wr, filePath) + const doc = await vscode.workspace.openTextDocument(absoluteFilePath) + await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) + // there are times when initialization leave the panel behind any files opened + // ensure the panel is redrawn on the right side first + vscode.commands.executeCommand('coderoad.open_webview') + } catch (error) { + console.log(`Failed to open file ${filePath}`, error) } - const absoluteFilePath = join(wr, filePath) - const doc = await vscode.workspace.openTextDocument(absoluteFilePath) - await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) - // there are times when initialization leave the panel behind any files opened - // ensure the panel is redrawn on the right side first - vscode.commands.executeCommand('coderoad.open_webview') - } catch (error) { - console.log(`Failed to open file ${filePath}`, error) } } } diff --git a/src/actions/tutorialConfig.ts b/src/actions/tutorialConfig.ts index 42e3b931..a9e2c63f 100644 --- a/src/actions/tutorialConfig.ts +++ b/src/actions/tutorialConfig.ts @@ -3,28 +3,27 @@ import * as vscode from 'vscode' import * as git from '../services/git' interface TutorialConfigParams { - tutorial: G.Tutorial, + config: G.TutorialConfig, alreadyConfigured?: boolean onComplete?(): void } -const tutorialConfig = async ({tutorial, alreadyConfigured, onComplete}: TutorialConfigParams) => { - console.log('---------- tutorialConfig -----------') +const tutorialConfig = async ({config, alreadyConfigured, }: TutorialConfigParams) => { if (!alreadyConfigured) { // setup git, add remote await git.initIfNotExists() // TODO: if remote not already set - await git.setupRemote(tutorial.repo.uri) - if (onComplete) {onComplete()} + await git.setupRemote(config.repo.uri) } - // TODO: allow multiple coding languages in a tutorial - const language = tutorial.codingLanguage.toLowerCase() + // allow multiple coding languages in a tutorial + const languages: string[] = config.codingLanguages.map((lang: G.CodingLanguage) => lang.toLowerCase()) // setup onSave hook vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { - if (document.uri.scheme === 'file' && language === document.languageId) { + // @ts-ignore // issue with GQL enums in TS + if (document.uri.scheme === 'file' && languages.includes(document.languageId)) { vscode.commands.executeCommand('coderoad.run_test') } }) diff --git a/src/channel/index.ts b/src/channel/index.ts index dd27cc37..ecc33de6 100644 --- a/src/channel/index.ts +++ b/src/channel/index.ts @@ -82,21 +82,33 @@ class Channel implements Channel { return // configure test runner, language, git case 'EDITOR_TUTORIAL_CONFIG': - const tutorialData = action.payload.tutorial + const tutorialData: G.Tutorial = action.payload.tutorial + // setup tutorial config (save listener, test runner, etc) this.context.setTutorial(this.workspaceState, tutorialData) - tutorialConfig({ - tutorial: tutorialData, - // must await async git setup or commit loading fails - onComplete: () => this.send({type: 'TUTORIAL_CONFIGURED'}) - }) + + const data: G.TutorialData = tutorialData.version.data + + await tutorialConfig({config: data.config}) + + // run init setup actions + if (data.init) { + const setup: G.StepActions | null | undefined = data.init.setup + if (setup) { + setupActions(this.workspaceRoot, setup) + } + } + + // report back to the webview that setup is complete + this.send({type: 'TUTORIAL_CONFIGURED'}) return case 'EDITOR_TUTORIAL_CONTINUE_CONFIG': const tutorialContinue: G.Tutorial | null = this.context.tutorial.get() if (!tutorialContinue) { throw new Error('Invalid tutorial to continue') } + const continueConfig: G.TutorialConfig = tutorialContinue.version.data.config tutorialConfig({ - tutorial: tutorialContinue, + config: continueConfig, alreadyConfigured: true }) return diff --git a/src/channel/state/Position.ts b/src/channel/state/Position.ts index 68800217..6c774432 100644 --- a/src/channel/state/Position.ts +++ b/src/channel/state/Position.ts @@ -3,7 +3,6 @@ import * as G from 'typings/graphql' const defaultValue: CR.Position = { levelId: '', - stageId: '', stepId: '', } @@ -31,7 +30,11 @@ class Position { return this.value } - const {levels} = tutorial.version + if (!tutorial || !tutorial.version || !tutorial.version.data || !tutorial.version.data.levels) { + throw new Error('Error setting position from progress') + } + + const {levels} = tutorial.version.data const lastLevelIndex: number | undefined = levels.findIndex((l: G.Level) => !progress.levels[l.id]) // TODO: consider all levels complete as progress.complete @@ -40,15 +43,7 @@ class Position { } const currentLevel: G.Level = levels[lastLevelIndex] - const {stages} = currentLevel - - const lastStageIndex: number | undefined = stages.findIndex((s: G.Stage) => !progress.stages[s.id]) - if (lastStageIndex >= stages.length) { - throw new Error('Error setting progress stage') - } - const currentStage: G.Stage = stages[lastStageIndex] - - const {steps} = currentStage + const {steps} = currentLevel const lastStepIndex: number | undefined = steps.findIndex((s: G.Step) => !progress.steps[s.id]) if (lastStepIndex >= steps.length) { @@ -60,7 +55,6 @@ class Position { this.value = { levelId: currentLevel.id, - stageId: currentStage.id, stepId: currentStep.id, } return this.value diff --git a/src/channel/state/Progress.ts b/src/channel/state/Progress.ts index c4180ac5..99799f8f 100644 --- a/src/channel/state/Progress.ts +++ b/src/channel/state/Progress.ts @@ -6,7 +6,6 @@ import Storage from '../../services/storage' const defaultValue: CR.Progress = { levels: {}, - stages: {}, steps: {}, complete: false } diff --git a/src/editor/ReactWebView.ts b/src/editor/ReactWebView.ts index 64d1f948..91483acb 100644 --- a/src/editor/ReactWebView.ts +++ b/src/editor/ReactWebView.ts @@ -52,8 +52,6 @@ class ReactWebView { // // // prevents moving coderoad panel on top of left panel // vscode.window.onDidChangeVisibleTextEditors((textEditors: vscode.TextEditor[]) => { - // console.log('onDidChangeVisibleTextEditors') - // console.log(textEditors) // // updateWindows() // }) diff --git a/src/services/git/index.ts b/src/services/git/index.ts index 20c3571e..fc05ee0e 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -42,7 +42,7 @@ export function loadCommit(commit: string): Promise { /* save commit - git commit -am '${level}/${stage}/${step} complete' + git commit -am '${level}/${step} complete' */ export async function saveCommit(message: string): Promise { diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index 1c456d56..3a228bed 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -16,10 +16,10 @@ class Storage { this.defaultValue = defaultValue } public get = async (): Promise => { - const value: string | undefined = await this.storage.get(this.key) - if (value) { - return JSON.parse(value) - } + // const value: string | undefined = await this.storage.get(this.key) + // if (value) { + // return JSON.parse(value) + // } return this.defaultValue } public set = (value: T): void => { diff --git a/src/test/runTest.ts b/src/test/runTest.ts index 9f6cb22a..798971a4 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -3,7 +3,6 @@ import * as path from 'path' import {runTests} from 'vscode-test' async function main() { - console.log('__dirname', __dirname) // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` const extensionDevelopmentPath: string = path.resolve(__dirname, '../../') diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index f40bee78..2a4c4cd1 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -15,7 +15,6 @@ suite('Extension tests', () => { await vscode.commands.executeCommand('coderoad.start') await setTimeout(() => Promise.resolve(), 5000) // const webview = vscode.window.activeTextEditor - // console.log(webview) assert.equal(2, 2) }) diff --git a/typings/context.d.ts b/typings/context.d.ts index d8ba6721..27a42ded 100644 --- a/typings/context.d.ts +++ b/typings/context.d.ts @@ -1,21 +1,18 @@ import * as CR from './index' -export interface Step extends Exclude { - status: { - complete: boolean - active: boolean - } +interface ProgressStatus { + active: boolean + complete: boolean } -export interface ReceivedEvent { - data: CR.Action +export interface Step extends Exclude { + status: ProgressStatus } -export interface StageStepStatus { - active: boolean - complete: boolean +export interface ReceivedEvent { + data: CR.Action } -export interface StageWithStatus extends CR.TutorialStage { - status: StageStepStatus +export interface LevelWithStatus extends CR.TutorialLevel { + status: ProgressStatus } diff --git a/typings/graphql.d.ts b/typings/graphql.d.ts index 779318c8..93763bee 100644 --- a/typings/graphql.d.ts +++ b/typings/graphql.d.ts @@ -15,7 +15,7 @@ export type Scalars = { }; -export enum CodingLanguageEnum { +export enum CodingLanguage { Javascript = 'JAVASCRIPT' } @@ -24,13 +24,27 @@ export type CreateTokenInput = { accessToken: Scalars['String'], }; +export type CreateTutorialSummaryInput = { + title: Scalars['String'], + description: Scalars['String'], +}; + +export type CreateTutorialVersionInput = { + data: Scalars['JSON'], +}; -export enum EditorEnum { +export type CreateTutorialVersionOutput = { + __typename?: 'createTutorialVersionOutput', + success?: Maybe, +}; + + +export enum Editor { Vscode = 'VSCODE' } export type EditorLoginInput = { - editor: EditorEnum, + editor: Editor, machineId: Scalars['String'], sessionId: Scalars['String'], }; @@ -41,6 +55,7 @@ export type EditorLoginOutput = { token: Scalars['String'], }; +/** Information linked from a GitHub account */ export type GithubUser = { __typename?: 'GithubUser', id: Scalars['ID'], @@ -52,25 +67,23 @@ export type GithubUser = { +/** Logical groupings of tasks */ export type Level = { __typename?: 'Level', id: Scalars['ID'], title: Scalars['String'], - text: Scalars['String'], - stage?: Maybe, - stages: Array, + description: Scalars['String'], + steps: Array, setup?: Maybe, status: 'ACTIVE' | 'COMPLETE' | 'INCOMPLETE', }; - -export type LevelStageArgs = { - stageId: Scalars['ID'] -}; - export type Mutation = { __typename?: 'Mutation', + /** Login used from a coding editor */ editorLogin?: Maybe, + /** Create a new tutorial */ + createTutorialVersion?: Maybe, }; @@ -78,15 +91,16 @@ export type MutationEditorLoginArgs = { input: EditorLoginInput }; + +export type MutationCreateTutorialVersionArgs = { + input: CreateTutorialVersionInput +}; + export type Query = { __typename?: 'Query', tutorial?: Maybe, tutorials?: Maybe>>, - user?: Maybe, - level?: Maybe, - stage?: Maybe, - step?: Maybe, - stepActions?: Maybe, + viewer?: Maybe, }; @@ -94,133 +108,110 @@ export type QueryTutorialArgs = { id: Scalars['ID'] }; - -export type QueryLevelArgs = { - id: Scalars['ID'] -}; - - -export type QueryStageArgs = { - id: Scalars['ID'] -}; - - -export type QueryStepArgs = { - id: Scalars['ID'] -}; - - -export type QueryStepActionsArgs = { - id: Scalars['ID'] -}; - export enum Role { Admin = 'ADMIN', EditorUser = 'EDITOR_USER' } -export type Stage = { - __typename?: 'Stage', - id: Scalars['ID'], - title: Scalars['String'], - text: Scalars['String'], - step?: Maybe, - steps: Array, - setup?: Maybe, - status: 'ACTIVE' | 'COMPLETE' | 'INCOMPLETE', -}; - - -export type StageStepArgs = { - stepId: Scalars['ID'] -}; - +/** A level task */ export type Step = { __typename?: 'Step', id: Scalars['ID'], title: Scalars['String'], - text: Scalars['String'], - setup?: Maybe, - solution?: Maybe, + description: Scalars['String'], + setup: StepActions, + solution: StepActions, status: 'ACTIVE' | 'COMPLETE' | 'INCOMPLETE', }; +/** Load commits, open files or run commands */ export type StepActions = { __typename?: 'StepActions', id: Scalars['ID'], commits: Array, - files: Array, - commands: Array, + files?: Maybe>, + commands?: Maybe>, }; -export enum TestRunnerEnum { +export enum TestRunner { Jest = 'JEST' } +/** A tutorial for use in VSCode CodeRoad */ export type Tutorial = { __typename?: 'Tutorial', id: Scalars['ID'], - repo: TutorialRepo, createdBy: User, createdAt: Scalars['DateTime'], - updatedBy: User, - updatedAt: Scalars['DateTime'], - codingLanguage: CodingLanguageEnum, - testRunner: TestRunnerEnum, - title: Scalars['String'], - text: Scalars['String'], - releasedAt?: Maybe, - releasedBy?: Maybe, version: TutorialVersion, versions: Array, - completed: Scalars['Boolean'], + completed?: Maybe, }; +/** A tutorial for use in VSCode CodeRoad */ export type TutorialVersionArgs = { version?: Maybe }; +/** Configure environment in editor for git, testing & parsing files */ +export type TutorialConfig = { + __typename?: 'TutorialConfig', + testRunner: TestRunner, + codingLanguages: Array, + repo: TutorialRepo, +}; + +/** Data for tutorial */ +export type TutorialData = { + __typename?: 'TutorialData', + config: TutorialConfig, + init?: Maybe, + levels: Array, +}; + +/** Data that loads on startup */ +export type TutorialInit = { + __typename?: 'TutorialInit', + setup?: Maybe, +}; + +/** Repo referenced by commmits in the tutorial */ export type TutorialRepo = { __typename?: 'TutorialRepo', - tutorialId: Scalars['ID'], uri: Scalars['String'], branch: Scalars['String'], - name: Scalars['String'], - owner: Scalars['String'], + name?: Maybe, + owner?: Maybe, }; +/** Summary of tutorial used when selecting tutorial */ +export type TutorialSummary = { + __typename?: 'TutorialSummary', + title: Scalars['String'], + description: Scalars['String'], +}; + +/** A version of a tutorial */ export type TutorialVersion = { __typename?: 'TutorialVersion', tutorialId: Scalars['ID'], version: Scalars['String'], - coderoadVersion: Scalars['String'], createdAt: Scalars['DateTime'], createdBy: User, + updatedBy: User, + updatedAt: Scalars['DateTime'], publishedAt?: Maybe, publishedBy?: Maybe, - level?: Maybe, - levels: Array, - stage?: Maybe, - step?: Maybe, - completed: Scalars['Boolean'], -}; - - -export type TutorialVersionLevelArgs = { - levelId: Scalars['ID'] -}; - - -export type TutorialVersionStageArgs = { - stageId: Scalars['ID'] -}; - - -export type TutorialVersionStepArgs = { - stepId: Scalars['ID'] + summary: TutorialSummary, + data: TutorialData, + completed?: Maybe, }; +/** + * Users is useful for tracking completion progress + * & credit for tutorial creation/contributions + **/ export type User = { __typename?: 'User', id: Scalars['ID'], @@ -233,10 +224,6 @@ export type User = { githubUser?: Maybe, }; -export type TutorialSummaryFragment = ( - {__typename?: 'Tutorial'} - & Pick -); @@ -312,28 +299,34 @@ export type ResolversTypes = { Query: ResolverTypeWrapper<{}>, ID: ResolverTypeWrapper, Tutorial: ResolverTypeWrapper, - TutorialRepo: ResolverTypeWrapper, - String: ResolverTypeWrapper, User: ResolverTypeWrapper, + String: ResolverTypeWrapper, DateTime: ResolverTypeWrapper, GithubUser: ResolverTypeWrapper, - CodingLanguageEnum: CodingLanguageEnum, - TestRunnerEnum: TestRunnerEnum, TutorialVersion: ResolverTypeWrapper, - Level: ResolverTypeWrapper, - Stage: ResolverTypeWrapper, - Step: ResolverTypeWrapper, + TutorialSummary: ResolverTypeWrapper, + TutorialData: ResolverTypeWrapper, + TutorialConfig: ResolverTypeWrapper, + TestRunner: TestRunner, + CodingLanguage: CodingLanguage, + TutorialRepo: ResolverTypeWrapper, + TutorialInit: ResolverTypeWrapper, StepActions: ResolverTypeWrapper, Commit: ResolverTypeWrapper, + Level: ResolverTypeWrapper, + Step: ResolverTypeWrapper, Boolean: ResolverTypeWrapper, Mutation: ResolverTypeWrapper<{}>, editorLoginInput: EditorLoginInput, - EditorEnum: EditorEnum, + Editor: Editor, editorLoginOutput: ResolverTypeWrapper, + createTutorialVersionInput: CreateTutorialVersionInput, JSON: ResolverTypeWrapper, + createTutorialVersionOutput: ResolverTypeWrapper, JSONObject: ResolverTypeWrapper, Role: Role, createTokenInput: CreateTokenInput, + createTutorialSummaryInput: CreateTutorialSummaryInput, }; /** Mapping between all available schema types and the resolvers parents */ @@ -341,28 +334,34 @@ export type ResolversParentTypes = { Query: {}, ID: Scalars['ID'], Tutorial: Tutorial, - TutorialRepo: TutorialRepo, - String: Scalars['String'], User: User, + String: Scalars['String'], DateTime: Scalars['DateTime'], GithubUser: GithubUser, - CodingLanguageEnum: CodingLanguageEnum, - TestRunnerEnum: TestRunnerEnum, TutorialVersion: TutorialVersion, - Level: Level, - Stage: Stage, - Step: Step, + TutorialSummary: TutorialSummary, + TutorialData: TutorialData, + TutorialConfig: TutorialConfig, + TestRunner: TestRunner, + CodingLanguage: CodingLanguage, + TutorialRepo: TutorialRepo, + TutorialInit: TutorialInit, StepActions: StepActions, Commit: Scalars['Commit'], + Level: Level, + Step: Step, Boolean: Scalars['Boolean'], Mutation: {}, editorLoginInput: EditorLoginInput, - EditorEnum: EditorEnum, + Editor: Editor, editorLoginOutput: EditorLoginOutput, + createTutorialVersionInput: CreateTutorialVersionInput, JSON: Scalars['JSON'], + createTutorialVersionOutput: CreateTutorialVersionOutput, JSONObject: Scalars['JSONObject'], Role: Role, createTokenInput: CreateTokenInput, + createTutorialSummaryInput: CreateTutorialSummaryInput, }; export type AuthDirectiveResolver>}> = DirectiveResolverFn; @@ -371,6 +370,10 @@ export interface CommitScalarConfig extends GraphQLScalarTypeConfig = { + success?: Resolver, ParentType, ContextType>, +}; + export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig { name: 'DateTime' } @@ -399,89 +402,86 @@ export interface JsonObjectScalarConfig extends GraphQLScalarTypeConfig = { id?: Resolver, title?: Resolver, - text?: Resolver, - stage?: Resolver, ParentType, ContextType, RequireFields>, - stages?: Resolver, ParentType, ContextType>, + description?: Resolver, + steps?: Resolver, ParentType, ContextType>, setup?: Resolver, ParentType, ContextType>, }; export type MutationResolvers = { editorLogin?: Resolver, ParentType, ContextType, RequireFields>, + createTutorialVersion?: Resolver, ParentType, ContextType, RequireFields>, }; export type QueryResolvers = { tutorial?: Resolver, ParentType, ContextType, RequireFields>, tutorials?: Resolver>>, ParentType, ContextType>, - user?: Resolver, ParentType, ContextType>, - level?: Resolver, ParentType, ContextType, RequireFields>, - stage?: Resolver, ParentType, ContextType, RequireFields>, - step?: Resolver, ParentType, ContextType, RequireFields>, - stepActions?: Resolver, ParentType, ContextType, RequireFields>, -}; - -export type StageResolvers = { - id?: Resolver, - title?: Resolver, - text?: Resolver, - step?: Resolver, ParentType, ContextType, RequireFields>, - steps?: Resolver, ParentType, ContextType>, - setup?: Resolver, ParentType, ContextType>, + viewer?: Resolver, ParentType, ContextType>, }; export type StepResolvers = { id?: Resolver, title?: Resolver, - text?: Resolver, - setup?: Resolver, ParentType, ContextType>, - solution?: Resolver, ParentType, ContextType>, + description?: Resolver, + setup?: Resolver, + solution?: Resolver, }; export type StepActionsResolvers = { id?: Resolver, commits?: Resolver, ParentType, ContextType>, - files?: Resolver, ParentType, ContextType>, - commands?: Resolver, ParentType, ContextType>, + files?: Resolver>, ParentType, ContextType>, + commands?: Resolver>, ParentType, ContextType>, }; export type TutorialResolvers = { id?: Resolver, - repo?: Resolver, createdBy?: Resolver, createdAt?: Resolver, - updatedBy?: Resolver, - updatedAt?: Resolver, - codingLanguage?: Resolver, - testRunner?: Resolver, - title?: Resolver, - text?: Resolver, - releasedAt?: Resolver, ParentType, ContextType>, - releasedBy?: Resolver, ParentType, ContextType>, version?: Resolver, versions?: Resolver, ParentType, ContextType>, - completed?: Resolver, + completed?: Resolver, ParentType, ContextType>, +}; + +export type TutorialConfigResolvers = { + testRunner?: Resolver, + codingLanguages?: Resolver, ParentType, ContextType>, + repo?: Resolver, +}; + +export type TutorialDataResolvers = { + config?: Resolver, + init?: Resolver, ParentType, ContextType>, + levels?: Resolver, ParentType, ContextType>, +}; + +export type TutorialInitResolvers = { + setup?: Resolver, ParentType, ContextType>, }; export type TutorialRepoResolvers = { - tutorialId?: Resolver, uri?: Resolver, branch?: Resolver, - name?: Resolver, - owner?: Resolver, + name?: Resolver, ParentType, ContextType>, + owner?: Resolver, ParentType, ContextType>, +}; + +export type TutorialSummaryResolvers = { + title?: Resolver, + description?: Resolver, }; export type TutorialVersionResolvers = { tutorialId?: Resolver, version?: Resolver, - coderoadVersion?: Resolver, createdAt?: Resolver, createdBy?: Resolver, + updatedBy?: Resolver, + updatedAt?: Resolver, publishedAt?: Resolver, ParentType, ContextType>, publishedBy?: Resolver, ParentType, ContextType>, - level?: Resolver, ParentType, ContextType, RequireFields>, - levels?: Resolver, ParentType, ContextType>, - stage?: Resolver, ParentType, ContextType, RequireFields>, - step?: Resolver, ParentType, ContextType, RequireFields>, - completed?: Resolver, + summary?: Resolver, + data?: Resolver, + completed?: Resolver, ParentType, ContextType>, }; export type UserResolvers = { @@ -497,6 +497,7 @@ export type UserResolvers = { Commit?: GraphQLScalarType, + createTutorialVersionOutput?: CreateTutorialVersionOutputResolvers, DateTime?: GraphQLScalarType, editorLoginOutput?: EditorLoginOutputResolvers, GithubUser?: GithubUserResolvers, @@ -505,11 +506,14 @@ export type Resolvers = { Level?: LevelResolvers, Mutation?: MutationResolvers, Query?: QueryResolvers, - Stage?: StageResolvers, Step?: StepResolvers, StepActions?: StepActionsResolvers, Tutorial?: TutorialResolvers, + TutorialConfig?: TutorialConfigResolvers, + TutorialData?: TutorialDataResolvers, + TutorialInit?: TutorialInitResolvers, TutorialRepo?: TutorialRepoResolvers, + TutorialSummary?: TutorialSummaryResolvers, TutorialVersion?: TutorialVersionResolvers, User?: UserResolvers, }; @@ -529,25 +533,4 @@ export type DirectiveResolvers = { * @deprecated * Use "DirectiveResolvers" root object instead. If you wish to get "IDirectiveResolvers", add "typesPrefix: I" to your config. */ -export type IDirectiveResolvers = DirectiveResolvers; - -export interface IntrospectionResultData { - __schema: { - types: { - kind: string; - name: string; - possibleTypes: { - name: string; - }[]; - }[]; - }; -} - -// @ts-ignore -const result: IntrospectionResultData = { - "__schema": { - "types": [] - } -}; - -export default result; +export type IDirectiveResolvers = DirectiveResolvers; \ No newline at end of file diff --git a/typings/index.d.ts b/typings/index.d.ts index 8b23f8e9..5f2bf1b7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -3,17 +3,6 @@ import Storage from '../src/services/storage' import * as G from './graphql' export interface TutorialLevel { - stageList: string[] - content: { - title: string - text: string - } - actions?: { - setup: TutorialAction - } -} - -export interface TutorialStage { stepList: string[] content: { title: string @@ -54,9 +43,6 @@ export interface TutorialData { levels: { [levelId: string]: TutorialLevel } - stages: { - [stageId: string]: TutorialStage - } steps: { [stepId: string]: TutorialStep } @@ -90,9 +76,6 @@ export interface Progress { levels: { [levelId: string]: boolean } - stages: { - [stageId: string]: boolean - } steps: { [stepId: string]: boolean } @@ -106,7 +89,6 @@ export interface StepProgress { // current tutorial position export interface Position { levelId: string - stageId: string stepId: string complete?: boolean } @@ -154,8 +136,7 @@ export interface MachineStateSchema { Initialize: {} Summary: {} LoadNext: {} - Level: {} - Stage: { + Level: { states: { Load: {} Normal: {} @@ -163,7 +144,7 @@ export interface MachineStateSchema { TestPass: {} TestFail: {} StepNext: {} - StageComplete: {} + LevelComplete: {} } } Completed: {} diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index 307ecb6c..d7dd4e79 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -8,7 +8,6 @@ import ContinuePage from './containers/Continue' import NewPage from './containers/New' import SummaryPage from './containers/Tutorial/SummaryPage' import LevelSummaryPage from './containers/Tutorial/LevelPage' -import StageSummaryPage from './containers/Tutorial/StagePage' import CompletedPage from './containers/Tutorial/CompletedPage' const { Route } = Router @@ -40,9 +39,6 @@ const Routes = () => { - - - diff --git a/web-app/src/containers/Continue/index.tsx b/web-app/src/containers/Continue/index.tsx index a4cd2970..2504e0d1 100644 --- a/web-app/src/containers/Continue/index.tsx +++ b/web-app/src/containers/Continue/index.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import { Button, Card } from '@alifd/next' import * as CR from 'typings' -import * as T from 'typings/graphql' +import * as G from 'typings/graphql' interface Props { - tutorial: T.Tutorial + tutorial: G.Tutorial onContinue(): void onNew(): void } @@ -14,8 +14,8 @@ export const ContinuePage = (props: Props) => (

Continue

-

{props.tutorial.title}

-

{props.tutorial.text}

+

{props.tutorial.version.summary.title}

+

{props.tutorial.version.summary.description}

diff --git a/web-app/src/containers/New/TutorialList/index.tsx b/web-app/src/containers/New/TutorialList/index.tsx index bd85f14b..be17cf9e 100644 --- a/web-app/src/containers/New/TutorialList/index.tsx +++ b/web-app/src/containers/New/TutorialList/index.tsx @@ -1,34 +1,34 @@ import * as React from 'react' import channel from '../../../services/channel' -import * as T from 'typings/graphql' +import * as G from 'typings/graphql' import TutorialItem from './TutorialItem' interface Props { - tutorialList: T.Tutorial[] + tutorialList: G.Tutorial[] } const TutorialList = (props: Props) => { - const onSelect = (tutorial: T.Tutorial) => { - channel.machineSend({ - type: 'TUTORIAL_START', - payload: { - tutorial, - } - }) - } - return ( -
- {props.tutorialList.map((tutorial: T.Tutorial) => ( - onSelect(tutorial)} - title={tutorial.title || ''} - text={tutorial.text || ''} - /> - ))} -
- ) + const onSelect = (tutorial: G.Tutorial) => { + channel.machineSend({ + type: 'TUTORIAL_START', + payload: { + tutorial, + }, + }) + } + return ( +
+ {props.tutorialList.map((tutorial: G.Tutorial) => ( + onSelect(tutorial)} + title={tutorial.version.summary.title || ''} + text={tutorial.version.summary.description || ''} + /> + ))} +
+ ) } export default TutorialList diff --git a/web-app/src/containers/New/index.tsx b/web-app/src/containers/New/index.tsx index 0086f950..a8750542 100644 --- a/web-app/src/containers/New/index.tsx +++ b/web-app/src/containers/New/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useQuery } from '@apollo/react-hooks' -import * as T from 'typings/graphql' +import * as G from 'typings/graphql' import * as CR from 'typings' import queryTutorials from '../../services/apollo/queries/tutorials' @@ -9,7 +9,7 @@ import ErrorView from '../../components/Error' import TutorialList from './TutorialList' interface Props { - tutorialList: T.Tutorial[] + tutorialList: G.Tutorial[] } export const NewPage = (props: Props) => ( @@ -25,8 +25,12 @@ interface ContainerProps { send(action: CR.Action): void } +interface TutorialsData { + tutorials: G.Tutorial[] +} + const NewPageContainer = (props: ContainerProps) => { - const { data, loading, error } = useQuery(queryTutorials) + const { data, loading, error } = useQuery(queryTutorials) if (loading) { return } @@ -35,6 +39,10 @@ const NewPageContainer = (props: ContainerProps) => { return } + if (!data) { + return null + } + return ( diff --git a/web-app/src/containers/Tutorial/StagePage/Stage/StepDescription/index.tsx b/web-app/src/containers/Tutorial/LevelPage/Level/StepDescription/index.tsx similarity index 100% rename from web-app/src/containers/Tutorial/StagePage/Stage/StepDescription/index.tsx rename to web-app/src/containers/Tutorial/LevelPage/Level/StepDescription/index.tsx diff --git a/web-app/src/containers/Tutorial/LevelPage/Level/index.tsx b/web-app/src/containers/Tutorial/LevelPage/Level/index.tsx index 785bc088..b2165c2d 100644 --- a/web-app/src/containers/Tutorial/LevelPage/Level/index.tsx +++ b/web-app/src/containers/Tutorial/LevelPage/Level/index.tsx @@ -1,74 +1,71 @@ -import { Step } from '@alifd/next' +import { Button, Step } from '@alifd/next' import * as React from 'react' -import * as T from 'typings/graphql' +import * as G from 'typings/graphql' import Markdown from '../../../../components/Markdown' -import LevelStageSummary from './LevelStageSummary' +import StepDescription from './StepDescription' const styles = { - card: {}, + card: { + padding: 0, + }, content: { padding: '0rem 1rem', paddingBottom: '1rem', }, - list: { - padding: '0rem', - }, options: { padding: '0rem 1rem', }, steps: { - padding: '1rem 0.5rem', + padding: '1rem 0rem', }, title: {}, } interface Props { - level: T.Level - onNext(): void + level: G.Level + onContinue(): void + onLoadSolution(): void } -const Level = ({ level, onNext }: Props) => { - if (!level || !level.stages) { - throw new Error('No level stages found') +const Level = ({ level, onContinue, onLoadSolution }: Props) => { + if (!level.steps) { + throw new Error('No Stage steps found') } - const activeIndex = level.stages.findIndex((stage: T.Stage | null) => stage && stage.status === 'ACTIVE') || 0 + + // grab the active step + const activeIndex: number = level.steps.findIndex((step: G.Step | null) => { + return step && step.status === 'ACTIVE' + }) + return (

{level.title}

- {level.text || ''} + {level.description || ''}
- - {level.stages.map((stage: T.Stage | null, index: number) => { - if (!stage) { + + {level.steps.map((step: G.Step | null, index: number) => { + if (!step) { return null } - const active = stage.status === 'ACTIVE' - const clickHandler = active - ? onNext - : () => { - /* empty */ - } - // note - must add click handler to title, content & step.item - // as all are separated components return ( - {stage.title || `Stage ${index + 1}`} - - } - content={} - onClick={clickHandler} + key={step.id} + title={step.title || `Step ${index + 1}`} + content={} /> ) })}
+ + {level.status === 'COMPLETE' && ( +
+ +
+ )}
) } diff --git a/web-app/src/containers/Tutorial/LevelPage/index.tsx b/web-app/src/containers/Tutorial/LevelPage/index.tsx index aa4ba130..a3f7af6c 100644 --- a/web-app/src/containers/Tutorial/LevelPage/index.tsx +++ b/web-app/src/containers/Tutorial/LevelPage/index.tsx @@ -1,50 +1,45 @@ import * as React from 'react' import * as CR from 'typings' import * as G from 'typings/graphql' +import * as selectors from '../../../services/selectors' import Level from './Level' -interface LevelProps { - level: G.Level - send(action: string): void -} - -export const LevelSummaryPage = (props: LevelProps) => { - const onNext = (): void => { - props.send('NEXT') - } - return -} - -interface ContainerProps { +interface PageProps { context: CR.MachineContext - send(action: string): void + send(action: CR.Action): void } -const LevelSummaryPageContainer = (props: ContainerProps) => { - const { tutorial, position, progress } = props.context +const LevelSummaryPageContainer = (props: PageProps) => { + const { position, progress } = props.context - if (!tutorial) { - throw new Error('Tutorial not found in LevelSummaryPageContainer') - } + const level: G.Level = selectors.currentLevel(props.context) - const level: G.Level | undefined = tutorial.version.levels.find((l: G.Level) => l.id === position.levelId) + const onContinue = (): void => { + props.send({ + type: 'LEVEL_NEXT', + payload: { + LevelId: position.levelId, + }, + }) + } - if (!level) { - throw new Error('Level not found in LevelSummaryPageContainer') + const onLoadSolution = (): void => { + props.send({ type: 'STEP_SOLUTION_LOAD' }) } - level.stages.forEach((stage: G.Stage) => { - if (stage.id === position.stageId) { - stage.status = 'ACTIVE' - } else if (progress.stages[stage.id]) { - stage.status = 'COMPLETE' + level.steps.forEach((step: G.Step) => { + if (progress.steps[step.id]) { + step.status = 'COMPLETE' + } else if (step.id === position.stepId) { + step.status = 'ACTIVE' } else { - stage.status = 'INCOMPLETE' + step.status = 'INCOMPLETE' } }) + level.status = progress.levels[position.levelId] ? 'COMPLETE' : 'ACTIVE' - return + return } export default LevelSummaryPageContainer diff --git a/web-app/src/containers/Tutorial/LevelPage/Level/LevelStageSummary.tsx b/web-app/src/containers/Tutorial/LevelsPage/Level/LevelStageSummary.tsx similarity index 83% rename from web-app/src/containers/Tutorial/LevelPage/Level/LevelStageSummary.tsx rename to web-app/src/containers/Tutorial/LevelsPage/Level/LevelStageSummary.tsx index 185ad3f0..2a45df98 100644 --- a/web-app/src/containers/Tutorial/LevelPage/Level/LevelStageSummary.tsx +++ b/web-app/src/containers/Tutorial/LevelsPage/Level/LevelStageSummary.tsx @@ -23,17 +23,17 @@ const styles = { } interface Props { - stage: G.Stage + level: G.Level onNext(): void } const LevelStageSummary = (props: Props) => { - const { stage, onNext } = props - const active = stage.status === 'ACTIVE' + const { level, onNext } = props + const active = level.status === 'ACTIVE' return (
- {stage.text || ''} + {level.description || ''}
) diff --git a/web-app/src/containers/Tutorial/LevelsPage/Level/index.tsx b/web-app/src/containers/Tutorial/LevelsPage/Level/index.tsx new file mode 100644 index 00000000..be881e91 --- /dev/null +++ b/web-app/src/containers/Tutorial/LevelsPage/Level/index.tsx @@ -0,0 +1,77 @@ +import { Step } from '@alifd/next' +import * as React from 'react' +import * as G from 'typings/graphql' + +import Markdown from '../../../../components/Markdown' +import LevelStageSummary from './LevelStageSummary' + +const styles = { + card: {}, + content: { + padding: '0rem 1rem', + paddingBottom: '1rem', + }, + list: { + padding: '0rem', + }, + options: { + padding: '0rem 1rem', + }, + steps: { + padding: '1rem 0.5rem', + }, + title: {}, +} + +interface Props { + level: G.Level + onNext(): void +} + +const Level = ({ level, onNext }: Props) => { + return
Level
+ // if (!level || !level.stages) { + // throw new Error('No level stages found') + // } + // const activeIndex = level.stages.findIndex((stage: G.Stage | null) => stage && stage.status === 'ACTIVE') || 0 + // return ( + //
+ //
+ //

{level.title}

+ // {level.text || ''} + //
+ //
+ // + // {level.stages.map((stage: G.Stage | null, index: number) => { + // if (!stage) { + // return null + // } + // const active = stage.status === 'ACTIVE' + // const clickHandler = active + // ? onNext + // : () => { + // /* empty */ + // } + // // note - must add click handler to title, content & step.item + // // as all are separated components + // return ( + // + // {stage.title || `Stage ${index + 1}`} + // + // } + // content={} + // onClick={clickHandler} + // /> + // ) + // })} + // + //
+ //
+ // ) +} + +export default Level diff --git a/web-app/src/containers/Tutorial/LevelsPage/index.tsx b/web-app/src/containers/Tutorial/LevelsPage/index.tsx new file mode 100644 index 00000000..947a36fa --- /dev/null +++ b/web-app/src/containers/Tutorial/LevelsPage/index.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import * as CR from 'typings' +import * as G from 'typings/graphql' + +import Level from './Level' + +interface LevelProps { + level: G.Level + send(action: string): void +} + +export const LevelsSummaryPage = (props: LevelProps) => { + const onNext = (): void => { + props.send('NEXT') + } + return +} + +interface ContainerProps { + context: CR.MachineContext + send(action: string): void +} + +const LevelsPageContainer = (props: ContainerProps) => { + return
LevelsPage
+ + // const { tutorial, position, progress } = props.context + + // if (!tutorial) { + // throw new Error('Tutorial not found in LevelSummaryPageContainer') + // } + + // const level: G.Level | undefined = tutorial.version.data.levels.find((l: G.Level) => l.id === position.levelId) + + // if (!level) { + // throw new Error('Level not found in LevelSummaryPageContainer') + // } + + // level.stages.forEach((stage: G.Stage) => { + // if (stage.id === position.stageId) { + // stage.status = 'ACTIVE' + // } else if (progress.stages[stage.id]) { + // stage.status = 'COMPLETE' + // } else { + // stage.status = 'INCOMPLETE' + // } + // }) + + // return +} + +export default LevelsPageContainer diff --git a/web-app/src/containers/Tutorial/StagePage/Stage/index.tsx b/web-app/src/containers/Tutorial/StagePage/Stage/index.tsx deleted file mode 100644 index 99f42279..00000000 --- a/web-app/src/containers/Tutorial/StagePage/Stage/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Button, Step } from '@alifd/next' -import * as React from 'react' -import * as T from 'typings/graphql' - -import Markdown from '../../../../components/Markdown' -import StepDescription from './StepDescription' - -const styles = { - card: { - padding: 0, - }, - content: { - padding: '0rem 1rem', - paddingBottom: '1rem', - }, - options: { - padding: '0rem 1rem', - }, - steps: { - padding: '1rem 0rem', - }, - title: {}, -} - -interface Props { - stage: T.Stage - onContinue(): void - onLoadSolution(): void -} - -const Stage = ({ stage, onContinue, onLoadSolution }: Props) => { - if (!stage.steps) { - throw new Error('No Stage steps found') - } - - // grab the active step - const activeIndex: number = stage.steps.findIndex((step: T.Step | null) => { - return step && step.status === 'ACTIVE' - }) - - return ( -
-
-

{stage.title}

- {stage.text || ''} -
-
- - {stage.steps.map((step: T.Step | null, index: number) => { - if (!step) { - return null - } - return ( - } - /> - ) - })} - -
- - {stage.status === 'COMPLETE' && ( -
- -
- )} -
- ) -} - -export default Stage diff --git a/web-app/src/containers/Tutorial/StagePage/index.tsx b/web-app/src/containers/Tutorial/StagePage/index.tsx deleted file mode 100644 index 27590dc0..00000000 --- a/web-app/src/containers/Tutorial/StagePage/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react' -import * as CR from 'typings' -import * as G from 'typings/graphql' -import * as selectors from '../../../services/selectors' - -import Stage from './Stage' - -interface PageProps { - context: CR.MachineContext - send(action: CR.Action): void -} - -const StageSummaryPageContainer = (props: PageProps) => { - const { position, progress } = props.context - - const stage: G.Stage = selectors.currentStage(props.context) - - const onContinue = (): void => { - props.send({ - type: 'STAGE_NEXT', - payload: { - stageId: position.stageId, - }, - }) - } - - const onLoadSolution = (): void => { - props.send({ type: 'STEP_SOLUTION_LOAD' }) - } - - stage.steps.forEach((step: G.Step) => { - if (progress.steps[step.id]) { - step.status = 'COMPLETE' - } else if (step.id === position.stepId) { - step.status = 'ACTIVE' - } else { - step.status = 'INCOMPLETE' - } - }) - stage.status = progress.stages[position.stageId] ? 'COMPLETE' : 'ACTIVE' - - return -} - -export default StageSummaryPageContainer diff --git a/web-app/src/containers/Tutorial/SummaryPage/Summary/index.tsx b/web-app/src/containers/Tutorial/SummaryPage/Summary/index.tsx index 2daf7a38..80fa0f42 100644 --- a/web-app/src/containers/Tutorial/SummaryPage/Summary/index.tsx +++ b/web-app/src/containers/Tutorial/SummaryPage/Summary/index.tsx @@ -16,15 +16,15 @@ const styles = { interface Props { title: string - text: string + description: string onNext(): void } -const Summary = ({ title, text, onNext }: Props) => ( +const Summary = ({ title, description, onNext }: Props) => (

{title}

-

{text}

+

{description}

diff --git a/web-app/src/containers/Tutorial/SummaryPage/index.tsx b/web-app/src/containers/Tutorial/SummaryPage/index.tsx index 95d17989..cf3109f2 100644 --- a/web-app/src/containers/Tutorial/SummaryPage/index.tsx +++ b/web-app/src/containers/Tutorial/SummaryPage/index.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import * as G from 'typings/graphql' import * as CR from 'typings' import { useQuery } from '@apollo/react-hooks' @@ -11,13 +12,22 @@ interface PageProps { send(action: CR.Action): void } +interface TutorialData { + tutorial: G.Tutorial +} + +interface TutorialDataVariables { + tutorialId: string + version: string +} + const SummaryPage = (props: PageProps) => { const { tutorial } = props.context if (!tutorial) { throw new Error('Tutorial not found in summary page') } - const { loading, error, data } = useQuery(queryTutorial, { + const { loading, error, data } = useQuery(queryTutorial, { fetchPolicy: 'network-only', // for debugging purposes variables: { tutorialId: tutorial.id, @@ -33,6 +43,10 @@ const SummaryPage = (props: PageProps) => { return } + if (!data) { + return null + } + const onNext = () => props.send({ type: 'LOAD_TUTORIAL', @@ -41,9 +55,9 @@ const SummaryPage = (props: PageProps) => { }, }) - const { title, text } = data.tutorial + const { title, description } = data.tutorial.version.summary - return + return } export default SummaryPage diff --git a/web-app/src/services/apollo/mutations/authenticate.ts b/web-app/src/services/apollo/mutations/authenticate.ts index 1c02a307..b5a1a243 100644 --- a/web-app/src/services/apollo/mutations/authenticate.ts +++ b/web-app/src/services/apollo/mutations/authenticate.ts @@ -4,7 +4,7 @@ export default gql` mutation Authenticate( $machineId: String!, $sessionId: String!, - $editor: EditorEnum! + $editor: Editor! ) { editorLogin(input: { machineId: $machineId, diff --git a/web-app/src/services/apollo/queries/tutorial.ts b/web-app/src/services/apollo/queries/tutorial.ts index fe4a27d1..27746e76 100644 --- a/web-app/src/services/apollo/queries/tutorial.ts +++ b/web-app/src/services/apollo/queries/tutorial.ts @@ -3,52 +3,49 @@ import {gql} from 'apollo-boost' export default gql` query getTutorial($tutorialId: ID!, $version: String) { tutorial(id: $tutorialId) { - id - title - text - codingLanguage - testRunner - repo { - uri + id + version (version: $version) { + version + summary { + title + description } - version(version: $version) { - version - coderoadVersion + data { + config { + testRunner + codingLanguages + repo { + uri + } + } + init { + setup { + commits + commands + } + } levels { id title - text + description setup { - id - commands commits + commands files } - stages { + steps { id title - text + description setup { - id - commands commits + commands files } - steps { - id - title - text - setup { - id - commands - commits - files - } - solution { - id - commands - commits - files + solution { + commits + commands + files } } } diff --git a/web-app/src/services/apollo/queries/tutorials.ts b/web-app/src/services/apollo/queries/tutorials.ts index e7ed5001..a91fced4 100644 --- a/web-app/src/services/apollo/queries/tutorials.ts +++ b/web-app/src/services/apollo/queries/tutorials.ts @@ -1,15 +1,23 @@ -import { gql } from 'apollo-boost' +import {gql} from 'apollo-boost' export default gql` query getTutorials { tutorials { id - title - text - codingLanguage version { version - coderoadVersion + createdBy { + id + } + createdAt + updatedBy { + id + } + updatedAt + summary { + title + description + } } } } diff --git a/web-app/src/services/selectors/position.ts b/web-app/src/services/selectors/position.ts index ec7b0061..0b02131e 100644 --- a/web-app/src/services/selectors/position.ts +++ b/web-app/src/services/selectors/position.ts @@ -5,17 +5,16 @@ import * as tutorial from './tutorial' export const defaultPosition = () => ({ levelId: '', - stageId: '', stepId: '' }) export const initialPosition = createSelector( tutorial.currentVersion, (version: G.TutorialVersion) => { + const level = version.data.levels[0] const position: CR.Position = { - levelId: version.levels[0].id, - stageId: version.levels[0].stages[0].id, - stepId: version.levels[0].stages[0].steps[0].id, + levelId: level.id, + stepId: level.steps[0].id, } return position } diff --git a/web-app/src/services/selectors/progress.ts b/web-app/src/services/selectors/progress.ts index 649272f5..60aa9bbf 100644 --- a/web-app/src/services/selectors/progress.ts +++ b/web-app/src/services/selectors/progress.ts @@ -1,6 +1,5 @@ export const defaultProgress = () => ({ levels: {}, - stages: {}, steps: {}, complete: false }) \ No newline at end of file diff --git a/web-app/src/services/selectors/tutorial.ts b/web-app/src/services/selectors/tutorial.ts index e653d7c6..ae785194 100644 --- a/web-app/src/services/selectors/tutorial.ts +++ b/web-app/src/services/selectors/tutorial.ts @@ -23,7 +23,7 @@ export const currentLevel = (context: MachineContext): G.Level => createSelector (version: G.TutorialVersion): G.Level => { // merge in the updated position // sent with the test to ensure consistency - const levels: G.Level[] = version.levels + const levels: G.Level[] = version.data.levels const level: G.Level | undefined = levels.find((l: G.Level) => l.id === context.position.levelId) @@ -33,22 +33,10 @@ export const currentLevel = (context: MachineContext): G.Level => createSelector return level })(context) -export const currentStage = (context: MachineContext): G.Stage => createSelector( - currentLevel, - (level: G.Level): G.Stage => { - const stages: G.Stage[] = level.stages - const stage: G.Stage | undefined = stages.find((s: G.Stage) => s.id === context.position.stageId) - if (!stage) { - throw new Error('No Stage found') - } - return stage - } -)(context) - export const currentStep = (context: MachineContext): G.Step => createSelector( - currentStage, - (stage: G.Stage): G.Step => { - const steps: G.Step[] = stage.steps + currentLevel, + (level: G.Level): G.Step => { + const steps: G.Step[] = level.steps const step: G.Step | undefined = steps.find((s: G.Step) => s.id === context.position.stepId) if (!step) { throw new Error('No Step found') diff --git a/web-app/src/services/state/actions/api.ts b/web-app/src/services/state/actions/api.ts index f5707c9a..5e522645 100644 --- a/web-app/src/services/state/actions/api.ts +++ b/web-app/src/services/state/actions/api.ts @@ -1,25 +1,39 @@ import * as CR from 'typings' +import * as G from 'typings/graphql' import client from '../../apollo' import authenticateMutation from '../../apollo/mutations/authenticate' import {setAuthToken} from '../../apollo/auth' import channel from '../../../services/channel' +interface AuthenticateData { + editorLogin: { + token: string + user: G.User + } +} + +interface AuthenticateVariables { + machineId: string + sessionId: string + editor: 'VSCODE' +} + export default { authenticate: (async (context: CR.MachineContext): Promise => { - const result = await client.mutate({ + const result = await client.mutate({ mutation: authenticateMutation, variables: { machineId: context.env.machineId, sessionId: context.env.sessionId, editor: 'VSCODE', } - }) - + }).catch(console.error) if (!result || !result.data) { // TODO: handle failed authentication console.error('ERROR: Authentication failed') + return } const {token} = result.data.editorLogin // add token to headers diff --git a/web-app/src/services/state/actions/context.ts b/web-app/src/services/state/actions/context.ts index 7ac2857f..cb32bce7 100644 --- a/web-app/src/services/state/actions/context.ts +++ b/web-app/src/services/state/actions/context.ts @@ -28,7 +28,7 @@ export default { return event.payload.tutorial }, progress: (): CR.Progress => { - return {levels: {}, stages: {}, steps: {}, complete: false} + return {levels: {}, steps: {}, complete: false} } }), initTutorial: assign({ @@ -53,8 +53,8 @@ export default { const {position} = context // merge in the updated position // sent with the test to ensure consistency - const stage: G.Stage = selectors.currentStage(context) - const steps: G.Step[] = stage.steps + const level: G.Level = selectors.currentLevel(context) + const steps: G.Step[] = level.steps // final step but not completed if (steps[steps.length - 1].id === position.stepId) { @@ -74,70 +74,49 @@ export default { }, }), // @ts-ignore - updateStagePosition: assign({ - position: (context: CR.MachineContext): CR.Position => { - const {position} = context - - const level: G.Level = selectors.currentLevel(context) - const stages: G.Stage[] = level.stages - - const stageIndex = stages.findIndex((s: G.Stage) => s.id === position.stageId) - const stage: G.Stage = stages[stageIndex + 1] - - const nextPosition: CR.Position = { - ...position, - stageId: stage.id, - stepId: stage.steps[0].id, - } - - return nextPosition - }, - }), - // @ts-ignore updateLevelPosition: assign({ position: (context: CR.MachineContext): CR.Position => { const {position} = context const version = selectors.currentVersion(context) // merge in the updated position // sent with the test to ensure consistency - const levels: G.Level[] = version.levels + const levels: G.Level[] = version.data.levels const levelIndex = levels.findIndex((l: G.Level) => l.id === position.levelId) const level: G.Level = levels[levelIndex + 1] const nextPosition: CR.Position = { levelId: level.id, - stageId: level.stages[0].id, - stepId: level.stages[0].steps[0].id, + stepId: level.steps[0].id, } return nextPosition }, }), // @ts-ignore - updateStepProgress: assign({ + updateLevelProgress: assign({ progress: (context: CR.MachineContext, event: CR.MachineEvent): CR.Progress => { // update progress by tracking completed - const currentProgress: CR.Progress = context.progress + const {progress, position} = context - const {stepId} = event.payload + const levelId: string = position.levelId - currentProgress.steps[stepId] = true + progress.levels[levelId] = true - return currentProgress + return progress }, }), // @ts-ignore - updateStageProgress: assign({ + updateStepProgress: assign({ progress: (context: CR.MachineContext, event: CR.MachineEvent): CR.Progress => { // update progress by tracking completed - const {progress, position} = context + const currentProgress: CR.Progress = context.progress - const stageId: string = position.stageId + const {stepId} = event.payload - progress.stages[stageId] = true + currentProgress.steps[stepId] = true - return progress + return currentProgress }, }), // @ts-ignore @@ -152,9 +131,8 @@ export default { const version = selectors.currentVersion(context) const level = selectors.currentLevel(context) - const stage = selectors.currentStage(context) - const steps: G.Step[] = stage.steps + const steps: G.Step[] = level.steps const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId) const stepComplete = progress.steps[position.stepId] @@ -168,27 +146,13 @@ export default { return {type: 'NEXT_STEP', payload: {position: nextPosition}} } - // has next stage? - - const {stages} = level - const stageIndex = stages.findIndex((s: G.Stage) => s.id === position.stageId) - const finalStage = (stageIndex > -1 && stageIndex === stages.length - 1) - const hasNextStage = (!finalStage) + // has next level? - // NEXT STAGE - if (hasNextStage) { - const nextStage = stages[stageIndex + 1] - const nextPosition = { - levelId: position.levelId, - stageId: nextStage.id, - stepId: nextStage.steps[0].id, - } - return {type: 'NEXT_STAGE', payload: {position: nextPosition}} + if (!context.tutorial) { + throw new Error('Tutorial not found') } - // has next level? - - const {levels} = version + const levels = context.tutorial.version.data.levels || [] const levelIndex = levels.findIndex((l: G.Level) => l.id === position.levelId) const finalLevel = (levelIndex > -1 && levelIndex === levels.length - 1) const hasNextLevel = (!finalLevel) @@ -196,11 +160,9 @@ export default { // NEXT LEVEL if (hasNextLevel) { const nextLevel = levels[levelIndex + 1] - const nextStage = nextLevel.stages[0] const nextPosition = { levelId: nextLevel.id, - stageId: nextStage.id, - stepId: nextStage.steps[0].id, + stepId: nextLevel.steps[0].id, } return {type: 'NEXT_LEVEL', payload: {position: nextPosition}} } @@ -211,9 +173,9 @@ export default { stepNext: send((context: CR.MachineContext): CR.Action => { const {position, progress} = context - const stage: G.Stage = selectors.currentStage(context) + const level: G.Level = selectors.currentLevel(context) - const {steps} = stage + const {steps} = level // TODO: verify not -1 const stepIndex = steps.findIndex((s: G.Step) => s.id === position.stepId) const finalStep = stepIndex === steps.length - 1 @@ -231,7 +193,7 @@ export default { } } else { return { - type: 'STAGE_COMPLETE' + type: 'LEVEL_COMPLETE' } } }), diff --git a/web-app/src/services/state/actions/editor.ts b/web-app/src/services/state/actions/editor.ts index 8b5b8d0b..813d784e 100644 --- a/web-app/src/services/state/actions/editor.ts +++ b/web-app/src/services/state/actions/editor.ts @@ -5,6 +5,15 @@ import channel from '../../channel' import client from '../../apollo' import tutorialQuery from '../../apollo/queries/tutorial' +interface TutorialData { + tutorial: G.Tutorial +} + +interface TutorialDataVariables { + tutorialId: string + version: string +} + export default { loadEnv() { channel.editorSend({ @@ -34,7 +43,7 @@ export default { throw new Error('Tutorial not available to load') } - client.query({ + client.query({ query: tutorialQuery, variables: { tutorialId: context.tutorial.id, @@ -69,16 +78,6 @@ export default { }) } }, - loadStage(context: CR.MachineContext): void { - const stage: G.Stage = selectors.currentStage(context) - if (stage.setup) { - // load step actions - channel.editorSend({ - type: 'SETUP_ACTIONS', - payload: stage.setup, - }) - } - }, loadStep(context: CR.MachineContext): void { const step: G.Step = selectors.currentStep(context) if (step.setup) { diff --git a/web-app/src/services/state/machine.ts b/web-app/src/services/state/machine.ts index 232f61b6..db5ab0f0 100644 --- a/web-app/src/services/state/machine.ts +++ b/web-app/src/services/state/machine.ts @@ -14,10 +14,9 @@ export const machine = Machine