diff --git a/src/editor/views/createWebview.ts b/src/editor/ReactWebView.ts similarity index 56% rename from src/editor/views/createWebview.ts rename to src/editor/ReactWebView.ts index 321403a0..de113d6b 100644 --- a/src/editor/views/createWebview.ts +++ b/src/editor/ReactWebView.ts @@ -2,116 +2,118 @@ import * as vscode from 'vscode' import * as CR from 'typings' import * as path from 'path' -import getNonce from './utils/nonce' -import onReceive from './onReceive' - /** * Manages React webview panels */ -class ReactPanel { - /** - * Track the currently panel. Only allow a single panel to exist at a time. - */ - public static currentPanel: ReactPanel | undefined = undefined +class ReactWebView { + // @ts-ignore + private panel: vscode.WebviewPanel + private extensionPath: string + private disposables: vscode.Disposable[] = [] + private onReceive: any // TODO: properly type + + public constructor(extensionPath: string) { + this.extensionPath = extensionPath + + // Create and show a new webview panel + this.panel = this.createWebviewPanel(vscode.ViewColumn.One) - private readonly _panel: vscode.WebviewPanel - private readonly _extensionPath: string - private _disposables: vscode.Disposable[] = [] + // Set the webview's initial html content + this.panel.webview.html = this.getHtmlForWebview() - public static async createOrShow(extensionPath: string): Promise { - // const hasActiveEditor = vscode.window.activeTextEditor + // Listen for when the panel is disposed + // This happens when the user closes the panel or when the panel is closed programatically + this.panel.onDidDispose(() => this.dispose(), null, this.disposables) - // if (!hasActiveEditor) { - // throw new Error('Should have an open file on launch') - // } - const column = vscode.ViewColumn.One + // Handle messages from the webview + const onReceive = (action: string | CR.Action) => vscode.commands.executeCommand('coderoad.receive_action', action) + this.panel.webview.onDidReceiveMessage(onReceive, null, this.disposables) + console.log('webview loaded') + } + public async createOrShow(column: number): Promise { // If we already have a panel, show it. // Otherwise, create a new panel. - if (ReactPanel.currentPanel) { - console.log('--- HAS CURRENT PANEL ---') - ReactPanel.currentPanel._panel.reveal(column) + if (this.panel && this.panel.webview) { + console.log('reveal') + this.panel.reveal(column) } else { - ReactPanel.currentPanel = new ReactPanel(extensionPath, column) + console.log('make new panel') + this.panel = this.createWebviewPanel(column) + } } - private constructor(extensionPath: string, column: vscode.ViewColumn) { - this._extensionPath = extensionPath - + private createWebviewPanel(column: number): vscode.WebviewPanel { const viewType = 'CodeRoad' const title = 'CodeRoad' const config = { // Enable javascript in the webview enableScripts: true, - // And restric the webview to only loading content from our extension's `media` directory. - localResourceRoots: [vscode.Uri.file(path.join(this._extensionPath, 'build'))], - + localResourceRoots: [vscode.Uri.file(path.join(this.extensionPath, 'build'))], // prevents destroying the window when it is in the background retainContextWhenHidden: true, } + return vscode.window.createWebviewPanel(viewType, title, column, config) + } - // Create and show a new webview panel - this._panel = vscode.window.createWebviewPanel(viewType, title, column, config) - - // Set the webview's initial html content - this._panel.webview.html = this._getHtmlForWebview() - - // Listen for when the panel is disposed - // This happens when the user closes the panel or when the panel is closed programatically - this._panel.onDidDispose(() => this.dispose(), null, this._disposables) - - // Handle messages from the webview - this._panel.webview.onDidReceiveMessage(onReceive, null, this._disposables) + private getNonce(): string { + let text = '' + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return text } public async postMessage(action: CR.Action): Promise { + console.log('webview postMessage') + console.log(action) // Send a message to the webview webview. // You can send any JSON serializable data. - const success = await this._panel.webview.postMessage(action) + const success = await this.panel.webview.postMessage(action) if (!success) { throw new Error(`Message post failure: ${JSON.stringify(action)}`) } + console.log('postMessage sent') } public dispose(): void { - ReactPanel.currentPanel = undefined - // Clean up our resources - this._panel.dispose() + this.panel.dispose() - while (this._disposables.length) { - const x = this._disposables.pop() + while (this.disposables.length) { + const x = this.disposables.pop() if (x) { x.dispose() } } } - private _getHtmlForWebview(): string { + private getHtmlForWebview(): string { // eslint-disable-next-line - const manifest = require(path.join(this._extensionPath, 'build', 'asset-manifest.json')) + const manifest = require(path.join(this.extensionPath, 'build', 'asset-manifest.json')) const mainScript = manifest.files['main.js'] // grab first chunk const chunk = Object.keys(manifest.files).filter(f => f.match(/^static\/js\/.+\.js$/))[0] const chunkScript = manifest.files[chunk] const mainStyle = manifest.files['main.css'] - const scriptPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'build', mainScript)) + const scriptPathOnDisk = vscode.Uri.file(path.join(this.extensionPath, 'build', mainScript)) const scriptUri = scriptPathOnDisk.with({ scheme: 'vscode-resource' }) - const chunkPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'build', chunkScript)) + const chunkPathOnDisk = vscode.Uri.file(path.join(this.extensionPath, 'build', chunkScript)) const chunkUri = chunkPathOnDisk.with({ scheme: 'vscode-resource' }) - const stylePathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'build', mainStyle)) + const stylePathOnDisk = vscode.Uri.file(path.join(this.extensionPath, 'build', mainStyle)) const styleUri = stylePathOnDisk.with({ scheme: 'vscode-resource' }) // Use a nonce to whitelist which scripts can be run - const nonce = getNonce() - const nonce2 = getNonce() - const nonce3 = getNonce() + const nonce = this.getNonce() + const nonce2 = this.getNonce() + const nonce3 = this.getNonce() - const output = ` + return ` @@ -121,7 +123,7 @@ class ReactPanel { - + @@ -133,9 +135,7 @@ class ReactPanel { ` - console.log(output) - return output } } -export default ReactPanel +export default ReactWebView diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index 2d000bb9..9ea0b92e 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -1,36 +1,84 @@ import * as vscode from 'vscode' -import start from './start' -import ReactPanel from '../views/createWebview' - -// import runTest from './runTest' -// import loadSolution from './loadSolution' -// import quit from './quit' +import { join } from 'path' +import { setStorage } from '../storage' +import ReactWebView from '../ReactWebView' +import * as CR from 'typings' const COMMANDS = { - // TUTORIAL_SETUP: 'coderoad.tutorial_setup', - START: 'coderoad.start', - OPEN_WEBVIEW: 'coderoad.open_webview' - // RUN_TEST: 'coderoad.test_run', - // LOAD_SOLUTION: 'coderoad.solution_load', - // QUIT: 'coderoad.quit', + START: 'coderoad.start', + NEW_OR_CONTINUE: 'coderoad.new_or_continue', + OPEN_WEBVIEW: 'coderoad.open_webview', + SEND_STATE: 'coderoad.send_state', + SEND_DATA: 'coderoad.send_data', + RECEIVE_ACTION: 'coderoad.receive_action', + OPEN_FILE: 'coderoad.open_file', + RUN_TEST: 'coderoad.test_run', +} + +interface CreateCommandProps { + context: vscode.ExtensionContext, + machine: CR.StateMachine, + storage: any, + git: any } +// React panel webview +let webview: any; -export default (context: vscode.ExtensionContext): void => { - const commands = { - [COMMANDS.START]: async function startCommand(): Promise { - return start(context) +export const createCommands = ({ context, machine, storage, git }: CreateCommandProps) => ({ + // initialize + [COMMANDS.START]: () => { + // set local storage workspace + setStorage(context.workspaceState) + + // activate machine + webview = new ReactWebView(context.extensionPath) + console.log('webview', webview.panel.webview.postMessage) + machine.activate() }, - [COMMANDS.OPEN_WEBVIEW]: () => { - ReactPanel.createOrShow(context.extensionPath); + [COMMANDS.NEW_OR_CONTINUE]: async () => { + // verify that the user has a tutorial & progress + // verify git is setup with a coderoad remote + const [tutorial, progress, hasGit, hasGitRemote] = await Promise.all([ + storage.getTutorial(), + storage.getProgress(), + git.gitVersion(), + git.gitCheckRemoteExists(), + ]) + const canContinue = !!(tutorial && progress && hasGit && hasGitRemote) + console.log('canContinue', canContinue) + // if a tutorial exists, 'CONTINUE' + // otherwise start from 'NEW' + machine.send(canContinue ? 'CONTINUE' : 'NEW') }, - // [COMMANDS.RUN_TEST]: runTest, - // [COMMANDS.LOAD_SOLUTION]: loadSolution, - // [COMMANDS.QUIT]: () => quit(context.subscriptions), - } - - for (const cmd in commands) { - const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) - context.subscriptions.push(command) - } -} + // open React webview + [COMMANDS.OPEN_WEBVIEW]: (column: number = vscode.ViewColumn.One) => { + webview.createOrShow(column); + }, + // open a file + [COMMANDS.OPEN_FILE]: async (relativeFilePath: string) => { + console.log(`OPEN_FILE ${JSON.stringify(relativeFilePath)}`) + try { + const workspaceRoot = vscode.workspace.rootPath + if (!workspaceRoot) { + throw new Error('No workspace root path') + } + const absoluteFilePath = join(workspaceRoot, relativeFilePath) + const doc = await vscode.workspace.openTextDocument(absoluteFilePath) + await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) + } catch (error) { + console.log(`Failed to open file ${relativeFilePath}`, error) + } + }, + // send messages to webview + [COMMANDS.SEND_STATE]: (payload: { data: any, state: any }) => { + webview.postMessage({ type: 'SET_STATE', payload }) + }, + [COMMANDS.SEND_DATA]: (payload: { data: any }) => { + webview.postMessage({ type: 'SET_DATA', payload }) + }, + [COMMANDS.RECEIVE_ACTION]: (action: string | CR.Action) => { + console.log('onReceiveAction', action) + machine.onReceive(action) + } +}) \ No newline at end of file diff --git a/src/editor/commands/quit.ts b/src/editor/commands/quit.ts deleted file mode 100644 index 06c2254b..00000000 --- a/src/editor/commands/quit.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as vscode from 'vscode' - -export default () => { - // TODO: quit -} diff --git a/src/editor/commands/start.ts b/src/editor/commands/start.ts deleted file mode 100644 index 84c4a77a..00000000 --- a/src/editor/commands/start.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as vscode from 'vscode' -import { setWorkspaceRoot } from '../../services/node' -import { setStorage } from '../../editor/storage' -import { activate as activateMachine, default as machine } from '../../state' -import * as storage from '../../services/storage' -import * as git from '../../services/git' -import * as CR from 'typings' - -let initialTutorial: CR.Tutorial | undefined -let initialProgress: CR.Progress = { - levels: {}, - stages: {}, - steps: {}, - complete: false, -} - -export default async function start(context: vscode.ExtensionContext): Promise { - console.log('TUTORIAL_START') - - // setup connection to workspace - await setWorkspaceRoot() - // set workspace context path - await setStorage(context.workspaceState) - - // initialize state machine - activateMachine() - - console.log('ACTION: start') - - // verify that the user has a tutorial & progress - // verify git is setup with a coderoad remote - const [tutorial, progress, hasGit, hasGitRemote] = await Promise.all([ - storage.getTutorial(), - storage.getProgress(), - git.gitVersion(), - git.gitCheckRemoteExists(), - ]) - initialTutorial = tutorial - initialProgress = progress - const canContinue = !!(tutorial && progress && hasGit && hasGitRemote) - console.log('canContinue', canContinue) - // if a tutorial exists, "CONTINUE" - // otherwise start from "NEW" - machine.send(canContinue ? 'CONTINUE' : 'NEW') -} \ No newline at end of file diff --git a/src/editor/index.ts b/src/editor/index.ts new file mode 100644 index 00000000..26504478 --- /dev/null +++ b/src/editor/index.ts @@ -0,0 +1,67 @@ +import * as vscode from 'vscode' +import * as CR from 'typings' +import { createCommands } from './commands' +import * as storage from '../services/storage' +import * as git from '../services/git' + +interface Props { + machine: CR.StateMachine, + setWorkspaceRoot(rootPath: string): void +} + +class Editor { + // extension context set on activation + // @ts-ignore + private context: vscode.ExtensionContext + private machine: CR.StateMachine + + constructor({ machine, setWorkspaceRoot }: Props) { + this.machine = machine + + // set workspace root for node executions + const { workspace } = vscode + const { rootPath } = workspace + if (!rootPath) { + throw new Error('Requires a workspace. Please open a folder') + } + setWorkspaceRoot(rootPath) + } + + private activateCommands = (): void => { + const commands = createCommands({ + context: this.context, + machine: this.machine, + storage, + git, + }) + for (const cmd in commands) { + const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) + this.context.subscriptions.push(command) + } + } + public activate = (context: vscode.ExtensionContext): void => { + console.log('ACTIVATE!') + this.context = context + // commands + this.activateCommands() + + // setup tasks or views here + + } + public deactivate = (): void => { + console.log('DEACTIVATE!') + // cleanup subscriptions/tasks + for (const disposable of this.context.subscriptions) { + disposable.dispose() + } + // shut down state machine + this.machine.deactivate() + } + + // execute vscode command + public dispatch = (type: string, payload: any) => { + vscode.commands.executeCommand(type, payload) + } +} + +export default Editor \ No newline at end of file diff --git a/src/editor/init.ts b/src/editor/init.ts deleted file mode 100644 index dbc9f64a..00000000 --- a/src/editor/init.ts +++ /dev/null @@ -1,33 +0,0 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import * as vscode from 'vscode' - -import { deactivate as deactivateMachine } from '../state' -import createCommands from './commands' -import createViews from './views' - -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { - console.log('ACTIVATE!') - - // commands - createCommands(context) - - // views - createViews(context) - - // tasks - // add tasks here -} - -// this method is called when your extension is deactivated -export function deactivate(context: vscode.ExtensionContext): void { - // cleanup subscriptions/tasks - console.log('deactivate context', context) - for (const disposable of context.subscriptions) { - disposable.dispose() - } - // shut down state machine - deactivateMachine() -} diff --git a/src/editor/views/index.ts b/src/editor/views/index.ts deleted file mode 100644 index 60a7d871..00000000 --- a/src/editor/views/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as vscode from 'vscode' - -const createViews = (context: vscode.ExtensionContext) => { - // TODO: create views -} - -export default createViews diff --git a/src/editor/views/onReceive.ts b/src/editor/views/onReceive.ts deleted file mode 100644 index ccd69eea..00000000 --- a/src/editor/views/onReceive.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as CR from 'typings' - -export default async (action: CR.Action): Promise => { - console.log('action', action) - // TODO: call state machine -} diff --git a/src/editor/views/utils/nonce.ts b/src/editor/views/utils/nonce.ts deleted file mode 100644 index 313819e1..00000000 --- a/src/editor/views/utils/nonce.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default function getNonce(): string { - let text = '' - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)) - } - return text -} diff --git a/src/extension.ts b/src/extension.ts index 40646445..ad32b1d8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,2 +1,16 @@ -export { activate, deactivate } from './editor/init' +import { setWorkspaceRoot } from './services/node' +import StateMachine from './state' +import Editor from './editor' + +// state machine that governs application logic +export const machine = new StateMachine() + +// vscode editor +export const editor = new Editor({ + machine, + setWorkspaceRoot, +}) + +export const activate = editor.activate +export const deactivate = editor.deactivate diff --git a/src/services/git/index.ts b/src/services/git/index.ts index 347ef142..4cd7e34a 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -1,5 +1,6 @@ +import * as vscode from 'vscode' import * as CR from 'typings' -import { exec, exists, openFile } from '../node' +import { exec, exists } from '../node' const gitOrigin = 'coderoad' @@ -40,7 +41,7 @@ export async function gitLoadCommits(actions: CR.TutorialAction): Promise if (files) { for (const filePath of files) { - openFile(filePath) + vscode.commands.executeCommand('coderoad.open_file', filePath) } } } diff --git a/src/services/node/index.ts b/src/services/node/index.ts index 0a5234d9..04aa976e 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -1,6 +1,4 @@ -import { workspace } from 'vscode' import * as fs from 'fs' -import * as vscode from 'vscode' import { join } from 'path' import { exec as cpExec } from 'child_process' import { promisify } from 'util' @@ -11,11 +9,7 @@ let workspaceRoot: string // set workspace root // other function will use this to target the correct cwd -export async function setWorkspaceRoot(): Promise { - const { rootPath } = workspace - if (!rootPath) { - throw new Error('Requires a workspace. Please open a folder') - } +export function setWorkspaceRoot(rootPath: string): void { workspaceRoot = rootPath } @@ -28,17 +22,6 @@ export const exec = (cmd: string): Promise<{ stdout: string; stderr: string }> = // collect all paths together export const exists = (...paths: string[]): boolean => fs.existsSync(join(workspaceRoot, ...paths)) -export const openFile = async (relativeFilePath: string): Promise => { - try { - const absoluteFilePath = join(workspaceRoot, relativeFilePath) - const doc = await vscode.workspace.openTextDocument(absoluteFilePath) - await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) - } catch (error) { - console.log(`Failed to open file ${relativeFilePath}`, error) - } -} - - // export async function clear(): Promise { // // remove all files including ignored // // NOTE: Linux only diff --git a/src/services/testResult.ts b/src/services/testResult.ts index b2e3fb0f..056646d1 100644 --- a/src/services/testResult.ts +++ b/src/services/testResult.ts @@ -2,7 +2,6 @@ import * as CR from 'typings' import * as vscode from 'vscode' import * as storage from './storage' - export async function onSuccess(position: CR.Position) { console.log('onSuccess', position) vscode.window.showInformationMessage('SUCCESS') diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 3c435d38..c2abb20b 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -48,5 +48,8 @@ export default { createWebview() { console.log('execute coderoad.open_webview') vscode.commands.executeCommand('coderoad.open_webview') + }, + newOrContinue() { + vscode.commands.executeCommand('coderoad.new_or_continue') } } \ No newline at end of file diff --git a/src/state/index.ts b/src/state/index.ts index 376b66ef..6a7cb3bb 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,31 +1,49 @@ -import { interpret } from 'xstate' +import { interpret, Interpreter } from 'xstate' +import * as CR from 'typings' import machine from './machine' +import * as vscode from 'vscode' -const machineOptions = { - logger: console.log, - devTools: true, - deferEvents: true, - execute: true -} // machine interpreter // https://xstate.js.org/docs/guides/interpretation.html -const service = interpret(machine, machineOptions) - // logging - .onTransition(state => { - console.log('state', state) - if (state.changed) { - console.log('transition') - console.log(state.value) - } - }) - -export function activate() { - // initialize - service.start() -} -export function deactivate() { - service.stop() +class StateMachine { + private machineOptions = { + logger: console.log, + devTools: true, + deferEvents: true, + execute: true + } + private service: Interpreter + constructor() { + this.service = interpret(machine, this.machineOptions) + // logging + .onTransition(state => { + console.log('onTransition', state) + if (state.changed) { + console.log('next state') + console.log(state.value) + vscode.commands.executeCommand('coderoad.send_state', { state: state.value, data: state.context }) + } else { + vscode.commands.executeCommand('coderoad.send_data', { data: state.context }) + } + }) + } + activate() { + // initialize + this.service.start() + } + deactivate() { + this.service.stop() + } + send(action: string | CR.Action) { + console.log('machine.send') + console.log(action) + this.service.send(action) + } + onReceive(action: string | CR.Action) { + console.log(action) + this.service.send(action) + } } -export default service +export default StateMachine diff --git a/src/state/machine.ts b/src/state/machine.ts index 49f6008b..273a915e 100644 --- a/src/state/machine.ts +++ b/src/state/machine.ts @@ -16,10 +16,16 @@ export const machine = Machine< initial: 'SelectTutorial', states: { SelectTutorial: { + onEntry: ['createWebview'], initial: 'Initial', states: { Initial: { - onEntry: ['createWebview'], + after: { + 1000: 'Startup' + } + }, + Startup: { + onEntry: ['newOrContinue'], on: { CONTINUE: 'ContinueTutorial', NEW: 'NewTutorial', diff --git a/src/state/message.ts b/src/state/message.ts new file mode 100644 index 00000000..571b2100 --- /dev/null +++ b/src/state/message.ts @@ -0,0 +1,13 @@ +// import panel from '../views/Panel' +import * as CR from 'typings' + +export const onSend = (action: CR.Action) => { + // if (!panel || !panel.currentPanel) { + // throw new Error('No valid panel available') + // } + // panel.currentPanel.postMessage(action) +} + +export const onReceive = (action: CR.Action) => { + console.log(`RECEIVED: ${JSON.stringify(action)}`) +} diff --git a/tsconfig.json b/tsconfig.json index 8cd5646e..cef79727 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,7 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "paths": { - "typings": ["./typings/index.d.ts"], + "typings": ["../typings/index.d.ts"], }, }, "exclude": [ diff --git a/src/typings/context.d.ts b/typings/context.d.ts similarity index 100% rename from src/typings/context.d.ts rename to typings/context.d.ts diff --git a/src/typings/index.d.ts b/typings/index.d.ts similarity index 93% rename from src/typings/index.d.ts rename to typings/index.d.ts index 0c16d246..044be525 100644 --- a/src/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,3 +1,5 @@ +import { send } from "xstate"; + export interface TutorialLevel { stageList: string[] content: { @@ -132,6 +134,7 @@ export interface MachineStateSchema { SelectTutorial: { states: { Initial: {} + Startup: {} NewTutorial: { states: { SelectTutorial: {} @@ -159,4 +162,11 @@ export interface MachineStateSchema { } } } -} \ No newline at end of file +} + +export interface StateMachine { + activate(): void + deactivate(): void + send(action: string | Action): void + onReceive(action: string | Action): void +} diff --git a/web-app/package.json b/web-app/package.json index f2e82aec..4ac6c449 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -19,7 +19,6 @@ "build": "react-scripts build", "postbuild": "cp -R ./build/ ../build/ && cp public/webpackBuild.js ../build/webpackBuild.js", "test": "react-scripts test", - "eject": "react-scripts eject", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook" }, diff --git a/web-app/src/App.css b/web-app/src/App.css deleted file mode 100644 index b41d297c..00000000 --- a/web-app/src/App.css +++ /dev/null @@ -1,33 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - animation: App-logo-spin infinite 20s linear; - height: 40vmin; - pointer-events: none; -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/web-app/src/App.test.tsx b/web-app/src/App.test.tsx deleted file mode 100644 index a754b201..00000000 --- a/web-app/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/web-app/src/App.tsx b/web-app/src/App.tsx deleted file mode 100644 index 3b8aacc4..00000000 --- a/web-app/src/App.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import './App.css'; - -const App: React.FC = () => { - return ( -
-
- Hello World -
-
- ); -} - -export default App; diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx new file mode 100644 index 00000000..ca1d1512 --- /dev/null +++ b/web-app/src/Routes.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import * as CR from 'typings' +import { send } from './utils/vscode' + +import Cond from './components/Cond' +import NewPage from './containers/New' +import ContinuePage from './containers/Continue' + + +interface ReceivedEvent { + data: CR.Action +} + +const Routes = () => { + const [state, setState] = React.useState({ SelectTutorial: 'Initial' }) + const [data, setData] = React.useState({}) + + + const handleEvent = (event: ReceivedEvent): void => { + const message = event.data + console.log('RECEIVED') + console.log(message) + // messages from core + if (message.type === 'SET_STATE') { + setState(message.payload.state) + setData(message.payload.data) + } else if (message.type === 'SET_DATA') { + setData(message.payload.data) + } + } + + // event bus listener + React.useEffect(() => { + const listener = 'message' + window.addEventListener(listener, handleEvent) + return () => { + window.removeEventListener(listener, handleEvent) + } + }) + + // TODO: refactor cond to user and accept first route as if/else if + return ( +
+
state: {JSON.stringify(state)}
+

data:{JSON.stringify(data)}

+ + send('TUTORIAL_START')} /> + + + console.log('continue!')} tutorials={[]} /> + +
+ ) +} + +export default Routes \ No newline at end of file diff --git a/web-app/src/components/Cond/index.tsx b/web-app/src/components/Cond/index.tsx new file mode 100644 index 00000000..9eed24ba --- /dev/null +++ b/web-app/src/components/Cond/index.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import { stateMatch } from './utils/state' + +interface Props { + state: any + path: string + children: React.ReactElement +} + +const Cond = (props: Props) => { + if (!stateMatch(props.state, props.path)) { + return null + } + return props.children +} + +export default Cond diff --git a/web-app/src/components/Cond/utils/state.ts b/web-app/src/components/Cond/utils/state.ts new file mode 100644 index 00000000..5706cc57 --- /dev/null +++ b/web-app/src/components/Cond/utils/state.ts @@ -0,0 +1,12 @@ +export function stateMatch(state: any, statePath: string) { + let current = state + let paths = statePath.split('.') + try { + for (const p of paths) { + current = current[p] + } + } catch (error) { + return false + } + return current !== undefined +} \ No newline at end of file diff --git a/web-app/src/components/Level/index.tsx b/web-app/src/components/Level/index.tsx index 403c9f3d..a48d8ee5 100644 --- a/web-app/src/components/Level/index.tsx +++ b/web-app/src/components/Level/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { Button, Card } from '@alifd/next' -import CR from '../../../../src/typings' +import CR from 'typings' const styles = { card: { diff --git a/web-app/src/components/Stage/index.tsx b/web-app/src/components/Stage/index.tsx index ac45304c..16b7a104 100644 --- a/web-app/src/components/Stage/index.tsx +++ b/web-app/src/components/Stage/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { Button, Card } from '@alifd/next' -import CR from '../../../../src/typings' +import CR from 'typings' import Step from '../Step' diff --git a/web-app/src/components/Step/index.tsx b/web-app/src/components/Step/index.tsx index 1e764e17..372c77d4 100644 --- a/web-app/src/components/Step/index.tsx +++ b/web-app/src/components/Step/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { Checkbox } from '@alifd/next' // import CC from '../../typings/client' -import CR from '../../../../src/typings' +import CR from 'typings' const styles = { card: { diff --git a/web-app/src/components/Summary/index.tsx b/web-app/src/components/Summary/index.tsx index 00509b27..e00ac952 100644 --- a/web-app/src/components/Summary/index.tsx +++ b/web-app/src/components/Summary/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { Button, Card } from '@alifd/next' -import CR from '../../../../src/typings' +import CR from 'typings' const styles = { card: { diff --git a/web-app/src/components/Continue/ContinueItem.tsx b/web-app/src/containers/Continue/ContinueItem.tsx similarity index 100% rename from web-app/src/components/Continue/ContinueItem.tsx rename to web-app/src/containers/Continue/ContinueItem.tsx diff --git a/web-app/src/components/Continue/index.tsx b/web-app/src/containers/Continue/index.tsx similarity index 93% rename from web-app/src/components/Continue/index.tsx rename to web-app/src/containers/Continue/index.tsx index c99195df..ea65eaac 100644 --- a/web-app/src/components/Continue/index.tsx +++ b/web-app/src/containers/Continue/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import CR from '../../../../src/typings' +import CR from 'typings' import ContinueItem from './ContinueItem' diff --git a/web-app/src/components/New/index.tsx b/web-app/src/containers/New/index.tsx similarity index 100% rename from web-app/src/components/New/index.tsx rename to web-app/src/containers/New/index.tsx diff --git a/web-app/src/index.tsx b/web-app/src/index.tsx index b7e93595..ba003317 100644 --- a/web-app/src/index.tsx +++ b/web-app/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; +import Routes from './Routes'; -ReactDOM.render(, document.getElementById('root') as HTMLElement) +import './styles/index.css'; + +ReactDOM.render(, document.getElementById('root') as HTMLElement) diff --git a/web-app/src/logo.svg b/web-app/src/logo.svg deleted file mode 100644 index 6b60c104..00000000 --- a/web-app/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/web-app/src/index.css b/web-app/src/styles/index.css similarity index 100% rename from web-app/src/index.css rename to web-app/src/styles/index.css diff --git a/web-app/src/utils/vscode.ts b/web-app/src/utils/vscode.ts new file mode 100644 index 00000000..c53a5ebf --- /dev/null +++ b/web-app/src/utils/vscode.ts @@ -0,0 +1,9 @@ +import { Action } from 'typings' + +declare var acquireVsCodeApi: any + +const vscode = acquireVsCodeApi() + +export function send(event: string | Action) { + return vscode.postMessage(event) +} diff --git a/web-app/stories/Continue.stories.tsx b/web-app/stories/Continue.stories.tsx index 6845b72c..1f4df9d0 100644 --- a/web-app/stories/Continue.stories.tsx +++ b/web-app/stories/Continue.stories.tsx @@ -3,7 +3,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' -import Continue from '../src/components/Continue' +import Continue from '../src/containers/Continue' import demo from './data/basic' storiesOf('Continue', module).add('Page', () => ) diff --git a/web-app/stories/New.stories.tsx b/web-app/stories/New.stories.tsx index 643311d0..5ade6701 100644 --- a/web-app/stories/New.stories.tsx +++ b/web-app/stories/New.stories.tsx @@ -3,6 +3,6 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' -import New from '../src/components/New' +import New from '../src/containers/New' storiesOf('New', module).add('Page', () => ) diff --git a/web-app/tsconfig.json b/web-app/tsconfig.json index e68470ec..04664a04 100644 --- a/web-app/tsconfig.json +++ b/web-app/tsconfig.json @@ -1,8 +1,10 @@ { + "extends": "./tsconfig.paths.json", "compilerOptions": { "target": "es5", "lib": [ "dom", + "dom.iterable", "esnext" ], "allowJs": true, @@ -16,21 +18,9 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "preserve", - "sourceMap": true, - "rootDirs": ["src", "stories"], - "baseUrl": "src", - "outDir": "build" + "jsx": "preserve" }, "include": [ - "src", - "../src/typings" - ], - "exclude": [ - "node_modules", - "build", - "scripts", - "jest", - "public" + "src" ] } diff --git a/web-app/tsconfig.paths.json b/web-app/tsconfig.paths.json new file mode 100644 index 00000000..89a64aa3 --- /dev/null +++ b/web-app/tsconfig.paths.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "rootDirs": [ + "src", + "stories" + ], + "paths": { + "typings": [ + "../../typings/index.d.ts" + ], + } + }, + "exclude": [ + "node_modules", + "build", + "scripts", + "jest", + "public" + ] +} \ No newline at end of file