Merge branch 'feature/public-api' into feature/sketch-collections
This commit is contained in:
commit
846d2bb7db
19 changed files with 93 additions and 57 deletions
17
.github/config.yml
vendored
Normal file
17
.github/config.yml
vendored
Normal 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
|
|
@ -46,4 +46,4 @@ deploy:
|
|||
|
||||
env:
|
||||
global:
|
||||
- APP_IMAGE_NAME=p5jswebeditor_app
|
||||
- APP_IMAGE_NAME=p5js-web-editor_app
|
||||
|
|
|
@ -131,12 +131,12 @@ class Nav extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleAddFile() {
|
||||
this.props.newFile();
|
||||
this.props.newFile(this.props.rootFile.id);
|
||||
this.setDropdown('none');
|
||||
}
|
||||
|
||||
handleAddFolder() {
|
||||
this.props.newFolder();
|
||||
this.props.newFolder(this.props.rootFile.id);
|
||||
this.setDropdown('none');
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ class Nav extends React.PureComponent {
|
|||
|
||||
handleDownload() {
|
||||
this.props.autosaveProject();
|
||||
this.props.exportProjectAsZip(this.props.project.id);
|
||||
projectActions.exportProjectAsZip(this.props.project.id);
|
||||
this.setDropdown('none');
|
||||
}
|
||||
|
||||
|
@ -719,7 +719,6 @@ Nav.propTypes = {
|
|||
setToastText: PropTypes.func.isRequired,
|
||||
saveProject: PropTypes.func.isRequired,
|
||||
autosaveProject: PropTypes.func.isRequired,
|
||||
exportProjectAsZip: PropTypes.func.isRequired,
|
||||
cloneProject: PropTypes.func.isRequired,
|
||||
user: PropTypes.shape({
|
||||
authenticated: PropTypes.bool.isRequired,
|
||||
|
@ -750,7 +749,10 @@ Nav.propTypes = {
|
|||
setAllAccessibleOutput: PropTypes.func.isRequired,
|
||||
newFile: 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 = {
|
||||
|
@ -767,7 +769,8 @@ function mapStateToProps(state) {
|
|||
return {
|
||||
project: state.project,
|
||||
user: state.user,
|
||||
unsavedChanges: state.ide.unsavedChanges
|
||||
unsavedChanges: state.ide.unsavedChanges,
|
||||
rootFile: state.files.filter(file => file.name === 'root')[0]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,12 @@ describe('Nav', () => {
|
|||
},
|
||||
startSketch: 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} />);
|
||||
|
||||
|
|
|
@ -41,14 +41,7 @@ export function updateFileContent(id, content) {
|
|||
export function createFile(formProps) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const selectedFile = state.files.find(file => file.isSelectedFile);
|
||||
const rootFile = state.files.find(file => file.name === 'root');
|
||||
let parentId;
|
||||
if (selectedFile.fileType === 'folder') {
|
||||
parentId = selectedFile.id;
|
||||
} else {
|
||||
parentId = rootFile.id;
|
||||
}
|
||||
const { parentId } = state.ide;
|
||||
if (state.project.id) {
|
||||
const postParams = {
|
||||
name: createUniqueName(formProps.name, parentId, state.files),
|
||||
|
@ -99,14 +92,7 @@ export function createFile(formProps) {
|
|||
export function createFolder(formProps) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const selectedFile = state.files.find(file => file.isSelectedFile);
|
||||
const rootFile = state.files.find(file => file.name === 'root');
|
||||
let parentId;
|
||||
if (selectedFile.fileType === 'folder') {
|
||||
parentId = selectedFile.id;
|
||||
} else {
|
||||
parentId = rootFile.id;
|
||||
}
|
||||
const { parentId } = state.ide;
|
||||
if (state.project.id) {
|
||||
const postParams = {
|
||||
name: createUniqueName(formProps.name, parentId, state.files),
|
||||
|
|
|
@ -62,9 +62,10 @@ export function resetSelectedFile(previousId) {
|
|||
};
|
||||
}
|
||||
|
||||
export function newFile() {
|
||||
export function newFile(parentId) {
|
||||
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 {
|
||||
type: ActionTypes.SHOW_NEW_FOLDER_MODAL
|
||||
type: ActionTypes.SHOW_NEW_FOLDER_MODAL,
|
||||
parentId
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ export function dropzoneSendingCallback(file, xhr, formData) {
|
|||
Object.keys(file.postData).forEach((key) => {
|
||||
formData.append(key, file.postData[key]);
|
||||
});
|
||||
formData.append('Content-type', '');
|
||||
formData.append('Content-type', file.type);
|
||||
formData.append('Content-length', '');
|
||||
formData.append('acl', 'public-read');
|
||||
}
|
||||
|
|
|
@ -56,19 +56,21 @@ class AssetList extends React.Component {
|
|||
<table className="asset-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>View</th>
|
||||
<th>Sketch</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Size</th>
|
||||
<th scope="col">Sketch</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assetList.map(asset =>
|
||||
(
|
||||
<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><Link to={asset.url} target="_blank">View</Link></td>
|
||||
<td><Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
|
@ -108,7 +108,15 @@ class Editor extends React.Component {
|
|||
delete this._cm.options.lint.options.errors;
|
||||
|
||||
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,
|
||||
[`Shift-${metaKey}-Enter`]: () => null,
|
||||
[`${metaKey}-F`]: 'findPersistent',
|
||||
|
@ -126,7 +134,7 @@ class Editor extends React.Component {
|
|||
this.props.clearConsole();
|
||||
this.props.startRefreshSketch();
|
||||
}
|
||||
}, 400));
|
||||
}, 1000));
|
||||
|
||||
this._cm.on('keyup', () => {
|
||||
const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`;
|
||||
|
|
|
@ -188,7 +188,7 @@ export class FileNode extends React.Component {
|
|||
<button
|
||||
aria-label="add file"
|
||||
onClick={() => {
|
||||
this.props.newFile();
|
||||
this.props.newFile(this.props.id);
|
||||
setTimeout(() => this.hideFileOptions(), 0);
|
||||
}}
|
||||
onBlur={this.onBlurComponent}
|
||||
|
@ -208,7 +208,7 @@ export class FileNode extends React.Component {
|
|||
<button
|
||||
aria-label="add folder"
|
||||
onClick={() => {
|
||||
this.props.newFolder();
|
||||
this.props.newFolder(this.props.id);
|
||||
setTimeout(() => this.hideFileOptions(), 0);
|
||||
}}
|
||||
onBlur={this.onBlurComponent}
|
||||
|
|
|
@ -5,6 +5,7 @@ import classNames from 'classnames';
|
|||
import InlineSVG from 'react-inlinesvg';
|
||||
import NewFileForm from './NewFileForm';
|
||||
import FileUploader from './FileUploader';
|
||||
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
||||
|
||||
const exitUrl = require('../../../images/exit.svg');
|
||||
|
||||
|
@ -71,8 +72,8 @@ function validate(formProps) {
|
|||
|
||||
if (!formProps.name) {
|
||||
errors.name = 'Please enter a name';
|
||||
} else if (!formProps.name.match(/(.+\.js$|.+\.css$|.+\.json$|.+\.txt$|.+\.csv$|.+\.tsv$)/i)) {
|
||||
errors.name = 'File must be of type JavaScript, CSS, JSON, TXT, CSV, or TSV.';
|
||||
} else if (!formProps.name.match(CREATE_FILE_REGEX)) {
|
||||
errors.name = 'Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert.';
|
||||
}
|
||||
|
||||
return errors;
|
||||
|
|
|
@ -66,6 +66,7 @@ class Sidebar extends React.Component {
|
|||
'sidebar--project-options': this.props.projectOptionsVisible,
|
||||
'sidebar--cant-edit': !canEditProject
|
||||
});
|
||||
const rootFile = this.props.files.filter(file => file.name === 'root')[0];
|
||||
|
||||
return (
|
||||
<nav className={sidebarClass} title="file-navigation" >
|
||||
|
@ -90,7 +91,7 @@ class Sidebar extends React.Component {
|
|||
<button
|
||||
aria-label="add folder"
|
||||
onClick={() => {
|
||||
this.props.newFolder();
|
||||
this.props.newFolder(rootFile.id);
|
||||
setTimeout(this.props.closeProjectOptions, 0);
|
||||
}}
|
||||
onBlur={this.onBlurComponent}
|
||||
|
@ -103,7 +104,7 @@ class Sidebar extends React.Component {
|
|||
<button
|
||||
aria-label="add file"
|
||||
onClick={() => {
|
||||
this.props.newFile();
|
||||
this.props.newFile(rootFile.id);
|
||||
setTimeout(this.props.closeProjectOptions, 0);
|
||||
}}
|
||||
onBlur={this.onBlurComponent}
|
||||
|
@ -116,7 +117,7 @@ class Sidebar extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<ConnectedFileNode
|
||||
id={this.props.files.filter(file => file.name === 'root')[0].id}
|
||||
id={rootFile.id}
|
||||
canEdit={canEditProject}
|
||||
/>
|
||||
</nav>
|
||||
|
|
|
@ -124,7 +124,7 @@ class Toolbar extends React.Component {
|
|||
</a>
|
||||
<input
|
||||
type="text"
|
||||
maxLength="256"
|
||||
maxLength="128"
|
||||
className="toolbar__project-name-input"
|
||||
value={this.props.project.name}
|
||||
onChange={this.handleProjectNameChange}
|
||||
|
|
|
@ -23,6 +23,7 @@ const initialState = {
|
|||
previousPath: '/',
|
||||
errorType: undefined,
|
||||
runtimeErrorWarningVisible: true,
|
||||
parentId: undefined
|
||||
};
|
||||
|
||||
const ide = (state = initialState, action) => {
|
||||
|
@ -38,7 +39,7 @@ const ide = (state = initialState, action) => {
|
|||
case ActionTypes.CONSOLE_EVENT:
|
||||
return Object.assign({}, state, { consoleEvent: action.event });
|
||||
case ActionTypes.SHOW_MODAL:
|
||||
return Object.assign({}, state, { modalIsVisible: true });
|
||||
return Object.assign({}, state, { modalIsVisible: true, parentId: action.parentId });
|
||||
case ActionTypes.HIDE_MODAL:
|
||||
return Object.assign({}, state, { modalIsVisible: false });
|
||||
case ActionTypes.COLLAPSE_SIDEBAR:
|
||||
|
@ -60,7 +61,7 @@ const ide = (state = initialState, action) => {
|
|||
case ActionTypes.CLOSE_PROJECT_OPTIONS:
|
||||
return Object.assign({}, state, { projectOptionsVisible: false });
|
||||
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:
|
||||
return Object.assign({}, state, { newFolderModalVisible: false });
|
||||
case ActionTypes.SHOW_SHARE_MODAL:
|
||||
|
|
|
@ -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-bottom: #{15 / $base-font-size}rem;
|
||||
height: #{32 / $base-font-size}rem;
|
||||
|
@ -32,6 +36,10 @@
|
|||
background-color: getThemifyVariable('background-color');
|
||||
}
|
||||
}
|
||||
|
||||
& th {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-table__row {
|
||||
|
|
|
@ -44,7 +44,9 @@ export function validateSettings(formProps) {
|
|||
if (formProps.currentPassword && !formProps.newPassword) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -67,10 +69,8 @@ export function validateSignup(formProps) {
|
|||
if (!formProps.password) {
|
||||
errors.password = 'Please enter a password';
|
||||
}
|
||||
if (formProps.password) {
|
||||
if (formProps.password.length < 6) {
|
||||
errors.password = 'Password must be at least 6 characters';
|
||||
}
|
||||
if (formProps.password && formProps.password.length < 6) {
|
||||
errors.password = 'Password must be at least 6 characters';
|
||||
}
|
||||
if (!formProps.confirmPassword) {
|
||||
errors.confirmPassword = 'Please enter a password confirmation';
|
||||
|
|
|
@ -130,7 +130,7 @@
|
|||
"jest-express": "^1.10.1",
|
||||
"js-beautify": "^1.8.9",
|
||||
"jsdom": "^9.8.3",
|
||||
"jshint": "^2.10.1",
|
||||
"jshint": "^2.10.2",
|
||||
"lodash": "^4.17.15",
|
||||
"loop-protect": "github:catarak/loop-protect",
|
||||
"mjml": "^3.3.2",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
export const fileExtensionsArray = ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'wav', 'flac', 'ogg',
|
||||
'oga', 'mp4', 'm4p', 'mp3', 'm4a', 'aiff', 'aif', 'm4v', 'aac', 'webm', 'mpg', 'mp2',
|
||||
'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,
|
||||
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
|
||||
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?)
|
||||
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 EXTERNAL_LINK_REGEX = /^(http:\/\/|https:\/\/)/;
|
||||
|
||||
export const CREATE_FILE_REGEX = /.+\.(json|txt|csv|tsv|js|css|frag|vert)$/i;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
header does not match `type`
|
||||
*/
|
||||
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;
|
||||
|
||||
if (hasContentType && !isCorrectType) {
|
||||
|
|
Loading…
Reference in a new issue