Merge branch 'feature/public-api' into feature/sketch-collections

This commit is contained in:
Cassie Tarakajian 2019-10-31 15:02:31 -04:00
commit 846d2bb7db
19 changed files with 93 additions and 57 deletions

17
.github/config.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# Configuration for welcome - https://github.com/behaviorbot/welcome
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
# Comment to be posted to on first time issues
newIssueWelcomeComment: >
Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, be sure to follow the issue template if you haven't already.
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: >
🎉 Thanks for opening this pull request! Please check out our [contributing guidelines](https://github.com/processing/p5.js-web-editor/blob/master/CONTRIBUTING.md) if you haven't already.
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
# Comment to be posted to on pull requests merged by a first time user

View File

@ -46,4 +46,4 @@ deploy:
env: env:
global: global:
- APP_IMAGE_NAME=p5jswebeditor_app - APP_IMAGE_NAME=p5js-web-editor_app

View File

@ -131,12 +131,12 @@ class Nav extends React.PureComponent {
} }
handleAddFile() { handleAddFile() {
this.props.newFile(); this.props.newFile(this.props.rootFile.id);
this.setDropdown('none'); this.setDropdown('none');
} }
handleAddFolder() { handleAddFolder() {
this.props.newFolder(); this.props.newFolder(this.props.rootFile.id);
this.setDropdown('none'); this.setDropdown('none');
} }
@ -174,7 +174,7 @@ class Nav extends React.PureComponent {
handleDownload() { handleDownload() {
this.props.autosaveProject(); this.props.autosaveProject();
this.props.exportProjectAsZip(this.props.project.id); projectActions.exportProjectAsZip(this.props.project.id);
this.setDropdown('none'); this.setDropdown('none');
} }
@ -719,7 +719,6 @@ Nav.propTypes = {
setToastText: PropTypes.func.isRequired, setToastText: PropTypes.func.isRequired,
saveProject: PropTypes.func.isRequired, saveProject: PropTypes.func.isRequired,
autosaveProject: PropTypes.func.isRequired, autosaveProject: PropTypes.func.isRequired,
exportProjectAsZip: PropTypes.func.isRequired,
cloneProject: PropTypes.func.isRequired, cloneProject: PropTypes.func.isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired, authenticated: PropTypes.bool.isRequired,
@ -750,7 +749,10 @@ Nav.propTypes = {
setAllAccessibleOutput: PropTypes.func.isRequired, setAllAccessibleOutput: PropTypes.func.isRequired,
newFile: PropTypes.func.isRequired, newFile: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired, newFolder: PropTypes.func.isRequired,
layout: PropTypes.oneOf(['dashboard', 'project']) layout: PropTypes.oneOf(['dashboard', 'project']),
rootFile: PropTypes.shape({
id: PropTypes.string.isRequired
}).isRequired
}; };
Nav.defaultProps = { Nav.defaultProps = {
@ -767,7 +769,8 @@ function mapStateToProps(state) {
return { return {
project: state.project, project: state.project,
user: state.user, user: state.user,
unsavedChanges: state.ide.unsavedChanges unsavedChanges: state.ide.unsavedChanges,
rootFile: state.files.filter(file => file.name === 'root')[0]
}; };
} }

View File

@ -39,7 +39,12 @@ describe('Nav', () => {
}, },
startSketch: jest.fn(), startSketch: jest.fn(),
stopSketch: jest.fn(), stopSketch: jest.fn(),
setAllAccessibleOutput: jest.fn() setAllAccessibleOutput: jest.fn(),
showToast: jest.fn(),
setToastText: jest.fn(),
rootFile: {
id: 'root-file'
}
}; };
const getWrapper = () => shallow(<NavComponent {...props} />); const getWrapper = () => shallow(<NavComponent {...props} />);

View File

