Merge pull request #1 from Bouncey/feat/monacoEditor

Feat/monaco editor
This commit is contained in:
Stuart Taylor
2018-04-08 21:04:30 +01:00
committed by Mrugesh Mohapatra
parent 939028df20
commit 1f5abb3dde
13 changed files with 192 additions and 159 deletions

View File

@ -1,4 +1,6 @@
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { dasherize } = require('./utils');
const { viewTypes } = require('./utils/challengeTypes');
const { blockNameify } = require('./utils/blockNameify');
@ -84,3 +86,27 @@ exports.createPages = ({ graphql, boundActionCreators }) => {
);
});
};
exports.modifyWebpackConfig = ({ config, stage, babelConfig }) => {
if (stage === 'build-javascript' || stage === 'develop') {
config.plugin('CopyWebpackPlugin', CopyWebpackPlugin, [
[
{
from: path.resolve(__dirname, './node_modules/monaco-editor/min/vs'),
to: 'vs'
}
]
]);
// remove the default 'js' loader so we can create our own
config.removeLoader('js');
// these modules are shipped with es6 code, we need to transform them due
// to the version of the uglifyjs plugin gatsby is using
config.loader('js', {
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)\/(?!ansi-styles|chalk)/,
loader: 'babel',
query: babelConfig
});
}
return config;
};

View File

@ -1,13 +1,14 @@
module.exports = {
moduleNameMapper: {
"\\.(jpg|jpeg|png|svg|woff|woff2)$": "<rootDir>/src/__mocks__/fileMock.js",
// Plain CSS - match css files that don't end with '.module.css' https://regex101.com/r/VzwrKH/4
"^(?!.*\\.module\\.css$).*\\.css$": "<rootDir>/src/__mocks__/styleMock.js",
'\\.(jpg|jpeg|png|svg|woff|woff2)$': '<rootDir>/src/__mocks__/fileMock.js',
// Plain CSS - match css files that don't end with
// '.module.css' https://regex101.com/r/VzwrKH/4
'^(?!.*\\.module\\.css$).*\\.css$': '<rootDir>/src/__mocks__/styleMock.js',
// CSS Modules - match files that end with 'module.css'
"\\.module\\.css$": "identity-obj-proxy" // CSS modules
'\\.module\\.css$': 'identity-obj-proxy'
},
testPathIgnorePatterns: ["/node_modules/", "<rootDir>/.cache/"],
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/.cache/'],
globals: {
__PATH_PREFIX__: ""
__PATH_PREFIX__: ''
}
};
};

View File

