diff --git a/.eslintrc b/.eslintrc index ea5baeef..17527587 100644 --- a/.eslintrc +++ b/.eslintrc @@ -24,7 +24,20 @@ "no-console": 0, "no-alert": 0, "no-underscore-dangle": 0, - "max-len": [1, 120, 4], + "max-len": [1, 120, 2, {ignoreComments: true}], + "quote-props": [1, "consistent-as-needed"], + "no-unused-vars": [1, {"vars": "local", "args": "none"}], + "consistent-return": ["error", { "treatUndefinedAsUnspecified": true }], + "no-param-reassign": [2, { "props": false }], + "react/self-closing-comp": ["error", { + "component": true, + "html": false + }], + "newline-per-chained-call": 0, + "react/prefer-stateless-function": [2, + { "ignorePureComponents": true + }], + "class-methods-use-this": 0 }, "plugins": [ "react", "jsx-a11y", "import" diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 53e35dee..c0f9778a 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -1,152 +1,159 @@ import React, { PropTypes } from 'react'; import { Link } from 'react-router'; -function Nav(props) { - return ( - - ); + })()} + +
+ This is a preview version of the editor, that has not yet been officially released. It is in development, you can report bugs + here. + Please use with caution. +
+ + ); + } } Nav.propTypes = { @@ -168,7 +175,16 @@ Nav.propTypes = { logoutUser: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, showShareModal: PropTypes.func.isRequired, - showErrorModal: PropTypes.func.isRequired + showErrorModal: PropTypes.func.isRequired, + unsavedChanges: PropTypes.bool.isRequired, + warnIfUnsavedChanges: PropTypes.func.isRequired +}; + +Nav.defaultProps = { + project: { + id: undefined, + owner: undefined + } }; export default Nav; diff --git a/client/constants.js b/client/constants.js index baa61b84..b6444700 100644 --- a/client/constants.js +++ b/client/constants.js @@ -53,7 +53,6 @@ export const COLLAPSE_CONSOLE = 'COLLAPSE_CONSOLE'; export const UPDATE_LINT_MESSAGE = 'UPDATE_LINT_MESSAGE'; export const CLEAR_LINT_MESSAGE = 'CLEAR_LINT_MESSAGE'; -export const UPDATE_LINENUMBER = 'UPDATE_LINENUMBER'; export const SHOW_FILE_OPTIONS = 'SHOW_FILE_OPTIONS'; export const HIDE_FILE_OPTIONS = 'HIDE_FILE_OPTIONS'; diff --git a/client/index.jsx b/client/index.jsx index 7d21c93a..0a4d78c7 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -15,5 +15,5 @@ render( , - document.getElementById('root') + document.getElementById('root') ); diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index b300cde0..9056c4a2 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -30,11 +30,15 @@ class App extends React.Component { } App.propTypes = { - children: PropTypes.object, + children: PropTypes.element, location: PropTypes.shape({ pathname: PropTypes.string - }), + }).isRequired, setPreviousPath: PropTypes.func.isRequired, }; +App.defaultProps = { + children: null +}; + export default connect(() => ({}), { setPreviousPath })(App); diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx index 31a451c4..6331d017 100644 --- a/client/modules/App/components/Overlay.jsx +++ b/client/modules/App/components/Overlay.jsx @@ -11,7 +11,11 @@ function Overlay(props) { } Overlay.propTypes = { - children: PropTypes.object + children: PropTypes.element +}; + +Overlay.defaultProps = { + children: null }; export default Overlay; diff --git a/client/modules/IDE/actions/editorAccessibility.js b/client/modules/IDE/actions/editorAccessibility.js index 3311d943..ffb88db4 100644 --- a/client/modules/IDE/actions/editorAccessibility.js +++ b/client/modules/IDE/actions/editorAccessibility.js @@ -14,10 +14,3 @@ export function clearLintMessage() { type: ActionTypes.CLEAR_LINT_MESSAGE }; } - -export function updateLineNumber(lineNumber) { - return { - type: ActionTypes.UPDATE_LINENUMBER, - lineNumber - }; -} diff --git a/client/modules/IDE/actions/files.js b/client/modules/IDE/actions/files.js index 6ded6a7b..d55765d5 100644 --- a/client/modules/IDE/actions/files.js +++ b/client/modules/IDE/actions/files.js @@ -1,9 +1,9 @@ -import * as ActionTypes from '../../../constants'; import axios from 'axios'; import objectID from 'bson-objectid'; import blobUtil from 'blob-util'; -import { setUnsavedChanges } from './ide'; import { reset } from 'redux-form'; +import * as ActionTypes from '../../../constants'; +import { setUnsavedChanges } from './ide'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -18,11 +18,11 @@ function createUniqueName(name, parentId, files) { .children.map(childFileId => files.find(file => file.id === childFileId)); let testName = name; let index = 1; - let existingName = siblingFiles.find((file) => name === file.name); + let existingName = siblingFiles.find(file => name === file.name); while (existingName) { testName = appendToFilename(name, `-${index}`); - index++; + index += 1; existingName = siblingFiles.find((file) => testName === file.name); // eslint-disable-line } return testName; @@ -56,7 +56,7 @@ export function createFile(formProps) { children: [] }; axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) - .then(response => { + .then((response) => { dispatch({ type: ActionTypes.CREATE_FILE, ...response.data, @@ -113,7 +113,7 @@ export function createFolder(formProps) { fileType: 'folder' }; axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) - .then(response => { + .then((response) => { dispatch({ type: ActionTypes.CREATE_FILE, ...response.data, @@ -200,7 +200,7 @@ export function deleteFile(id, parentId) { parentId }); }) - .catch(response => { + .catch((response) => { dispatch({ type: ActionTypes.ERROR, error: response.data diff --git a/client/modules/IDE/actions/preferences.js b/client/modules/IDE/actions/preferences.js index 5d8bc4b1..796f1151 100644 --- a/client/modules/IDE/actions/preferences.js +++ b/client/modules/IDE/actions/preferences.js @@ -1,5 +1,5 @@ -import * as ActionTypes from '../../../constants'; import axios from 'axios'; +import * as ActionTypes from '../../../constants'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -7,7 +7,7 @@ function updatePreferences(formParams, dispatch) { axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true }) .then(() => { }) - .catch((response) => dispatch({ + .catch(response => dispatch({ type: ActionTypes.ERROR, error: response.data })); diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index e1dead8b..b1a85e74 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -1,7 +1,8 @@ -import * as ActionTypes from '../../../constants'; import { browserHistory } from 'react-router'; import axios from 'axios'; import objectID from 'bson-objectid'; +import moment from 'moment'; +import * as ActionTypes from '../../../constants'; import { showToast, setToastText } from './toast'; import { setUnsavedChanges, justOpenedProject, @@ -9,7 +10,6 @@ import { setUnsavedChanges, setProjectSavedTime, resetProjectSavedTime, showErrorModal } from './ide'; -import moment from 'moment'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -21,7 +21,7 @@ export function getProject(id) { dispatch(resetProjectSavedTime()); } axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true }) - .then(response => { + .then((response) => { dispatch({ type: ActionTypes.SET_PROJECT, project: response.data, @@ -86,7 +86,7 @@ export function saveProject(autosave = false) { }); } else { axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) - .then(response => { + .then((response) => { dispatch(setUnsavedChanges(false)); dispatch(setProjectSavedTime(moment().format())); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); @@ -108,7 +108,7 @@ export function saveProject(autosave = false) { } } }) - .catch(response => { + .catch((response) => { if (response.status === 403) { dispatch(showErrorModal('staleSession')); } else { @@ -131,7 +131,7 @@ export function autosaveProject() { export function createProject() { return (dispatch) => { axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true }) - .then(response => { + .then((response) => { browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); dispatch({ type: ActionTypes.NEW_PROJECT, @@ -192,7 +192,7 @@ export function cloneProject() { // const newFiles = state.files; const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles }); axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) - .then(response => { + .then((response) => { browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); dispatch({ type: ActionTypes.NEW_PROJECT, diff --git a/client/modules/IDE/actions/projects.js b/client/modules/IDE/actions/projects.js index 2561f7c9..bb9a0bf4 100644 --- a/client/modules/IDE/actions/projects.js +++ b/client/modules/IDE/actions/projects.js @@ -1,5 +1,5 @@ -import * as ActionTypes from '../../../constants'; import axios from 'axios'; +import * as ActionTypes from '../../../constants'; import { showErrorModal, setPreviousPath } from './ide'; import { resetProject } from './project'; @@ -14,7 +14,7 @@ export function getProjects(username) { url = `${ROOT_URL}/projects`; } axios.get(url, { withCredentials: true }) - .then(response => { + .then((response) => { dispatch({ type: ActionTypes.SET_PROJECTS, projects: response.data @@ -41,7 +41,7 @@ export function deleteProject(id) { id }); }) - .catch(response => { + .catch((response) => { if (response.status === 403) { dispatch(showErrorModal('staleSession')); } else { diff --git a/client/modules/IDE/actions/uploader.js b/client/modules/IDE/actions/uploader.js index fb7976dc..b8991736 100644 --- a/client/modules/IDE/actions/uploader.js +++ b/client/modules/IDE/actions/uploader.js @@ -1,7 +1,7 @@ import axios from 'axios'; import { createFile } from './files'; -const textFileRegex = /(text\/|application\/json)/; +const textFileRegex = /(text\/|application\/json)/; const s3BucketHttps = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB @@ -36,11 +36,11 @@ export function dropzoneAcceptCallback(file, done) { // check mime type // if text, local interceptor if (file.type.match(textFileRegex) && file.size < MAX_LOCAL_FILE_SIZE) { - localIntercept(file).then(result => { + localIntercept(file).then((result) => { file.content = result; // eslint-disable-line done('Uploading plaintext file locally.'); }) - .catch(result => { + .catch((result) => { done(`Failed to download file ${file.name}: ${result}`); console.warn(file); }); @@ -55,14 +55,14 @@ export function dropzoneAcceptCallback(file, done) { { withCredentials: true }) - .then(response => { + .then((response) => { file.custom_status = 'ready'; // eslint-disable-line file.postData = response.data; // eslint-disable-line file.s3 = response.data.key; // eslint-disable-line file.previewTemplate.className += ' uploading'; // eslint-disable-line done(); }) - .catch(response => { + .catch((response) => { file.custom_status = 'rejected'; // eslint-disable-line if (response.data.responseText && response.data.responseText.message) { done(response.data.responseText.message); @@ -76,7 +76,7 @@ export function dropzoneAcceptCallback(file, done) { export function dropzoneSendingCallback(file, xhr, formData) { return () => { if (!file.type.match(textFileRegex) || file.size >= MAX_LOCAL_FILE_SIZE) { - Object.keys(file.postData).forEach(key => { + Object.keys(file.postData).forEach((key) => { formData.append(key, file.postData[key]); }); formData.append('Content-type', ''); diff --git a/client/modules/IDE/components/About.jsx b/client/modules/IDE/components/About.jsx index c73092e9..7fa2bd39 100644 --- a/client/modules/IDE/components/About.jsx +++ b/client/modules/IDE/components/About.jsx @@ -1,10 +1,11 @@ import React, { PropTypes } from 'react'; import InlineSVG from 'react-inlinesvg'; +import { browserHistory } from 'react-router'; + const exitUrl = require('../../../images/exit.svg'); const squareLogoUrl = require('../../../images/p5js-square-logo.svg'); const playUrl = require('../../../images/play.svg'); const asteriskUrl = require('../../../images/p5-asterisk.svg'); -import { browserHistory } from 'react-router'; class About extends React.Component { constructor(props) { @@ -13,7 +14,7 @@ class About extends React.Component { } componentDidMount() { - this.refs.about.focus(); + this.aboutSection.focus(); } closeAboutModal() { @@ -22,7 +23,7 @@ class About extends React.Component { render() { return ( -
+
{ this.aboutSection = element; }} tabIndex="0">

Welcome

diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index 576d26a0..d3f2eabb 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -1,12 +1,13 @@ import React, { PropTypes } from 'react'; import InlineSVG from 'react-inlinesvg'; +import classNames from 'classnames'; + const upArrowUrl = require('../../../images/up-arrow.svg'); const downArrowUrl = require('../../../images/down-arrow.svg'); -import classNames from 'classnames'; class Console extends React.Component { componentDidUpdate() { - this.refs.console_messages.scrollTop = this.refs.console_messages.scrollHeight; + this.consoleMessages.scrollTop = this.consoleMessages.scrollHeight; } render() { @@ -16,13 +17,13 @@ class Console extends React.Component { }); return ( -
+

Console

- + @@ -31,20 +32,20 @@ class Console extends React.Component {
-
- {this.props.consoleEvents.map((consoleEvent, index) => { +
{ this.consoleMessages = element; }} className="preview-console__messages"> + {this.props.consoleEvents.map((consoleEvent) => { const args = consoleEvent.arguments; const method = consoleEvent.method; if (Object.keys(args).length === 0) { return ( -
- {'undefined'} +
+ {'undefined'}
); } return ( -
- {Object.keys(args).map((key) => {args[key]})} +
+ {Object.keys(args).map(key => {args[key]})}
); })} @@ -56,12 +57,18 @@ class Console extends React.Component { } Console.propTypes = { - consoleEvents: PropTypes.array, - isPlaying: PropTypes.bool.isRequired, + consoleEvents: PropTypes.arrayOf(PropTypes.shape({ + method: PropTypes.string.isRequired, + args: PropTypes.arrayOf(PropTypes.string) + })), isExpanded: PropTypes.bool.isRequired, collapseConsole: PropTypes.func.isRequired, expandConsole: PropTypes.func.isRequired, clearConsole: PropTypes.func.isRequired }; +Console.defaultProps = { + consoleEvents: [] +}; + export default Console; diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index 0117172c..6411ca55 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -1,12 +1,7 @@ import React, { PropTypes } from 'react'; -import EditorAccessibility from '../components/EditorAccessibility'; import CodeMirror from 'codemirror'; import beautifyJS from 'js-beautify'; -const beautifyCSS = beautifyJS.css; -const beautifyHTML = beautifyJS.html; -import '../../../utils/p5-javascript'; import 'codemirror/mode/css/css'; -import '../../../utils/htmlmixed'; import 'codemirror/addon/selection/active-line'; import 'codemirror/addon/lint/lint'; import 'codemirror/addon/lint/javascript-lint'; @@ -15,22 +10,27 @@ import 'codemirror/addon/lint/html-lint'; import 'codemirror/addon/comment/comment'; import 'codemirror/keymap/sublime'; import 'codemirror/addon/search/jump-to-line'; - import { JSHINT } from 'jshint'; -window.JSHINT = JSHINT; import { CSSLint } from 'csslint'; -window.CSSLint = CSSLint; import { HTMLHint } from 'htmlhint'; -window.HTMLHint = HTMLHint; -const beepUrl = require('../../../sounds/audioAlert.mp3'); import InlineSVG from 'react-inlinesvg'; +import classNames from 'classnames'; +import { debounce } from 'lodash'; +import '../../../utils/htmlmixed'; +import '../../../utils/p5-javascript'; +import Timer from '../components/Timer'; +import EditorAccessibility from '../components/EditorAccessibility'; + +const beautifyCSS = beautifyJS.css; +const beautifyHTML = beautifyJS.html; + +window.JSHINT = JSHINT; +window.CSSLint = CSSLint; +window.HTMLHint = HTMLHint; + +const beepUrl = require('../../../sounds/audioAlert.mp3'); const downArrowUrl = require('../../../images/down-arrow.svg'); const unsavedChangesDotUrl = require('../../../images/unsaved-changes-dot.svg'); -import classNames from 'classnames'; - -import { debounce } from 'lodash'; -import Timer from '../components/Timer'; - const rightArrowUrl = require('../../../images/right-arrow.svg'); const leftArrowUrl = require('../../../images/left-arrow.svg'); @@ -42,7 +42,7 @@ class Editor extends React.Component { componentDidMount() { this.beep = new Audio(beepUrl); this.widgets = []; - this._cm = CodeMirror(this.refs.container, { // eslint-disable-line + this._cm = CodeMirror(this.codemirrorContainer, { // eslint-disable-line theme: `p5-${this.props.theme}`, lineNumbers: true, styleActiveLine: true, @@ -64,8 +64,8 @@ class Editor extends React.Component { } }, 2000), options: { - asi: true, - eqeqeq: false, + 'asi': true, + 'eqeqeq': false, '-W041': false } } @@ -162,7 +162,7 @@ class Editor extends React.Component { initializeDocuments(files) { this._docs = {}; - files.forEach(file => { + files.forEach((file) => { if (file.name !== 'root') { this._docs[file.id] = CodeMirror.Doc(file.content, this.getFileMode(file.name)); // eslint-disable-line } @@ -189,7 +189,7 @@ class Editor extends React.Component { if (this.props.editorOptionsVisible) { this.props.closeEditorOptions(); } else { - this.refs.optionsButton.focus(); + this.optionsButton.focus(); this.props.showEditorOptions(); } } @@ -198,7 +198,7 @@ class Editor extends React.Component { render() { const editorSectionClass = classNames({ - editor: true, + 'editor': true, 'sidebar--contracted': !this.props.isExpanded, 'editor--options': this.props.editorOptionsVisible }); @@ -238,7 +238,7 @@ class Editor extends React.Component { className="editor__options-button" aria-label="editor options" tabIndex="0" - ref="optionsButton" + ref={(element) => { this.optionsButton = element; }} onClick={() => { this.toggleEditorOptions(); }} @@ -248,18 +248,17 @@ class Editor extends React.Component {
-
+
{ this.codemirrorContainer = element; }} className="editor-holder" tabIndex="0">
); @@ -268,11 +267,14 @@ class Editor extends React.Component { Editor.propTypes = { lintWarning: PropTypes.bool.isRequired, - lineNumber: PropTypes.string.isRequired, - lintMessages: PropTypes.array.isRequired, + lintMessages: PropTypes.arrayOf(PropTypes.shape({ + severity: PropTypes.string.isRequired, + line: PropTypes.number.isRequired, + message: PropTypes.string.isRequired, + id: PropTypes.number.isRequired + })).isRequired, updateLintMessage: PropTypes.func.isRequired, clearLintMessage: PropTypes.func.isRequired, - updateLineNumber: PropTypes.func.isRequired, indentationAmount: PropTypes.number.isRequired, isTabIndent: PropTypes.bool.isRequired, updateFileContent: PropTypes.func.isRequired, @@ -281,7 +283,7 @@ Editor.propTypes = { name: PropTypes.string.isRequired, content: PropTypes.string.isRequired, id: PropTypes.string.isRequired - }), + }).isRequired, editorOptionsVisible: PropTypes.bool.isRequired, showEditorOptions: PropTypes.func.isRequired, closeEditorOptions: PropTypes.func.isRequired, @@ -293,7 +295,11 @@ Editor.propTypes = { theme: PropTypes.string.isRequired, unsavedChanges: PropTypes.bool.isRequired, projectSavedTime: PropTypes.string.isRequired, - files: PropTypes.array.isRequired, + files: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + content: PropTypes.string.isRequired + })).isRequired, isExpanded: PropTypes.bool.isRequired, collapseSidebar: PropTypes.func.isRequired, expandSidebar: PropTypes.func.isRequired, @@ -301,4 +307,8 @@ Editor.propTypes = { clearConsole: PropTypes.func.isRequired }; +Editor.defaultProps = { + isUserOwner: false +}; + export default Editor; diff --git a/client/modules/IDE/components/EditorAccessibility.jsx b/client/modules/IDE/components/EditorAccessibility.jsx index dbab82ad..d56cb733 100644 --- a/client/modules/IDE/components/EditorAccessibility.jsx +++ b/client/modules/IDE/components/EditorAccessibility.jsx @@ -5,11 +5,11 @@ class EditorAccessibility extends React.Component { } render() { - let messages = []; + const messages = []; if (this.props.lintMessages.length > 0) { this.props.lintMessages.forEach((lintMessage, i) => { messages.push( -
  • +
  • {lintMessage.severity} in line {lintMessage.line} : {lintMessage.message} @@ -35,8 +35,12 @@ class EditorAccessibility extends React.Component { } EditorAccessibility.propTypes = { - lintMessages: PropTypes.array.isRequired, - lineNumber: PropTypes.string.isRequired, + lintMessages: PropTypes.arrayOf(PropTypes.shape({ + severity: PropTypes.string.isRequired, + line: PropTypes.number.isRequired, + message: PropTypes.string.isRequired, + id: PropTypes.number.isRequired + })).isRequired, }; export default EditorAccessibility; diff --git a/client/modules/IDE/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx index 7cd91f60..bf62721a 100644 --- a/client/modules/IDE/components/ErrorModal.jsx +++ b/client/modules/IDE/components/ErrorModal.jsx @@ -1,11 +1,12 @@ import React, { PropTypes } from 'react'; import InlineSVG from 'react-inlinesvg'; -const exitUrl = require('../../../images/exit.svg'); import { Link } from 'react-router'; +const exitUrl = require('../../../images/exit.svg'); + class ErrorModal extends React.Component { componentDidMount() { - this.refs.modal.focus(); + this.errorModal.focus(); } @@ -23,7 +24,7 @@ class ErrorModal extends React.Component { staleSession() { return (

    - It looks like you've been logged out. Please  + It looks like you've been logged out. Please  log in.

    ); @@ -39,7 +40,7 @@ class ErrorModal extends React.Component { render() { return ( -
    +
    { this.errorModal = element; }} tabIndex="0">

    Error

    ); })()} - {this.props.name} + { this.fileNameInput = element; }} onBlur={() => { this.validateFileName(); this.props.hideEditFileName(this.props.id); @@ -128,21 +125,26 @@ export class FileNode extends React.Component { -
    +
    @@ -205,7 +213,7 @@ export class FileNode extends React.Component { FileNode.propTypes = { id: PropTypes.string.isRequired, parentId: PropTypes.string, - children: PropTypes.array, + children: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, name: PropTypes.string.isRequired, fileType: PropTypes.string.isRequired, isSelectedFile: PropTypes.bool, @@ -226,9 +234,22 @@ FileNode.propTypes = { hideFolderChildren: PropTypes.func.isRequired }; +FileNode.defaultProps = { + id: '1', + name: 'test', + fileType: 'file', + children: [], + parentId: '0', + isSelectedFile: false, + isOptionsOpen: false, + isEditingName: false, + isFolderClosed: false +}; + function mapStateToProps(state, ownProps) { // this is a hack, state is updated before ownProps - return state.files.find((file) => file.id === ownProps.id) || { ...ownProps, name: 'test', fileType: 'file' }; + return state.files.find(file => file.id === ownProps.id) || { ...ownProps, name: 'test', fileType: 'file' }; + // return state.files.find(file => file.id === ownProps.id); } function mapDispatchToProps(dispatch) { diff --git a/client/modules/IDE/components/FileUploader.jsx b/client/modules/IDE/components/FileUploader.jsx index 9c315a7c..ef9a063f 100644 --- a/client/modules/IDE/components/FileUploader.jsx +++ b/client/modules/IDE/components/FileUploader.jsx @@ -1,9 +1,10 @@ import React, { PropTypes } from 'react'; import Dropzone from 'dropzone'; -const s3Bucket = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`; -import * as UploaderActions from '../actions/uploader'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import * as UploaderActions from '../actions/uploader'; + +const s3Bucket = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`; class FileUploader extends React.Component { componentDidMount() { diff --git a/client/modules/IDE/components/KeyboardShortcutModal.jsx b/client/modules/IDE/components/KeyboardShortcutModal.jsx index a500b4e4..fe2cb5ad 100644 --- a/client/modules/IDE/components/KeyboardShortcutModal.jsx +++ b/client/modules/IDE/components/KeyboardShortcutModal.jsx @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; import InlineSVG from 'react-inlinesvg'; + const exitUrl = require('../../../images/exit.svg'); class KeyboardShortcutModal extends React.Component { diff --git a/client/modules/IDE/components/NewFileModal.jsx b/client/modules/IDE/components/NewFileModal.jsx index 084788a8..ab0c43f0 100644 --- a/client/modules/IDE/components/NewFileModal.jsx +++ b/client/modules/IDE/components/NewFileModal.jsx @@ -1,11 +1,12 @@ import React, { PropTypes } from 'react'; import { reduxForm } from 'redux-form'; -import NewFileForm from './NewFileForm'; import classNames from 'classnames'; import InlineSVG from 'react-inlinesvg'; +import NewFileForm from './NewFileForm'; +import FileUploader from './FileUploader'; + const exitUrl = require('../../../images/exit.svg'); -import FileUploader from './FileUploader'; // At some point this will probably be generalized to a generic modal // in which you can insert different content @@ -21,16 +22,16 @@ class NewFileModal extends React.Component { } focusOnModal() { - this.refs.modal.focus(); + this.modal.focus(); } render() { const modalClass = classNames({ - modal: true, + 'modal': true, 'modal--reduced': !this.props.canUploadMedia }); return ( -
    +
    { this.modal = element; }}>

    Add File

    diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index 09cdd4aa..af062896 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -8,7 +8,7 @@ class NewFolderForm extends React.Component { } componentDidMount() { - this.refs.fileName.focus(); + this.fileName.focus(); } render() { @@ -27,7 +27,7 @@ class NewFolderForm extends React.Component { id="name" type="text" placeholder="Name" - ref="fileName" + ref={(element) => { this.fileName = element; }} {...domOnlyProps(name)} /> diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx index 09f826b3..a1430ba3 100644 --- a/client/modules/IDE/components/NewFolderModal.jsx +++ b/client/modules/IDE/components/NewFolderModal.jsx @@ -1,17 +1,18 @@ import React, { PropTypes } from 'react'; import { reduxForm } from 'redux-form'; import InlineSVG from 'react-inlinesvg'; -const exitUrl = require('../../../images/exit.svg'); import NewFolderForm from './NewFolderForm'; +const exitUrl = require('../../../images/exit.svg'); + class NewFolderModal extends React.Component { componentDidMount() { - this.refs.modal.focus(); + this.newFolderModal.focus(); } render() { return ( -
    +
    { this.newFolderModal = element; }} tabIndex="0">

    Add Folder

    diff --git a/client/modules/IDE/components/Preferences.jsx b/client/modules/IDE/components/Preferences.jsx index ba40283f..370077fa 100644 --- a/client/modules/IDE/components/Preferences.jsx +++ b/client/modules/IDE/components/Preferences.jsx @@ -49,7 +49,7 @@ class Preferences extends React.Component { render() { const beep = new Audio(beepUrl); const preferencesContainerClass = classNames({ - preferences: true, + 'preferences': true, 'preferences--selected': this.props.isVisible }); @@ -79,15 +79,14 @@ class Preferences extends React.Component { { this.fontSizeInput = element; }} onClick={() => { - this.refs.fontSizeInput.select(); + this.fontSizeInput.select(); }} > @@ -113,15 +112,14 @@ class Preferences extends React.Component { { this.indentationInput = element; }} onClick={() => { - this.refs.indentationInput.select(); + this.indentationInput.select(); }} > @@ -248,13 +246,13 @@ class Preferences extends React.Component { checked={!this.props.lintWarning} /> -
    beep.play()} aria-label="preview sound" > Preview sound -
    +
    @@ -303,7 +301,7 @@ class Preferences extends React.Component { id="text-output-off" className="preference__radio-button" value="Off" - checked={!Boolean(this.props.textOutput)} + checked={!(this.props.textOutput)} /> diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index b91d785b..33af7507 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -86,20 +86,18 @@ class PreviewFrame extends React.Component { this.renderFrameContents(); } - if (this.props.dispatchConsoleEvent) { - window.addEventListener('message', (messageEvent) => { - messageEvent.data.forEach(message => { - const args = message.arguments; - Object.keys(args).forEach((key) => { - if (args[key].includes('Exiting potential infinite loop')) { - this.props.stopSketch(); - this.props.expandConsole(); - } - }); + window.addEventListener('message', (messageEvent) => { + messageEvent.data.forEach((message) => { + const args = message.arguments; + Object.keys(args).forEach((key) => { + if (args[key].includes('Exiting potential infinite loop')) { + this.props.stopSketch(); + this.props.expandConsole(); + } }); - this.props.dispatchConsoleEvent(messageEvent.data); }); - } + this.props.dispatchConsoleEvent(messageEvent.data); + }); } componentDidUpdate(prevProps) { @@ -128,7 +126,6 @@ class PreviewFrame extends React.Component { if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) { this.renderSketch(); - return; } // small bug - if autorefresh is on, and the usr changes files @@ -136,11 +133,11 @@ class PreviewFrame extends React.Component { } componentWillUnmount() { - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).contentDocument.body); + ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body); } clearPreview() { - const doc = ReactDOM.findDOMNode(this); + const doc = this.iframeElement; doc.srcDoc = ''; } @@ -204,7 +201,7 @@ class PreviewFrame extends React.Component { scriptsToInject = scriptsToInject.concat(interceptorScripts); } - scriptsToInject.forEach(scriptToInject => { + scriptsToInject.forEach((scriptToInject) => { const script = sketchDoc.createElement('script'); script.src = scriptToInject; sketchDoc.head.appendChild(script); @@ -223,7 +220,7 @@ class PreviewFrame extends React.Component { resolvePathsForElementsWithAttribute(attr, sketchDoc, files) { const elements = sketchDoc.querySelectorAll(`[${attr}]`); const elementsArray = Array.prototype.slice.call(elements); - elementsArray.forEach(element => { + elementsArray.forEach((element) => { if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) { const resolvedFile = resolvePathToFile(element.getAttribute(attr), files); if (resolvedFile) { @@ -235,7 +232,7 @@ class PreviewFrame extends React.Component { resolveJSAndCSSLinks(files) { const newFiles = []; - files.forEach(file => { + files.forEach((file) => { const newFile = { ...file }; if (file.name.match(/.*\.js$/i)) { newFile.content = this.resolveJSLinksInString(newFile.content, files); @@ -251,7 +248,7 @@ class PreviewFrame extends React.Component { let newContent = content; let jsFileStrings = content.match(STRING_REGEX); jsFileStrings = jsFileStrings || []; - jsFileStrings.forEach(jsFileString => { + jsFileStrings.forEach((jsFileString) => { if (jsFileString.match(MEDIA_FILE_REGEX)) { const filePath = jsFileString.substr(1, jsFileString.length - 2); const resolvedFile = resolvePathToFile(filePath, files); @@ -275,7 +272,7 @@ class PreviewFrame extends React.Component { let newContent = content; let cssFileStrings = content.match(STRING_REGEX); cssFileStrings = cssFileStrings || []; - cssFileStrings.forEach(cssFileString => { + cssFileStrings.forEach((cssFileString) => { if (cssFileString.match(MEDIA_FILE_REGEX)) { const filePath = cssFileString.substr(1, cssFileString.length - 2); const resolvedFile = resolvePathToFile(filePath, files); @@ -292,7 +289,7 @@ class PreviewFrame extends React.Component { resolveScripts(sketchDoc, files) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); - scriptsInHTMLArray.forEach(script => { + scriptsInHTMLArray.forEach((script) => { if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) { const resolvedFile = resolvePathToFile(script.getAttribute('src'), files); if (resolvedFile) { @@ -313,13 +310,13 @@ class PreviewFrame extends React.Component { resolveStyles(sketchDoc, files) { const inlineCSSInHTML = sketchDoc.getElementsByTagName('style'); const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML); - inlineCSSInHTMLArray.forEach(style => { + inlineCSSInHTMLArray.forEach((style) => { style.innerHTML = this.resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line }); const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]'); const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML); - cssLinksInHTMLArray.forEach(css => { + cssLinksInHTMLArray.forEach((css) => { if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) { const resolvedFile = resolvePathToFile(css.getAttribute('href'), files); if (resolvedFile) { @@ -337,7 +334,7 @@ class PreviewFrame extends React.Component { } renderSketch() { - const doc = ReactDOM.findDOMNode(this); + const doc = this.iframeElement; if (this.props.isPlaying) { srcDoc.set(doc, this.injectLocalFiles()); if (this.props.endSketchRefresh) { @@ -350,7 +347,7 @@ class PreviewFrame extends React.Component { } renderFrameContents() { - const doc = ReactDOM.findDOMNode(this).contentDocument; + const doc = this.iframeElement.contentDocument; if (doc.readyState === 'complete') { this.renderSketch(); } else { @@ -366,8 +363,8 @@ class PreviewFrame extends React.Component { role="main" tabIndex="0" frameBorder="0" - ref="iframe" title="sketch output" + ref={(element) => { this.iframeElement = element; }} sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms" /> ); @@ -379,14 +376,16 @@ PreviewFrame.propTypes = { isTextOutputPlaying: PropTypes.bool.isRequired, textOutput: PropTypes.number.isRequired, setTextOutput: PropTypes.func.isRequired, - content: PropTypes.string, htmlFile: PropTypes.shape({ content: PropTypes.string.isRequired - }), - files: PropTypes.array.isRequired, - dispatchConsoleEvent: PropTypes.func, - children: PropTypes.element, - autorefresh: PropTypes.bool.isRequired, + }).isRequired, + files: PropTypes.arrayOf(PropTypes.shape({ + content: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + url: PropTypes.string, + id: PropTypes.string.isRequired + })).isRequired, + dispatchConsoleEvent: PropTypes.func.isRequired, endSketchRefresh: PropTypes.func.isRequired, previewIsRefreshing: PropTypes.bool.isRequired, fullView: PropTypes.bool, @@ -395,4 +394,8 @@ PreviewFrame.propTypes = { expandConsole: PropTypes.func.isRequired }; +PreviewFrame.defaultProps = { + fullView: false +}; + export default PreviewFrame; diff --git a/client/modules/IDE/components/ShareModal.jsx b/client/modules/IDE/components/ShareModal.jsx index 99a42626..ad8c030e 100644 --- a/client/modules/IDE/components/ShareModal.jsx +++ b/client/modules/IDE/components/ShareModal.jsx @@ -1,15 +1,16 @@ import React, { PropTypes } from 'react'; import InlineSVG from 'react-inlinesvg'; + const exitUrl = require('../../../images/exit.svg'); class ShareModal extends React.Component { componentDidMount() { - this.refs.shareModal.focus(); + this.shareModal.focus(); } render() { const hostname = window.location.origin; return ( -
    +
    { this.shareModal = element; }} tabIndex="0">

    Share Sketch

    - + `} />
    - +
    - +
    @@ -48,7 +52,7 @@ class ShareModal extends React.Component { ShareModal.propTypes = { projectId: PropTypes.string.isRequired, closeShareModal: PropTypes.func.isRequired, - ownerUsername: PropTypes.string + ownerUsername: PropTypes.string.isRequired }; export default ShareModal; diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx index 7c1a3ff1..f73bb7b6 100644 --- a/client/modules/IDE/components/Sidebar.jsx +++ b/client/modules/IDE/components/Sidebar.jsx @@ -1,10 +1,10 @@ import React, { PropTypes } from 'react'; import classNames from 'classnames'; import InlineSVG from 'react-inlinesvg'; -// import SidebarItem from './SidebarItem'; +import ConnectedFileNode from './FileNode'; + const folderUrl = require('../../../images/folder.svg'); const downArrowUrl = require('../../../images/down-arrow.svg'); -import ConnectedFileNode from './FileNode'; class Sidebar extends React.Component { constructor(props) { @@ -22,14 +22,14 @@ class Sidebar extends React.Component { if (this.props.projectOptionsVisible) { this.props.closeProjectOptions(); } else { - this.refs.sidebarOptions.focus(); + this.sidebarOptions.focus(); this.props.openProjectOptions(); } } render() { const sidebarClass = classNames({ - sidebar: true, + 'sidebar': true, 'sidebar--contracted': !this.props.isExpanded, 'sidebar--project-options': this.props.projectOptionsVisible }); @@ -48,7 +48,7 @@ class Sidebar extends React.Component { aria-label="add file or folder" className="sidebar__add" tabIndex="0" - ref="sidebarOptions" + ref={(element) => { this.sidebarOptions = element; }} onClick={this.toggleProjectOptions} onBlur={() => setTimeout(this.props.closeProjectOptions, 200)} > @@ -56,14 +56,14 @@ class Sidebar extends React.Component {
    @@ -75,17 +75,14 @@ class Sidebar extends React.Component { } Sidebar.propTypes = { - files: PropTypes.array.isRequired, + files: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + id: PropTypes.string.isRequired + })).isRequired, setSelectedFile: PropTypes.func.isRequired, isExpanded: PropTypes.bool.isRequired, projectOptionsVisible: PropTypes.bool.isRequired, newFile: PropTypes.func.isRequired, - showFileOptions: PropTypes.func.isRequired, - hideFileOptions: PropTypes.func.isRequired, - deleteFile: PropTypes.func.isRequired, - showEditFileName: PropTypes.func.isRequired, - hideEditFileName: PropTypes.func.isRequired, - updateFileName: PropTypes.func.isRequired, openProjectOptions: PropTypes.func.isRequired, closeProjectOptions: PropTypes.func.isRequired, newFolder: PropTypes.func.isRequired diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index d26479d5..a60a1add 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -3,10 +3,11 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import moment from 'moment'; import { Link, browserHistory } from 'react-router'; +import InlineSVG from 'react-inlinesvg'; import * as SketchActions from '../actions/projects'; import * as ProjectActions from '../actions/project'; import * as ToastActions from '../actions/toast'; -import InlineSVG from 'react-inlinesvg'; + const exitUrl = require('../../../images/exit.svg'); const trashCan = require('../../../images/trash-can.svg'); @@ -47,10 +48,11 @@ class SketchList extends React.Component { {this.props.sketches.map(sketch => - browserHistory.push(`/${username}/sketches/${sketch._id}`)} + onClick={() => browserHistory.push(`/${username}/sketches/${sketch.id}`)} > {(() => { // eslint-disable-line @@ -71,7 +73,7 @@ class SketchList extends React.Component { } })()} - {sketch.name} + {sketch.name} {moment(sketch.createdAt).format('MMM D, YYYY h:mm A')} {moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')} @@ -85,14 +87,23 @@ class SketchList extends React.Component { } SketchList.propTypes = { - user: PropTypes.object.isRequired, + user: PropTypes.shape({ + username: PropTypes.string + }).isRequired, getProjects: PropTypes.func.isRequired, - sketches: PropTypes.array.isRequired, + sketches: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired + })).isRequired, username: PropTypes.string, deleteProject: PropTypes.func.isRequired, previousPath: PropTypes.string.isRequired, - showToast: PropTypes.func.isRequired, - setToastText: PropTypes.func.isRequired +}; + +SketchList.defaultProps = { + username: undefined }; function mapStateToProps(state) { diff --git a/client/modules/IDE/components/TextOutput.jsx b/client/modules/IDE/components/TextOutput.jsx index b33343f6..bdd7c35a 100644 --- a/client/modules/IDE/components/TextOutput.jsx +++ b/client/modules/IDE/components/TextOutput.jsx @@ -2,14 +2,14 @@ import React from 'react'; class TextOutput extends React.Component { componentDidMount() { - this.refs.canvasTextOutput.focus(); + this.canvasTextOutput.focus(); } render() { return (
    { this.canvasTextOutput = element; }} tabIndex="0" aria-label="text-output" title="canvas text output" diff --git a/client/modules/IDE/components/Timer.jsx b/client/modules/IDE/components/Timer.jsx index e82873e9..e29d3f5e 100644 --- a/client/modules/IDE/components/Timer.jsx +++ b/client/modules/IDE/components/Timer.jsx @@ -49,4 +49,8 @@ Timer.propTypes = { isUserOwner: PropTypes.bool }; +Timer.defaultProps = { + isUserOwner: false +}; + export default Timer; diff --git a/client/modules/IDE/components/Toast.jsx b/client/modules/IDE/components/Toast.jsx index 8b56c5fd..fe2e788d 100644 --- a/client/modules/IDE/components/Toast.jsx +++ b/client/modules/IDE/components/Toast.jsx @@ -2,9 +2,10 @@ import React, { PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import InlineSVG from 'react-inlinesvg'; -const exitUrl = require('../../../images/exit.svg'); import * as ToastActions from '../actions/toast'; +const exitUrl = require('../../../images/exit.svg'); + function Toast(props) { return (
    diff --git a/client/modules/IDE/components/Toolbar.jsx b/client/modules/IDE/components/Toolbar.jsx index e522cdff..b1ce6b0d 100644 --- a/client/modules/IDE/components/Toolbar.jsx +++ b/client/modules/IDE/components/Toolbar.jsx @@ -1,12 +1,13 @@ import React, { PropTypes } from 'react'; import { Link } from 'react-router'; -const InlineSVG = require('react-inlinesvg'); +import classNames from 'classnames'; +import InlineSVG from 'react-inlinesvg'; + const playUrl = require('../../../images/play.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); const stopUrl = require('../../../images/stop.svg'); const preferencesUrl = require('../../../images/preferences.svg'); const editProjectNameUrl = require('../../../images/pencil.svg'); -import classNames from 'classnames'; class Toolbar extends React.Component { constructor(props) { @@ -38,19 +39,19 @@ class Toolbar extends React.Component { } render() { - let playButtonClass = classNames({ + const playButtonClass = classNames({ 'toolbar__play-button': true, 'toolbar__play-button--selected': this.props.isPlaying }); - let stopButtonClass = classNames({ + const stopButtonClass = classNames({ 'toolbar__stop-button': true, 'toolbar__stop-button--selected': !this.props.isPlaying }); - let preferencesButtonClass = classNames({ + const preferencesButtonClass = classNames({ 'toolbar__preferences-button': true, 'toolbar__preferences-button--selected': this.props.preferencesIsVisible }); - let nameContainerClass = classNames({ + const nameContainerClass = classNames({ 'toolbar__project-name-container': true, 'toolbar__project-name-container--editing': this.props.project.isEditingName }); @@ -110,7 +111,7 @@ class Toolbar extends React.Component { e.preventDefault(); this.originalProjectName = this.props.project.name; this.props.showEditProjectName(); - setTimeout(() => this.refs.projectNameInput.focus(), 0); + setTimeout(() => this.projectNameInput.focus(), 0); } }} > @@ -122,7 +123,7 @@ class Toolbar extends React.Component { className="toolbar__project-name-input" value={this.props.project.name} onChange={this.handleProjectNameChange} - ref="projectNameInput" + ref={(element) => { this.projectNameInput = element; }} onBlur={() => { this.validateProjectName(); this.props.hideEditProjectName(); @@ -157,7 +158,6 @@ class Toolbar extends React.Component { Toolbar.propTypes = { isPlaying: PropTypes.bool.isRequired, preferencesIsVisible: PropTypes.bool.isRequired, - startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, startTextOutput: PropTypes.func.isRequired, stopTextOutput: PropTypes.func.isRequired, @@ -182,4 +182,9 @@ Toolbar.propTypes = { clearConsole: PropTypes.func.isRequired }; +Toolbar.defaultProps = { + owner: undefined, + currentUser: undefined +}; + export default Toolbar; diff --git a/client/modules/IDE/pages/FullView.jsx b/client/modules/IDE/pages/FullView.jsx index 9866d873..5eb2979f 100644 --- a/client/modules/IDE/pages/FullView.jsx +++ b/client/modules/IDE/pages/FullView.jsx @@ -38,18 +38,34 @@ class FullView extends React.Component { FullView.propTypes = { params: PropTypes.shape({ project_id: PropTypes.string - }), + }).isRequired, project: PropTypes.shape({ name: PropTypes.string, owner: PropTypes.shape({ username: PropTypes.string }) }).isRequired, - htmlFile: PropTypes.object, - jsFiles: PropTypes.array, - cssFiles: PropTypes.array, + htmlFile: PropTypes.shape({ + id: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + jsFiles: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + })).isRequired, + cssFiles: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + })).isRequired, getProject: PropTypes.func.isRequired, - files: PropTypes.array + files: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + })).isRequired, }; function mapStateToProps(state) { diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index ed8c12c2..af689876 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -1,4 +1,8 @@ import React, { PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import SplitPane from 'react-split-pane'; import Editor from '../components/Editor'; import Sidebar from '../components/Sidebar'; import PreviewFrame from '../components/PreviewFrame'; @@ -13,9 +17,6 @@ import ErrorModal from '../components/ErrorModal'; import Nav from '../../../components/Nav'; import Console from '../components/Console'; import Toast from '../components/Toast'; -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; import * as FileActions from '../actions/files'; import * as IDEActions from '../actions/ide'; import * as ProjectActions from '../actions/project'; @@ -25,7 +26,6 @@ import * as UserActions from '../../User/actions'; import * as ToastActions from '../actions/toast'; import * as ConsoleActions from '../actions/console'; import { getHTMLFile } from '../reducers/files'; -import SplitPane from 'react-split-pane'; import Overlay from '../../App/components/Overlay'; import SketchList from '../components/SketchList'; import About from '../components/About'; @@ -55,7 +55,7 @@ class IDEView extends React.Component { this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; document.addEventListener('keydown', this.handleGlobalKeydown, false); - this.props.router.setRouteLeaveHook(this.props.route, (route) => this.warnIfUnsavedChanges(route)); + this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route)); window.onbeforeunload = () => this.warnIfUnsavedChanges(); @@ -104,7 +104,7 @@ class IDEView extends React.Component { } if (this.props.route.path !== prevProps.route.path) { - this.props.router.setRouteLeaveHook(this.props.route, (route) => this.warnIfUnsavedChanges(route)); + this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route)); } } @@ -120,16 +120,16 @@ class IDEView extends React.Component { } _handleConsolePaneOnDragFinished() { - this.consoleSize = this.refs.consolePane.state.draggedSize; - this.refs.consolePane.setState({ + this.consoleSize = this.consolePane.state.draggedSize; + this.consolePane.setState({ resized: false, draggedSize: undefined, }); } _handleSidebarPaneOnDragFinished() { - this.sidebarSize = this.refs.sidebarPane.state.draggedSize; - this.refs.sidebarPane.setState({ + this.sidebarSize = this.sidebarPane.state.draggedSize; + this.sidebarPane.setState({ resized: false, draggedSize: undefined }); @@ -140,7 +140,7 @@ class IDEView extends React.Component { if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) { e.preventDefault(); e.stopPropagation(); - if (this.isUserOwner() || this.props.user.authenticated && !this.props.project.owner) { + if (this.isUserOwner() || (this.props.user.authenticated && !this.props.project.owner)) { this.props.saveProject(); } // 13 === enter @@ -170,7 +170,7 @@ class IDEView extends React.Component { warnIfUnsavedChanges(route) { // eslint-disable-line if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) { // don't warn - } else if (route && this.props.location.pathname === '/login' || this.props.location.pathname === '/signup') { + } else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) { // don't warn } else if (this.props.ide.unsavedChanges) { if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) { @@ -202,7 +202,6 @@ class IDEView extends React.Component { { this.sidebarPane = element; }} onDragFinished={this._handleSidebarPaneOnDragFinished} allowResize={this.props.ide.sidebarIsExpanded} minSize={20} @@ -270,15 +269,15 @@ class IDEView extends React.Component { (this.refs.overlay.style.display = 'block')} - onDragFinished={() => (this.refs.overlay.style.display = 'none')} + onChange={() => (this.overlay.style.display = 'block')} + onDragFinished={() => (this.overlay.style.display = 'none')} > { this.consolePane = element; }} onDragFinished={this._handleConsolePaneOnDragFinished} allowResize={this.props.ide.consoleIsExpanded} className="editor-preview-subpanel" @@ -286,7 +285,6 @@ class IDEView extends React.Component {
    -
    +
    { this.overlay = element; }}>
    - {(() => { - if ((this.props.preferences.textOutput && this.props.ide.isPlaying) || this.props.ide.isTextOutputPlaying) { - return ( - - ); - } - return ''; - })()} + {(() => { + if ((this.props.preferences.textOutput && this.props.ide.isPlaying) || this.props.ide.isTextOutputPlaying) { + return ( + + ); + } + return ''; + })()}
    @@ -449,10 +443,10 @@ IDEView.propTypes = { project_id: PropTypes.string, username: PropTypes.string, reset_password_token: PropTypes.string, - }), + }).isRequired, location: PropTypes.shape({ pathname: PropTypes.string - }), + }).isRequired, getProject: PropTypes.func.isRequired, user: PropTypes.shape({ authenticated: PropTypes.bool.isRequired, @@ -483,12 +477,9 @@ IDEView.propTypes = { justOpenedProject: PropTypes.bool.isRequired, errorType: PropTypes.string }).isRequired, - startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, startTextOutput: PropTypes.func.isRequired, stopTextOutput: PropTypes.func.isRequired, - detectInfiniteLoops: PropTypes.func.isRequired, - resetInfiniteLoops: PropTypes.func.isRequired, project: PropTypes.shape({ id: PropTypes.string, name: PropTypes.string.isRequired, @@ -501,11 +492,9 @@ IDEView.propTypes = { openPreferences: PropTypes.func.isRequired, editorAccessibility: PropTypes.shape({ lintMessages: PropTypes.array.isRequired, - lineNumber: PropTypes.string.isRequired }).isRequired, updateLintMessage: PropTypes.func.isRequired, clearLintMessage: PropTypes.func.isRequired, - updateLineNumber: PropTypes.func.isRequired, preferences: PropTypes.shape({ fontSize: PropTypes.number.isRequired, indentationAmount: PropTypes.number.isRequired, @@ -524,14 +513,22 @@ IDEView.propTypes = { setAutosave: PropTypes.func.isRequired, setLintWarning: PropTypes.func.isRequired, setTextOutput: PropTypes.func.isRequired, - files: PropTypes.array.isRequired, + files: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + content: PropTypes.string.isRequired + })).isRequired, updateFileContent: PropTypes.func.isRequired, selectedFile: PropTypes.shape({ id: PropTypes.string.isRequired, content: PropTypes.string.isRequired - }), + }).isRequired, setSelectedFile: PropTypes.func.isRequired, - htmlFile: PropTypes.object.isRequired, + htmlFile: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + content: PropTypes.string.isRequired + }).isRequired, dispatchConsoleEvent: PropTypes.func.isRequired, newFile: PropTypes.func.isRequired, closeNewFileModal: PropTypes.func.isRequired, @@ -565,13 +562,11 @@ IDEView.propTypes = { toast: PropTypes.shape({ isVisible: PropTypes.bool.isRequired }).isRequired, - showToast: PropTypes.func.isRequired, - setToastText: PropTypes.func.isRequired, autosaveProject: PropTypes.func.isRequired, router: PropTypes.shape({ setRouteLeaveHook: PropTypes.func }).isRequired, - route: PropTypes.object.isRequired, + route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired, setUnsavedChanges: PropTypes.func.isRequired, setTheme: PropTypes.func.isRequired, setAutorefresh: PropTypes.func.isRequired, @@ -580,8 +575,10 @@ IDEView.propTypes = { startRefreshSketch: PropTypes.func.isRequired, setBlobUrl: PropTypes.func.isRequired, setPreviousPath: PropTypes.func.isRequired, - resetProject: PropTypes.func.isRequired, - console: PropTypes.array.isRequired, + console: PropTypes.arrayOf(PropTypes.shape({ + method: PropTypes.string.isRequired, + args: PropTypes.arrayOf(PropTypes.string) + })).isRequired, clearConsole: PropTypes.func.isRequired, showErrorModal: PropTypes.func.isRequired, hideErrorModal: PropTypes.func.isRequired diff --git a/client/modules/IDE/reducers/console.js b/client/modules/IDE/reducers/console.js index 215865c0..294342ff 100644 --- a/client/modules/IDE/reducers/console.js +++ b/client/modules/IDE/reducers/console.js @@ -2,11 +2,18 @@ import * as ActionTypes from '../../../constants'; const consoleMax = 200; const initialState = []; +let messageId = 0; const console = (state = initialState, action) => { + let messages; switch (action.type) { case ActionTypes.CONSOLE_EVENT: - return state.concat(action.event).slice(-consoleMax); + messages = [...action.event]; + messages.forEach((message) => { + message.id = messageId; + messageId += 1; + }); + return state.concat(messages).slice(-consoleMax); case ActionTypes.CLEAR_CONSOLE: return []; default: diff --git a/client/modules/IDE/reducers/editorAccessibility.js b/client/modules/IDE/reducers/editorAccessibility.js index 03defd11..4806fa42 100644 --- a/client/modules/IDE/reducers/editorAccessibility.js +++ b/client/modules/IDE/reducers/editorAccessibility.js @@ -1,21 +1,20 @@ import * as ActionTypes from '../../../constants'; const initialState = { - lineNumber: 'line', lintMessages: [] }; +let messageId = 0; const editorAccessibility = (state = initialState, action) => { switch (action.type) { case ActionTypes.UPDATE_LINT_MESSAGE: + messageId += 1; return Object.assign({}, state, { lintMessages: state.lintMessages.concat( - { severity: action.severity, line: action.line, message: action.message }) + { severity: action.severity, line: action.line, message: action.message, id: messageId }) }); case ActionTypes.CLEAR_LINT_MESSAGE: return Object.assign({}, state, { lintMessages: [] }); - case ActionTypes.UPDATE_LINENUMBER: - return Object.assign({}, state, { lineNumber: `line ${action.lineNumber}` }); default: return state; } diff --git a/client/modules/IDE/reducers/files.js b/client/modules/IDE/reducers/files.js index 4e9139b4..9cb624de 100644 --- a/client/modules/IDE/reducers/files.js +++ b/client/modules/IDE/reducers/files.js @@ -1,5 +1,5 @@ -import * as ActionTypes from '../../../constants'; import objectID from 'bson-objectid'; +import * as ActionTypes from '../../../constants'; const defaultSketch = `function setup() { createCanvas(400, 400); @@ -42,7 +42,8 @@ const initialState = () => { id: r, _id: r, children: [a, b, c], - fileType: 'folder' + fileType: 'folder', + content: '' }, { name: 'sketch.js', @@ -92,7 +93,7 @@ function deleteChild(state, parentId, id) { function deleteMany(state, ids) { const newState = [...state]; - ids.forEach(id => { + ids.forEach((id) => { let fileIndex; newState.find((file, index) => { if (file.id === id) { @@ -111,7 +112,7 @@ const files = (state, action) => { } switch (action.type) { case ActionTypes.UPDATE_FILE_CONTENT: - return state.map(file => { + return state.map((file) => { if (file.name !== action.name) { return file; } @@ -119,7 +120,7 @@ const files = (state, action) => { return Object.assign({}, file, { content: action.content }); }); case ActionTypes.SET_BLOB_URL: - return state.map(file => { + return state.map((file) => { if (file.name !== action.name) { return file; } @@ -151,7 +152,7 @@ const files = (state, action) => { fileType: action.fileType || 'file' }]; } case ActionTypes.SHOW_FILE_OPTIONS: - return state.map(file => { + return state.map((file) => { if (file.id !== action.id) { return file; } @@ -159,7 +160,7 @@ const files = (state, action) => { return Object.assign({}, file, { isOptionsOpen: true }); }); case ActionTypes.HIDE_FILE_OPTIONS: - return state.map(file => { + return state.map((file) => { if (file.id !== action.id) { return file; } @@ -167,7 +168,7 @@ const files = (state, action) => { return Object.assign({}, file, { isOptionsOpen: false }); }); case ActionTypes.UPDATE_FILE_NAME: - return state.map(file => { + return state.map((file) => { if (file.id !== action.id) { return file; } @@ -188,7 +189,7 @@ const files = (state, action) => { // return newState.filter(file => file.id !== action.id); } case ActionTypes.SHOW_EDIT_FILE_NAME: - return state.map(file => { + return state.map((file) => { if (file.id !== action.id) { return file; } @@ -196,7 +197,7 @@ const files = (state, action) => { return Object.assign({}, file, { isEditingName: true }); }); case ActionTypes.HIDE_EDIT_FILE_NAME: - return state.map(file => { + return state.map((file) => { if (file.id !== action.id) { return file; } @@ -204,21 +205,21 @@ const files = (state, action) => { return Object.assign({}, file, { isEditingName: false }); }); case ActionTypes.SET_SELECTED_FILE: - return state.map(file => { + return state.map((file) => { if (file.id === action.selectedFile) { return Object.assign({}, file, { isSelectedFile: true }); } return Object.assign({}, file, { isSelectedFile: false }); }); case ActionTypes.SHOW_FOLDER_CHILDREN: - return state.map(file => { + return state.map((file) => { if (file.id === action.id) { return Object.assign({}, file, { isFolderClosed: false }); } return file; }); case ActionTypes.HIDE_FOLDER_CHILDREN: - return state.map(file => { + return state.map((file) => { if (file.id === action.id) { return Object.assign({}, file, { isFolderClosed: true }); } @@ -229,9 +230,9 @@ const files = (state, action) => { } }; -export const getHTMLFile = (state) => state.filter(file => file.name.match(/.*\.html$/i))[0]; -export const getJSFiles = (state) => state.filter(file => file.name.match(/.*\.js$/i)); -export const getCSSFiles = (state) => state.filter(file => file.name.match(/.*\.css$/i)); -export const getLinkedFiles = (state) => state.filter(file => file.url); +export const getHTMLFile = state => state.filter(file => file.name.match(/.*\.html$/i))[0]; +export const getJSFiles = state => state.filter(file => file.name.match(/.*\.js$/i)); +export const getCSSFiles = state => state.filter(file => file.name.match(/.*\.css$/i)); +export const getLinkedFiles = state => state.filter(file => file.url); export default files; diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index 2a89d519..e09e72ce 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -1,5 +1,5 @@ -import * as ActionTypes from '../../../constants'; import generate from 'project-name-generator'; +import * as ActionTypes from '../../../constants'; const initialState = () => { const generatedString = generate({ words: 2 }).spaced; diff --git a/client/modules/IDE/reducers/projects.js b/client/modules/IDE/reducers/projects.js index f167c8a3..05acd147 100644 --- a/client/modules/IDE/reducers/projects.js +++ b/client/modules/IDE/reducers/projects.js @@ -5,7 +5,7 @@ const sketches = (state = [], action) => { case ActionTypes.SET_PROJECTS: return action.projects; case ActionTypes.DELETE_PROJECT: - return state.filter((sketch) => + return state.filter(sketch => sketch.id !== action.id ); default: diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 21617ac5..89818ee8 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -1,6 +1,6 @@ -import * as ActionTypes from '../../constants'; import { browserHistory } from 'react-router'; import axios from 'axios'; +import * as ActionTypes from '../../constants'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; @@ -16,9 +16,9 @@ export function authError(error) { export function signUpUser(previousPath, formValues) { return (dispatch) => { axios.post(`${ROOT_URL}/signup`, formValues, { withCredentials: true }) - .then(response => { + .then((response) => { dispatch({ type: ActionTypes.AUTH_USER, - user: response.data + user: response.data }); dispatch(justOpenedProject()); browserHistory.push(previousPath); @@ -48,9 +48,9 @@ export function loginUserFailure(error) { export function validateAndLoginUser(previousPath, formProps, dispatch) { return new Promise((resolve, reject) => { loginUser(formProps) - .then(response => { + .then((response) => { dispatch({ type: ActionTypes.AUTH_USER, - user: response.data + user: response.data }); dispatch({ type: ActionTypes.SET_PREFERENCES, @@ -60,7 +60,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) { browserHistory.push(previousPath); resolve(); }) - .catch(response => { + .catch((response) => { reject({ password: response.data.message, _error: 'Login failed!' }); }); }); @@ -69,7 +69,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) { export function getUser() { return (dispatch) => { axios.get(`${ROOT_URL}/session`, { withCredentials: true }) - .then(response => { + .then((response) => { dispatch({ type: ActionTypes.AUTH_USER, user: response.data @@ -79,7 +79,7 @@ export function getUser() { preferences: response.data.preferences }); }) - .catch(response => { + .catch((response) => { dispatch(authError(response.data.error)); }); }; @@ -88,13 +88,13 @@ export function getUser() { export function validateSession() { return (dispatch, getState) => { axios.get(`${ROOT_URL}/session`, { withCredentials: true }) - .then(response => { + .then((response) => { const state = getState(); if (state.user.username !== response.data.username) { dispatch(showErrorModal('staleSession')); } }) - .catch(response => { + .catch((response) => { if (response.status === 404) { dispatch(showErrorModal('staleSession')); } diff --git a/client/modules/User/components/LoginForm.jsx b/client/modules/User/components/LoginForm.jsx index 31047940..b1049049 100644 --- a/client/modules/User/components/LoginForm.jsx +++ b/client/modules/User/components/LoginForm.jsx @@ -40,9 +40,14 @@ LoginForm.propTypes = { handleSubmit: PropTypes.func.isRequired, validateAndLoginUser: PropTypes.func.isRequired, submitting: PropTypes.bool, - invalid: PropTypes.bool, pristine: PropTypes.bool, previousPath: PropTypes.string.isRequired }; +LoginForm.defaultProps = { + submitting: false, + pristine: true, + invalid: false +}; + export default LoginForm; diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.jsx index 3dc57eba..6e28de49 100644 --- a/client/modules/User/components/NewPasswordForm.jsx +++ b/client/modules/User/components/NewPasswordForm.jsx @@ -44,7 +44,13 @@ NewPasswordForm.propTypes = { pristine: PropTypes.bool, params: PropTypes.shape({ reset_password_token: PropTypes.string, - }), + }).isRequired, +}; + +NewPasswordForm.defaultProps = { + invalid: false, + pristine: true, + submitting: false }; export default NewPasswordForm; diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx index cbc3c14f..2c3dd4c9 100644 --- a/client/modules/User/components/ResetPasswordForm.jsx +++ b/client/modules/User/components/ResetPasswordForm.jsx @@ -31,7 +31,13 @@ ResetPasswordForm.propTypes = { pristine: PropTypes.bool, user: PropTypes.shape({ resetPasswordInitiate: PropTypes.bool - }) + }).isRequired +}; + +ResetPasswordForm.defaultProps = { + submitting: false, + pristine: true, + invalid: false }; export default ResetPasswordForm; diff --git a/client/modules/User/components/SignupForm.jsx b/client/modules/User/components/SignupForm.jsx index ffc33cf6..58513d1e 100644 --- a/client/modules/User/components/SignupForm.jsx +++ b/client/modules/User/components/SignupForm.jsx @@ -69,4 +69,10 @@ SignupForm.propTypes = { previousPath: PropTypes.string.isRequired }; +SignupForm.defaultProps = { + submitting: false, + pristine: true, + invalid: false +}; + export default SignupForm; diff --git a/client/modules/User/pages/LoginView.jsx b/client/modules/User/pages/LoginView.jsx index 2cabbeef..255d6408 100644 --- a/client/modules/User/pages/LoginView.jsx +++ b/client/modules/User/pages/LoginView.jsx @@ -1,10 +1,10 @@ import React, { PropTypes } from 'react'; import { reduxForm } from 'redux-form'; +import { Link, browserHistory } from 'react-router'; +import InlineSVG from 'react-inlinesvg'; import { validateAndLoginUser } from '../actions'; import LoginForm from '../components/LoginForm'; // import GithubButton from '../components/GithubButton'; -import { Link, browserHistory } from 'react-router'; -import InlineSVG from 'react-inlinesvg'; const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); @@ -41,7 +41,7 @@ class LoginView extends React.Component { {/*

    Or

    */}

    - Don't have an account?  + Don't have an account?  Sign Up

    diff --git a/client/modules/User/pages/NewPasswordView.jsx b/client/modules/User/pages/NewPasswordView.jsx index d1f0bd4b..03f6da64 100644 --- a/client/modules/User/pages/NewPasswordView.jsx +++ b/client/modules/User/pages/NewPasswordView.jsx @@ -1,11 +1,12 @@ import React, { PropTypes } from 'react'; import { reduxForm } from 'redux-form'; -import NewPasswordForm from '../components/NewPasswordForm'; -import * as UserActions from '../actions'; -import { bindActionCreators } from 'redux'; import classNames from 'classnames'; import { browserHistory } from 'react-router'; import InlineSVG from 'react-inlinesvg'; +import { bindActionCreators } from 'redux'; +import NewPasswordForm from '../components/NewPasswordForm'; +import * as UserActions from '../actions'; + const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); @@ -55,11 +56,11 @@ class NewPasswordView extends React.Component { NewPasswordView.propTypes = { params: PropTypes.shape({ reset_password_token: PropTypes.string, - }), + }).isRequired, validateResetPasswordToken: PropTypes.func.isRequired, user: PropTypes.shape({ resetPasswordInvalid: PropTypes.bool - }) + }).isRequired }; function validate(formProps) { diff --git a/client/modules/User/pages/ResetPasswordView.jsx b/client/modules/User/pages/ResetPasswordView.jsx index 230bdc4e..1badd774 100644 --- a/client/modules/User/pages/ResetPasswordView.jsx +++ b/client/modules/User/pages/ResetPasswordView.jsx @@ -1,11 +1,12 @@ import React, { PropTypes } from 'react'; import { Link, browserHistory } from 'react-router'; -import * as UserActions from '../actions'; -import { bindActionCreators } from 'redux'; -import { reduxForm } from 'redux-form'; -import ResetPasswordForm from '../components/ResetPasswordForm'; import classNames from 'classnames'; import InlineSVG from 'react-inlinesvg'; +import { bindActionCreators } from 'redux'; +import { reduxForm } from 'redux-form'; +import * as UserActions from '../actions'; +import ResetPasswordForm from '../components/ResetPasswordForm'; + const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); @@ -43,7 +44,7 @@ class ResetPasswordView extends React.Component {

    Reset Your Password

    - Your password reset email should arrive shortly. If you don't see it, check + Your password reset email should arrive shortly. If you don't see it, check in your spam folder as sometimes it can end up there.

    diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.jsx index f43310a2..d5759957 100644 --- a/client/modules/User/pages/SignupView.jsx +++ b/client/modules/User/pages/SignupView.jsx @@ -1,11 +1,12 @@ import React, { PropTypes } from 'react'; import { bindActionCreators } from 'redux'; -import * as UserActions from '../actions'; -import { reduxForm } from 'redux-form'; -import SignupForm from '../components/SignupForm'; import axios from 'axios'; import { Link, browserHistory } from 'react-router'; import InlineSVG from 'react-inlinesvg'; +import { reduxForm } from 'redux-form'; +import * as UserActions from '../actions'; +import SignupForm from '../components/SignupForm'; + const exitUrl = require('../../../images/exit.svg'); const logoUrl = require('../../../images/p5js-logo.svg'); @@ -66,7 +67,7 @@ function asyncValidate(formProps, dispatch, props) { queryParams[fieldToValidate] = formProps[fieldToValidate]; queryParams.check_type = fieldToValidate; return axios.get('/api/signup/duplicate_check', { params: queryParams }) - .then(response => { + .then((response) => { if (response.data.exists) { const error = {}; error[fieldToValidate] = response.data.message; @@ -90,7 +91,7 @@ function validate(formProps) { if (!formProps.email) { errors.email = 'Please enter an email.'; - } else if (!formProps.email.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) { + } else if (!formProps.email.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) { errors.email = 'Please enter a valid email address.'; } diff --git a/client/modules/User/reducers.js b/client/modules/User/reducers.js index 9e3b5b9c..04220f58 100644 --- a/client/modules/User/reducers.js +++ b/client/modules/User/reducers.js @@ -4,7 +4,7 @@ const user = (state = { authenticated: false }, action) => { switch (action.type) { case ActionTypes.AUTH_USER: return { ...action.user, - authenticated: true }; + authenticated: true }; case ActionTypes.UNAUTH_USER: return { authenticated: false diff --git a/client/reducers.js b/client/reducers.js index b31912e8..4505623a 100644 --- a/client/reducers.js +++ b/client/reducers.js @@ -1,4 +1,5 @@ import { combineReducers } from 'redux'; +import { reducer as form } from 'redux-form'; import files from './modules/IDE/reducers/files'; import ide from './modules/IDE/reducers/ide'; import preferences from './modules/IDE/reducers/preferences'; @@ -8,7 +9,6 @@ import user from './modules/User/reducers'; import sketches from './modules/IDE/reducers/projects'; import toast from './modules/IDE/reducers/toast'; import console from './modules/IDE/reducers/console'; -import { reducer as form } from 'redux-form'; const rootReducer = combineReducers({ form, diff --git a/client/routes.jsx b/client/routes.jsx index 45b2b326..50159175 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -14,21 +14,21 @@ const checkAuth = (store) => { store.dispatch(getUser()); }; -const routes = (store) => +const routes = store => ( - - - - - - - - - - - - - + + + + + + + + + + + + + ); export default routes; diff --git a/client/styles/abstracts/_placeholders.scss b/client/styles/abstracts/_placeholders.scss index 93e72b2d..fc1a4876 100644 --- a/client/styles/abstracts/_placeholders.scss +++ b/client/styles/abstracts/_placeholders.scss @@ -175,3 +175,16 @@ height:1px; overflow:hidden; } + + +%link { + @include themify() { + text-decoration: none; + color: getThemifyVariable('inactive-text-color'); + cursor: pointer; + &:hover { + text-decoration: none; + color: getThemifyVariable('primary-text-color'); + } + } +} \ No newline at end of file diff --git a/client/styles/base/_base.scss b/client/styles/base/_base.scss index ea422343..75ae3c76 100644 --- a/client/styles/base/_base.scss +++ b/client/styles/base/_base.scss @@ -6,10 +6,13 @@ html, body { font-size: #{$base-font-size}px; } -body, input, button { +body, input { @include themify() { color: getThemifyVariable('primary-text-color'); } +} + +body, input, button { font-family: Montserrat, sans-serif; } @@ -20,13 +23,7 @@ body, input, button { a { @include themify() { - text-decoration: none; - color: getThemifyVariable('inactive-text-color'); - cursor: pointer; - &:hover { - text-decoration: none; - color: getThemifyVariable('primary-text-color'); - } + @extend %link; } } @@ -34,10 +31,6 @@ input, button { font-size: 1rem; } -button:focus { - outline: none; -} - input { padding: #{5 / $base-font-size}rem; border: 1px solid ; @@ -55,6 +48,14 @@ input[type="submit"] { } } +button { + @include themify() { + @extend %link; + } + background: transparent; + border: none; +} + h2 { font-size: #{21 / $base-font-size}em; } @@ -71,3 +72,6 @@ h6 { thead { text-align: left; } +th { + text-align: left; +} diff --git a/client/styles/components/_console.scss b/client/styles/components/_console.scss index 68897e9e..93a55dcd 100644 --- a/client/styles/components/_console.scss +++ b/client/styles/components/_console.scss @@ -97,6 +97,11 @@ } .preview-console__clear { + @include themify() { + @extend %link; + } + background: transparent; + border: none; padding-right: #{10 / $base-font-size}rem; .preview-console--collapsed & { display: none; diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss index 926e4e55..e684cd96 100644 --- a/client/styles/components/_preferences.scss +++ b/client/styles/components/_preferences.scss @@ -24,7 +24,8 @@ margin-right: #{-6 / $base-font-size}rem; } -.preference button { +.preference__minus-button, +.preference__plus-button { @include themify() { @extend %preferences-button; width: #{32 / $base-font-size}rem; diff --git a/client/styles/components/_sidebar.scss b/client/styles/components/_sidebar.scss index a24e1e7d..7b9e999b 100644 --- a/client/styles/components/_sidebar.scss +++ b/client/styles/components/_sidebar.scss @@ -56,6 +56,7 @@ height: #{20 / $base-font-size}rem; font-size: #{12 / $base-font-size}rem; cursor: pointer; + position: relative; @include themify() { color: map-get($theme-map, 'inactive-text-color'); &:hover > .file-item__content .sidebar__file-item-name { @@ -106,6 +107,15 @@ .sidebar__file-item--editing & { display: none; } + &:before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ''; + width: 100%; + } } .sidebar__file-item-show-options { @@ -225,6 +235,8 @@ & svg { height: #{10 / $base-font-size}rem; } + background-color: transparent; + border: none; } .sidebar__file-item-closed { @@ -260,6 +272,14 @@ width: #{145 / $base-font-size}rem; } +.sidebar__file-item-option { + @include themify() { + @extend %link; + } + background-color: transparent; + border: none; +} + .sidebar__file-item--closed .file-item__children { display: none; diff --git a/package.json b/package.json index 6646923c..1c6a952d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress", "test": "echo \"Error: no test specified\" && exit 1", "fetch-examples": "node fetch-examples.js", - "postinstall" : "git submodule update --remote --recursive" + "postinstall": "git submodule update --remote --recursive" }, "main": "index.js", "author": "Cassie Tarakajian", @@ -20,27 +20,25 @@ "url": "git+https://github.com/catarak/p5.js-web-editor.git" }, "devDependencies": { - "babel-eslint": "^6.1.0", + "babel-eslint": "^7.1.1", "babel-loader": "^6.2.4", "babel-plugin-transform-react-constant-elements": "^6.8.0", "babel-plugin-transform-react-inline-elements": "^6.8.0", "babel-plugin-transform-react-remove-prop-types": "^0.2.6", - "babel-polyfill": "^6.8.0", "babel-preset-es2015": "^6.6.0", "babel-preset-es2015-native-modules": "^6.9.2", "babel-preset-react": "^6.5.0", "babel-preset-react-hmre": "^1.1.1", "babel-preset-react-optimize": "^1.0.1", "babel-preset-stage-0": "^6.5.0", - "babel-register": "^6.8.0", "chunk-manifest-webpack-plugin": "^0.1.0", "css-loader": "^0.23.1", "cssnano": "^3.7.1", - "eslint": "^2.13.1", - "eslint-config-airbnb": "^9.0.1", - "eslint-plugin-import": "^1.9.2", - "eslint-plugin-jsx-a11y": "^1.5.3", - "eslint-plugin-react": "^5.2.2", + "eslint": "^3.14.0", + "eslint-config-airbnb": "^14.0.0", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^3.0.2", + "eslint-plugin-react": "^6.9.0", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.8.5", "node-sass": "^3.7.0", @@ -49,14 +47,8 @@ "postcss-focus": "^1.0.0", "postcss-loader": "^0.9.1", "postcss-reporter": "^1.3.3", - "redux-devtools": "^3.3.1", - "redux-devtools-dock-monitor": "^1.1.1", - "redux-devtools-log-monitor": "^1.0.11", "sass-loader": "^3.2.0", "style-loader": "^0.13.1", - "webpack": "^1.14.0", - "webpack-dev-middleware": "^1.6.1", - "webpack-hot-middleware": "^2.10.0", "webpack-manifest-plugin": "^1.1.0" }, "engines": { @@ -67,6 +59,8 @@ "async": "^2.0.0", "axios": "^0.12.0", "babel-core": "^6.8.0", + "babel-polyfill": "^6.8.0", + "babel-register": "^6.8.0", "bcrypt-nodejs": "0.0.3", "blob-util": "^1.2.1", "body-parser": "^1.15.1", @@ -106,6 +100,9 @@ "react-router": "^2.6.0", "react-split-pane": "^0.1.44", "redux": "^3.5.2", + "redux-devtools": "^3.3.1", + "redux-devtools-dock-monitor": "^1.1.1", + "redux-devtools-log-monitor": "^1.0.11", "redux-form": "^5.3.3", "redux-thunk": "^2.1.0", "request": "^2.76.0", @@ -113,6 +110,9 @@ "s3-policy": "^0.2.0", "shortid": "^2.2.6", "srcdoc-polyfill": "^0.2.0", + "webpack": "^1.14.0", + "webpack-dev-middleware": "^1.6.1", + "webpack-hot-middleware": "^2.10.0", "xhr": "^2.2.1" } } diff --git a/server/config/passport.js b/server/config/passport.js index 7ec0e1ba..515f816c 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -1,9 +1,9 @@ +import User from '../models/user'; + const passport = require('passport'); const GitHubStrategy = require('passport-github').Strategy; const LocalStrategy = require('passport-local').Strategy; -import User from '../models/user'; - passport.serializeUser((user, done) => { done(null, user.id); }); @@ -30,7 +30,7 @@ passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, don return done(null, false, { msg: 'Invalid email or password.' }); }); }) - .catch((err) => done(null, false, { msg: err })); + .catch(err => done(null, false, { msg: err })); })); /** @@ -42,20 +42,19 @@ passport.use(new GitHubStrategy({ callbackURL: '/auth/github/callback', passReqToCallback: true }, (req, accessToken, refreshToken, profile, done) => { - User.findOne({ github: profile.id }, (err, existingUser) => { + User.findOne({ github: profile.id }, (findByGithubErr, existingUser) => { if (existingUser) { - return done(null, existingUser); + done(null, existingUser); + return; } - User.findOne({ email: profile._json.email }, (err, existingEmailUser) => { + User.findOne({ email: profile._json.email }, (findByEmailErr, existingEmailUser) => { if (existingEmailUser) { existingEmailUser.email = existingEmailUser.email || profile._json.email; existingEmailUser.github = profile.id; existingEmailUser.username = existingEmailUser.username || profile.username; existingEmailUser.tokens.push({ kind: 'github', accessToken }); existingEmailUser.name = existingEmailUser.name || profile.displayName; - existingEmailUser.save((err) => { - return done(null, existingEmailUser); - }); + existingEmailUser.save(saveErr => done(null, existingEmailUser)); } else { const user = new User(); user.email = profile._json.email; @@ -63,9 +62,7 @@ passport.use(new GitHubStrategy({ user.username = profile.username; user.tokens.push({ kind: 'github', accessToken }); user.name = profile.displayName; - user.save((err) => { - return done(null, user); - }); + user.save(saveErr => done(null, user)); } }); }); diff --git a/server/controllers/aws.controller.js b/server/controllers/aws.controller.js index 9ced0bf2..7a1b098c 100644 --- a/server/controllers/aws.controller.js +++ b/server/controllers/aws.controller.js @@ -26,3 +26,5 @@ export function signS3(req, res) { }; return res.json(result); } + +export default signS3; diff --git a/server/controllers/embed.controller.js b/server/controllers/embed.controller.js index 8e8db4ad..b5cfa9ef 100644 --- a/server/controllers/embed.controller.js +++ b/server/controllers/embed.controller.js @@ -1,10 +1,10 @@ +import jsdom, { serializeDocument } from 'jsdom'; import Project from '../models/project'; import { injectMediaUrls, resolvePathsForElementsWithAttribute, resolveScripts, resolveStyles } from '../utils/previewGeneration'; -import jsdom, { serializeDocument } from 'jsdom'; export function serveProject(req, res) { Project.findById(req.params.project_id) @@ -32,3 +32,5 @@ export function serveProject(req, res) { }); }); } + +export default serveProject; diff --git a/server/controllers/file.controller.js b/server/controllers/file.controller.js index dab443c8..6cf4af5a 100644 --- a/server/controllers/file.controller.js +++ b/server/controllers/file.controller.js @@ -16,16 +16,18 @@ export function createFile(req, res) { }, (err, updatedProject) => { if (err) { console.log(err); - return res.json({ success: false }); + res.json({ success: false }); + return; } const newFile = updatedProject.files[updatedProject.files.length - 1]; updatedProject.files.id(req.body.parentId).children.push(newFile.id); - updatedProject.save(innerErr => { + updatedProject.save((innerErr) => { if (innerErr) { console.log(innerErr); - return res.json({ success: false }); + res.json({ success: false }); + return; } - return res.json(updatedProject.files[updatedProject.files.length - 1]); + res.json(updatedProject.files[updatedProject.files.length - 1]); }); }); } @@ -38,13 +40,13 @@ function getAllDescendantIds(files, nodeId) { } function deleteMany(files, ids) { - ids.forEach(id => { + ids.forEach((id) => { files.id(id).remove(); }); } function deleteChild(files, parentId, id) { - files = files.map((file) => { + return files.map((file) => { if (file.id === parentId) { file.children = file.children.filter(child => child !== id); return file; @@ -57,11 +59,11 @@ export function deleteFile(req, res) { Project.findById(req.params.project_id, (err, project) => { const idsToDelete = getAllDescendantIds(project.files, req.params.file_id); deleteMany(project.files, [req.params.file_id, ...idsToDelete]); - deleteChild(project.files, req.query.parentId, req.params.file_id); + project.files = deleteChild(project.files, req.query.parentId, req.params.file_id); // project.files.id(req.params.file_id).remove(); // const childrenArray = project.files.id(req.query.parentId).children; // project.files.id(req.query.parentId).children = childrenArray.filter(id => id !== req.params.file_id); - project.save(innerErr => { + project.save((innerErr) => { res.json(project.files); }); }); @@ -70,12 +72,14 @@ export function deleteFile(req, res) { export function getFileContent(req, res) { Project.findById(req.params.project_id, (err, project) => { if (err) { - return res.status(404).send({ success: false, message: 'Project with that id does not exist.' }); + res.status(404).send({ success: false, message: 'Project with that id does not exist.' }); + return; } const filePath = req.params[0]; const resolvedFile = resolvePathToFile(filePath, project.files); if (!resolvedFile) { - return res.status(404).send({ success: false, message: 'File with that name and path does not exist.' }); + res.status(404).send({ success: false, message: 'File with that name and path does not exist.' }); + return; } res.send(resolvedFile.content); }); diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index aa4ee40d..00ce586d 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -1,13 +1,13 @@ -import Project from '../models/project'; -import User from '../models/user'; import archiver from 'archiver'; import request from 'request'; -import moment from 'moment'; +import Project from '../models/project'; +import User from '../models/user'; export function createProject(req, res) { if (!req.user) { - return res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); + res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); + return; } let projectValues = { @@ -17,20 +17,27 @@ export function createProject(req, res) { projectValues = Object.assign(projectValues, req.body); Project.create(projectValues, (err, newProject) => { - if (err) { return res.json({ success: false }); } + if (err) { + res.json({ success: false }); + return; + } Project.populate(newProject, { path: 'user', select: 'username' }, (innerErr, newProjectWithUser) => { - if (innerErr) { return res.json({ success: false }); } - return res.json(newProjectWithUser); + if (innerErr) { + res.json({ success: false }); + return; + } + res.json(newProjectWithUser); }); }); } export function updateProject(req, res) { - Project.findById(req.params.project_id, (err, project) => { + Project.findById(req.params.project_id, (findProjectErr, project) => { if (!req.user || !project.user.equals(req.user._id)) { - return res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); + res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); + return; } // if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) { // return res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' }); @@ -40,27 +47,29 @@ export function updateProject(req, res) { $set: req.body }) .populate('user', 'username') - .exec((err, updatedProject) => { - if (err) { - console.log(err); - return res.json({ success: false }); + .exec((updateProjectErr, updatedProject) => { + if (updateProjectErr) { + console.log(updateProjectErr); + res.json({ success: false }); + return; } if (updatedProject.files.length !== req.body.files.length) { const oldFileIds = updatedProject.files.map(file => file.id); const newFileIds = req.body.files.map(file => file.id); const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1); - staleIds.forEach(staleId => { + staleIds.forEach((staleId) => { updatedProject.files.id(staleId).remove(); }); updatedProject.save((innerErr) => { if (innerErr) { console.log(innerErr); - return res.json({ success: false }); + res.json({ success: false }); + return; } - return res.json(updatedProject); + res.json(updatedProject); }); } - return res.json(updatedProject); + res.json(updatedProject); }); }); } @@ -77,15 +86,17 @@ export function getProject(req, res) { } export function deleteProject(req, res) { - Project.findById(req.params.project_id, (err, project) => { + Project.findById(req.params.project_id, (findProjectErr, project) => { if (!req.user || !project.user.equals(req.user._id)) { - return res.status(403).json({ success: false, message: 'Session does not match owner of project.' }); + res.status(403).json({ success: false, message: 'Session does not match owner of project.' }); + return; } - Project.remove({ _id: req.params.project_id }, (err) => { - if (err) { - return res.status(404).send({ message: 'Project with that id does not exist' }); + Project.remove({ _id: req.params.project_id }, (removeProjectError) => { + if (removeProjectError) { + res.status(404).send({ message: 'Project with that id does not exist' }); + return; } - return res.json({ success: true }); + res.json({ success: true }); }); }); } @@ -100,7 +111,7 @@ export function getProjects(req, res) { }); } else { // could just move this to client side - return res.json([]); + res.json([]); } } @@ -108,18 +119,18 @@ export function getProjectsForUser(req, res) { if (req.params.username) { User.findOne({ username: req.params.username }, (err, user) => { if (!user) { - return res.status(404).json({ message: 'User with that username does not exist.' }); + res.status(404).json({ message: 'User with that username does not exist.' }); + return; } Project.find({ user: user._id }) // eslint-disable-line no-underscore-dangle .sort('-createdAt') .select('name files id createdAt updatedAt') - .exec((err, projects) => res.json(projects)); + .exec((innerErr, projects) => res.json(projects)); }); } else { // could just move this to client side - return res.json([]); + res.json([]); } - return null; } function buildZip(project, req, res) { @@ -127,7 +138,6 @@ function buildZip(project, req, res) { const rootFile = project.files.find(file => file.name === 'root'); const numFiles = project.files.filter(file => file.fileType !== 'folder').length; const files = project.files; - const projectName = project.name; let numCompletedFiles = 0; zip.on('error', (err) => { @@ -140,27 +150,25 @@ function buildZip(project, req, res) { function addFileToZip(file, path) { if (file.fileType === 'folder') { const newPath = file.name === 'root' ? path : `${path}${file.name}/`; - file.children.forEach(fileId => { + file.children.forEach((fileId) => { const childFile = files.find(f => f.id === fileId); (() => { addFileToZip(childFile, newPath); })(); }); - } else { - if (file.url) { - request({ method: 'GET', url: file.url, encoding: null }, (err, response, body) => { - zip.append(body, { name: `${path}${file.name}` }); - numCompletedFiles += 1; - if (numCompletedFiles === numFiles) { - zip.finalize(); - } - }); - } else { - zip.append(file.content, { name: `${path}${file.name}` }); + } else if (file.url) { + request({ method: 'GET', url: file.url, encoding: null }, (err, response, body) => { + zip.append(body, { name: `${path}${file.name}` }); numCompletedFiles += 1; if (numCompletedFiles === numFiles) { zip.finalize(); } + }); + } else { + zip.append(file.content, { name: `${path}${file.name}` }); + numCompletedFiles += 1; + if (numCompletedFiles === numFiles) { + zip.finalize(); } } } diff --git a/server/controllers/user.controller.js b/server/controllers/user.controller.js index 648fd870..306f7ee5 100644 --- a/server/controllers/user.controller.js +++ b/server/controllers/user.controller.js @@ -1,8 +1,8 @@ -import User from '../models/user'; import crypto from 'crypto'; import async from 'async'; import nodemailer from 'nodemailer'; import mg from 'nodemailer-mailgun-transport'; +import User from '../models/user'; export function createUser(req, res, next) { const user = new User({ @@ -12,17 +12,25 @@ export function createUser(req, res, next) { }); User.findOne({ email: req.body.email }, - (err, existingUser) => { // eslint-disable-line consistent-return - if (err) { res.status(404).send({ error: err }); } + (err, existingUser) => { + if (err) { + res.status(404).send({ error: err }); + return; + } if (existingUser) { - return res.status(422).send({ error: 'Email is in use' }); + res.status(422).send({ error: 'Email is in use' }); + return; } - user.save((saveErr) => { // eslint-disable-line consistent-return - if (saveErr) { return next(saveErr); } - req.logIn(user, (loginErr) => { // eslint-disable-line consistent-return + user.save((saveErr) => { + if (saveErr) { + next(saveErr); + return; + } + req.logIn(user, (loginErr) => { if (loginErr) { - return next(loginErr); + next(loginErr); + return; } res.json({ email: req.user.email, @@ -58,21 +66,24 @@ export function duplicateUserCheck(req, res) { export function updatePreferences(req, res) { User.findById(req.user.id, (err, user) => { if (err) { - return res.status(500).json({ error: err }); + res.status(500).json({ error: err }); + return; } if (!user) { - return res.status(404).json({ error: 'Document not found' }); + res.status(404).json({ error: 'Document not found' }); + return; } const preferences = Object.assign({}, user.preferences, req.body.preferences); user.preferences = preferences; - user.save((err) => { - if (err) { - return res.status(500).json({ error: err }); + user.save((saveErr) => { + if (saveErr) { + res.status(500).json({ error: saveErr }); + return; } - return res.json(user.preferences); + res.json(user.preferences); }); }); } @@ -88,13 +99,14 @@ export function resetPasswordInitiate(req, res) { (token, done) => { User.findOne({ email: req.body.email }, (err, user) => { if (!user) { - return res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); + res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); + return; } user.resetPasswordToken = token; user.resetPasswordExpires = Date.now() + 3600000; // 1 hour - user.save((err) => { - done(err, token, user); + user.save((saveErr) => { + done(saveErr, token, user); }); }); }, @@ -124,41 +136,41 @@ export function resetPasswordInitiate(req, res) { ], (err) => { if (err) { console.log(err); - return res.json({ success: false }); + res.json({ success: false }); + return; } - // send email here - return res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); + res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); }); } export function validateResetPasswordToken(req, res) { User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => { if (!user) { - return res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' }); + res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' }); + return; } res.json({ success: true }); }); } export function updatePassword(req, res) { - User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function (err, user) { + User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => { if (!user) { - return res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' }); + res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' }); + return; } user.password = req.body.password; user.resetPasswordToken = undefined; user.resetPasswordExpires = undefined; - user.save(function (err) { - req.logIn(user, function (err) { - return res.json({ - email: req.user.email, - username: req.user.username, - preferences: req.user.preferences, - id: req.user._id - }); - }); + user.save((saveErr) => { + req.logIn(user, loginErr => res.json({ + email: req.user.email, + username: req.user.username, + preferences: req.user.preferences, + id: req.user._id + })); }); }); diff --git a/server/examples.js b/server/examples.js index 658df6fa..2e2eda8d 100644 --- a/server/examples.js +++ b/server/examples.js @@ -3,10 +3,9 @@ import Q from 'q'; import mongoose from 'mongoose'; import objectID from 'bson-objectid'; import shortid from 'shortid'; +import eachSeries from 'async/eachSeries'; import User from './models/user'; import Project from './models/project'; -import async from 'async'; -import eachSeries from 'async/eachSeries'; const defaultHTML = ` @@ -30,8 +29,8 @@ const defaultCSS = } `; -const client_id = process.env.GITHUB_ID; -const client_secret = process.env.GITHUB_SECRET; +const clientId = process.env.GITHUB_ID; +const clientSecret = process.env.GITHUB_SECRET; const headers = { 'User-Agent': 'p5js-web-editor/0.0.1' }; @@ -41,144 +40,106 @@ mongoose.connection.on('error', () => { process.exit(1); }); -getp5User(); - -function getp5User() { - User.findOne({ username: 'p5' }, (err, user) => { - if (err) throw err; - - if (!user) { - user = new User({ - username: 'p5', - email: 'p5-examples@gmail.com', - password: 'test' - }); - user.save(err => { - if (err) throw err; - console.log('Created a user p5' + user); - }); - } - - Project.find({ user: user._id }, (err, projects) => { - // if there are already some sketches, delete them - console.log('Deleting old projects...'); - projects.forEach(project => { - Project.remove({ _id: project._id }, err => { - if (err) throw err; - }); - }); - }); - - return getCategories() - .then(getSketchesInCategories) - .then(getSketchContent) - .then(createProjectsInP5user); - }); -} - function getCategories() { - let categories = []; + const categories = []; const options = { - url: 'https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/en?client_id=' + - client_id + '&client_secret=' + client_secret, + url: `https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/en?client_id=${ + clientId}&client_secret=${clientSecret}`, method: 'GET', headers }; - return rp(options).then(res => { + return rp(options).then((res) => { const json = JSON.parse(res); - json.forEach(metadata => { + json.forEach((metadata) => { let category = ''; - for (let j = 1; j < metadata.name.split('_').length; j++) { - category += metadata.name.split('_')[j] + ' '; + for (let j = 1; j < metadata.name.split('_').length; j += 1) { + category += `${metadata.name.split('_')[j]} `; } categories.push({ url: metadata.url, name: category }); }); return categories; - }).catch(err => { + }).catch((err) => { throw err; }); } function getSketchesInCategories(categories) { - return Q.all(categories.map(category => { + return Q.all(categories.map((category) => { const options = { - url: category.url.replace('?ref=master', '') + '?client_id=' + client_id + '&client_secret=' + client_secret, + url: `${category.url.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`, method: 'GET', headers }; - return rp(options).then(res => { - let projectsInOneCategory = []; + return rp(options).then((res) => { + const projectsInOneCategory = []; const examples = JSON.parse(res); - examples.forEach(example => { + examples.forEach((example) => { let projectName; if (example.name === '02_Instance_Container.js') { - for (let i = 1; i < 5; i++) { - const instanceProjectName = category.name + ': ' + 'Instance Container ' + i; + for (let i = 1; i < 5; i += 1) { + const instanceProjectName = `${category.name}: Instance Container ${i}`; projectsInOneCategory.push({ sketchUrl: example.download_url, projectName: instanceProjectName }); } } else { if (example.name.split('_')[1]) { - projectName = category.name + ': ' + example.name.split('_').slice(1).join(' ').replace('.js', ''); + projectName = `${category.name}: ${example.name.split('_').slice(1).join(' ').replace('.js', '')}`; } else { - projectName = category.name + ': ' + example.name.replace('.js', ''); + projectName = `${category.name}: ${example.name.replace('.js', '')}`; } projectsInOneCategory.push({ sketchUrl: example.download_url, projectName }); } }); return projectsInOneCategory; - }).catch(err => { + }).catch((err) => { throw err; }); })); } function getSketchContent(projectsInAllCategories) { - return Q.all(projectsInAllCategories.map(projectsInOneCategory => { - return Q.all(projectsInOneCategory.map(project => { - const options = { - url: project.sketchUrl.replace('?ref=master', '') + '?client_id=' + client_id + '&client_secret=' + client_secret, - method: 'GET', - headers - }; + return Q.all(projectsInAllCategories.map(projectsInOneCategory => Q.all(projectsInOneCategory.map((project) => { + const options = { + url: `${project.sketchUrl.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`, + method: 'GET', + headers + }; - return rp(options).then(res => { - const noNumberprojectName = project.projectName.replace(/(\d+)/g, ''); - if (noNumberprojectName === 'Instance Mode : Instance Container ') { - for (let i = 0; i < 4; i++) { - let splitedRes = res.split('*/')[1].split('')[i] + '\n'; - project.sketchContent = splitedRes.replace('p5.js', 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js'); - } - } else { - project.sketchContent = res; + return rp(options).then((res) => { + const noNumberprojectName = project.projectName.replace(/(\d+)/g, ''); + if (noNumberprojectName === 'Instance Mode : Instance Container ') { + for (let i = 0; i < 4; i += 1) { + const splitedRes = `${res.split('*/')[1].split('')[i]}\n`; + project.sketchContent = splitedRes.replace('p5.js', 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js'); } - return project; - }).catch(err => { - throw err; - }); - })); - })); + } else { + project.sketchContent = res; + } + return project; + }).catch((err) => { + throw err; + }); + })))); } function createProjectsInP5user(projectsInAllCategories) { - let assetsfiles = []; const options = { - url: 'https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/assets?client_id=' + - client_id + '&client_secret=' + client_secret, + url: `https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/assets?client_id=${ + clientId}&client_secret=${clientSecret}`, method: 'GET', headers }; - rp(options).then(res => { + rp(options).then((res) => { const assets = JSON.parse(res); User.findOne({ username: 'p5' }, (err, user) => { if (err) throw err; - async.eachSeries(projectsInAllCategories, (projectsInOneCategory, categoryCallback) => { - async.eachSeries(projectsInOneCategory, (project, projectCallback) => { + eachSeries(projectsInAllCategories, (projectsInOneCategory, categoryCallback) => { + eachSeries(projectsInOneCategory, (project, projectCallback) => { let newProject; const a = objectID().toHexString(); const b = objectID().toHexString(); @@ -267,12 +228,12 @@ function createProjectsInP5user(projectsInAllCategories) { }); } - let assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) || project.sketchContent.match(/assets\/[\w-]*/g) || []; + const assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) || project.sketchContent.match(/assets\/[\w-]*/g) || []; - assetsInProject.forEach((assetName, i) => { - assetName = assetName.split('assets/')[1]; + assetsInProject.forEach((assetNamePath, i) => { + let assetName = assetNamePath.split('assets/')[1]; - assets.forEach(asset => { + assets.forEach((asset) => { if (asset.name === assetName || asset.name.split('.')[0] === assetName) { assetName = asset.name; } @@ -301,25 +262,61 @@ function createProjectsInP5user(projectsInAllCategories) { children: [], fileType: 'file' }); - console.log('create assets: ' + assetName); + console.log(`create assets: ${assetName}`); // add asset file inside the newly created assets folder at index 4 newProject.files[4].children.push(fileID); } }); - newProject.save((err, newProject) => { - if (err) throw err; - console.log('Created a new project in p5 user: ' + newProject.name); + newProject.save((saveErr, savedProject) => { + if (saveErr) throw saveErr; + console.log(`Created a new project in p5 user: ${savedProject.name}`); projectCallback(); }); - }, (err) => { + }, (categoryErr) => { categoryCallback(); }); - }, (err) => { + }, (examplesErr) => { process.exit(); }); }); - }).catch(err => { + }).catch((err) => { throw err; }); } + +function getp5User() { + User.findOne({ username: 'p5' }, (err, user) => { + if (err) throw err; + + let p5User = user; + if (!p5User) { + p5User = new User({ + username: 'p5', + email: 'p5-examples@gmail.com', + password: 'test' + }); + p5User.save((saveErr) => { + if (saveErr) throw saveErr; + console.log(`Created a user p5${p5User}`); + }); + } + + Project.find({ user: p5User._id }, (projectsErr, projects) => { + // if there are already some sketches, delete them + console.log('Deleting old projects...'); + projects.forEach((project) => { + Project.remove({ _id: project._id }, (removeErr) => { + if (removeErr) throw removeErr; + }); + }); + }); + + return getCategories() + .then(getSketchesInCategories) + .then(getSketchContent) + .then(createProjectsInP5user); + }); +} + +getp5User(); diff --git a/server/models/project.js b/server/models/project.js index 0ec09122..991d118d 100644 --- a/server/models/project.js +++ b/server/models/project.js @@ -1,7 +1,8 @@ import mongoose from 'mongoose'; -const Schema = mongoose.Schema; import shortid from 'shortid'; +const Schema = mongoose.Schema; + const fileSchema = new Schema({ name: { type: String, default: 'sketch.js' }, content: { type: String, default: '' }, @@ -11,7 +12,7 @@ const fileSchema = new Schema({ isSelectedFile: { type: Boolean } }, { timestamps: true, _id: true }); -fileSchema.virtual('id').get(function () { +fileSchema.virtual('id').get(function getFileId() { return this._id.toHexString(); }); @@ -26,7 +27,7 @@ const projectSchema = new Schema({ _id: { type: String, default: shortid.generate } }, { timestamps: true }); -projectSchema.virtual('id').get(function () { +projectSchema.virtual('id').get(function getProjectId() { return this._id; }); diff --git a/server/models/user.js b/server/models/user.js index 34243650..cd43c7c9 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,7 +1,9 @@ import mongoose from 'mongoose'; -const Schema = mongoose.Schema; + const bcrypt = require('bcrypt-nodejs'); +const Schema = mongoose.Schema; + const userSchema = new Schema({ name: { type: String, default: '' }, username: { type: String, required: true, unique: true }, @@ -39,7 +41,7 @@ userSchema.pre('save', function checkPassword(next) { // eslint-disable-line con }); }); -userSchema.virtual('id').get(function () { +userSchema.virtual('id').get(function idToString() { return this._id.toHexString(); }); diff --git a/server/routes/server.routes.js b/server/routes/server.routes.js index 0bc73f5a..000eb7bc 100644 --- a/server/routes/server.routes.js +++ b/server/routes/server.routes.js @@ -1,8 +1,9 @@ import { Router } from 'express'; -const router = new Router(); import { renderIndex } from '../views/index'; import { get404Sketch } from '../views/404Page'; -import { userExists } from '../controllers/user.controller.js'; +import { userExists } from '../controllers/user.controller'; + +const router = new Router(); // this is intended to be a temporary file // until i figure out isomorphic rendering @@ -48,7 +49,7 @@ router.route('/about').get((req, res) => { }); router.route('/:username/sketches').get((req, res) => { - userExists(req.params.username, (exists) => ( + userExists(req.params.username, exists => ( exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) )); }); diff --git a/server/routes/user.routes.js b/server/routes/user.routes.js index da8fd02b..a295e96c 100644 --- a/server/routes/user.routes.js +++ b/server/routes/user.routes.js @@ -1,5 +1,6 @@ import { Router } from 'express'; import * as UserController from '../controllers/user.controller'; + const router = new Router(); router.route('/signup').post(UserController.createUser); diff --git a/server/server.js b/server/server.js index 8e8529da..af8a55ae 100644 --- a/server/server.js +++ b/server/server.js @@ -3,24 +3,15 @@ import mongoose from 'mongoose'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import session from 'express-session'; -const MongoStore = require('connect-mongo')(session); +import connectMongo from 'connect-mongo'; import passport from 'passport'; import path from 'path'; // Webpack Requirements import webpack from 'webpack'; -import config from '../webpack.config.dev'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; - -const app = new Express(); - -// Run Webpack dev server in development mode -if (process.env.NODE_ENV === 'development') { - const compiler = webpack(config); - app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); - app.use(webpackHotMiddleware(compiler)); -} +import config from '../webpack.config.dev'; // Import all required modules import serverConfig from './config'; @@ -35,6 +26,16 @@ import embedRoutes from './routes/embed.routes'; import { renderIndex } from './views/index'; import { get404Sketch } from './views/404Page'; +const app = new Express(); +const MongoStore = connectMongo(session); + +// Run Webpack dev server in development mode +if (process.env.NODE_ENV === 'development') { + const compiler = webpack(config); + app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); + app.use(webpackHotMiddleware(compiler)); +} + // Body parser, cookie parser, sessions, serve public assets app.use(Express.static(path.resolve(__dirname, '../static'))); diff --git a/server/utils/filePath.js b/server/utils/filePath.js index 519d1c37..118be0a2 100644 --- a/server/utils/filePath.js +++ b/server/utils/filePath.js @@ -15,7 +15,7 @@ export function resolvePathToFile(filePath, files) { file._id.valueOf().toString() === childFileId.valueOf() ) ); - childFiles.some(childFile => { + childFiles.some((childFile) => { if (childFile.name === filePathSegment) { currentFile = childFile; foundChild = true; @@ -30,3 +30,5 @@ export function resolvePathToFile(filePath, files) { }); return resolvedFile; } + +export default resolvePathToFile; diff --git a/server/utils/previewGeneration.js b/server/utils/previewGeneration.js index 87bae5d9..5b44d41a 100644 --- a/server/utils/previewGeneration.js +++ b/server/utils/previewGeneration.js @@ -10,7 +10,7 @@ function resolveLinksInString(content, files, projectId) { let fileStrings = content.match(STRING_REGEX); const fileStringRegex = /^('|")(?!(http:\/\/|https:\/\/)).*('|")$/i; fileStrings = fileStrings || []; - fileStrings.forEach(fileString => { + fileStrings.forEach((fileString) => { // if string does not begin with http or https if (fileString.match(fileStringRegex)) { const filePath = fileString.substr(1, fileString.length - 2); @@ -35,7 +35,7 @@ function resolveLinksInString(content, files, projectId) { } export function injectMediaUrls(filesToInject, allFiles, projectId) { - filesToInject.forEach(file => { + filesToInject.forEach((file) => { file.content = resolveLinksInString(file.content, allFiles, projectId); }); } @@ -43,7 +43,7 @@ export function injectMediaUrls(filesToInject, allFiles, projectId) { export function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) { const elements = sketchDoc.querySelectorAll(`[${attr}]`); const elementsArray = Array.prototype.slice.call(elements); - elementsArray.forEach(element => { + elementsArray.forEach((element) => { if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) { const resolvedFile = resolvePathToFile(element.getAttribute(attr), files); if (resolvedFile) { @@ -56,7 +56,7 @@ export function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) { export function resolveScripts(sketchDoc, files, projectId) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); - scriptsInHTMLArray.forEach(script => { + scriptsInHTMLArray.forEach((script) => { if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) { const resolvedFile = resolvePathToFile(script.getAttribute('src'), files); if (resolvedFile) { @@ -76,13 +76,13 @@ export function resolveScripts(sketchDoc, files, projectId) { export function resolveStyles(sketchDoc, files, projectId) { const inlineCSSInHTML = sketchDoc.getElementsByTagName('style'); const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML); - inlineCSSInHTMLArray.forEach(style => { + inlineCSSInHTMLArray.forEach((style) => { style.innerHTML = resolveLinksInString(style.innerHTML, files, projectId); }); const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]'); const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML); - cssLinksInHTMLArray.forEach(css => { + cssLinksInHTMLArray.forEach((css) => { if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) { const resolvedFile = resolvePathToFile(css.getAttribute('href'), files); if (resolvedFile) { diff --git a/server/views/404Page.js b/server/views/404Page.js index b54076df..01587879 100644 --- a/server/views/404Page.js +++ b/server/views/404Page.js @@ -20,19 +20,19 @@ export function get404Sketch(callback) { instanceMode = jsFiles.find(file => file.name === 'sketch.js').content.includes('Instance Mode'); - jsFiles.forEach(file => { // Add js files as script tags + jsFiles.forEach((file) => { // Add js files as script tags const html = htmlFile.split(''); html[0] = `${html[0]}`; htmlFile = html.join(''); }); - cssFiles.forEach(file => { // Add css files as style tags + cssFiles.forEach((file) => { // Add css files as style tags const html = htmlFile.split(''); html[0] = `${html[0]}`; htmlFile = html.join(''); }); - linkedFiles.forEach(file => { // Add linked files as link tags + linkedFiles.forEach((file) => { // Add linked files as link tags const html = htmlFile.split(''); html[1] = `${html[1]}`; htmlFile = html.join(''); @@ -112,3 +112,6 @@ export function get404Sketch(callback) { } }); } + +export default get404Sketch; + diff --git a/static/p5-interceptor b/static/p5-interceptor index a1c12672..9fa934af 160000 --- a/static/p5-interceptor +++ b/static/p5-interceptor @@ -1 +1 @@ -Subproject commit a1c126721ac667f7750ec181c8ccf363f2658d8b +Subproject commit 9fa934af4ca85853ba2fe911558d5f658621ea0b