@ -41,14 +41,7 @@ export function updateFileContent(id, content) {
export function createFile(formProps) { export function createFile(formProps) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const selectedFile = state.files.find(file => file.isSelectedFile); const { parentId } = state.ide;
const rootFile = state.files.find(file => file.name === 'root');
let parentId;
if (selectedFile.fileType === 'folder') {
parentId = selectedFile.id;
} else {
parentId = rootFile.id;
}
if (state.project.id) { if (state.project.id) {
const postParams = { const postParams = {
name: createUniqueName(formProps.name, parentId, state.files), name: createUniqueName(formProps.name, parentId, state.files),
@ -99,14 +92,7 @@ export function createFile(formProps) {
export function createFolder(formProps) { export function createFolder(formProps) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const selectedFile = state.files.find(file => file.isSelectedFile); const { parentId } = state.ide;
const rootFile = state.files.find(file => file.name === 'root');
let parentId;
if (selectedFile.fileType === 'folder') {
parentId = selectedFile.id;
} else {
parentId = rootFile.id;
}
if (state.project.id) { if (state.project.id) {
const postParams = { const postParams = {
name: createUniqueName(formProps.name, parentId, state.files), name: createUniqueName(formProps.name, parentId, state.files),

View File

@ -62,9 +62,10 @@ export function resetSelectedFile(previousId) {
}; };
} }
export function newFile() { export function newFile(parentId) {
return { return {
type: ActionTypes.SHOW_MODAL type: ActionTypes.SHOW_MODAL,
parentId
}; };
} }
@ -122,9 +123,10 @@ export function closeProjectOptions() {
}; };
} }
export function newFolder() { export function newFolder(parentId) {
return { return {
type: ActionTypes.SHOW_NEW_FOLDER_MODAL type: ActionTypes.SHOW_NEW_FOLDER_MODAL,
parentId
}; };
} }

View File

@ -82,7 +82,7 @@ export function dropzoneSendingCallback(file, xhr, formData) {
Object.keys(file.postData).forEach((key) => { Object.keys(file.postData).forEach((key) => {
formData.append(key, file.postData[key]); formData.append(key, file.postData[key]);
}); });
formData.append('Content-type', ''); formData.append('Content-type', file.type);
formData.append('Content-length', ''); formData.append('Content-length', '');
formData.append('acl', 'public-read'); formData.append('acl', 'public-read');
} }

View File

@ -56,19 +56,21 @@ class AssetList extends React.Component {
<table className="asset-table"> <table className="asset-table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th scope="col">Name</th>
<th>Size</th> <th scope="col">Size</th>
<th>View</th> <th scope="col">Sketch</th>
<th>Sketch</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{assetList.map(asset => {assetList.map(asset =>
( (
<tr className="asset-table__row" key={asset.key}> <tr className="asset-table__row" key={asset.key}>
<td>{asset.name}</td> <th scope="row">
<Link to={asset.url} target="_blank">
{asset.name}
</Link>
</th>
<td>{prettyBytes(asset.size)}</td> <td>{prettyBytes(asset.size)}</td>
<td><Link to={asset.url} target="_blank">View</Link></td>
<td><Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td> <td><Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
</tr> </tr>
))} ))}

View File

@ -108,7 +108,15 @@ class Editor extends React.Component {
delete this._cm.options.lint.options.errors; delete this._cm.options.lint.options.errors;
this._cm.setOption('extraKeys', { this._cm.setOption('extraKeys', {
Tab: cm => cm.replaceSelection(' '.repeat(INDENTATION_AMOUNT)), Tab: (cm) => {
// might need to specify and indent more?
const selection = cm.doc.getSelection();
if (selection.length > 0) {
cm.execCommand('indentMore');
} else {
cm.replaceSelection(' '.repeat(INDENTATION_AMOUNT));
}
},
[`${metaKey}-Enter`]: () => null, [`${metaKey}-Enter`]: () => null,
[`Shift-${metaKey}-Enter`]: () => null, [`Shift-${metaKey}-Enter`]: () => null,
[`${metaKey}-F`]: 'findPersistent', [`${metaKey}-F`]: 'findPersistent',
@ -126,7 +134,7 @@ class Editor extends React.Component {
this.props.clearConsole(); this.props.clearConsole();
this.props.startRefreshSketch(); this.props.startRefreshSketch();
} }
}, 400)); }, 1000));
this._cm.on('keyup', () => { this._cm.on('keyup', () => {
const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`; const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`;

View File

@ -188,7 +188,7 @@ export class FileNode extends React.Component {
<button <button
aria-label="add file" aria-label="add file"
onClick={() => { onClick={() => {
this.props.newFile(); this.props.newFile(this.props.id);
setTimeout(() => this.hideFileOptions(), 0); setTimeout(() => this.hideFileOptions(), 0);
}} }}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
@ -208,7 +208,7 @@ export class FileNode extends React.Component {
<button <button
aria-label="add folder" aria-label="add folder"
onClick={() => { onClick={() => {
this.props.newFolder(); this.props.newFolder(this.props.id);
setTimeout(() => this.hideFileOptions(), 0); setTimeout(() => this.hideFileOptions(), 0);
}} }}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}

View File

@ -5,6 +5,7 @@ import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import NewFileForm from './NewFileForm'; import NewFileForm from './NewFileForm';
import FileUploader from './FileUploader'; import FileUploader from './FileUploader';
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
@ -71,8 +72,8 @@ function validate(formProps) {
if (!formProps.name) { if (!formProps.name) {
errors.name = 'Please enter a name'; errors.name = 'Please enter a name';
} else if (!formProps.name.match(/(.+\.js$|.+\.css$|.+\.json$|.+\.txt$|.+\.csv$|.+\.tsv$)/i)) { } else if (!formProps.name.match(CREATE_FILE_REGEX)) {
errors.name = 'File must be of type JavaScript, CSS, JSON, TXT, CSV, or TSV.'; errors.name = 'Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert.';
} }
return errors; return errors;

View File

@ -66,6 +66,7 @@ class Sidebar extends React.Component {
'sidebar--project-options': this.props.projectOptionsVisible, 'sidebar--project-options': this.props.projectOptionsVisible,
'sidebar--cant-edit': !canEditProject 'sidebar--cant-edit': !canEditProject
}); });
const rootFile = this.props.files.filter(file => file.name === 'root')[0];
return ( return (
<nav className={sidebarClass} title="file-navigation" > <nav className={sidebarClass} title="file-navigation" >
@ -90,7 +91,7 @@ class Sidebar extends React.Component {
<button <button
aria-label="add folder" aria-label="add folder"
onClick={() => { onClick={() => {
this.props.newFolder(); this.props.newFolder(rootFile.id);
setTimeout(this.props.closeProjectOptions, 0); setTimeout(this.props.closeProjectOptions, 0);
}} }}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
@ -103,7 +104,7 @@ class Sidebar extends React.Component {
<button <button
aria-label="add file" aria-label="add file"
onClick={() => { onClick={() => {
this.props.newFile(); this.props.newFile(rootFile.id);
setTimeout(this.props.closeProjectOptions, 0); setTimeout(this.props.closeProjectOptions, 0);
}} }}
onBlur={this.onBlurComponent} onBlur={this.onBlurComponent}
@ -116,7 +117,7 @@ class Sidebar extends React.Component {
</div> </div>
</div> </div>
<ConnectedFileNode <ConnectedFileNode
id={this.props.files.filter(file => file.name === 'root')[0].id} id={rootFile.id}
canEdit={canEditProject} canEdit={canEditProject}
/> />
</nav> </nav>

View File

@ -124,7 +124,7 @@ class Toolbar extends React.Component {
</a> </a>
<input <input
type="text" type="text"
maxLength="256" maxLength="128"
className="toolbar__project-name-input" className="toolbar__project-name-input"
value={this.props.project.name} value={this.props.project.name}
onChange={this.handleProjectNameChange} onChange={this.handleProjectNameChange}

View File

@ -23,6 +23,7 @@ const initialState = {
previousPath: '/', previousPath: '/',
errorType: undefined, errorType: undefined,
runtimeErrorWarningVisible: true, runtimeErrorWarningVisible: true,
parentId: undefined
}; };
const ide = (state = initialState, action) => { const ide = (state = initialState, action) => {
@ -38,7 +39,7 @@ const ide = (state = initialState, action) => {
case ActionTypes.CONSOLE_EVENT: case ActionTypes.CONSOLE_EVENT:
return Object.assign({}, state, { consoleEvent: action.event }); return Object.assign({}, state, { consoleEvent: action.event });
case ActionTypes.SHOW_MODAL: case ActionTypes.SHOW_MODAL:
return Object.assign({}, state, { modalIsVisible: true }); return Object.assign({}, state, { modalIsVisible: true, parentId: action.parentId });
case ActionTypes.HIDE_MODAL: case ActionTypes.HIDE_MODAL:
return Object.assign({}, state, { modalIsVisible: false }); return Object.assign({}, state, { modalIsVisible: false });
case ActionTypes.COLLAPSE_SIDEBAR: case ActionTypes.COLLAPSE_SIDEBAR:
@ -60,7 +61,7 @@ const ide = (state = initialState, action) => {
case ActionTypes.CLOSE_PROJECT_OPTIONS: case ActionTypes.CLOSE_PROJECT_OPTIONS:
return Object.assign({}, state, { projectOptionsVisible: false }); return Object.assign({}, state, { projectOptionsVisible: false });
case ActionTypes.SHOW_NEW_FOLDER_MODAL: case ActionTypes.SHOW_NEW_FOLDER_MODAL:
return Object.assign({}, state, { newFolderModalVisible: true }); return Object.assign({}, state, { newFolderModalVisible: true, parentId: action.parentId });
case ActionTypes.CLOSE_NEW_FOLDER_MODAL: case ActionTypes.CLOSE_NEW_FOLDER_MODAL:
return Object.assign({}, state, { newFolderModalVisible: false }); return Object.assign({}, state, { newFolderModalVisible: false });
case ActionTypes.SHOW_SHARE_MODAL: case ActionTypes.SHOW_SHARE_MODAL:

View File

@ -21,7 +21,11 @@
} }
} }
& th { .asset-table__row>th:nth-child(1) {
padding-left: #{12 / $base-font-size}rem;
}
& thead th {
padding-top: #{10 / $base-font-size}rem; padding-top: #{10 / $base-font-size}rem;
padding-bottom: #{15 / $base-font-size}rem; padding-bottom: #{15 / $base-font-size}rem;
height: #{32 / $base-font-size}rem; height: #{32 / $base-font-size}rem;
@ -32,6 +36,10 @@
background-color: getThemifyVariable('background-color'); background-color: getThemifyVariable('background-color');
} }
} }
& th {
font-weight: normal;
}
} }
.asset-table__row { .asset-table__row {

View File

@ -44,7 +44,9 @@ export function validateSettings(formProps) {
if (formProps.currentPassword && !formProps.newPassword) { if (formProps.currentPassword && !formProps.newPassword) {
errors.newPassword = 'Please enter a new password or leave the current password empty.'; errors.newPassword = 'Please enter a new password or leave the current password empty.';
} }
if (formProps.newPassword && formProps.newPassword.length < 6) {
errors.newPassword = 'Password must be at least 6 characters';
}
return errors; return errors;
} }
@ -67,10 +69,8 @@ export function validateSignup(formProps) {
if (!formProps.password) { if (!formProps.password) {
errors.password = 'Please enter a password'; errors.password = 'Please enter a password';
} }
if (formProps.password) { if (formProps.password && formProps.password.length < 6) {
if (formProps.password.length < 6) { errors.password = 'Password must be at least 6 characters';
errors.password = 'Password must be at least 6 characters';
}
} }
if (!formProps.confirmPassword) { if (!formProps.confirmPassword) {
errors.confirmPassword = 'Please enter a password confirmation'; errors.confirmPassword = 'Please enter a password confirmation';

View File

@ -130,7 +130,7 @@
"jest-express": "^1.10.1", "jest-express": "^1.10.1",
"js-beautify": "^1.8.9", "js-beautify": "^1.8.9",
"jsdom": "^9.8.3", "jsdom": "^9.8.3",
"jshint": "^2.10.1", "jshint": "^2.10.2",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"loop-protect": "github:catarak/loop-protect", "loop-protect": "github:catarak/loop-protect",
"mjml": "^3.3.2", "mjml": "^3.3.2",

View File

@ -3,7 +3,7 @@
export const fileExtensionsArray = ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'wav', 'flac', 'ogg', export const fileExtensionsArray = ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'wav', 'flac', 'ogg',
'oga', 'mp4', 'm4p', 'mp3', 'm4a', 'aiff', 'aif', 'm4v', 'aac', 'webm', 'mpg', 'mp2', 'oga', 'mp4', 'm4p', 'mp3', 'm4a', 'aiff', 'aif', 'm4v', 'aac', 'webm', 'mpg', 'mp2',
'mpeg', 'mpe', 'mpv', 'js', 'jsx', 'html', 'htm', 'css', 'json', 'csv', 'obj', 'svg', 'mpeg', 'mpe', 'mpv', 'js', 'jsx', 'html', 'htm', 'css', 'json', 'csv', 'obj', 'svg',
'otf', 'ttf', 'txt', 'mov', 'vert', 'frag']; 'otf', 'ttf', 'txt', 'mov', 'vert', 'frag', 'bin'];
export const mimeTypes = `image/*,audio/*,text/javascript,text/html,text/css, export const mimeTypes = `image/*,audio/*,text/javascript,text/html,text/css,
application/json,application/x-font-ttf,application/x-font-truetype,text/plain, application/json,application/x-font-ttf,application/x-font-truetype,text/plain,
@ -22,6 +22,8 @@ export const STRING_REGEX = /(['"])((\\\1|.)*?)\1/gm;
// these are files that have to be linked to with a blob url // these are files that have to be linked to with a blob url
export const PLAINTEXT_FILE_REGEX = /.+\.(json|txt|csv|vert|frag|tsv)$/i; export const PLAINTEXT_FILE_REGEX = /.+\.(json|txt|csv|vert|frag|tsv)$/i;
// these are files that users would want to edit as text (maybe svg should be here?) // these are files that users would want to edit as text (maybe svg should be here?)
export const TEXT_FILE_REGEX = /.+\.(json|txt|csv|vert|frag|js|css|html|htm|jsx)$/i; export const TEXT_FILE_REGEX = /.+\.(json|txt|csv|tsv|vert|frag|js|css|html|htm|jsx)$/i;
export const NOT_EXTERNAL_LINK_REGEX = /^(?!(http:\/\/|https:\/\/))/; export const NOT_EXTERNAL_LINK_REGEX = /^(?!(http:\/\/|https:\/\/))/;
export const EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/; export const EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/;
export const CREATE_FILE_REGEX = /.+\.(json|txt|csv|tsv|js|css|frag|vert)$/i;

View File

@ -4,7 +4,7 @@
header does not match `type` header does not match `type`
*/ */
const requestsOfType = type => (req, res, next) => { const requestsOfType = type => (req, res, next) => {
const hasContentType = req.get('content-type') !== null; const hasContentType = req.get('content-type') !== undefined && req.get('content-type') !== null;
const isCorrectType = req.is(type) === null || req.is(type) === type; const isCorrectType = req.is(type) === null || req.is(type) === type;
if (hasContentType && !isCorrectType) { if (hasContentType && !isCorrectType) {