@ -10,7 +10,7 @@
"babel-standalone": "^6.26.0",
"brace": "^0.11.1",
"chai": "^4.1.2",
"codemirror": "^5.36.0",
"copy-webpack-plugin": "^4.5.1",
"debug": "^3.1.0",
"dotenv": "^5.0.1",
"enzyme": "^3.3.0",
@ -29,9 +29,9 @@
"mongodb": "^3.0.5",
"react": "16",
"react-bootstrap": "^0.32.1",
"react-codemirror2": "^4.2.1",
"react-dom": "16",
"react-helmet": "^5.2.0",
"react-monaco-editor": "^0.14.1",
"react-redux": "^5.0.7",
"react-reflex": "^2.2.1",
"react-router-redux": "^5.0.0-alpha.9",
@ -52,14 +52,14 @@
"build:frame-runner": "webpack --config ./webpack-frame-runner.js",
"build:loop-protect": "webpack --config ./webpack-loop-protect.js",
"develop": "yarn build:frame-runner && gatsby develop",
"format": "yarn format:gatsby && yarn format:src",
"format": "yarn format:gatsby && yarn format:src && yarn lint",
"format:gatsby": "prettier --write './gatsby*.js'",
"format:src": "prettier --write './src/**/*.js'",
"lint": "yarn lint:gatsby && yarn lint:src",
"lint:gatsby": "eslint ./gatsby*.js --fix",
"lint:src": "eslint ./src . --fix",
"pretty": "yarn format && yarn lint",
"test": "jest src",
"test": "yarn format && jest src",
"test:watch": "jest --watch src"
},
"jest": {

View File

@ -1,6 +1,6 @@
/* eslint-disable */
import React from 'react';
const mockComponent = name => props =>
React.createElement(name, props, props.children);
export default mockComponent('MockedLink');

View File

@ -13,7 +13,6 @@ const propTypes = {
};
class ShowMap extends PureComponent {
renderSuperBlocks(superBlocks) {
const { nodes } = this.props;
return superBlocks.map(superBlock => (

View File

@ -12,8 +12,6 @@ Enzyme.configure({ adapter: new Adapter() });
const renderer = new ShallowRenderer();
test('<Map /> snapshot', () => {
const component = renderer.render(
<Map nodes={mockNodes} />,
);
const component = renderer.render(<Map nodes={mockNodes} />);
expect(component).toMatchSnapshot('Map');
});

View File

@ -15,65 +15,53 @@ const renderer = new ShallowRenderer();
test('<Block /> not expanded snapshot', () => {
const toggleSpy = sinon.spy();
const componentToRender = (
<Block
blockDashedName='block-a'
challenges={
mockNodes.filter(
node => node.block === 'block-a'
)
}
isExpanded={false}
toggleBlock={toggleSpy}
/>
);
<Block
blockDashedName='block-a'
challenges={mockNodes.filter(node => node.block === 'block-a')}
isExpanded={false}
toggleBlock={toggleSpy}
/>
);
const component = renderer.render(componentToRender);
expect(component).toMatchSnapshot('block-not-expanded');
});
test('<Block expanded snapshot', () => {
const toggleSpy = sinon.spy();
const componentToRender = (
<Block
blockDashedName='block-a'
challenges={
mockNodes.filter(
node => node.block === 'block-a'
)
}
isExpanded={true}
toggleBlock={toggleSpy}
/>
);
<Block
blockDashedName='block-a'
challenges={mockNodes.filter(node => node.block === 'block-a')}
isExpanded={true}
toggleBlock={toggleSpy}
/>
);
const component = renderer.render(componentToRender);
expect(component).toMatchSnapshot('block-expanded');
});
test('<Block /> should handle toggle clicks correctly', () => {
const toggleSpy = sinon.spy();
const props = {
blockDashedName: 'block-a',
challenges: mockNodes.filter(
node => node.block === 'block-a'
),
challenges: mockNodes.filter(node => node.block === 'block-a'),
isExpanded: false,
toggleBlock: toggleSpy
};
const componentToRender = (
<Block { ...props } />
);
const componentToRender = <Block {...props} />;
const enzymeWrapper = Enzyme.shallow(componentToRender);
expect(toggleSpy.called).toBe(false);
expect(
enzymeWrapper.find('.map-title').find('h5').text()
enzymeWrapper
.find('.map-title')
.find('h5')
.text()
).toBe('Block A');
enzymeWrapper.find('.map-title').simulate('click');
@ -84,9 +72,10 @@ test('<Block /> should handle toggle clicks correctly', () => {
enzymeWrapper.setProps({ ...props, isExpanded: true });
expect(
enzymeWrapper.find('.map-title').find('h5').text()
enzymeWrapper
.find('.map-title')
.find('h5')
.text()
).toBe('Block A');
expect(
enzymeWrapper.find('ul').length
).toBe(1);
expect(enzymeWrapper.find('ul').length).toBe(1);
});

View File

@ -20,13 +20,10 @@ test('<SuperBlock /> not expanded snapshot', () => {
superBlock: 'Super Block One',
toggleSuperBlock: toggleSpy
};
const componentToRender = (
<SuperBlock { ...props } />
);
const componentToRender = <SuperBlock {...props} />;
const component = renderer.render(componentToRender);
expect(component).toMatchSnapshot('superBlock-not-expanded');
});
test('<SuperBlock /> expanded snapshot', () => {
@ -37,9 +34,7 @@ test('<SuperBlock /> expanded snapshot', () => {
superBlock: 'Super Block One',
toggleSuperBlock: toggleSpy
};
const componentToRender = (
<SuperBlock { ...props } />
);
const componentToRender = <SuperBlock {...props} />;
const component = renderer.render(componentToRender);
expect(component).toMatchSnapshot('superBlock-expanded');
@ -53,18 +48,17 @@ test('<SuperBlock should handle toggle clicks correctly', () => {
superBlock: 'Super Block One',
toggleSuperBlock: toggleSpy
};
const componentToRender = (
<SuperBlock { ...props } />
);
const componentToRender = <SuperBlock {...props} />;
const enzymeWrapper = Enzyme.shallow(componentToRender);
expect(toggleSpy.called).toBe(false);
expect(
enzymeWrapper.find('.map-title').find('h4').text()
enzymeWrapper
.find('.map-title')
.find('h4')
.text()
).toBe('Super Block One');
expect(
enzymeWrapper.find('ul').length
).toBe(0);
expect(enzymeWrapper.find('ul').length).toBe(0);
enzymeWrapper.find('.map-title').simulate('click');
@ -74,9 +68,10 @@ test('<SuperBlock should handle toggle clicks correctly', () => {
enzymeWrapper.setProps({ ...props, isExpanded: true });
expect(
enzymeWrapper.find('.map-title').find('h4').text()
enzymeWrapper
.find('.map-title')
.find('h4')
.text()
).toBe('Super Block One');
expect(
enzymeWrapper.find('ul').length
).toBe(1);
expect(enzymeWrapper.find('ul').length).toBe(1);
});

View File

@ -10,10 +10,7 @@ import Spacer from './Spacer';
Enzyme.configure({ adapter: new Adapter() });
test('<Spacer /> snapshot', () => {
const component = renderer.create(
<Spacer />,
);
const component = renderer.create(<Spacer />);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@ -2,18 +2,10 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Controlled as CodeMirror } from 'react-codemirror2';
import MonacoEditor from 'react-monaco-editor';
import { executeChallenge, updateFile } from '../../redux';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
if (typeof window !== 'undefined') {
require('codemirror/mode/htmlmixed/htmlmixed');
require('codemirror/mode/javascript/javascript');
}
const propTypes = {
contents: PropTypes.string,
executeChallenge: PropTypes.func.isRequired,
@ -34,7 +26,8 @@ const mapDispatchToProps = dispatch =>
);
const modeMap = {
html: 'htmlmixed',
css: 'css',
html: 'html',
js: 'javascript',
jsx: 'javascript'
};
@ -42,60 +35,57 @@ const modeMap = {
class Editor extends PureComponent {
constructor(...props) {
super(...props);
this.options = {
selectOnLineNumbers: true
};
this._editor = null;
this.focusEditor = this.focusEditor.bind(this);
}
handleChange = editorValue => {
componentWillUnmount() {
document.removeEventListener('keyup', this.focusEditor);
}
editorDidMount(editor, monaco) {
this._editor = editor;
document.addEventListener('keyup', this.focusEditor);
this._editor.addAction({
id: 'execute-challenge',
label: 'Run tests',
keybindings: [
/* eslint-disable no-bitwise */
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter)
],
run: this.props.executeChallenge
});
}
focusEditor(e) {
// e key to focus editor
if (e.keyCode === 69) {
console.log('focusing');
this._editor.focus();
}
}
onChange(editorValue) {
const { updateFile, fileKey } = this.props;
updateFile({ key: fileKey, editorValue });
};
}
render() {
const { contents, executeChallenge, ext } = this.props;
const { contents, ext } = this.props;
return (
<div className='classic-editor editor'>
<CodeMirror
onBeforeChange={(editor, something, newValue) =>
this.handleChange(newValue)
}
options={{
mode: modeMap[ext],
theme: 'material',
lineNumbers: true,
lineWrapping: true,
extraKeys: {
Esc() {
document.activeElement.blur();
},
Tab(cm) {
if (cm.somethingSelected()) {
return cm.indentSelection('add');
}
const spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
return cm.replaceSelection(spaces);
},
'Shift-Tab': function(cm) {
return cm.indentSelection('subtract');
},
'Ctrl-Enter': function() {
executeChallenge();
return false;
},
'Cmd-Enter': function() {
executeChallenge();
return false;
}
// TODO: Not working in cm2
// 'Ctrl-/': function(cm) {
// cm.toggleComment();
// },
// 'Cmd-/': function(cm) {
// cm.toggleComment();
// }
}
}}
<base href='/' />
<MonacoEditor
editorDidMount={::this.editorDidMount}
language={modeMap[ext]}
onChange={::this.onChange}
options={this.options}
theme='vs-dark'
value={contents}
/>
</div>

View File

@ -1,28 +1,35 @@
import React from 'react';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Controlled as CodeMirror } from 'react-codemirror2';
import './output.css';
const defaultOptions = {
lineNumbers: false,
lineWrapping: true,
mode: 'javascript',
readOnly: 'nocursor'
};
import MonacoEditor from 'react-monaco-editor';
const propTypes = {
defaultOutput: PropTypes.string,
output: PropTypes.string
};
const options = {
lineNumbers: false,
minimap: {
enabled: false
},
readOnly: true,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden'
}
};
function Output({ output, defaultOutput }) {
return (
<CodeMirror
className='challenge-log'
options={{ ...defaultOptions, theme: 'material' }}
value={output || defaultOutput}
/>
<Fragment>
<base href='/' />
<MonacoEditor
className='challenge-output'
height={150}
options={options}
value={output ? output : defaultOutput}
/>
</Fragment>
);
}

View File

@ -1,3 +0,0 @@
.react-codemirror2.challenge-log > .CodeMirror {
height: 150px;
}

View File

@ -2101,10 +2101,6 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
codemirror@^5.36.0:
version "5.36.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.36.0.tgz#1172ad9dc298056c06e0b34e5ccd23825ca15b40"
collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@ -2326,6 +2322,19 @@ copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
copy-webpack-plugin@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c"
dependencies:
cacache "^10.0.4"
find-cache-dir "^1.0.0"
globby "^7.1.1"
is-glob "^4.0.0"
loader-utils "^1.1.0"
minimatch "^3.0.4"
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
copyfiles@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-1.2.0.tgz#a8da3ac41aa2220ae29bd3c58b6984294f2c593c"
@ -2814,6 +2823,13 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
dir-glob@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
dependencies:
arrify "^1.0.1"
path-type "^3.0.0"
discontinuous-range@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
@ -4360,6 +4376,17 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
globby@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680"
dependencies:
array-union "^1.0.1"
dir-glob "^2.0.0"
glob "^7.1.2"
ignore "^3.3.5"
pify "^3.0.0"
slash "^1.0.0"
got@^6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
@ -4729,7 +4756,7 @@ iferr@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
ignore@^3.3.3:
ignore@^3.3.3, ignore@^3.3.5:
version "3.3.7"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
@ -5874,7 +5901,7 @@ loader-utils@^0.2.11, loader-utils@^0.2.15, loader-utils@^0.2.16, loader-utils@^
json5 "^0.5.0"
object-assign "^4.0.1"
loader-utils@^1.0.2:
loader-utils@^1.0.2, loader-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
dependencies:
@ -6342,6 +6369,10 @@ moment@2.x.x, moment@^2.16.0:
version "2.21.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.21.0.tgz#2a114b51d2a6ec9e6d83cf803f838a878d8a023a"
monaco-editor@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.10.1.tgz#8c96c4f15b6b5258bf92cbde93cad8a7e3007e14"
mongodb-core@2.1.19:
version "2.1.19"
resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.19.tgz#00fbd5e5a3573763b9171cfd844e60a8f2a3a18b"
@ -6891,7 +6922,7 @@ p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
p-limit@^1.1.0:
p-limit@^1.0.0, p-limit@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
dependencies:
@ -7063,7 +7094,7 @@ path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
path-type@3.0.0:
path-type@3.0.0, path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
dependencies:
@ -7959,10 +7990,6 @@ react-bootstrap@^0.32.1:
uncontrollable "^4.1.0"
warning "^3.0.0"
react-codemirror2@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-4.2.1.tgz#4ad3c5c60ebbcb34880f961721b51527324ec021"
react-deep-force-update@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-2.1.1.tgz#8ea4263cd6455a050b37445b3f08fd839d86e909"
@ -8052,6 +8079,13 @@ react-measure@^2.0.2:
prop-types "^15.5.10"
resize-observer-polyfill "^1.4.2"
react-monaco-editor@^0.14.1:
version "0.14.1"
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.14.1.tgz#f5163e119e8a7dc79b992cb3fd7af887547d7efd"
dependencies:
monaco-editor "^0.10.0"
prop-types "^15.5.10"
react-overlays@^0.8.0:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.8.3.tgz#fad65eea5b24301cca192a169f5dddb0b20d3ac5"