Merge branch 'develop' of https://github.com/processing/p5.js-web-editor into feature/mobile-canvas

This commit is contained in:
ghalestrilo 2020-06-15 14:20:28 -03:00
commit 96261db108
25 changed files with 165 additions and 126 deletions

3
.github/FUNDING.yml vendored
View file

@ -1 +1,2 @@
custom: https://processingfoundation.org/support github: processing
custom: https://processingfoundation.org/

View file

@ -10,14 +10,13 @@ import * as projectActions from '../modules/IDE/actions/project';
import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences'; import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences';
import { logoutUser } from '../modules/User/actions'; import { logoutUser } from '../modules/User/actions';
import getConfig from '../utils/getConfig';
import { metaKeyName, } from '../utils/metaKey'; import { metaKeyName, } from '../utils/metaKey';
import CaretLeftIcon from '../images/left-arrow.svg'; import CaretLeftIcon from '../images/left-arrow.svg';
import TriangleIcon from '../images/down-filled-triangle.svg'; import TriangleIcon from '../images/down-filled-triangle.svg';
import LogoIcon from '../images/p5js-logo-small.svg'; import LogoIcon from '../images/p5js-logo-small.svg';
const __process = (typeof global !== 'undefined' ? global : window).process;
class Nav extends React.PureComponent { class Nav extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -272,7 +271,7 @@ class Nav extends React.PureComponent {
New New
</button> </button>
</li> </li>
{ __process.env.LOGIN_ENABLED && (!this.props.project.owner || this.isUserOwner()) && { getConfig('LOGIN_ENABLED') && (!this.props.project.owner || this.isUserOwner()) &&
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
<button <button
onClick={this.handleSave} onClick={this.handleSave}
@ -324,7 +323,7 @@ class Nav extends React.PureComponent {
Open Open
</Link> </Link>
</li> } </li> }
{__process.env.UI_COLLECTIONS_ENABLED && {getConfig('UI_COLLECTIONS_ENABLED') &&
this.props.user.authenticated && this.props.user.authenticated &&
this.props.project.id && this.props.project.id &&
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
@ -337,7 +336,7 @@ class Nav extends React.PureComponent {
Add to Collection Add to Collection
</Link> </Link>
</li>} </li>}
{ __process.env.EXAMPLES_ENABLED && { getConfig('EXAMPLES_ENABLED') &&
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
<Link <Link
to="/p5/sketches" to="/p5/sketches"
@ -587,7 +586,7 @@ class Nav extends React.PureComponent {
My sketches My sketches
</Link> </Link>
</li> </li>
{__process.env.UI_COLLECTIONS_ENABLED && {getConfig('UI_COLLECTIONS_ENABLED') &&
<li className="nav__dropdown-item"> <li className="nav__dropdown-item">
<Link <Link
to={`/${this.props.user.username}/collections`} to={`/${this.props.user.username}/collections`}
@ -635,7 +634,7 @@ class Nav extends React.PureComponent {
} }
renderUserMenu(navDropdownState) { renderUserMenu(navDropdownState) {
const isLoginEnabled = __process.env.LOGIN_ENABLED; const isLoginEnabled = getConfig('LOGIN_ENABLED');
const isAuthenticated = this.props.user.authenticated; const isAuthenticated = this.props.user.authenticated;
if (isLoginEnabled && isAuthenticated) { if (isLoginEnabled && isAuthenticated) {

View file

@ -1,11 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import getConfig from '../../utils/getConfig';
import DevTools from './components/DevTools'; import DevTools from './components/DevTools';
import { setPreviousPath } from '../IDE/actions/ide'; import { setPreviousPath } from '../IDE/actions/ide';
const __process = (typeof global !== 'undefined' ? global : window).process;
class App extends React.Component { class App extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
@ -35,7 +34,7 @@ class App extends React.Component {
render() { render() {
return ( return (
<div className="app"> <div className="app">
{this.state.isMounted && !window.devToolsExtension && __process.env.NODE_ENV === 'development' && <DevTools />} {this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && <DevTools />}
{this.props.children} {this.props.children}
</div> </div>
); );

View file

@ -1,10 +1,7 @@
import axios from 'axios'; import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader'; import { startLoader, stopLoader } from './loader';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
function setAssets(assets, totalSize) { function setAssets(assets, totalSize) {
return { return {
type: ActionTypes.SET_ASSETS, type: ActionTypes.SET_ASSETS,
@ -16,7 +13,7 @@ function setAssets(assets, totalSize) {
export function getAssets() { export function getAssets() {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
axios.get(`${ROOT_URL}/S3/objects`, { withCredentials: true }) apiClient.get('/S3/objects')
.then((response) => { .then((response) => {
dispatch(setAssets(response.data.assets, response.data.totalSize)); dispatch(setAssets(response.data.assets, response.data.totalSize));
dispatch(stopLoader()); dispatch(stopLoader());
@ -39,7 +36,7 @@ export function deleteAsset(assetKey) {
export function deleteAssetRequest(assetKey) { export function deleteAssetRequest(assetKey) {
return (dispatch) => { return (dispatch) => {
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true }) apiClient.delete(`/S3/${assetKey}`)
.then((response) => { .then((response) => {
dispatch(deleteAsset(assetKey)); dispatch(deleteAsset(assetKey));
}) })

View file

@ -1,11 +1,9 @@
import axios from 'axios';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader'; import { startLoader, stopLoader } from './loader';
import { setToastText, showToast } from './toast'; import { setToastText, showToast } from './toast';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
const TOAST_DISPLAY_TIME_MS = 1500; const TOAST_DISPLAY_TIME_MS = 1500;
@ -15,11 +13,11 @@ export function getCollections(username) {
dispatch(startLoader()); dispatch(startLoader());
let url; let url;
if (username) { if (username) {
url = `${ROOT_URL}/${username}/collections`; url = `/${username}/collections`;
} else { } else {
url = `${ROOT_URL}/collections`; url = '/collections';
} }
axios.get(url, { withCredentials: true }) apiClient.get(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.SET_COLLECTIONS, type: ActionTypes.SET_COLLECTIONS,
@ -41,8 +39,8 @@ export function getCollections(username) {
export function createCollection(collection) { export function createCollection(collection) {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
const url = `${ROOT_URL}/collections`; const url = '/collections';
return axios.post(url, collection, { withCredentials: true }) return apiClient.post(url, collection)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.CREATE_COLLECTION type: ActionTypes.CREATE_COLLECTION
@ -73,8 +71,8 @@ export function createCollection(collection) {
export function addToCollection(collectionId, projectId) { export function addToCollection(collectionId, projectId) {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`; const url = `/collections/${collectionId}/${projectId}`;
return axios.post(url, { withCredentials: true }) return apiClient.post(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.ADD_TO_COLLECTION, type: ActionTypes.ADD_TO_COLLECTION,
@ -105,8 +103,8 @@ export function addToCollection(collectionId, projectId) {
export function removeFromCollection(collectionId, projectId) { export function removeFromCollection(collectionId, projectId) {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`; const url = `/collections/${collectionId}/${projectId}`;
return axios.delete(url, { withCredentials: true }) return apiClient.delete(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.REMOVE_FROM_COLLECTION, type: ActionTypes.REMOVE_FROM_COLLECTION,
@ -136,8 +134,8 @@ export function removeFromCollection(collectionId, projectId) {
export function editCollection(collectionId, { name, description }) { export function editCollection(collectionId, { name, description }) {
return (dispatch) => { return (dispatch) => {
const url = `${ROOT_URL}/collections/${collectionId}`; const url = `/collections/${collectionId}`;
return axios.patch(url, { name, description }, { withCredentials: true }) return apiClient.patch(url, { name, description })
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.EDIT_COLLECTION, type: ActionTypes.EDIT_COLLECTION,
@ -159,8 +157,8 @@ export function editCollection(collectionId, { name, description }) {
export function deleteCollection(collectionId) { export function deleteCollection(collectionId) {
return (dispatch) => { return (dispatch) => {
const url = `${ROOT_URL}/collections/${collectionId}`; const url = `/collections/${collectionId}`;
return axios.delete(url, { withCredentials: true }) return apiClient.delete(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.DELETE_COLLECTION, type: ActionTypes.DELETE_COLLECTION,

View file

@ -1,13 +1,11 @@
import axios from 'axios';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import blobUtil from 'blob-util'; import blobUtil from 'blob-util';
import { reset } from 'redux-form'; import { reset } from 'redux-form';
import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide'; import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide';
import { setProjectSavedTime } from './project'; import { setProjectSavedTime } from './project';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
function appendToFilename(filename, string) { function appendToFilename(filename, string) {
const dotIndex = filename.lastIndexOf('.'); const dotIndex = filename.lastIndexOf('.');
@ -50,7 +48,7 @@ export function createFile(formProps) {
parentId, parentId,
children: [] children: []
}; };
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) apiClient.post(`/projects/${state.project.id}/files`, postParams)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.CREATE_FILE, type: ActionTypes.CREATE_FILE,
@ -106,7 +104,7 @@ export function createFolder(formProps) {
parentId, parentId,
fileType: 'folder' fileType: 'folder'
}; };
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) apiClient.post(`/projects/${state.project.id}/files`, postParams)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.CREATE_FILE, type: ActionTypes.CREATE_FILE,
@ -161,7 +159,7 @@ export function deleteFile(id, parentId) {
parentId parentId
} }
}; };
axios.delete(`${ROOT_URL}/projects/${state.project.id}/files/${id}`, deleteConfig, { withCredentials: true }) apiClient.delete(`/projects/${state.project.id}/files/${id}`, deleteConfig)
.then(() => { .then(() => {
dispatch({ dispatch({
type: ActionTypes.DELETE_FILE, type: ActionTypes.DELETE_FILE,

View file

@ -1,11 +1,8 @@
import axios from 'axios'; import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
function updatePreferences(formParams, dispatch) { function updatePreferences(formParams, dispatch) {
axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true }) apiClient.put('/preferences', formParams)
.then(() => { .then(() => {
}) })
.catch((error) => { .catch((error) => {

View file

@ -1,8 +1,9 @@
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import axios from 'axios';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import each from 'async/each'; import each from 'async/each';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import apiClient from '../../../utils/apiClient';
import getConfig from '../../../utils/getConfig';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { showToast, setToastText } from './toast'; import { showToast, setToastText } from './toast';
import { import {
@ -14,8 +15,7 @@ import {
} from './ide'; } from './ide';
import { clearState, saveState } from '../../../persistState'; import { clearState, saveState } from '../../../persistState';
const __process = (typeof global !== 'undefined' ? global : window).process; const ROOT_URL = getConfig('API_URL');
const ROOT_URL = __process.env.API_URL;
export function setProject(project) { export function setProject(project) {
return { return {
@ -52,7 +52,7 @@ export function setNewProject(project) {
export function getProject(id, username) { export function getProject(id, username) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(justOpenedProject()); dispatch(justOpenedProject());
axios.get(`${ROOT_URL}/${username}/projects/${id}`, { withCredentials: true }) apiClient.get(`/${username}/projects/${id}`)
.then((response) => { .then((response) => {
dispatch(setProject(response.data)); dispatch(setProject(response.data));
dispatch(setUnsavedChanges(false)); dispatch(setUnsavedChanges(false));
@ -142,7 +142,7 @@ export function saveProject(selectedFile = null, autosave = false) {
fileToUpdate.content = selectedFile.content; fileToUpdate.content = selectedFile.content;
} }
if (state.project.id) { if (state.project.id) {
return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true }) return apiClient.put(`/projects/${state.project.id}`, formParams)
.then((response) => { .then((response) => {
dispatch(endSavingProject()); dispatch(endSavingProject());
dispatch(setUnsavedChanges(false)); dispatch(setUnsavedChanges(false));
@ -177,7 +177,7 @@ export function saveProject(selectedFile = null, autosave = false) {
}); });
} }
return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) return apiClient.post('/projects', formParams)
.then((response) => { .then((response) => {
dispatch(endSavingProject()); dispatch(endSavingProject());
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data); const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
@ -260,7 +260,7 @@ export function cloneProject(id) {
if (!id) { if (!id) {
resolve(getState()); resolve(getState());
} else { } else {
fetch(`${ROOT_URL}/projects/${id}`) apiClient.get(`/projects/${id}`)
.then(res => res.json()) .then(res => res.json())
.then(data => resolve({ .then(data => resolve({
files: data.files, files: data.files,
@ -287,7 +287,7 @@ export function cloneProject(id) {
const formParams = { const formParams = {
url: file.url url: file.url
}; };
axios.post(`${ROOT_URL}/S3/copy`, formParams, { withCredentials: true }) apiClient.post('/S3/copy', formParams)
.then((response) => { .then((response) => {
file.url = response.data.url; file.url = response.data.url;
callback(null); callback(null);
@ -298,7 +298,7 @@ export function cloneProject(id) {
}, (err) => { }, (err) => {
// if not errors in duplicating the files on S3, then duplicate it // if not errors in duplicating the files on S3, then duplicate it
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles }); const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles });
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) apiClient.post('/projects', formParams)
.then((response) => { .then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch(setNewProject(response.data)); dispatch(setNewProject(response.data));
@ -337,7 +337,7 @@ export function setProjectSavedTime(updatedAt) {
export function changeProjectName(id, newName) { export function changeProjectName(id, newName) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
axios.put(`${ROOT_URL}/projects/${id}`, { name: newName }, { withCredentials: true }) apiClient.put(`/projects/${id}`, { name: newName })
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
dispatch({ dispatch({
@ -364,7 +364,7 @@ export function changeProjectName(id, newName) {
export function deleteProject(id) { export function deleteProject(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
axios.delete(`${ROOT_URL}/projects/${id}`, { withCredentials: true }) apiClient.delete(`/projects/${id}`)
.then(() => { .then(() => {
const state = getState(); const state = getState();
if (id === state.project.id) { if (id === state.project.id) {

View file

@ -1,21 +1,18 @@
import axios from 'axios'; import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import { startLoader, stopLoader } from './loader'; import { startLoader, stopLoader } from './loader';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
// eslint-disable-next-line // eslint-disable-next-line
export function getProjects(username) { export function getProjects(username) {
return (dispatch) => { return (dispatch) => {
dispatch(startLoader()); dispatch(startLoader());
let url; let url;
if (username) { if (username) {
url = `${ROOT_URL}/${username}/projects`; url = `/${username}/projects`;
} else { } else {
url = `${ROOT_URL}/projects`; url = '/projects';
} }
axios.get(url, { withCredentials: true }) apiClient.get(url)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.SET_PROJECTS, type: ActionTypes.SET_PROJECTS,

View file

@ -1,11 +1,10 @@
import axios from 'axios'; import apiClient from '../../../utils/apiClient';
import getConfig from '../../../utils/getConfig';
import { createFile } from './files'; import { createFile } from './files';
import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils'; import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils';
const __process = (typeof global !== 'undefined' ? global : window).process; const s3BucketHttps = getConfig('S3_BUCKET_URL_BASE') ||
const s3BucketHttps = __process.env.S3_BUCKET_URL_BASE || `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
const ROOT_URL = __process.env.API_URL;
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
function localIntercept(file, options = {}) { function localIntercept(file, options = {}) {
@ -46,18 +45,13 @@ export function dropzoneAcceptCallback(userId, file, done) {
}); });
} else { } else {
file.postData = []; // eslint-disable-line file.postData = []; // eslint-disable-line
axios.post( apiClient.post('/S3/sign', {
`${ROOT_URL}/S3/sign`, { name: file.name,
name: file.name, type: file.type,
type: file.type, size: file.size,
size: file.size, userId
userId
// _csrf: document.getElementById('__createPostToken').value // _csrf: document.getElementById('__createPostToken').value
}, })
{
withCredentials: true
}
)
.then((response) => { .then((response) => {
file.custom_status = 'ready'; // eslint-disable-line file.custom_status = 'ready'; // eslint-disable-line
file.postData = response.data; // eslint-disable-line file.postData = response.data; // eslint-disable-line

View file

@ -3,8 +3,9 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
const __process = (typeof global !== 'undefined' ? global : window).process; import getConfig from '../../../utils/getConfig';
const limit = __process.env.UPLOAD_LIMIT || 250000000;
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const MAX_SIZE_B = limit; const MAX_SIZE_B = limit;
const formatPercent = (percent) => { const formatPercent = (percent) => {

View file

@ -4,11 +4,11 @@ import Dropzone from 'dropzone';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as UploaderActions from '../actions/uploader'; import * as UploaderActions from '../actions/uploader';
import getConfig from '../../../utils/getConfig';
import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils'; import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils';
const __process = (typeof global !== 'undefined' ? global : window).process; const s3Bucket = getConfig('S3_BUCKET_URL_BASE') ||
const s3Bucket = __process.env.S3_BUCKET_URL_BASE || `https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
class FileUploader extends React.Component { class FileUploader extends React.Component {
componentDidMount() { componentDidMount() {

View file

@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router'; import { Link } from 'react-router';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import getConfig from '../../../utils/getConfig';
import FileUploader from './FileUploader'; import FileUploader from './FileUploader';
import { getreachedTotalSizeLimit } from '../selectors/users'; import { getreachedTotalSizeLimit } from '../selectors/users';
import ExitIcon from '../../../images/exit.svg'; import ExitIcon from '../../../images/exit.svg';
const __process = (typeof global !== 'undefined' ? global : window).process; const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const limit = __process.env.UPLOAD_LIMIT || 250000000;
const limitText = prettyBytes(limit); const limitText = prettyBytes(limit);
class UploadFileModal extends React.Component { class UploadFileModal extends React.Component {

View file

@ -1,10 +1,10 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import getConfig from '../../../utils/getConfig';
const __process = (typeof global !== 'undefined' ? global : window).process;
const getAuthenticated = state => state.user.authenticated; const getAuthenticated = state => state.user.authenticated;
const getTotalSize = state => state.user.totalSize; const getTotalSize = state => state.user.totalSize;
const getAssetsTotalSize = state => state.assets.totalSize; const getAssetsTotalSize = state => state.assets.totalSize;
const limit = __process.env.UPLOAD_LIMIT || 250000000; const limit = getConfig('UPLOAD_LIMIT') || 250000000;
export const getCanUploadMedia = createSelector( export const getCanUploadMedia = createSelector(
getAuthenticated, getAuthenticated,

View file

@ -1,13 +1,9 @@
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import axios from 'axios';
import * as ActionTypes from '../../constants'; import * as ActionTypes from '../../constants';
import apiClient from '../../utils/apiClient';
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
import { showToast, setToastText } from '../IDE/actions/toast'; import { showToast, setToastText } from '../IDE/actions/toast';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
export function authError(error) { export function authError(error) {
return { return {
type: ActionTypes.AUTH_ERROR, type: ActionTypes.AUTH_ERROR,
@ -17,7 +13,7 @@ export function authError(error) {
export function signUpUser(previousPath, formValues) { export function signUpUser(previousPath, formValues) {
return (dispatch) => { return (dispatch) => {
axios.post(`${ROOT_URL}/signup`, formValues, { withCredentials: true }) apiClient.post('/signup', formValues)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.AUTH_USER, type: ActionTypes.AUTH_USER,
@ -34,7 +30,7 @@ export function signUpUser(previousPath, formValues) {
} }
export function loginUser(formValues) { export function loginUser(formValues) {
return axios.post(`${ROOT_URL}/login`, formValues, { withCredentials: true }); return apiClient.post('/login', formValues);
} }
export function loginUserSuccess(user) { export function loginUserSuccess(user) {
@ -74,7 +70,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
export function getUser() { export function getUser() {
return (dispatch) => { return (dispatch) => {
axios.get(`${ROOT_URL}/session`, { withCredentials: true }) apiClient.get('/session')
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.AUTH_USER, type: ActionTypes.AUTH_USER,
@ -95,7 +91,7 @@ export function getUser() {
export function validateSession() { export function validateSession() {
return (dispatch, getState) => { return (dispatch, getState) => {
axios.get(`${ROOT_URL}/session`, { withCredentials: true }) apiClient.get('/session')
.then((response) => { .then((response) => {
const state = getState(); const state = getState();
if (state.user.username !== response.data.username) { if (state.user.username !== response.data.username) {
@ -113,7 +109,7 @@ export function validateSession() {
export function logoutUser() { export function logoutUser() {
return (dispatch) => { return (dispatch) => {
axios.get(`${ROOT_URL}/logout`, { withCredentials: true }) apiClient.get('/logout')
.then(() => { .then(() => {
dispatch({ dispatch({
type: ActionTypes.UNAUTH_USER type: ActionTypes.UNAUTH_USER
@ -131,7 +127,7 @@ export function initiateResetPassword(formValues) {
dispatch({ dispatch({
type: ActionTypes.RESET_PASSWORD_INITIATE type: ActionTypes.RESET_PASSWORD_INITIATE
}); });
axios.post(`${ROOT_URL}/reset-password`, formValues, { withCredentials: true }) apiClient.post('/reset-password', formValues)
.then(() => { .then(() => {
// do nothing // do nothing
}) })
@ -150,7 +146,7 @@ export function initiateVerification() {
dispatch({ dispatch({
type: ActionTypes.EMAIL_VERIFICATION_INITIATE type: ActionTypes.EMAIL_VERIFICATION_INITIATE
}); });
axios.post(`${ROOT_URL}/verify/send`, {}, { withCredentials: true }) apiClient.post('/verify/send', {})
.then(() => { .then(() => {
// do nothing // do nothing
}) })
@ -170,7 +166,7 @@ export function verifyEmailConfirmation(token) {
type: ActionTypes.EMAIL_VERIFICATION_VERIFY, type: ActionTypes.EMAIL_VERIFICATION_VERIFY,
state: 'checking', state: 'checking',
}); });
return axios.get(`${ROOT_URL}/verify?t=${token}`, {}, { withCredentials: true }) return apiClient.get(`/verify?t=${token}`, {})
.then(response => dispatch({ .then(response => dispatch({
type: ActionTypes.EMAIL_VERIFICATION_VERIFIED, type: ActionTypes.EMAIL_VERIFICATION_VERIFIED,
message: response.data, message: response.data,
@ -194,7 +190,7 @@ export function resetPasswordReset() {
export function validateResetPasswordToken(token) { export function validateResetPasswordToken(token) {
return (dispatch) => { return (dispatch) => {
axios.get(`${ROOT_URL}/reset-password/${token}`) apiClient.get(`/reset-password/${token}`)
.then(() => { .then(() => {
// do nothing if the token is valid // do nothing if the token is valid
}) })
@ -206,7 +202,7 @@ export function validateResetPasswordToken(token) {
export function updatePassword(token, formValues) { export function updatePassword(token, formValues) {
return (dispatch) => { return (dispatch) => {
axios.post(`${ROOT_URL}/reset-password/${token}`, formValues) apiClient.post(`/reset-password/${token}`, formValues)
.then((response) => { .then((response) => {
dispatch(loginUserSuccess(response.data)); dispatch(loginUserSuccess(response.data));
browserHistory.push('/'); browserHistory.push('/');
@ -226,7 +222,7 @@ export function updateSettingsSuccess(user) {
export function updateSettings(formValues) { export function updateSettings(formValues) {
return dispatch => return dispatch =>
axios.put(`${ROOT_URL}/account`, formValues, { withCredentials: true }) apiClient.put('/account', formValues)
.then((response) => { .then((response) => {
dispatch(updateSettingsSuccess(response.data)); dispatch(updateSettingsSuccess(response.data));
browserHistory.push('/'); browserHistory.push('/');
@ -248,7 +244,7 @@ export function createApiKeySuccess(user) {
export function createApiKey(label) { export function createApiKey(label) {
return dispatch => return dispatch =>
axios.post(`${ROOT_URL}/account/api-keys`, { label }, { withCredentials: true }) apiClient.post('/account/api-keys', { label })
.then((response) => { .then((response) => {
dispatch(createApiKeySuccess(response.data)); dispatch(createApiKeySuccess(response.data));
}) })
@ -260,7 +256,7 @@ export function createApiKey(label) {
export function removeApiKey(keyId) { export function removeApiKey(keyId) {
return dispatch => return dispatch =>
axios.delete(`${ROOT_URL}/account/api-keys/${keyId}`, { withCredentials: true }) apiClient.delete(`/account/api-keys/${keyId}`)
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.API_KEY_REMOVED, type: ActionTypes.API_KEY_REMOVED,

View file

@ -3,18 +3,15 @@ import React from 'react';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import axios from 'axios';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions'; import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
import AccountForm from '../components/AccountForm'; import AccountForm from '../components/AccountForm';
import apiClient from '../../../utils/apiClient';
import { validateSettings } from '../../../utils/reduxFormUtils'; import { validateSettings } from '../../../utils/reduxFormUtils';
import SocialAuthButton from '../components/SocialAuthButton'; import SocialAuthButton from '../components/SocialAuthButton';
import APIKeyForm from '../components/APIKeyForm'; import APIKeyForm from '../components/APIKeyForm';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
function SocialLoginPanel(props) { function SocialLoginPanel(props) {
return ( return (
<React.Fragment> <React.Fragment>
@ -96,7 +93,7 @@ function asyncValidate(formProps, dispatch, props) {
const queryParams = {}; const queryParams = {};
queryParams[fieldToValidate] = formProps[fieldToValidate]; queryParams[fieldToValidate] = formProps[fieldToValidate];
queryParams.check_type = fieldToValidate; queryParams.check_type = fieldToValidate;
return axios.get(`${ROOT_URL}/signup/duplicate_check`, { params: queryParams }) return apiClient.get('/signup/duplicate_check', { params: queryParams })
.then((response) => { .then((response) => {
if (response.data.exists) { if (response.data.exists) {
const error = {}; const error = {};

View file

@ -1,19 +1,16 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import axios from 'axios';
import { Link, browserHistory } from 'react-router'; import { Link, browserHistory } from 'react-router';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import * as UserActions from '../actions'; import * as UserActions from '../actions';
import SignupForm from '../components/SignupForm'; import SignupForm from '../components/SignupForm';
import apiClient from '../../../utils/apiClient';
import { validateSignup } from '../../../utils/reduxFormUtils'; import { validateSignup } from '../../../utils/reduxFormUtils';
import SocialAuthButton from '../components/SocialAuthButton'; import SocialAuthButton from '../components/SocialAuthButton';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
const __process = (typeof global !== 'undefined' ? global : window).process;
const ROOT_URL = __process.env.API_URL;
class SignupView extends React.Component { class SignupView extends React.Component {
gotoHomePage = () => { gotoHomePage = () => {
browserHistory.push('/'); browserHistory.push('/');
@ -86,7 +83,7 @@ function asyncValidate(formProps, dispatch, props) {
const queryParams = {}; const queryParams = {};
queryParams[fieldToValidate] = formProps[fieldToValidate]; queryParams[fieldToValidate] = formProps[fieldToValidate];
queryParams.check_type = fieldToValidate; queryParams.check_type = fieldToValidate;
return axios.get(`${ROOT_URL}/signup/duplicate_check`, { params: queryParams }) return apiClient.get('/signup/duplicate_check', { params: queryParams })
.then((response) => { .then((response) => {
if (response.data.exists) { if (response.data.exists) {
errors[fieldToValidate] = response.data.message; errors[fieldToValidate] = response.data.message;

View file

@ -3,15 +3,14 @@ import thunk from 'redux-thunk';
import DevTools from './modules/App/components/DevTools'; import DevTools from './modules/App/components/DevTools';
import rootReducer from './reducers'; import rootReducer from './reducers';
import { clearState, loadState } from './persistState'; import { clearState, loadState } from './persistState';
import getConfig from './utils/getConfig';
const __process = (typeof global !== 'undefined' ? global : window).process;
export default function configureStore(initialState) { export default function configureStore(initialState) {
const enhancers = [ const enhancers = [
applyMiddleware(thunk), applyMiddleware(thunk),
]; ];
if (__process.env.CLIENT && __process.env.NODE_ENV === 'development') { if (getConfig('CLIENT') && getConfig('NODE_ENV') === 'development') {
// Enable DevTools only when rendering on client and during development. // Enable DevTools only when rendering on client and during development.
enhancers.push(window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument()); enhancers.push(window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument());
} }

17
client/utils/apiClient.js Normal file
View file

@ -0,0 +1,17 @@
import axios from 'axios';
import getConfig from './getConfig';
const ROOT_URL = getConfig('API_URL');
/**
* Configures an Axios instance with the correct API URL
*/
function createClientInstance() {
return axios.create({
baseURL: ROOT_URL,
withCredentials: true
});
}
export default createClientInstance();

View file

@ -60,7 +60,7 @@ export const getAllScriptOffsets = (htmlFile) => {
if (ind === -1) { if (ind === -1) {
foundJSScript = false; foundJSScript = false;
} else { } else {
endFilenameInd = htmlFile.indexOf('.js', ind + startTag.length + 3); endFilenameInd = htmlFile.indexOf('.js', ind + startTag.length + 1);
filename = htmlFile.substring(ind + startTag.length, endFilenameInd); filename = htmlFile.substring(ind + startTag.length, endFilenameInd);
lineOffset = htmlFile.substring(0, ind).split('\n').length + hijackConsoleErrorsScriptLength; lineOffset = htmlFile.substring(0, ind).split('\n').length + hijackConsoleErrorsScriptLength;
offs.push([lineOffset, filename]); offs.push([lineOffset, filename]);

24
client/utils/getConfig.js Normal file
View file

@ -0,0 +1,24 @@
function isTestEnvironment() {
// eslint-disable-next-line no-use-before-define
return getConfig('NODE_ENV', { warn: false }) === 'test';
}
/**
* Returns config item from environment
*/
function getConfig(key, options = { warn: !isTestEnvironment() }) {
if (key == null) {
throw new Error('"key" must be provided to getConfig()');
}
const env = (typeof global !== 'undefined' ? global : window)?.process?.env || {};
const value = env[key];
if (value == null && options?.warn !== false) {
console.warn(`getConfig("${key}") returned null`);
}
return value;
}
export default getConfig;

View file

@ -0,0 +1,28 @@
import getConfig from './getConfig';
describe('utils/getConfig()', () => {
beforeEach(() => {
delete global.process.env.CONFIG_TEST_KEY_NAME;
delete window.process.env.CONFIG_TEST_KEY_NAME;
});
it('throws if key is not defined', () => {
expect(() => getConfig(/* key is missing */)).toThrow(/must be provided/);
});
it('fetches from global.process', () => {
global.process.env.CONFIG_TEST_KEY_NAME = 'editor.p5js.org';
expect(getConfig('CONFIG_TEST_KEY_NAME')).toBe('editor.p5js.org');
});
it('fetches from window.process', () => {
window.process.env.CONFIG_TEST_KEY_NAME = 'editor.p5js.org';
expect(getConfig('CONFIG_TEST_KEY_NAME')).toBe('editor.p5js.org');
});
it('warns but does not throw if no value found', () => {
expect(() => getConfig('CONFIG_TEST_KEY_NAME')).not.toThrow();
});
});

View file

@ -13,19 +13,19 @@ This project release guide is based on
1. `$ git checkout develop` 1. `$ git checkout develop`
2. `$ git checkout -b release-<newversion>` 2. `$ git checkout -b release-<newversion>`
3. Do all of the release branch testing necessary. This could be as simple as running `npm test:ci`, or it could take user testing over a few days. 3. Do all of the release branch testing necessary. This could be as simple as running `npm test:ci`, or it could take user testing over a few days.
4. `$ git checkout release` 4. `$ npm version <newversion>` (see [npm-version](https://docs.npmjs.com/cli/version) for valid values of <newversion>).
5. `$ git merge --no-ff release-<newversion>` 5. `$ git checkout release`
6. `$ npm version <newversion>` (see [npm-version](https://docs.npmjs.com/cli/version) for valid values of <newversion>). 6. `$ git merge --no-ff release-<newversion>`
7. `$ git push && git push --tags` 7. `$ git push && git push --tags`
8. `$ git checkout develop` 8. `$ git checkout develop`
9. `$ git merge --no-ff release-<newversion>` 9. Create a release on GitHub. Make sure that you release from the `release` branch! You can do this in one of two ways:
10. Create a release on GitHub. You can do this in one of two ways:
1. (Preferred) Use the [`hub` command line tool](https://hub.github.com/). You can automate adding all commit messages since the last release with the following command: 1. (Preferred) Use the [`hub` command line tool](https://hub.github.com/). You can automate adding all commit messages since the last release with the following command:
```sh ```sh
$ hub release create -d -m "<newversion>" -m "$(git log `git describe --tags --abbrev=0 HEAD^`..HEAD --oneline)" <newversion>` $ hub release create -d -m "<newversion>" -m "$(git log `git describe --tags --abbrev=0 HEAD^`..HEAD --oneline)" <newversion>`
``` ```
Note that this creates a draft release, which you can then edit on GitHub. This allows you to create release notes from the list of commit messages, but then edit these notes as you wish. Note that this creates a draft release, which you can then edit on GitHub. This allows you to create release notes from the list of commit messages, but then edit these notes as you wish.
2. [Draft a new release on Github](https://github.com/processing/p5.js-web-editor/releases/new). 2. [Draft a new release on Github](https://github.com/processing/p5.js-web-editor/releases/new).
10. `$ git merge --no-ff release-<newversion>`
Travis CI will automatically deploy the release to production, as well as push a production tagged Docker image to DockerHub. Travis CI will automatically deploy the release to production, as well as push a production tagged Docker image to DockerHub.

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "p5.js-web-editor", "name": "p5.js-web-editor",
"version": "0.0.1", "version": "1.0.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "p5.js-web-editor", "name": "p5.js-web-editor",
"version": "0.0.1", "version": "1.0.3",
"description": "The web editor for p5.js.", "description": "The web editor for p5.js.",
"scripts": { "scripts": {
"clean": "rimraf dist", "clean": "rimraf dist",