Merge branch 'release-1.0.4' into release
This commit is contained in:
commit
9767a749cd
77 changed files with 9185 additions and 2869 deletions
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
|
@ -1 +1,2 @@
|
||||||
custom: https://processingfoundation.org/support
|
github: processing
|
||||||
|
custom: https://processingfoundation.org/
|
||||||
|
|
|
@ -14,6 +14,7 @@ COPY .babelrc index.js nodemon.json ./
|
||||||
COPY ./webpack ./webpack
|
COPY ./webpack ./webpack
|
||||||
COPY client ./client
|
COPY client ./client
|
||||||
COPY server ./server
|
COPY server ./server
|
||||||
|
COPY translations/locales ./translations/locales
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
|
||||||
FROM development as build
|
FROM development as build
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import SortArrowUp from '../images/sort-arrow-up.svg';
|
|
||||||
import SortArrowDown from '../images/sort-arrow-down.svg';
|
|
||||||
import Github from '../images/github.svg';
|
|
||||||
import Google from '../images/google.svg';
|
|
||||||
import Plus from '../images/plus-icon.svg';
|
|
||||||
import Close from '../images/close.svg';
|
|
||||||
import DropdownArrow from '../images/down-filled-triangle.svg';
|
|
||||||
|
|
||||||
// HOC that adds the right web accessibility props
|
|
||||||
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
|
|
||||||
|
|
||||||
// could also give these a default size, color, etc. based on the theme
|
|
||||||
// Need to add size to these - like small icon, medium icon, large icon. etc.
|
|
||||||
function withLabel(SvgComponent) {
|
|
||||||
const Icon = (props) => {
|
|
||||||
const { 'aria-label': ariaLabel } = props;
|
|
||||||
if (ariaLabel) {
|
|
||||||
return (<SvgComponent
|
|
||||||
{...props}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
role="img"
|
|
||||||
focusable="false"
|
|
||||||
/>);
|
|
||||||
}
|
|
||||||
return (<SvgComponent
|
|
||||||
{...props}
|
|
||||||
aria-hidden
|
|
||||||
focusable="false"
|
|
||||||
/>);
|
|
||||||
};
|
|
||||||
|
|
||||||
Icon.propTypes = {
|
|
||||||
'aria-label': PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
Icon.defaultProps = {
|
|
||||||
'aria-label': null
|
|
||||||
};
|
|
||||||
|
|
||||||
return Icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SortArrowUpIcon = withLabel(SortArrowUp);
|
|
||||||
export const SortArrowDownIcon = withLabel(SortArrowDown);
|
|
||||||
export const GithubIcon = withLabel(Github);
|
|
||||||
export const GoogleIcon = withLabel(Google);
|
|
||||||
export const PlusIcon = withLabel(Plus);
|
|
||||||
export const CloseIcon = withLabel(Close);
|
|
||||||
export const DropdownArrowIcon = withLabel(DropdownArrow);
|
|
|
@ -1,18 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { select } from '@storybook/addon-knobs';
|
|
||||||
|
|
||||||
import * as icons from './icons';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Common/Icons',
|
|
||||||
component: icons
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AllIcons = () => {
|
|
||||||
const names = Object.keys(icons);
|
|
||||||
|
|
||||||
const SelectedIcon = icons[select('name', names, names[0])];
|
|
||||||
return (
|
|
||||||
<SelectedIcon />
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,12 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { prop } from '../theme';
|
||||||
import SortArrowUp from '../images/sort-arrow-up.svg';
|
import SortArrowUp from '../images/sort-arrow-up.svg';
|
||||||
import SortArrowDown from '../images/sort-arrow-down.svg';
|
import SortArrowDown from '../images/sort-arrow-down.svg';
|
||||||
import Github from '../images/github.svg';
|
import Github from '../images/github.svg';
|
||||||
import Google from '../images/google.svg';
|
import Google from '../images/google.svg';
|
||||||
import Plus from '../images/plus-icon.svg';
|
import Plus from '../images/plus-icon.svg';
|
||||||
import Close from '../images/close.svg';
|
import Close from '../images/close.svg';
|
||||||
|
import Exit from '../images/exit.svg';
|
||||||
import DropdownArrow from '../images/down-filled-triangle.svg';
|
import DropdownArrow from '../images/down-filled-triangle.svg';
|
||||||
|
import Preferences from '../images/preferences.svg';
|
||||||
|
import Play from '../images/triangle-arrow-right.svg';
|
||||||
|
|
||||||
// HOC that adds the right web accessibility props
|
// HOC that adds the right web accessibility props
|
||||||
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
|
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
|
||||||
|
@ -15,16 +20,33 @@ import DropdownArrow from '../images/down-filled-triangle.svg';
|
||||||
// Need to add size to these - like small icon, medium icon, large icon. etc.
|
// Need to add size to these - like small icon, medium icon, large icon. etc.
|
||||||
function withLabel(SvgComponent) {
|
function withLabel(SvgComponent) {
|
||||||
const Icon = (props) => {
|
const Icon = (props) => {
|
||||||
|
const StyledIcon = styled(SvgComponent)`
|
||||||
|
&&& {
|
||||||
|
color: ${prop('Icon.default')};
|
||||||
|
& g, & path, & polygon {
|
||||||
|
opacity: 1;
|
||||||
|
fill: ${prop('Icon.default')};
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: ${prop('Icon.hover')};
|
||||||
|
& g, & path, & polygon {
|
||||||
|
opacity: 1;
|
||||||
|
fill: ${prop('Icon.hover')};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const { 'aria-label': ariaLabel } = props;
|
const { 'aria-label': ariaLabel } = props;
|
||||||
if (ariaLabel) {
|
if (ariaLabel) {
|
||||||
return (<SvgComponent
|
return (<StyledIcon
|
||||||
{...props}
|
{...props}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
role="img"
|
role="img"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
return (<SvgComponent
|
return (<StyledIcon
|
||||||
{...props}
|
{...props}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
focusable="false"
|
focusable="false"
|
||||||
|
@ -48,4 +70,7 @@ export const GithubIcon = withLabel(Github);
|
||||||
export const GoogleIcon = withLabel(Google);
|
export const GoogleIcon = withLabel(Google);
|
||||||
export const PlusIcon = withLabel(Plus);
|
export const PlusIcon = withLabel(Plus);
|
||||||
export const CloseIcon = withLabel(Close);
|
export const CloseIcon = withLabel(Close);
|
||||||
|
export const ExitIcon = withLabel(Exit);
|
||||||
export const DropdownArrowIcon = withLabel(DropdownArrow);
|
export const DropdownArrowIcon = withLabel(DropdownArrow);
|
||||||
|
export const PreferencesIcon = withLabel(Preferences);
|
||||||
|
export const PlayIcon = withLabel(Play);
|
||||||
|
|
|
@ -4,20 +4,21 @@ import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import i18next from 'i18next';
|
||||||
import * as IDEActions from '../modules/IDE/actions/ide';
|
import * as IDEActions from '../modules/IDE/actions/ide';
|
||||||
import * as toastActions from '../modules/IDE/actions/toast';
|
import * as toastActions from '../modules/IDE/actions/toast';
|
||||||
import * as projectActions from '../modules/IDE/actions/project';
|
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);
|
||||||
|
@ -56,6 +57,10 @@ class Nav extends React.PureComponent {
|
||||||
this.handleFocusForHelp = this.handleFocus.bind(this, 'help');
|
this.handleFocusForHelp = this.handleFocus.bind(this, 'help');
|
||||||
this.toggleDropdownForAccount = this.toggleDropdown.bind(this, 'account');
|
this.toggleDropdownForAccount = this.toggleDropdown.bind(this, 'account');
|
||||||
this.handleFocusForAccount = this.handleFocus.bind(this, 'account');
|
this.handleFocusForAccount = this.handleFocus.bind(this, 'account');
|
||||||
|
this.toggleDropdownForLang = this.toggleDropdown.bind(this, 'lang');
|
||||||
|
this.handleFocusForLang = this.handleFocus.bind(this, 'lang');
|
||||||
|
this.handleLangSelection = this.handleLangSelection.bind(this);
|
||||||
|
|
||||||
this.closeDropDown = this.closeDropDown.bind(this);
|
this.closeDropDown = this.closeDropDown.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +169,13 @@ class Nav extends React.PureComponent {
|
||||||
this.setDropdown('none');
|
this.setDropdown('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLangSelection(event) {
|
||||||
|
i18next.changeLanguage(event.target.value);
|
||||||
|
this.props.showToast(1500);
|
||||||
|
this.props.setToastText('LangChange');
|
||||||
|
this.setDropdown('none');
|
||||||
|
}
|
||||||
|
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
this.props.logoutUser();
|
this.props.logoutUser();
|
||||||
this.setDropdown('none');
|
this.setDropdown('none');
|
||||||
|
@ -234,7 +246,7 @@ class Nav extends React.PureComponent {
|
||||||
<Link to="/" className="nav__back-link">
|
<Link to="/" className="nav__back-link">
|
||||||
<CaretLeftIcon className="nav__back-icon" focusable="false" aria-hidden="true" />
|
<CaretLeftIcon className="nav__back-icon" focusable="false" aria-hidden="true" />
|
||||||
<span className="nav__item-header">
|
<span className="nav__item-header">
|
||||||
Back to Editor
|
{this.props.t('BackEditor')}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -259,7 +271,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header">File</span>
|
<span className="nav__item-header">{this.props.t('File')}</span>
|
||||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
|
@ -269,17 +281,17 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
New
|
{this.props.t('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}
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Save
|
{this.props.t('Save')}
|
||||||
<span className="nav__keyboard-shortcut">{metaKeyName}+S</span>
|
<span className="nav__keyboard-shortcut">{metaKeyName}+S</span>
|
||||||
</button>
|
</button>
|
||||||
</li> }
|
</li> }
|
||||||
|
@ -290,7 +302,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Duplicate
|
{this.props.t('Duplicate')}
|
||||||
</button>
|
</button>
|
||||||
</li> }
|
</li> }
|
||||||
{ this.props.project.id &&
|
{ this.props.project.id &&
|
||||||
|
@ -300,7 +312,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Share
|
{this.props.t('Share')}
|
||||||
</button>
|
</button>
|
||||||
</li> }
|
</li> }
|
||||||
{ this.props.project.id &&
|
{ this.props.project.id &&
|
||||||
|
@ -310,7 +322,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForFile}
|
onFocus={this.handleFocusForFile}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Download
|
{this.props.t('Download')}
|
||||||
</button>
|
</button>
|
||||||
</li> }
|
</li> }
|
||||||
{ this.props.user.authenticated &&
|
{ this.props.user.authenticated &&
|
||||||
|
@ -321,10 +333,10 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
Open
|
{this.props.t('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">
|
||||||
|
@ -334,10 +346,10 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
Add to Collection
|
{this.props.t('AddToCollection')}
|
||||||
</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"
|
||||||
|
@ -345,7 +357,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
Examples
|
{this.props.t('Examples')}
|
||||||
</Link>
|
</Link>
|
||||||
</li> }
|
</li> }
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -361,7 +373,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header">Edit</span>
|
<span className="nav__item-header">{this.props.t('Edit')}</span>
|
||||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown" >
|
<ul className="nav__dropdown" >
|
||||||
|
@ -374,7 +386,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForEdit}
|
onFocus={this.handleFocusForEdit}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Tidy Code
|
{this.props.t('TidyCode')}
|
||||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+Tab</span>
|
<span className="nav__keyboard-shortcut">{'\u21E7'}+Tab</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -384,7 +396,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForEdit}
|
onFocus={this.handleFocusForEdit}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Find
|
{this.props.t('Find')}
|
||||||
<span className="nav__keyboard-shortcut">{metaKeyName}+F</span>
|
<span className="nav__keyboard-shortcut">{metaKeyName}+F</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -394,7 +406,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForEdit}
|
onFocus={this.handleFocusForEdit}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Find Next
|
{this.props.t('FindNext')}
|
||||||
<span className="nav__keyboard-shortcut">{metaKeyName}+G</span>
|
<span className="nav__keyboard-shortcut">{metaKeyName}+G</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -404,7 +416,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForEdit}
|
onFocus={this.handleFocusForEdit}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Find Previous
|
{this.props.t('FindPrevious')}
|
||||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span>
|
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -421,7 +433,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header">Sketch</span>
|
<span className="nav__item-header">{this.props.t('Sketch')}</span>
|
||||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
|
@ -431,7 +443,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForSketch}
|
onFocus={this.handleFocusForSketch}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Add File
|
{this.props.t('AddFile')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -440,7 +452,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForSketch}
|
onFocus={this.handleFocusForSketch}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Add Folder
|
{this.props.t('AddFolder')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -449,7 +461,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForSketch}
|
onFocus={this.handleFocusForSketch}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Run
|
{this.props.t('Run')}
|
||||||
<span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span>
|
<span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -459,7 +471,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForSketch}
|
onFocus={this.handleFocusForSketch}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Stop
|
{this.props.t('Stop')}
|
||||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span>
|
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -496,7 +508,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="nav__item-header">Help</span>
|
<span className="nav__item-header">{this.props.t('Help')}</span>
|
||||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
|
@ -506,7 +518,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.handleKeyboardShortcuts}
|
onClick={this.handleKeyboardShortcuts}
|
||||||
>
|
>
|
||||||
Keyboard Shortcuts
|
{this.props.t('KeyboardShortcuts')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -517,7 +529,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForHelp}
|
onFocus={this.handleFocusForHelp}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>Reference
|
>{this.props.t('Reference')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -527,7 +539,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
About
|
{this.props.t('About')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -536,18 +548,73 @@ class Nav extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderLanguageMenu(navDropdownState) {
|
||||||
|
return (
|
||||||
|
<ul className="nav__items-right" title="user-menu">
|
||||||
|
<li className={navDropdownState.lang}>
|
||||||
|
<button
|
||||||
|
onClick={this.toggleDropdownForLang}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
onFocus={this.clearHideTimeout}
|
||||||
|
onMouseOver={() => {
|
||||||
|
if (this.state.dropdownOpen !== 'none') {
|
||||||
|
this.setDropdown('lang');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="nav__item-header"> {this.props.t('Lang')}</span>
|
||||||
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<ul className="nav__dropdown">
|
||||||
|
|
||||||
|
<li className="nav__dropdown-item">
|
||||||
|
<button
|
||||||
|
onFocus={this.handleFocusForLang}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
value="it"
|
||||||
|
onClick={e => this.handleLangSelection(e)}
|
||||||
|
>
|
||||||
|
Italian (Test Fallback)
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className="nav__dropdown-item">
|
||||||
|
<button
|
||||||
|
onFocus={this.handleFocusForLang}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
value="en-US"
|
||||||
|
onClick={e => this.handleLangSelection(e)}
|
||||||
|
>English
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className="nav__dropdown-item">
|
||||||
|
<button
|
||||||
|
onFocus={this.handleFocusForLang}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
value="es-419"
|
||||||
|
onClick={e => this.handleLangSelection(e)}
|
||||||
|
>
|
||||||
|
Español
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
renderUnauthenticatedUserMenu(navDropdownState) {
|
renderUnauthenticatedUserMenu(navDropdownState) {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<Link to="/login" className="nav__auth-button">
|
<Link to="/login" className="nav__auth-button">
|
||||||
<span className="nav__item-header">Log in</span>
|
<span className="nav__item-header">{this.props.t('Login')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<span className="nav__item-or">or</span>
|
<span className="nav__item-or">{this.props.t('LoginOr')}</span>
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<Link to="/signup" className="nav__auth-button">
|
<Link to="/signup" className="nav__auth-button">
|
||||||
<span className="nav__item-header">Sign up</span>
|
<span className="nav__item-header">{this.props.t('SignUp')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -558,7 +625,7 @@ class Nav extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<span>Hello, {this.props.user.username}!</span>
|
<span>{this.props.t('Hello')}, {this.props.user.username}!</span>
|
||||||
</li>
|
</li>
|
||||||
<span className="nav__item-spacer">|</span>
|
<span className="nav__item-spacer">|</span>
|
||||||
<li className={navDropdownState.account}>
|
<li className={navDropdownState.account}>
|
||||||
|
@ -573,7 +640,7 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
My Account
|
{this.props.t('MyAccount')}
|
||||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<ul className="nav__dropdown">
|
<ul className="nav__dropdown">
|
||||||
|
@ -584,10 +651,10 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
My sketches
|
{this.props.t('MySketches')}
|
||||||
</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`}
|
||||||
|
@ -595,7 +662,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
My collections
|
{this.props.t('MyCollections')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
@ -606,7 +673,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
My assets
|
{this.props.t('MyAssets')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -616,7 +683,7 @@ class Nav extends React.PureComponent {
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
>
|
>
|
||||||
Settings
|
{this.props.t('Settings')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
|
@ -625,7 +692,7 @@ class Nav extends React.PureComponent {
|
||||||
onFocus={this.handleFocusForAccount}
|
onFocus={this.handleFocusForAccount}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
>
|
>
|
||||||
Log out
|
{this.props.t('LogOut')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -635,7 +702,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) {
|
||||||
|
@ -678,6 +745,10 @@ class Nav extends React.PureComponent {
|
||||||
account: classNames({
|
account: classNames({
|
||||||
'nav__item': true,
|
'nav__item': true,
|
||||||
'nav__item--open': this.state.dropdownOpen === 'account'
|
'nav__item--open': this.state.dropdownOpen === 'account'
|
||||||
|
}),
|
||||||
|
lang: classNames({
|
||||||
|
'nav__item': true,
|
||||||
|
'nav__item--open': this.state.dropdownOpen === 'lang'
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -685,6 +756,7 @@ class Nav extends React.PureComponent {
|
||||||
<header>
|
<header>
|
||||||
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
||||||
{this.renderLeftLayout(navDropdownState)}
|
{this.renderLeftLayout(navDropdownState)}
|
||||||
|
{getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)}
|
||||||
{this.renderUserMenu(navDropdownState)}
|
{this.renderUserMenu(navDropdownState)}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
@ -735,7 +807,9 @@ Nav.propTypes = {
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
})
|
}),
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Nav.defaultProps = {
|
Nav.defaultProps = {
|
||||||
|
@ -768,5 +842,5 @@ const mapDispatchToProps = {
|
||||||
setAllAccessibleOutput
|
setAllAccessibleOutput
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav));
|
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)));
|
||||||
export { Nav as NavComponent };
|
export { Nav as NavComponent };
|
||||||
|
|
|
@ -1,183 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { FileNode } from '../../modules/IDE/components/FileNode';
|
|
||||||
|
|
||||||
describe('<FileNode />', () => {
|
|
||||||
let component;
|
|
||||||
let props = {};
|
|
||||||
let input;
|
|
||||||
let renameTriggerButton;
|
|
||||||
const changeName = (newFileName) => {
|
|
||||||
renameTriggerButton.simulate('click');
|
|
||||||
input.simulate('change', { target: { value: newFileName } });
|
|
||||||
input.simulate('blur');
|
|
||||||
};
|
|
||||||
const getState = () => component.state();
|
|
||||||
const getUpdatedName = () => getState().updatedName;
|
|
||||||
|
|
||||||
describe('with valid props, regardless of filetype', () => {
|
|
||||||
['folder', 'file'].forEach((fileType) => {
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
id: '0',
|
|
||||||
name: 'test.jsx',
|
|
||||||
fileType,
|
|
||||||
canEdit: true,
|
|
||||||
children: [],
|
|
||||||
authenticated: false,
|
|
||||||
setSelectedFile: jest.fn(),
|
|
||||||
deleteFile: jest.fn(),
|
|
||||||
updateFileName: jest.fn(),
|
|
||||||
resetSelectedFile: jest.fn(),
|
|
||||||
newFile: jest.fn(),
|
|
||||||
newFolder: jest.fn(),
|
|
||||||
showFolderChildren: jest.fn(),
|
|
||||||
hideFolderChildren: jest.fn(),
|
|
||||||
openUploadFileModal: jest.fn(),
|
|
||||||
setProjectName: jest.fn(),
|
|
||||||
};
|
|
||||||
component = shallow(<FileNode {...props} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when changing name', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
input = component.find('.sidebar__file-item-input');
|
|
||||||
renameTriggerButton = component
|
|
||||||
.find('.sidebar__file-item-option')
|
|
||||||
.first();
|
|
||||||
component.setState({ isEditing: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('to an empty name', () => {
|
|
||||||
const newName = '';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
|
|
||||||
it('should not save', () => expect(props.updateFileName).not.toHaveBeenCalled());
|
|
||||||
it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('as file with valid props', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
id: '0',
|
|
||||||
name: 'test.jsx',
|
|
||||||
fileType: 'file',
|
|
||||||
canEdit: true,
|
|
||||||
children: [],
|
|
||||||
authenticated: false,
|
|
||||||
setSelectedFile: jest.fn(),
|
|
||||||
deleteFile: jest.fn(),
|
|
||||||
updateFileName: jest.fn(),
|
|
||||||
resetSelectedFile: jest.fn(),
|
|
||||||
newFile: jest.fn(),
|
|
||||||
newFolder: jest.fn(),
|
|
||||||
showFolderChildren: jest.fn(),
|
|
||||||
hideFolderChildren: jest.fn(),
|
|
||||||
openUploadFileModal: jest.fn()
|
|
||||||
};
|
|
||||||
component = shallow(<FileNode {...props} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when changing name', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
input = component.find('.sidebar__file-item-input');
|
|
||||||
renameTriggerButton = component
|
|
||||||
.find('.sidebar__file-item-option')
|
|
||||||
.first();
|
|
||||||
component.setState({ isEditing: true });
|
|
||||||
});
|
|
||||||
it('should render', () => expect(component).toBeDefined());
|
|
||||||
|
|
||||||
// it('should debug', () => console.log(component.debug()));
|
|
||||||
|
|
||||||
describe('to a valid filename', () => {
|
|
||||||
const newName = 'newname.jsx';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
|
|
||||||
it('should save the name', () => {
|
|
||||||
expect(props.updateFileName).toBeCalledWith(props.id, newName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Failure Scenarios
|
|
||||||
|
|
||||||
describe('to an extensionless filename', () => {
|
|
||||||
const newName = 'extensionless';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
});
|
|
||||||
it('should not save', () => expect(props.setProjectName).not.toHaveBeenCalled());
|
|
||||||
it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
|
|
||||||
describe('to different extension', () => {
|
|
||||||
const newName = 'name.gif';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
|
|
||||||
it('should not save', () => expect(props.setProjectName).not.toHaveBeenCalled());
|
|
||||||
it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('to just an extension', () => {
|
|
||||||
const newName = '.jsx';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
|
|
||||||
it('should not save', () => expect(props.updateFileName).not.toHaveBeenCalled());
|
|
||||||
it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('as folder with valid props', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
id: '0',
|
|
||||||
children: [],
|
|
||||||
name: 'filename',
|
|
||||||
fileType: 'folder',
|
|
||||||
canEdit: true,
|
|
||||||
authenticated: false,
|
|
||||||
setSelectedFile: jest.fn(),
|
|
||||||
deleteFile: jest.fn(),
|
|
||||||
updateFileName: jest.fn(),
|
|
||||||
resetSelectedFile: jest.fn(),
|
|
||||||
newFile: jest.fn(),
|
|
||||||
newFolder: jest.fn(),
|
|
||||||
showFolderChildren: jest.fn(),
|
|
||||||
hideFolderChildren: jest.fn(),
|
|
||||||
openUploadFileModal: jest.fn()
|
|
||||||
};
|
|
||||||
component = shallow(<FileNode {...props} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when changing name', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
input = component.find('.sidebar__file-item-input');
|
|
||||||
renameTriggerButton = component
|
|
||||||
.find('.sidebar__file-item-option')
|
|
||||||
.first();
|
|
||||||
component.setState({ isEditing: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('to a foldername', () => {
|
|
||||||
const newName = 'newfoldername';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
|
|
||||||
it('should save', () => expect(props.updateFileName).toBeCalledWith(props.id, newName));
|
|
||||||
it('should update name', () => expect(getUpdatedName()).toEqual(newName));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('to a filename', () => {
|
|
||||||
const newName = 'filename.jsx';
|
|
||||||
beforeEach(() => changeName(newName));
|
|
||||||
|
|
||||||
it('should not save', () => expect(props.updateFileName).not.toHaveBeenCalled());
|
|
||||||
it('should reset name', () => expect(getUpdatedName()).toEqual(props.name));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { render } from '@testing-library/react';
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
import { NavComponent } from './../Nav';
|
|
||||||
|
import { NavComponent } from '../Nav';
|
||||||
|
|
||||||
describe('Nav', () => {
|
describe('Nav', () => {
|
||||||
const props = {
|
const props = {
|
||||||
|
@ -44,19 +44,12 @@ describe('Nav', () => {
|
||||||
setToastText: jest.fn(),
|
setToastText: jest.fn(),
|
||||||
rootFile: {
|
rootFile: {
|
||||||
id: 'root-file'
|
id: 'root-file'
|
||||||
}
|
},
|
||||||
|
t: jest.fn()
|
||||||
};
|
};
|
||||||
const getWrapper = () => shallow(<NavComponent {...props} />);
|
|
||||||
|
|
||||||
test('it renders main navigation', () => {
|
|
||||||
const nav = getWrapper();
|
|
||||||
expect(nav.exists('.nav')).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const tree = renderer
|
const { asFragment } = render(<NavComponent {...props} />);
|
||||||
.create(<NavComponent {...props} />)
|
expect(asFragment()).toMatchSnapshot();
|
||||||
.toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { ToolbarComponent } from '../../modules/IDE/components/Toolbar';
|
|
||||||
|
|
||||||
|
|
||||||
const initialProps = {
|
|
||||||
isPlaying: false,
|
|
||||||
preferencesIsVisible: false,
|
|
||||||
stopSketch: jest.fn(),
|
|
||||||
setProjectName: jest.fn(),
|
|
||||||
openPreferences: jest.fn(),
|
|
||||||
showEditProjectName: jest.fn(),
|
|
||||||
hideEditProjectName: jest.fn(),
|
|
||||||
infiniteLoop: false,
|
|
||||||
autorefresh: false,
|
|
||||||
setAutorefresh: jest.fn(),
|
|
||||||
setTextOutput: jest.fn(),
|
|
||||||
setGridOutput: jest.fn(),
|
|
||||||
startSketch: jest.fn(),
|
|
||||||
startAccessibleSketch: jest.fn(),
|
|
||||||
saveProject: jest.fn(),
|
|
||||||
currentUser: 'me',
|
|
||||||
originalProjectName: 'testname',
|
|
||||||
|
|
||||||
owner: {
|
|
||||||
username: 'me'
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
name: 'testname',
|
|
||||||
isEditingName: false,
|
|
||||||
id: 'id',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
describe('<ToolbarComponent />', () => {
|
|
||||||
let component;
|
|
||||||
let props = initialProps;
|
|
||||||
let input;
|
|
||||||
let renameTriggerButton;
|
|
||||||
const changeName = (newFileName) => {
|
|
||||||
component.find('.toolbar__project-name').simulate('click', { preventDefault: jest.fn() });
|
|
||||||
input = component.find('.toolbar__project-name-input');
|
|
||||||
renameTriggerButton = component.find('.toolbar__edit-name-button');
|
|
||||||
renameTriggerButton.simulate('click');
|
|
||||||
input.simulate('change', { target: { value: newFileName } });
|
|
||||||
input.simulate('blur');
|
|
||||||
};
|
|
||||||
const setProps = (additionalProps) => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
...additionalProps,
|
|
||||||
|
|
||||||
project: {
|
|
||||||
...props.project,
|
|
||||||
...(additionalProps || {}).project
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test Cases
|
|
||||||
|
|
||||||
describe('with valid props', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
setProps();
|
|
||||||
component = shallow(<ToolbarComponent {...props} />);
|
|
||||||
});
|
|
||||||
it('renders', () => expect(component).toBeDefined());
|
|
||||||
|
|
||||||
describe('when use owns sketch', () => {
|
|
||||||
beforeEach(() => setProps({ currentUser: props.owner.username }));
|
|
||||||
|
|
||||||
describe('when changing sketch name', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
setProps({
|
|
||||||
project: { isEditingName: true, name: 'testname' },
|
|
||||||
setProjectName: jest.fn(name => component.setProps({ project: { name } })),
|
|
||||||
});
|
|
||||||
component = shallow(<ToolbarComponent {...props} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('to a valid name', () => {
|
|
||||||
beforeEach(() => changeName('hello'));
|
|
||||||
it('should save', () => expect(props.setProjectName).toBeCalledWith('hello'));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('to an empty name', () => {
|
|
||||||
beforeEach(() => changeName(''));
|
|
||||||
it('should set name to empty', () => expect(props.setProjectName).toBeCalledWith(''));
|
|
||||||
it(
|
|
||||||
'should detect empty name and revert to original',
|
|
||||||
() => expect(props.setProjectName).toHaveBeenLastCalledWith(initialProps.project.name)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when user does not own sketch', () => {
|
|
||||||
beforeEach(() => setProps({ currentUser: 'not-the-owner' }));
|
|
||||||
|
|
||||||
it('should disable edition', () => expect(component.find('.toolbar__edit-name-button')).toEqual({}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,346 +1,219 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Nav renders correctly 1`] = `
|
exports[`Nav renders correctly 1`] = `
|
||||||
<header>
|
<DocumentFragment>
|
||||||
|
<header>
|
||||||
<nav
|
<nav
|
||||||
className="nav"
|
class="nav"
|
||||||
title="main-navigation"
|
title="main-navigation"
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className="nav__items-left"
|
class="nav__items-left"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__item-logo"
|
class="nav__item-logo"
|
||||||
>
|
>
|
||||||
<test-file-stub
|
<test-file-stub
|
||||||
aria-label="p5.js Logo"
|
aria-label="p5.js Logo"
|
||||||
className="svg__logo"
|
classname="svg__logo"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
role="img"
|
role="img"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__item"
|
class="nav__item"
|
||||||
>
|
|
||||||
<button
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
>
|
||||||
|
<button>
|
||||||
<span
|
<span
|
||||||
className="nav__item-header"
|
class="nav__item-header"
|
||||||
>
|
/>
|
||||||
File
|
|
||||||
</span>
|
|
||||||
<test-file-stub
|
<test-file-stub
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="nav__item-header-triangle"
|
classname="nav__item-header-triangle"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
className="nav__dropdown"
|
class="nav__dropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
New
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Duplicate
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Share
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<a
|
<a />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
style={Object {}}
|
|
||||||
>
|
|
||||||
Open
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__item"
|
class="nav__item"
|
||||||
>
|
|
||||||
<button
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
>
|
||||||
|
<button>
|
||||||
<span
|
<span
|
||||||
className="nav__item-header"
|
class="nav__item-header"
|
||||||
>
|
/>
|
||||||
Edit
|
|
||||||
</span>
|
|
||||||
<test-file-stub
|
<test-file-stub
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="nav__item-header-triangle"
|
classname="nav__item-header-triangle"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
className="nav__dropdown"
|
class="nav__dropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Tidy Code
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⇧
|
⇧+Tab
|
||||||
+Tab
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Find
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⌃
|
⌃+F
|
||||||
+F
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Find Next
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⌃
|
⌃+G
|
||||||
+G
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Find Previous
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⇧
|
⇧+⌃+G
|
||||||
+
|
|
||||||
⌃
|
|
||||||
+G
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__item"
|
class="nav__item"
|
||||||
>
|
|
||||||
<button
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
>
|
||||||
|
<button>
|
||||||
<span
|
<span
|
||||||
className="nav__item-header"
|
class="nav__item-header"
|
||||||
>
|
/>
|
||||||
Sketch
|
|
||||||
</span>
|
|
||||||
<test-file-stub
|
<test-file-stub
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="nav__item-header-triangle"
|
classname="nav__item-header-triangle"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
className="nav__dropdown"
|
class="nav__dropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Add File
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Add Folder
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Run
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⌃
|
⌃+Enter
|
||||||
+Enter
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button>
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Stop
|
|
||||||
<span
|
<span
|
||||||
className="nav__keyboard-shortcut"
|
class="nav__keyboard-shortcut"
|
||||||
>
|
>
|
||||||
⇧
|
⇧+⌃+Enter
|
||||||
+
|
|
||||||
⌃
|
|
||||||
+Enter
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__item"
|
class="nav__item"
|
||||||
>
|
|
||||||
<button
|
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
>
|
||||||
|
<button>
|
||||||
<span
|
<span
|
||||||
className="nav__item-header"
|
class="nav__item-header"
|
||||||
>
|
/>
|
||||||
Help
|
|
||||||
</span>
|
|
||||||
<test-file-stub
|
<test-file-stub
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="nav__item-header-triangle"
|
classname="nav__item-header-triangle"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
className="nav__dropdown"
|
class="nav__dropdown"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<button
|
<button />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
>
|
|
||||||
Keyboard Shortcuts
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://p5js.org/reference/"
|
href="https://p5js.org/reference/"
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
/>
|
||||||
Reference
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
className="nav__dropdown-item"
|
class="nav__dropdown-item"
|
||||||
>
|
>
|
||||||
<a
|
<a />
|
||||||
onBlur={[Function]}
|
|
||||||
onClick={[Function]}
|
|
||||||
onFocus={[Function]}
|
|
||||||
style={Object {}}
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
20
client/components/mobile/Footer.jsx
Normal file
20
client/components/mobile/Footer.jsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { prop, remSize } from '../../theme';
|
||||||
|
|
||||||
|
const background = prop('MobilePanel.default.background');
|
||||||
|
const textColor = prop('primaryTextColor');
|
||||||
|
|
||||||
|
const Footer = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
background: ${background};
|
||||||
|
color: ${textColor};
|
||||||
|
padding: ${remSize(12)};
|
||||||
|
padding-left: ${remSize(32)};
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
bottom: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Footer;
|
80
client/components/mobile/Header.jsx
Normal file
80
client/components/mobile/Header.jsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { prop, remSize } from '../../theme';
|
||||||
|
|
||||||
|
const background = prop('MobilePanel.default.background');
|
||||||
|
const textColor = prop('primaryTextColor');
|
||||||
|
|
||||||
|
|
||||||
|
const HeaderDiv = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
background: ${props => (props.transparent ? 'transparent' : background)};
|
||||||
|
color: ${textColor};
|
||||||
|
padding: ${remSize(12)};
|
||||||
|
padding-left: ${remSize(16)};
|
||||||
|
padding-right: ${remSize(16)};
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
svg {
|
||||||
|
max-height: ${remSize(32)};
|
||||||
|
padding: ${remSize(4)}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const TitleContainer = styled.div`
|
||||||
|
margin-left: ${remSize(4)};
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
${props => props.padded && `h2{
|
||||||
|
padding-top: ${remSize(10)};
|
||||||
|
padding-bottom: ${remSize(10)};
|
||||||
|
}`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Header = ({
|
||||||
|
title, subtitle, leftButton, children, transparent
|
||||||
|
}) => (
|
||||||
|
<HeaderDiv transparent={transparent}>
|
||||||
|
{leftButton}
|
||||||
|
<TitleContainer padded={subtitle === null}>
|
||||||
|
{title && <h2>{title}</h2>}
|
||||||
|
{subtitle && <h3>{subtitle}</h3>}
|
||||||
|
</TitleContainer>
|
||||||
|
<IconContainer>
|
||||||
|
{children}
|
||||||
|
</IconContainer>
|
||||||
|
</HeaderDiv>
|
||||||
|
);
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
subtitle: PropTypes.string,
|
||||||
|
leftButton: PropTypes.element,
|
||||||
|
children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
|
||||||
|
transparent: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.defaultProps = {
|
||||||
|
title: null,
|
||||||
|
subtitle: null,
|
||||||
|
leftButton: null,
|
||||||
|
children: [],
|
||||||
|
transparent: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
8
client/components/mobile/IDEWrapper.jsx
Normal file
8
client/components/mobile/IDEWrapper.jsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { remSize } from '../../theme';
|
||||||
|
|
||||||
|
export default styled.div`
|
||||||
|
z-index: 0;
|
||||||
|
margin-top: ${remSize(16)};
|
||||||
|
`;
|
31
client/components/mobile/IconButton.jsx
Normal file
31
client/components/mobile/IconButton.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import Button from '../../common/Button';
|
||||||
|
import { remSize } from '../../theme';
|
||||||
|
|
||||||
|
const ButtonWrapper = styled(Button)`
|
||||||
|
width: ${remSize(48)};
|
||||||
|
> svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconButton = (props) => {
|
||||||
|
const { icon, ...otherProps } = props;
|
||||||
|
const Icon = icon;
|
||||||
|
|
||||||
|
return (<ButtonWrapper
|
||||||
|
iconBefore={<Icon />}
|
||||||
|
kind={Button.kinds.inline}
|
||||||
|
focusable="false"
|
||||||
|
{...otherProps}
|
||||||
|
/>);
|
||||||
|
};
|
||||||
|
|
||||||
|
IconButton.propTypes = {
|
||||||
|
icon: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IconButton;
|
19
client/components/mobile/MobileScreen.jsx
Normal file
19
client/components/mobile/MobileScreen.jsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const Screen = ({ children, fullscreen }) => (
|
||||||
|
<div className={fullscreen && 'fullscreen-preview'}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Screen.defaultProps = {
|
||||||
|
fullscreen: false
|
||||||
|
};
|
||||||
|
|
||||||
|
Screen.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
fullscreen: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Screen;
|
67
client/components/mobile/PreferencePicker.jsx
Normal file
67
client/components/mobile/PreferencePicker.jsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { prop, remSize } from '../../theme';
|
||||||
|
|
||||||
|
|
||||||
|
const PreferenceTitle = styled.h4.attrs(props => ({ ...props, className: 'preference__title' }))`
|
||||||
|
color: ${prop('primaryTextColor')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Preference = styled.div.attrs(props => ({ ...props, className: 'preference' }))`
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
align-items: baseline !important;
|
||||||
|
justify-items: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionLabel = styled.label.attrs({ className: 'preference__option' })`
|
||||||
|
font-size: ${remSize(14)} !important;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PreferencePicker = ({
|
||||||
|
title, value, onSelect, options,
|
||||||
|
}) => (
|
||||||
|
<Preference>
|
||||||
|
<PreferenceTitle>{title}</PreferenceTitle>
|
||||||
|
<div className="preference__options">
|
||||||
|
{options.map(option => (
|
||||||
|
<React.Fragment key={`${option.name}-${option.id}`} >
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
onChange={() => onSelect(option.value)}
|
||||||
|
aria-label={option.ariaLabel}
|
||||||
|
name={option.name}
|
||||||
|
key={`${option.name}-${option.id}-input`}
|
||||||
|
id={option.id}
|
||||||
|
className="preference__radio-button"
|
||||||
|
value={option.value}
|
||||||
|
checked={value === option.value}
|
||||||
|
/>
|
||||||
|
<OptionLabel
|
||||||
|
key={`${option.name}-${option.id}-label`}
|
||||||
|
htmlFor={option.id}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</OptionLabel>
|
||||||
|
</React.Fragment>))}
|
||||||
|
</div>
|
||||||
|
</Preference>
|
||||||
|
);
|
||||||
|
|
||||||
|
PreferencePicker.defaultProps = {
|
||||||
|
options: []
|
||||||
|
};
|
||||||
|
|
||||||
|
PreferencePicker.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
|
||||||
|
options: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
ariaLabel: PropTypes.string,
|
||||||
|
})),
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreferencePicker;
|
38
client/i18n.js
Normal file
38
client/i18n.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
import Backend from 'i18next-http-backend';
|
||||||
|
|
||||||
|
const fallbackLng = ['en-US'];
|
||||||
|
const availableLanguages = ['en-US', 'es-419'];
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
loadPath: 'locales/{{lng}}/translations.json',
|
||||||
|
requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })
|
||||||
|
mode: 'no-cors'
|
||||||
|
},
|
||||||
|
allowMultiLoading: false, // set loadPath: '/locales/resources.json?lng={{lng}}&ns={{ns}}' to adapt to multiLoading
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next) // pass the i18n instance to react-i18next.
|
||||||
|
.use(LanguageDetector)// to detect the language from currentBrowser
|
||||||
|
.use(Backend) // to fetch the data from server
|
||||||
|
.init({
|
||||||
|
lng: 'en-US',
|
||||||
|
defaultNS: 'WebEditor',
|
||||||
|
fallbackLng, // if user computer language is not on the list of available languages, than we will be using the fallback language specified earlier
|
||||||
|
debug: false,
|
||||||
|
backend: options,
|
||||||
|
getAsync: false,
|
||||||
|
initImmediate: false,
|
||||||
|
useSuspense: true,
|
||||||
|
whitelist: availableLanguages,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // react already safes from xss
|
||||||
|
},
|
||||||
|
saveMissing: false, // if a key is not found AND this flag is set to true, i18next will call the handler missingKeyHandler
|
||||||
|
missingKeyHandler: false // function(lng, ns, key, fallbackValue) { } custom logic about how to handle the missing keys
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
@ -7,6 +7,8 @@ import { Router, browserHistory } from 'react-router';
|
||||||
import configureStore from './store';
|
import configureStore from './store';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import ThemeProvider from './modules/App/components/ThemeProvider';
|
import ThemeProvider from './modules/App/components/ThemeProvider';
|
||||||
|
import Loader from './modules/App/components/loader';
|
||||||
|
import i18n from './i18n';
|
||||||
|
|
||||||
require('./styles/main.scss');
|
require('./styles/main.scss');
|
||||||
|
|
||||||
|
@ -29,6 +31,8 @@ const App = () => (
|
||||||
const HotApp = hot(App);
|
const HotApp = hot(App);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<HotApp />,
|
<Suspense fallback={(<Loader />)}>
|
||||||
|
<HotApp />
|
||||||
|
</Suspense>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|
5
client/jest.setup.js
Normal file
5
client/jest.setup.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import '@babel/polyfill';
|
||||||
|
|
||||||
|
// See: https://github.com/testing-library/jest-dom
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import '@testing-library/jest-dom';
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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));
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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));
|
||||||
|
@ -155,18 +155,20 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
if (!autosave) {
|
if (!autosave) {
|
||||||
if (state.ide.justOpenedProject && state.preferences.autosave) {
|
if (state.ide.justOpenedProject && state.preferences.autosave) {
|
||||||
dispatch(showToast(5500));
|
dispatch(showToast(5500));
|
||||||
dispatch(setToastText('Project saved.'));
|
dispatch(setToastText('Sketch saved.'));
|
||||||
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
||||||
dispatch(resetJustOpenedProject());
|
dispatch(resetJustOpenedProject());
|
||||||
} else {
|
} else {
|
||||||
dispatch(showToast(1500));
|
dispatch(showToast(1500));
|
||||||
dispatch(setToastText('Project saved.'));
|
dispatch(setToastText('Sketch saved.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
dispatch(endSavingProject());
|
dispatch(endSavingProject());
|
||||||
|
dispatch(setToastText('Failed to save sketch.'));
|
||||||
|
dispatch(showToast(1500));
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
} else if (response.status === 409) {
|
} else if (response.status === 409) {
|
||||||
|
@ -177,7 +179,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);
|
||||||
|
@ -195,18 +197,20 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
if (!autosave) {
|
if (!autosave) {
|
||||||
if (state.preferences.autosave) {
|
if (state.preferences.autosave) {
|
||||||
dispatch(showToast(5500));
|
dispatch(showToast(5500));
|
||||||
dispatch(setToastText('Project saved.'));
|
dispatch(setToastText('Sketch saved.'));
|
||||||
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
||||||
dispatch(resetJustOpenedProject());
|
dispatch(resetJustOpenedProject());
|
||||||
} else {
|
} else {
|
||||||
dispatch(showToast(1500));
|
dispatch(showToast(1500));
|
||||||
dispatch(setToastText('Project saved.'));
|
dispatch(setToastText('Sketch saved.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
dispatch(endSavingProject());
|
dispatch(endSavingProject());
|
||||||
|
dispatch(setToastText('Failed to save sketch.'));
|
||||||
|
dispatch(showToast(1500));
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
dispatch(showErrorModal('staleSession'));
|
dispatch(showErrorModal('staleSession'));
|
||||||
} else {
|
} else {
|
||||||
|
@ -260,7 +264,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 +291,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 +302,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 +341,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 +368,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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import SquareLogoIcon from '../../../images/p5js-square-logo.svg';
|
import SquareLogoIcon from '../../../images/p5js-square-logo.svg';
|
||||||
// import PlayIcon from '../../../images/play.svg';
|
// import PlayIcon from '../../../images/play.svg';
|
||||||
import AsteriskIcon from '../../../images/p5-asterisk.svg';
|
import AsteriskIcon from '../../../images/p5-asterisk.svg';
|
||||||
|
|
||||||
function About(props) {
|
function About(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="about__content">
|
<div className="about__content">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | About</title>
|
<title>p5.js Web Editor | About </title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="about__content-column">
|
<div className="about__content-column">
|
||||||
<SquareLogoIcon className="about__logo" role="img" aria-label="p5.js Logo" focusable="false" />
|
<SquareLogoIcon className="about__logo" role="img" aria-label="p5.js Logo" focusable="false" />
|
||||||
|
@ -25,7 +26,7 @@ function About(props) {
|
||||||
</p> */}
|
</p> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="about__content-column">
|
<div className="about__content-column">
|
||||||
<h3 className="about__content-column-title">New to p5.js?</h3>
|
<h3 className="about__content-column-title">{t('NewP5')}</h3>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
<a
|
<a
|
||||||
href="https://p5js.org/examples/"
|
href="https://p5js.org/examples/"
|
||||||
|
@ -33,7 +34,7 @@ function About(props) {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Examples
|
{t('Examples')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
|
@ -43,12 +44,12 @@ function About(props) {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Learn
|
{t('Learn')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="about__content-column">
|
<div className="about__content-column">
|
||||||
<h3 className="about__content-column-title">Resources</h3>
|
<h3 className="about__content-column-title">{t('Resources')}</h3>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
<a
|
<a
|
||||||
href="https://p5js.org/libraries/"
|
href="https://p5js.org/libraries/"
|
||||||
|
@ -56,7 +57,7 @@ function About(props) {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Libraries
|
{t('Libraries')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
|
@ -66,7 +67,7 @@ function About(props) {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Reference
|
{t('Reference')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__content-column-list">
|
<p className="about__content-column-list">
|
||||||
|
@ -76,7 +77,7 @@ function About(props) {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||||
Forum
|
{t('Forum')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,7 +87,7 @@ function About(props) {
|
||||||
href="https://github.com/processing/p5.js-web-editor"
|
href="https://github.com/processing/p5.js-web-editor"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>Contribute
|
>{t('Contribute')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__footer-list">
|
<p className="about__footer-list">
|
||||||
|
@ -94,7 +95,7 @@ function About(props) {
|
||||||
href="https://github.com/processing/p5.js-web-editor/issues/new"
|
href="https://github.com/processing/p5.js-web-editor/issues/new"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>Report a bug
|
>{t('Report')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="about__footer-list">
|
<p className="about__footer-list">
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -206,12 +206,14 @@ export class FileNode extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<button
|
<button
|
||||||
|
aria-label="Name"
|
||||||
className="sidebar__file-item-name"
|
className="sidebar__file-item-name"
|
||||||
onClick={this.handleFileClick}
|
onClick={this.handleFileClick}
|
||||||
>
|
>
|
||||||
{this.state.updatedName}
|
{this.state.updatedName}
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
|
data-testid="input"
|
||||||
type="text"
|
type="text"
|
||||||
className="sidebar__file-item-input"
|
className="sidebar__file-item-input"
|
||||||
value={this.state.updatedName}
|
value={this.state.updatedName}
|
||||||
|
|
31
client/modules/IDE/components/FileNode.stories.jsx
Normal file
31
client/modules/IDE/components/FileNode.stories.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import { FileNode } from './FileNode';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'IDE/FileNode',
|
||||||
|
component: FileNode
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Show = () => (
|
||||||
|
<FileNode
|
||||||
|
id="nodeId"
|
||||||
|
parantId="parentId"
|
||||||
|
name="File name"
|
||||||
|
fileType="jpeg"
|
||||||
|
isSelectedFile
|
||||||
|
isFolderClosed={false}
|
||||||
|
setSelectedFile={action('setSelectedFile')}
|
||||||
|
deleteFile={action('deleteFile')}
|
||||||
|
updateFileName={action('updateFileName')}
|
||||||
|
resetSelectedFile={action('resetSelectedFile')}
|
||||||
|
newFile={action('newFile')}
|
||||||
|
newFolder={action('newFolder')}
|
||||||
|
showFolderChildren={action('showFolderChildren')}
|
||||||
|
hideFolderChildren={action('hideFolderChildren')}
|
||||||
|
openUploadFileModal={action('openUploadFileModal')}
|
||||||
|
canEdit
|
||||||
|
authenticated
|
||||||
|
/>
|
||||||
|
);
|
127
client/modules/IDE/components/FileNode.test.jsx
Normal file
127
client/modules/IDE/components/FileNode.test.jsx
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||||
|
import { FileNode } from './FileNode';
|
||||||
|
|
||||||
|
describe('<FileNode />', () => {
|
||||||
|
const changeName = (newFileName) => {
|
||||||
|
const renameButton = screen.getByText(/Rename/i);
|
||||||
|
fireEvent.click(renameButton);
|
||||||
|
|
||||||
|
const input = screen.getByTestId('input');
|
||||||
|
fireEvent.change(input, { target: { value: newFileName } });
|
||||||
|
fireEvent.blur(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectFileNameToBe = async (expectedName) => {
|
||||||
|
const name = screen.getByLabelText(/Name/i);
|
||||||
|
await waitFor(() => within(name).queryByText(expectedName));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFileNode = (fileType, extraProps = {}) => {
|
||||||
|
const props = {
|
||||||
|
...extraProps,
|
||||||
|
id: '0',
|
||||||
|
name: fileType === 'folder' ? 'afolder' : 'test.jsx',
|
||||||
|
fileType,
|
||||||
|
canEdit: true,
|
||||||
|
children: [],
|
||||||
|
authenticated: false,
|
||||||
|
setSelectedFile: jest.fn(),
|
||||||
|
deleteFile: jest.fn(),
|
||||||
|
updateFileName: jest.fn(),
|
||||||
|
resetSelectedFile: jest.fn(),
|
||||||
|
newFile: jest.fn(),
|
||||||
|
newFolder: jest.fn(),
|
||||||
|
showFolderChildren: jest.fn(),
|
||||||
|
hideFolderChildren: jest.fn(),
|
||||||
|
openUploadFileModal: jest.fn(),
|
||||||
|
setProjectName: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<FileNode {...props} />);
|
||||||
|
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('fileType: file', () => {
|
||||||
|
it('cannot change to an empty name', async () => {
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName('');
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change to a valid filename', async () => {
|
||||||
|
const newName = 'newname.jsx';
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
|
||||||
|
await expectFileNameToBe(newName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must have an extension', async () => {
|
||||||
|
const newName = 'newname';
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change to a different extension', async () => {
|
||||||
|
const newName = 'newname.gif';
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot be just an extension', async () => {
|
||||||
|
const newName = '.jsx';
|
||||||
|
const props = renderFileNode('file');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fileType: folder', () => {
|
||||||
|
it('cannot change to an empty name', async () => {
|
||||||
|
const props = renderFileNode('folder');
|
||||||
|
|
||||||
|
changeName('');
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can change to another name', async () => {
|
||||||
|
const newName = 'foldername';
|
||||||
|
const props = renderFileNode('folder');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
|
||||||
|
await expectFileNameToBe(newName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot have a file extension', async () => {
|
||||||
|
const newName = 'foldername.jsx';
|
||||||
|
const props = renderFileNode('folder');
|
||||||
|
|
||||||
|
changeName(newName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||||
|
await expectFileNameToBe(props.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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() {
|
||||||
|
|
|
@ -1,53 +1,55 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { metaKeyName, } from '../../../utils/metaKey';
|
import { metaKeyName, } from '../../../utils/metaKey';
|
||||||
|
|
||||||
function KeyboardShortcutModal() {
|
function KeyboardShortcutModal() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="keyboard-shortcuts">
|
<div className="keyboard-shortcuts">
|
||||||
<h3 className="keyboard-shortcuts__title">Code Editing</h3>
|
<h3 className="keyboard-shortcuts__title">{t('CodeEditing')}</h3>
|
||||||
<p className="keyboard-shortcuts__description">
|
<p className="keyboard-shortcuts__description">
|
||||||
Code editing keyboard shortcuts follow <a href="https://shortcuts.design/toolspage-sublimetext.html" target="_blank" rel="noopener noreferrer">Sublime Text shortcuts</a>.
|
{t('Code editing keyboard shortcuts follow')} <a href="https://shortcuts.design/toolspage-sublimetext.html" target="_blank" rel="noopener noreferrer">{t('Sublime Text shortcuts')}</a>.
|
||||||
</p>
|
</p>
|
||||||
<ul className="keyboard-shortcuts__list">
|
<ul className="keyboard-shortcuts__list">
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">{'\u21E7'} + Tab</span>
|
<span className="keyboard-shortcut__command">{'\u21E7'} + Tab</span>
|
||||||
<span>Tidy</span>
|
<span>{t('Tidy')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + F
|
{metaKeyName} + F
|
||||||
</span>
|
</span>
|
||||||
<span>Find Text</span>
|
<span>{t('FindText')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + G
|
{metaKeyName} + G
|
||||||
</span>
|
</span>
|
||||||
<span>Find Next Text Match</span>
|
<span>{t('FindNextTextMatch')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + {'\u21E7'} + G
|
{metaKeyName} + {'\u21E7'} + G
|
||||||
</span>
|
</span>
|
||||||
<span>Find Previous Text Match</span>
|
<span>{t('FindPreviousTextMatch')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + [
|
{metaKeyName} + [
|
||||||
</span>
|
</span>
|
||||||
<span>Indent Code Left</span>
|
<span>{t('IndentCodeLeft')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + ]
|
{metaKeyName} + ]
|
||||||
</span>
|
</span>
|
||||||
<span>Indent Code Right</span>
|
<span>{t('IndentCodeRight')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + /
|
{metaKeyName} + /
|
||||||
</span>
|
</span>
|
||||||
<span>Comment Line</span>
|
<span>{t('CommentLine')}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 className="keyboard-shortcuts__title">General</h3>
|
<h3 className="keyboard-shortcuts__title">General</h3>
|
||||||
|
@ -56,31 +58,31 @@ function KeyboardShortcutModal() {
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + S
|
{metaKeyName} + S
|
||||||
</span>
|
</span>
|
||||||
<span>Save</span>
|
<span>{t('Save')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + Enter
|
{metaKeyName} + Enter
|
||||||
</span>
|
</span>
|
||||||
<span>Start Sketch</span>
|
<span>{t('StartSketch')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + {'\u21E7'} + Enter
|
{metaKeyName} + {'\u21E7'} + Enter
|
||||||
</span>
|
</span>
|
||||||
<span>Stop Sketch</span>
|
<span>{t('StopSketch')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + {'\u21E7'} + 1
|
{metaKeyName} + {'\u21E7'} + 1
|
||||||
</span>
|
</span>
|
||||||
<span>Turn on Accessible Output</span>
|
<span>{t('TurnOnAccessibleOutput')}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="keyboard-shortcut-item">
|
<li className="keyboard-shortcut-item">
|
||||||
<span className="keyboard-shortcut__command">
|
<span className="keyboard-shortcut__command">
|
||||||
{metaKeyName} + {'\u21E7'} + 2
|
{metaKeyName} + {'\u21E7'} + 2
|
||||||
</span>
|
</span>
|
||||||
<span>Turn off Accessible Output</span>
|
<span>{t('TurnOffAccessibleOutput')}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
// import { bindActionCreators } from 'redux';
|
// import { bindActionCreators } from 'redux';
|
||||||
// import { connect } from 'react-redux';
|
// import { connect } from 'react-redux';
|
||||||
// import * as PreferencesActions from '../actions/preferences';
|
// import * as PreferencesActions from '../actions/preferences';
|
||||||
|
@ -98,13 +99,13 @@ class Preferences extends React.Component {
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<div className="tabs__titles">
|
<div className="tabs__titles">
|
||||||
<Tab><h4 className="tabs__title">General Settings</h4></Tab>
|
<Tab><h4 className="tabs__title">{this.props.t('GeneralSettings')}</h4></Tab>
|
||||||
<Tab><h4 className="tabs__title">Accessibility</h4></Tab>
|
<Tab><h4 className="tabs__title">{this.props.t('Accessibility')}</h4></Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Theme</h4>
|
<h4 className="preference__title">{this.props.t('Theme')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -116,7 +117,7 @@ class Preferences extends React.Component {
|
||||||
value="light"
|
value="light"
|
||||||
checked={this.props.theme === 'light'}
|
checked={this.props.theme === 'light'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="light-theme-on" className="preference__option">Light</label>
|
<label htmlFor="light-theme-on" className="preference__option">{this.props.t('Light')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setTheme('dark')}
|
onChange={() => this.props.setTheme('dark')}
|
||||||
|
@ -127,7 +128,7 @@ class Preferences extends React.Component {
|
||||||
value="dark"
|
value="dark"
|
||||||
checked={this.props.theme === 'dark'}
|
checked={this.props.theme === 'dark'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="dark-theme-on" className="preference__option">Dark</label>
|
<label htmlFor="dark-theme-on" className="preference__option">{this.props.t('Dark')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setTheme('contrast')}
|
onChange={() => this.props.setTheme('contrast')}
|
||||||
|
@ -138,11 +139,11 @@ class Preferences extends React.Component {
|
||||||
value="contrast"
|
value="contrast"
|
||||||
checked={this.props.theme === 'contrast'}
|
checked={this.props.theme === 'contrast'}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="high-contrast-theme-on" className="preference__option">High Contrast</label>
|
<label htmlFor="high-contrast-theme-on" className="preference__option">{this.props.t('HighContrast')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Text size</h4>
|
<h4 className="preference__title">{this.props.t('TextSize')}</h4>
|
||||||
<button
|
<button
|
||||||
className="preference__minus-button"
|
className="preference__minus-button"
|
||||||
onClick={this.decreaseFontSize}
|
onClick={this.decreaseFontSize}
|
||||||
|
@ -150,7 +151,7 @@ class Preferences extends React.Component {
|
||||||
disabled={this.state.fontSize <= 8}
|
disabled={this.state.fontSize <= 8}
|
||||||
>
|
>
|
||||||
<MinusIcon focusable="false" aria-hidden="true" />
|
<MinusIcon focusable="false" aria-hidden="true" />
|
||||||
<h6 className="preference__label">Decrease</h6>
|
<h6 className="preference__label">{this.props.t('Decrease')}</h6>
|
||||||
</button>
|
</button>
|
||||||
<form onSubmit={this.onFontInputSubmit}>
|
<form onSubmit={this.onFontInputSubmit}>
|
||||||
<input
|
<input
|
||||||
|
@ -171,11 +172,11 @@ class Preferences extends React.Component {
|
||||||
disabled={this.state.fontSize >= 36}
|
disabled={this.state.fontSize >= 36}
|
||||||
>
|
>
|
||||||
<PlusIcon focusable="false" aria-hidden="true" />
|
<PlusIcon focusable="false" aria-hidden="true" />
|
||||||
<h6 className="preference__label">Increase</h6>
|
<h6 className="preference__label">{this.props.t('Increase')}</h6>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Autosave</h4>
|
<h4 className="preference__title">{this.props.t('Autosave')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -187,7 +188,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={this.props.autosave}
|
checked={this.props.autosave}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="autosave-on" className="preference__option">On</label>
|
<label htmlFor="autosave-on" className="preference__option">{this.props.t('On')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setAutosave(false)}
|
onChange={() => this.props.setAutosave(false)}
|
||||||
|
@ -198,11 +199,11 @@ class Preferences extends React.Component {
|
||||||
value="Off"
|
value="Off"
|
||||||
checked={!this.props.autosave}
|
checked={!this.props.autosave}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="autosave-off" className="preference__option">Off</label>
|
<label htmlFor="autosave-off" className="preference__option">{this.props.t('Off')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Word Wrap</h4>
|
<h4 className="preference__title">{this.props.t('WordWrap')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -214,7 +215,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={this.props.linewrap}
|
checked={this.props.linewrap}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="linewrap-on" className="preference__option">On</label>
|
<label htmlFor="linewrap-on" className="preference__option">{this.props.t('On')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setLinewrap(false)}
|
onChange={() => this.props.setLinewrap(false)}
|
||||||
|
@ -225,13 +226,13 @@ class Preferences extends React.Component {
|
||||||
value="Off"
|
value="Off"
|
||||||
checked={!this.props.linewrap}
|
checked={!this.props.linewrap}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="linewrap-off" className="preference__option">Off</label>
|
<label htmlFor="linewrap-off" className="preference__option">{this.props.t('Off')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Line numbers</h4>
|
<h4 className="preference__title">{this.props.t('LineNumbers')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -243,7 +244,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={this.props.lineNumbers}
|
checked={this.props.lineNumbers}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="line-numbers-on" className="preference__option">On</label>
|
<label htmlFor="line-numbers-on" className="preference__option">{this.props.t('On')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setLineNumbers(false)}
|
onChange={() => this.props.setLineNumbers(false)}
|
||||||
|
@ -254,11 +255,11 @@ class Preferences extends React.Component {
|
||||||
value="Off"
|
value="Off"
|
||||||
checked={!this.props.lineNumbers}
|
checked={!this.props.lineNumbers}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="line-numbers-off" className="preference__option">Off</label>
|
<label htmlFor="line-numbers-off" className="preference__option">{this.props.t('Off')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Lint warning sound</h4>
|
<h4 className="preference__title">{this.props.t('LintWarningSound')}</h4>
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -270,7 +271,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={this.props.lintWarning}
|
checked={this.props.lintWarning}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="lint-warning-on" className="preference__option">On</label>
|
<label htmlFor="lint-warning-on" className="preference__option">{this.props.t('On')}</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
onChange={() => this.props.setLintWarning(false)}
|
onChange={() => this.props.setLintWarning(false)}
|
||||||
|
@ -281,19 +282,19 @@ class Preferences extends React.Component {
|
||||||
value="Off"
|
value="Off"
|
||||||
checked={!this.props.lintWarning}
|
checked={!this.props.lintWarning}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="lint-warning-off" className="preference__option">Off</label>
|
<label htmlFor="lint-warning-off" className="preference__option">{this.props.t('Off')}</label>
|
||||||
<button
|
<button
|
||||||
className="preference__preview-button"
|
className="preference__preview-button"
|
||||||
onClick={() => beep.play()}
|
onClick={() => beep.play()}
|
||||||
aria-label="preview sound"
|
aria-label="preview sound"
|
||||||
>
|
>
|
||||||
Preview sound
|
{this.props.t('PreviewSound')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="preference">
|
<div className="preference">
|
||||||
<h4 className="preference__title">Accessible text-based canvas</h4>
|
<h4 className="preference__title">{this.props.t('AccessibleTextBasedCanvas')}</h4>
|
||||||
<h6 className="preference__subtitle">Used with screen reader</h6>
|
<h6 className="preference__subtitle">{this.props.t('UsedScreenReader')}</h6>
|
||||||
|
|
||||||
<div className="preference__options">
|
<div className="preference__options">
|
||||||
<input
|
<input
|
||||||
|
@ -307,7 +308,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={(this.props.textOutput)}
|
checked={(this.props.textOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="text-output-on" className="preference__option preference__canvas">Plain-text</label>
|
<label htmlFor="text-output-on" className="preference__option preference__canvas">{this.props.t('PlainText')}</label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
@ -319,7 +320,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={(this.props.gridOutput)}
|
checked={(this.props.gridOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="table-output-on" className="preference__option preference__canvas">Table-text</label>
|
<label htmlFor="table-output-on" className="preference__option preference__canvas">{this.props.t('TableText')}</label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
@ -331,7 +332,7 @@ class Preferences extends React.Component {
|
||||||
value="On"
|
value="On"
|
||||||
checked={(this.props.soundOutput)}
|
checked={(this.props.soundOutput)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="sound-output-on" className="preference__option preference__canvas">Sound</label>
|
<label htmlFor="sound-output-on" className="preference__option preference__canvas">{this.props.t('Sound')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
@ -360,6 +361,7 @@ Preferences.propTypes = {
|
||||||
setLintWarning: PropTypes.func.isRequired,
|
setLintWarning: PropTypes.func.isRequired,
|
||||||
theme: PropTypes.string.isRequired,
|
theme: PropTypes.string.isRequired,
|
||||||
setTheme: PropTypes.func.isRequired,
|
setTheme: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Preferences;
|
export default withTranslation('WebEditor')(Preferences);
|
||||||
|
|
|
@ -22,6 +22,23 @@ import {
|
||||||
import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets }
|
import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets }
|
||||||
from '../../../utils/consoleUtils';
|
from '../../../utils/consoleUtils';
|
||||||
|
|
||||||
|
|
||||||
|
const shouldRenderSketch = (props, prevProps = undefined) => {
|
||||||
|
const { isPlaying, previewIsRefreshing, fullView } = props;
|
||||||
|
|
||||||
|
// if the user explicitly clicks on the play button
|
||||||
|
if (isPlaying && previewIsRefreshing) return true;
|
||||||
|
|
||||||
|
if (!prevProps) return false;
|
||||||
|
|
||||||
|
return (props.isPlaying !== prevProps.isPlaying // if sketch starts or stops playing, want to rerender
|
||||||
|
|| props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying // if user switches textoutput preferences
|
||||||
|
|| props.textOutput !== prevProps.textOutput
|
||||||
|
|| props.gridOutput !== prevProps.gridOutput
|
||||||
|
|| props.soundOutput !== prevProps.soundOutput
|
||||||
|
|| (fullView && props.files[0].id !== prevProps.files[0].id));
|
||||||
|
};
|
||||||
|
|
||||||
class PreviewFrame extends React.Component {
|
class PreviewFrame extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -30,53 +47,25 @@ class PreviewFrame extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener('message', this.handleConsoleEvent);
|
window.addEventListener('message', this.handleConsoleEvent);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...this.props,
|
||||||
|
previewIsRefreshing: this.props.previewIsRefreshing,
|
||||||
|
isAccessibleOutputPlaying: this.props.isAccessibleOutputPlaying
|
||||||
|
};
|
||||||
|
if (shouldRenderSketch(props)) this.renderSketch();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
// if sketch starts or stops playing, want to rerender
|
if (shouldRenderSketch(this.props, prevProps)) this.renderSketch();
|
||||||
if (this.props.isPlaying !== prevProps.isPlaying) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the user explicitly clicks on the play button
|
|
||||||
if (this.props.isPlaying && this.props.previewIsRefreshing) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if user switches textoutput preferences
|
|
||||||
if (this.props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.textOutput !== prevProps.textOutput) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.gridOutput !== prevProps.gridOutput) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.soundOutput !== prevProps.soundOutput) {
|
|
||||||
this.renderSketch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
|
|
||||||
this.renderSketch();
|
|
||||||
}
|
|
||||||
|
|
||||||
// small bug - if autorefresh is on, and the usr changes files
|
// small bug - if autorefresh is on, and the usr changes files
|
||||||
// in the sketch, preview will reload
|
// in the sketch, preview will reload
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('message', this.handleConsoleEvent);
|
window.removeEventListener('message', this.handleConsoleEvent);
|
||||||
ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body);
|
const iframeBody = this.iframeElement.contentDocument.body;
|
||||||
|
if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); }
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConsoleEvent(messageEvent) {
|
handleConsoleEvent(messageEvent) {
|
||||||
|
@ -249,16 +238,18 @@ class PreviewFrame extends React.Component {
|
||||||
jsFileStrings.forEach((jsFileString) => {
|
jsFileStrings.forEach((jsFileString) => {
|
||||||
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
||||||
const filePath = jsFileString.substr(1, jsFileString.length - 2);
|
const filePath = jsFileString.substr(1, jsFileString.length - 2);
|
||||||
|
const quoteCharacter = jsFileString.substr(0, 1);
|
||||||
const resolvedFile = resolvePathToFile(filePath, files);
|
const resolvedFile = resolvePathToFile(filePath, files);
|
||||||
|
|
||||||
if (resolvedFile) {
|
if (resolvedFile) {
|
||||||
if (resolvedFile.url) {
|
if (resolvedFile.url) {
|
||||||
newContent = newContent.replace(filePath, resolvedFile.url);
|
newContent = newContent.replace(jsFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
|
||||||
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
|
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
|
||||||
// could also pull file from API instead of using bloburl
|
// could also pull file from API instead of using bloburl
|
||||||
const blobURL = getBlobUrl(resolvedFile);
|
const blobURL = getBlobUrl(resolvedFile);
|
||||||
this.props.setBlobUrl(resolvedFile, blobURL);
|
this.props.setBlobUrl(resolvedFile, blobURL);
|
||||||
const filePathRegex = new RegExp(filePath, 'gi');
|
|
||||||
newContent = newContent.replace(filePathRegex, blobURL);
|
newContent = newContent.replace(jsFileString, quoteCharacter + blobURL + quoteCharacter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,10 +265,11 @@ class PreviewFrame extends React.Component {
|
||||||
cssFileStrings.forEach((cssFileString) => {
|
cssFileStrings.forEach((cssFileString) => {
|
||||||
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
||||||
const filePath = cssFileString.substr(1, cssFileString.length - 2);
|
const filePath = cssFileString.substr(1, cssFileString.length - 2);
|
||||||
|
const quoteCharacter = cssFileString.substr(0, 1);
|
||||||
const resolvedFile = resolvePathToFile(filePath, files);
|
const resolvedFile = resolvePathToFile(filePath, files);
|
||||||
if (resolvedFile) {
|
if (resolvedFile) {
|
||||||
if (resolvedFile.url) {
|
if (resolvedFile.url) {
|
||||||
newContent = newContent.replace(filePath, resolvedFile.url);
|
newContent = newContent.replace(cssFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,6 +345,8 @@ class PreviewFrame extends React.Component {
|
||||||
'preview-frame': true,
|
'preview-frame': true,
|
||||||
'preview-frame--full-view': this.props.fullView
|
'preview-frame--full-view': this.props.fullView
|
||||||
});
|
});
|
||||||
|
const sandboxAttributes =
|
||||||
|
'allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals allow-downloads';
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
id="canvas_frame"
|
id="canvas_frame"
|
||||||
|
@ -362,7 +356,7 @@ class PreviewFrame extends React.Component {
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
title="sketch preview"
|
title="sketch preview"
|
||||||
ref={(element) => { this.iframeElement = element; }}
|
ref={(element) => { this.iframeElement = element; }}
|
||||||
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals"
|
sandbox={sandboxAttributes}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -393,7 +387,7 @@ PreviewFrame.propTypes = {
|
||||||
clearConsole: PropTypes.func.isRequired,
|
clearConsole: PropTypes.func.isRequired,
|
||||||
cmController: PropTypes.shape({
|
cmController: PropTypes.shape({
|
||||||
getContent: PropTypes.func
|
getContent: PropTypes.func
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
PreviewFrame.defaultProps = {
|
PreviewFrame.defaultProps = {
|
||||||
|
|
|
@ -2,15 +2,17 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import * as ToastActions from '../actions/toast';
|
import * as ToastActions from '../actions/toast';
|
||||||
|
|
||||||
import ExitIcon from '../../../images/exit.svg';
|
import ExitIcon from '../../../images/exit.svg';
|
||||||
|
|
||||||
function Toast(props) {
|
function Toast(props) {
|
||||||
|
const { t } = useTranslation('WebEditor');
|
||||||
return (
|
return (
|
||||||
<section className="toast">
|
<section className="toast">
|
||||||
<p>
|
<p>
|
||||||
{props.text}
|
{t(props.text)}
|
||||||
</p>
|
</p>
|
||||||
<button className="toast__close" onClick={props.hideToast} aria-label="Close Alert" >
|
<button className="toast__close" onClick={props.hideToast} aria-label="Close Alert" >
|
||||||
<ExitIcon focusable="false" aria-hidden="true" />
|
<ExitIcon focusable="false" aria-hidden="true" />
|
||||||
|
|
|
@ -17,21 +17,36 @@ class Toolbar extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||||
this.handleProjectNameChange = this.handleProjectNameChange.bind(this);
|
this.handleProjectNameChange = this.handleProjectNameChange.bind(this);
|
||||||
|
this.handleProjectNameSave = this.handleProjectNameSave.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
projectNameInputValue: props.project.name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress(event) {
|
handleKeyPress(event) {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
this.props.hideEditProjectName();
|
this.props.hideEditProjectName();
|
||||||
|
this.projectNameInput.blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProjectNameChange(event) {
|
handleProjectNameChange(event) {
|
||||||
this.props.setProjectName(event.target.value);
|
this.setState({ projectNameInputValue: event.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateProjectName() {
|
handleProjectNameSave() {
|
||||||
if ((this.props.project.name.trim()).length === 0) {
|
const newProjectName = this.state.projectNameInputValue.trim();
|
||||||
this.props.setProjectName(this.originalProjectName);
|
if (newProjectName.length === 0) {
|
||||||
|
this.setState({
|
||||||
|
projectNameInputValue: this.props.project.name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.props.setProjectName(newProjectName);
|
||||||
|
this.props.hideEditProjectName();
|
||||||
|
if (this.props.project.id) {
|
||||||
|
this.props.saveProject();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +123,6 @@ class Toolbar extends React.Component {
|
||||||
className="toolbar__project-name"
|
className="toolbar__project-name"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (canEditProjectName) {
|
if (canEditProjectName) {
|
||||||
this.originalProjectName = this.props.project.name;
|
|
||||||
this.props.showEditProjectName();
|
this.props.showEditProjectName();
|
||||||
setTimeout(() => this.projectNameInput.focus(), 0);
|
setTimeout(() => this.projectNameInput.focus(), 0);
|
||||||
}
|
}
|
||||||
|
@ -130,16 +144,11 @@ class Toolbar extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
maxLength="128"
|
maxLength="128"
|
||||||
className="toolbar__project-name-input"
|
className="toolbar__project-name-input"
|
||||||
value={this.props.project.name}
|
aria-label="New sketch name"
|
||||||
|
value={this.state.projectNameInputValue}
|
||||||
onChange={this.handleProjectNameChange}
|
onChange={this.handleProjectNameChange}
|
||||||
ref={(element) => { this.projectNameInput = element; }}
|
ref={(element) => { this.projectNameInput = element; }}
|
||||||
onBlur={() => {
|
onBlur={this.handleProjectNameSave}
|
||||||
this.validateProjectName();
|
|
||||||
this.props.hideEditProjectName();
|
|
||||||
if (this.props.project.id) {
|
|
||||||
this.props.saveProject();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
/>
|
||||||
{(() => { // eslint-disable-line
|
{(() => { // eslint-disable-line
|
||||||
|
|
84
client/modules/IDE/components/Toolbar.test.jsx
Normal file
84
client/modules/IDE/components/Toolbar.test.jsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
|
||||||
|
import { ToolbarComponent } from './Toolbar';
|
||||||
|
|
||||||
|
const renderComponent = (extraProps = {}) => {
|
||||||
|
const props = lodash.merge({
|
||||||
|
isPlaying: false,
|
||||||
|
preferencesIsVisible: false,
|
||||||
|
stopSketch: jest.fn(),
|
||||||
|
setProjectName: jest.fn(),
|
||||||
|
openPreferences: jest.fn(),
|
||||||
|
showEditProjectName: jest.fn(),
|
||||||
|
hideEditProjectName: jest.fn(),
|
||||||
|
infiniteLoop: false,
|
||||||
|
autorefresh: false,
|
||||||
|
setAutorefresh: jest.fn(),
|
||||||
|
setTextOutput: jest.fn(),
|
||||||
|
setGridOutput: jest.fn(),
|
||||||
|
startSketch: jest.fn(),
|
||||||
|
startAccessibleSketch: jest.fn(),
|
||||||
|
saveProject: jest.fn(),
|
||||||
|
currentUser: 'me',
|
||||||
|
originalProjectName: 'testname',
|
||||||
|
|
||||||
|
owner: {
|
||||||
|
username: 'me'
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
name: 'testname',
|
||||||
|
isEditingName: false,
|
||||||
|
id: 'id',
|
||||||
|
},
|
||||||
|
}, extraProps);
|
||||||
|
|
||||||
|
render(<ToolbarComponent {...props} />);
|
||||||
|
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<ToolbarComponent />', () => {
|
||||||
|
it('sketch owner can switch to sketch name editing mode', async () => {
|
||||||
|
const props = renderComponent();
|
||||||
|
const sketchName = screen.getByLabelText('Edit sketch name');
|
||||||
|
|
||||||
|
fireEvent.click(sketchName);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.showEditProjectName).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-owner can\t switch to sketch editing mode', async () => {
|
||||||
|
const props = renderComponent({ currentUser: 'not-me' });
|
||||||
|
const sketchName = screen.getByLabelText('Edit sketch name');
|
||||||
|
|
||||||
|
fireEvent.click(sketchName);
|
||||||
|
|
||||||
|
expect(sketchName).toBeDisabled();
|
||||||
|
await waitFor(() => expect(props.showEditProjectName).not.toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sketch owner can change name', async () => {
|
||||||
|
const props = renderComponent({ project: { isEditingName: true } });
|
||||||
|
|
||||||
|
const sketchNameInput = screen.getByLabelText('New sketch name');
|
||||||
|
fireEvent.change(sketchNameInput, { target: { value: 'my new sketch name' } });
|
||||||
|
fireEvent.blur(sketchNameInput);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name'));
|
||||||
|
await waitFor(() => expect(props.saveProject).toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sketch owner can\'t change to empty name', async () => {
|
||||||
|
const props = renderComponent({ project: { isEditingName: true } });
|
||||||
|
|
||||||
|
const sketchNameInput = screen.getByLabelText('New sketch name');
|
||||||
|
fireEvent.change(sketchNameInput, { target: { value: '' } });
|
||||||
|
fireEvent.blur(sketchNameInput);
|
||||||
|
|
||||||
|
await waitFor(() => expect(props.setProjectName).not.toHaveBeenCalled());
|
||||||
|
await waitFor(() => expect(props.saveProject).not.toHaveBeenCalled());
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import SplitPane from 'react-split-pane';
|
import SplitPane from 'react-split-pane';
|
||||||
import Editor from '../components/Editor';
|
import Editor from '../components/Editor';
|
||||||
|
@ -34,11 +35,38 @@ import AddToCollectionList from '../components/AddToCollectionList';
|
||||||
import Feedback from '../components/Feedback';
|
import Feedback from '../components/Feedback';
|
||||||
import { CollectionSearchbar } from '../components/Searchbar';
|
import { CollectionSearchbar } from '../components/Searchbar';
|
||||||
|
|
||||||
|
function getTitle(props) {
|
||||||
|
const { id } = props.project;
|
||||||
|
return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUserOwner(props) {
|
||||||
|
return props.project.owner && props.project.owner.id === props.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function warnIfUnsavedChanges(props) { // eslint-disable-line
|
||||||
|
const { route } = props.route;
|
||||||
|
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
|
||||||
|
// don't warn
|
||||||
|
props.persistState();
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
} else if (route && (props.location.pathname === '/login' || props.location.pathname === '/signup')) {
|
||||||
|
// don't warn
|
||||||
|
props.persistState();
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
} else if (props.ide.unsavedChanges) {
|
||||||
|
if (!window.confirm(props.t('WarningUnsavedChanges'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
props.setUnsavedChanges(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class IDEView extends React.Component {
|
class IDEView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
|
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
|
||||||
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
|
consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
|
||||||
|
@ -62,9 +90,9 @@ class IDEView extends React.Component {
|
||||||
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
|
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
|
||||||
document.addEventListener('keydown', this.handleGlobalKeydown, false);
|
document.addEventListener('keydown', this.handleGlobalKeydown, false);
|
||||||
|
|
||||||
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
|
this.props.router.setRouteLeaveHook(this.props.route, this.handleUnsavedChanges);
|
||||||
|
|
||||||
window.onbeforeunload = () => this.warnIfUnsavedChanges();
|
window.onbeforeunload = this.handleUnsavedChanges;
|
||||||
|
|
||||||
this.autosaveInterval = null;
|
this.autosaveInterval = null;
|
||||||
}
|
}
|
||||||
|
@ -92,7 +120,7 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.isUserOwner() && this.props.project.id) {
|
if (isUserOwner(this.props) && this.props.project.id) {
|
||||||
if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) {
|
if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) {
|
||||||
if (
|
if (
|
||||||
this.props.selectedFile.name === prevProps.selectedFile.name &&
|
this.props.selectedFile.name === prevProps.selectedFile.name &&
|
||||||
|
@ -113,7 +141,7 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.route.path !== prevProps.route.path) {
|
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, () => warnIfUnsavedChanges(this.props));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,21 +151,12 @@ class IDEView extends React.Component {
|
||||||
this.autosaveInterval = null;
|
this.autosaveInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle = () => {
|
|
||||||
const { id } = this.props.project;
|
|
||||||
return id ? `p5.js Web Editor | ${this.props.project.name}` : 'p5.js Web Editor';
|
|
||||||
}
|
|
||||||
|
|
||||||
isUserOwner() {
|
|
||||||
return this.props.project.owner && this.props.project.owner.id === this.props.user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGlobalKeydown(e) {
|
handleGlobalKeydown(e) {
|
||||||
// 83 === s
|
// 83 === s
|
||||||
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
|
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (this.isUserOwner() || (this.props.user.authenticated && !this.props.project.owner)) {
|
if (isUserOwner(this.props) || (this.props.user.authenticated && !this.props.project.owner)) {
|
||||||
this.props.saveProject(this.cmController.getContent());
|
this.props.saveProject(this.cmController.getContent());
|
||||||
} else if (this.props.user.authenticated) {
|
} else if (this.props.user.authenticated) {
|
||||||
this.props.cloneProject();
|
this.props.cloneProject();
|
||||||
|
@ -186,39 +205,23 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warnIfUnsavedChanges(route) { // eslint-disable-line
|
handleUnsavedChanges = () => warnIfUnsavedChanges(this.props);
|
||||||
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
|
|
||||||
// don't warn
|
|
||||||
this.props.persistState();
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
} else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) {
|
|
||||||
// don't warn
|
|
||||||
this.props.persistState();
|
|
||||||
window.onbeforeunload = null;
|
|
||||||
} else if (this.props.ide.unsavedChanges) {
|
|
||||||
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.props.setUnsavedChanges(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ide">
|
<div className="ide">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{this.getTitle()}</title>
|
<title>{getTitle(this.props)}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
{this.props.toast.isVisible && <Toast />}
|
{this.props.toast.isVisible && <Toast />}
|
||||||
<Nav
|
<Nav
|
||||||
warnIfUnsavedChanges={this.warnIfUnsavedChanges}
|
warnIfUnsavedChanges={this.handleUnsavedChanges}
|
||||||
cmController={this.cmController}
|
cmController={this.cmController}
|
||||||
/>
|
/>
|
||||||
<Toolbar />
|
<Toolbar key={this.props.project.id} />
|
||||||
{this.props.ide.preferencesIsVisible &&
|
{this.props.ide.preferencesIsVisible &&
|
||||||
<Overlay
|
<Overlay
|
||||||
title="Settings"
|
title={this.props.t('Settings')}
|
||||||
ariaLabel="settings"
|
ariaLabel="settings"
|
||||||
closeOverlay={this.props.closePreferences}
|
closeOverlay={this.props.closePreferences}
|
||||||
>
|
>
|
||||||
|
@ -313,7 +316,7 @@ class IDEView extends React.Component {
|
||||||
isExpanded={this.props.ide.sidebarIsExpanded}
|
isExpanded={this.props.ide.sidebarIsExpanded}
|
||||||
expandSidebar={this.props.expandSidebar}
|
expandSidebar={this.props.expandSidebar}
|
||||||
collapseSidebar={this.props.collapseSidebar}
|
collapseSidebar={this.props.collapseSidebar}
|
||||||
isUserOwner={this.isUserOwner()}
|
isUserOwner={isUserOwner(this.props)}
|
||||||
clearConsole={this.props.clearConsole}
|
clearConsole={this.props.clearConsole}
|
||||||
consoleEvents={this.props.console}
|
consoleEvents={this.props.console}
|
||||||
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
|
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
|
||||||
|
@ -334,7 +337,7 @@ class IDEView extends React.Component {
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
<section className="preview-frame-holder">
|
<section className="preview-frame-holder">
|
||||||
<header className="preview-frame__header">
|
<header className="preview-frame__header">
|
||||||
<h2 className="preview-frame__title">Preview</h2>
|
<h2 className="preview-frame__title">{this.props.t('Preview')}</h2>
|
||||||
</header>
|
</header>
|
||||||
<div className="preview-frame__content">
|
<div className="preview-frame__content">
|
||||||
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
|
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
|
||||||
|
@ -395,7 +398,7 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
{ this.props.location.pathname === '/about' &&
|
{ this.props.location.pathname === '/about' &&
|
||||||
<Overlay
|
<Overlay
|
||||||
title="About"
|
title={this.props.t('About')}
|
||||||
previousPath={this.props.ide.previousPath}
|
previousPath={this.props.ide.previousPath}
|
||||||
ariaLabel="about"
|
ariaLabel="about"
|
||||||
>
|
>
|
||||||
|
@ -441,7 +444,7 @@ class IDEView extends React.Component {
|
||||||
}
|
}
|
||||||
{this.props.ide.keyboardShortcutVisible &&
|
{this.props.ide.keyboardShortcutVisible &&
|
||||||
<Overlay
|
<Overlay
|
||||||
title="Keyboard Shortcuts"
|
title={this.props.t('KeyboardShortcuts')}
|
||||||
ariaLabel="keyboard shortcuts"
|
ariaLabel="keyboard shortcuts"
|
||||||
closeOverlay={this.props.closeKeyboardShortcutModal}
|
closeOverlay={this.props.closeKeyboardShortcutModal}
|
||||||
>
|
>
|
||||||
|
@ -604,12 +607,12 @@ IDEView.propTypes = {
|
||||||
showErrorModal: PropTypes.func.isRequired,
|
showErrorModal: PropTypes.func.isRequired,
|
||||||
hideErrorModal: PropTypes.func.isRequired,
|
hideErrorModal: PropTypes.func.isRequired,
|
||||||
clearPersistedState: PropTypes.func.isRequired,
|
clearPersistedState: PropTypes.func.isRequired,
|
||||||
persistState: PropTypes.func.isRequired,
|
|
||||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
startSketch: PropTypes.func.isRequired,
|
startSketch: PropTypes.func.isRequired,
|
||||||
openUploadFileModal: PropTypes.func.isRequired,
|
openUploadFileModal: PropTypes.func.isRequired,
|
||||||
closeUploadFileModal: PropTypes.func.isRequired
|
closeUploadFileModal: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
@ -646,4 +649,6 @@ function mapDispatchToProps(dispatch) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView));
|
|
||||||
|
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)));
|
||||||
|
|
||||||
|
|
250
client/modules/IDE/pages/MobileIDEView.jsx
Normal file
250
client/modules/IDE/pages/MobileIDEView.jsx
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// Imports to be Refactored
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import * as FileActions from '../actions/files';
|
||||||
|
import * as IDEActions from '../actions/ide';
|
||||||
|
import * as ProjectActions from '../actions/project';
|
||||||
|
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
|
||||||
|
import * as PreferencesActions from '../actions/preferences';
|
||||||
|
import * as UserActions from '../../User/actions';
|
||||||
|
import * as ToastActions from '../actions/toast';
|
||||||
|
import * as ConsoleActions from '../actions/console';
|
||||||
|
import { getHTMLFile } from '../reducers/files';
|
||||||
|
|
||||||
|
// Local Imports
|
||||||
|
import Editor from '../components/Editor';
|
||||||
|
import { PreferencesIcon, PlayIcon, ExitIcon } from '../../../common/icons';
|
||||||
|
|
||||||
|
import IconButton from '../../../components/mobile/IconButton';
|
||||||
|
import Header from '../../../components/mobile/Header';
|
||||||
|
import Screen from '../../../components/mobile/MobileScreen';
|
||||||
|
import Footer from '../../../components/mobile/Footer';
|
||||||
|
import IDEWrapper from '../../../components/mobile/IDEWrapper';
|
||||||
|
|
||||||
|
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
|
||||||
|
|
||||||
|
const MobileIDEView = (props) => {
|
||||||
|
const {
|
||||||
|
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
|
||||||
|
selectedFile, updateFileContent, files,
|
||||||
|
closeEditorOptions, showEditorOptions, showKeyboardShortcutModal, setUnsavedChanges,
|
||||||
|
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
||||||
|
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [tmController, setTmController] = useState(null); // eslint-disable-line
|
||||||
|
const [overlay, setOverlay] = useState(null); // eslint-disable-line
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen fullscreen>
|
||||||
|
<Header
|
||||||
|
title={project.name}
|
||||||
|
subtitle={selectedFile.name}
|
||||||
|
leftButton={
|
||||||
|
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
to="/mobile/preferences"
|
||||||
|
onClick={() => setOverlay('preferences')}
|
||||||
|
icon={PreferencesIcon}
|
||||||
|
aria-label="Open preferences menu"
|
||||||
|
/>
|
||||||
|
<IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<IDEWrapper>
|
||||||
|
<Editor
|
||||||
|
lintWarning={preferences.lintWarning}
|
||||||
|
linewrap={preferences.linewrap}
|
||||||
|
lintMessages={editorAccessibility.lintMessages}
|
||||||
|
updateLintMessage={updateLintMessage}
|
||||||
|
clearLintMessage={clearLintMessage}
|
||||||
|
file={selectedFile}
|
||||||
|
updateFileContent={updateFileContent}
|
||||||
|
fontSize={preferences.fontSize}
|
||||||
|
lineNumbers={preferences.lineNumbers}
|
||||||
|
files={files}
|
||||||
|
editorOptionsVisible={ide.editorOptionsVisible}
|
||||||
|
showEditorOptions={showEditorOptions}
|
||||||
|
closeEditorOptions={closeEditorOptions}
|
||||||
|
showKeyboardShortcutModal={showKeyboardShortcutModal}
|
||||||
|
setUnsavedChanges={setUnsavedChanges}
|
||||||
|
isPlaying={ide.isPlaying}
|
||||||
|
theme={preferences.theme}
|
||||||
|
startRefreshSketch={startRefreshSketch}
|
||||||
|
stopSketch={stopSketch}
|
||||||
|
autorefresh={preferences.autorefresh}
|
||||||
|
unsavedChanges={ide.unsavedChanges}
|
||||||
|
projectSavedTime={project.updatedAt}
|
||||||
|
isExpanded={ide.sidebarIsExpanded}
|
||||||
|
expandSidebar={expandSidebar}
|
||||||
|
collapseSidebar={collapseSidebar}
|
||||||
|
isUserOwner={isUserOwner(props)}
|
||||||
|
clearConsole={clearConsole}
|
||||||
|
consoleEvents={console}
|
||||||
|
showRuntimeErrorWarning={showRuntimeErrorWarning}
|
||||||
|
hideRuntimeErrorWarning={hideRuntimeErrorWarning}
|
||||||
|
runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible}
|
||||||
|
provideController={setTmController}
|
||||||
|
/>
|
||||||
|
</IDEWrapper>
|
||||||
|
<Footer><h2>Bottom Bar</h2></Footer>
|
||||||
|
</Screen>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
MobileIDEView.propTypes = {
|
||||||
|
|
||||||
|
preferences: PropTypes.shape({
|
||||||
|
fontSize: PropTypes.number.isRequired,
|
||||||
|
autosave: PropTypes.bool.isRequired,
|
||||||
|
linewrap: PropTypes.bool.isRequired,
|
||||||
|
lineNumbers: PropTypes.bool.isRequired,
|
||||||
|
lintWarning: PropTypes.bool.isRequired,
|
||||||
|
textOutput: PropTypes.bool.isRequired,
|
||||||
|
gridOutput: PropTypes.bool.isRequired,
|
||||||
|
soundOutput: PropTypes.bool.isRequired,
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
autorefresh: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
ide: PropTypes.shape({
|
||||||
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
|
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||||
|
consoleEvent: PropTypes.array,
|
||||||
|
modalIsVisible: PropTypes.bool.isRequired,
|
||||||
|
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||||
|
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||||
|
preferencesIsVisible: PropTypes.bool.isRequired,
|
||||||
|
projectOptionsVisible: PropTypes.bool.isRequired,
|
||||||
|
newFolderModalVisible: PropTypes.bool.isRequired,
|
||||||
|
shareModalVisible: PropTypes.bool.isRequired,
|
||||||
|
shareModalProjectId: PropTypes.string.isRequired,
|
||||||
|
shareModalProjectName: PropTypes.string.isRequired,
|
||||||
|
shareModalProjectUsername: PropTypes.string.isRequired,
|
||||||
|
editorOptionsVisible: PropTypes.bool.isRequired,
|
||||||
|
keyboardShortcutVisible: PropTypes.bool.isRequired,
|
||||||
|
unsavedChanges: PropTypes.bool.isRequired,
|
||||||
|
infiniteLoop: PropTypes.bool.isRequired,
|
||||||
|
previewIsRefreshing: PropTypes.bool.isRequired,
|
||||||
|
infiniteLoopMessage: PropTypes.string.isRequired,
|
||||||
|
projectSavedTime: PropTypes.string,
|
||||||
|
previousPath: PropTypes.string.isRequired,
|
||||||
|
justOpenedProject: PropTypes.bool.isRequired,
|
||||||
|
errorType: PropTypes.string,
|
||||||
|
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||||
|
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
editorAccessibility: PropTypes.shape({
|
||||||
|
lintMessages: PropTypes.array.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
project: PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
id: PropTypes.string
|
||||||
|
}),
|
||||||
|
updatedAt: PropTypes.string
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
startSketch: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
updateLintMessage: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
clearLintMessage: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
selectedFile: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
updateFileContent: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
files: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string.isRequired
|
||||||
|
})).isRequired,
|
||||||
|
|
||||||
|
closeEditorOptions: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
showEditorOptions: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
setUnsavedChanges: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
startRefreshSketch: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
stopSketch: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
expandSidebar: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
collapseSidebar: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
clearConsole: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
console: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
method: PropTypes.string.isRequired,
|
||||||
|
args: PropTypes.arrayOf(PropTypes.string)
|
||||||
|
})).isRequired,
|
||||||
|
|
||||||
|
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
user: PropTypes.shape({
|
||||||
|
authenticated: PropTypes.bool.isRequired,
|
||||||
|
id: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
files: state.files,
|
||||||
|
selectedFile: state.files.find(file => file.isSelectedFile) ||
|
||||||
|
state.files.find(file => file.name === 'sketch.js') ||
|
||||||
|
state.files.find(file => file.name !== 'root'),
|
||||||
|
htmlFile: getHTMLFile(state.files),
|
||||||
|
ide: state.ide,
|
||||||
|
preferences: state.preferences,
|
||||||
|
editorAccessibility: state.editorAccessibility,
|
||||||
|
user: state.user,
|
||||||
|
project: state.project,
|
||||||
|
toast: state.toast,
|
||||||
|
console: state.console
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
EditorAccessibilityActions,
|
||||||
|
FileActions,
|
||||||
|
ProjectActions,
|
||||||
|
IDEActions,
|
||||||
|
PreferencesActions,
|
||||||
|
UserActions,
|
||||||
|
ToastActions,
|
||||||
|
ConsoleActions
|
||||||
|
),
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));
|
|
@ -37,6 +37,11 @@ const getSortedCollections = createSelector(
|
||||||
return orderBy(collections, 'name', 'desc');
|
return orderBy(collections, 'name', 'desc');
|
||||||
}
|
}
|
||||||
return orderBy(collections, 'name', 'asc');
|
return orderBy(collections, 'name', 'asc');
|
||||||
|
} else if (field === 'numItems') {
|
||||||
|
if (direction === DIRECTION.DESC) {
|
||||||
|
return orderBy(collections, 'items.length', 'desc');
|
||||||
|
}
|
||||||
|
return orderBy(collections, 'items.length', 'asc');
|
||||||
}
|
}
|
||||||
const sortedCollections = [...collections].sort((a, b) => {
|
const sortedCollections = [...collections].sort((a, b) => {
|
||||||
const result =
|
const result =
|
||||||
|
|
|
@ -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,
|
||||||
|
|
226
client/modules/Mobile/MobilePreferences.jsx
Normal file
226
client/modules/Mobile/MobilePreferences.jsx
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import * as PreferencesActions from '../IDE/actions/preferences';
|
||||||
|
import * as IdeActions from '../IDE/actions/ide';
|
||||||
|
|
||||||
|
import IconButton from '../../components/mobile/IconButton';
|
||||||
|
import Screen from '../../components/mobile/MobileScreen';
|
||||||
|
import Header from '../../components/mobile/Header';
|
||||||
|
import PreferencePicker from '../../components/mobile/PreferencePicker';
|
||||||
|
import { ExitIcon } from '../../common/icons';
|
||||||
|
import { remSize, prop } from '../../theme';
|
||||||
|
|
||||||
|
const Content = styled.div`
|
||||||
|
z-index: 0;
|
||||||
|
margin-top: ${remSize(68)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const SettingsHeader = styled(Header)`
|
||||||
|
background: transparent;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SectionHeader = styled.h2`
|
||||||
|
color: ${prop('primaryTextColor')};
|
||||||
|
padding-top: ${remSize(32)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SectionSubeader = styled.h3`
|
||||||
|
color: ${prop('primaryTextColor')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
const MobilePreferences = (props) => {
|
||||||
|
const {
|
||||||
|
setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, lineNumbers, lintWarning
|
||||||
|
} = props;
|
||||||
|
const {
|
||||||
|
theme, autosave, linewrap, textOutput, gridOutput, soundOutput, setLineNumbers, setLintWarning
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const generalSettings = [
|
||||||
|
{
|
||||||
|
title: 'Theme',
|
||||||
|
value: theme,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'light', label: 'light', ariaLabel: 'light theme on', name: 'light theme', id: 'light-theme-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'dark', label: 'dark', ariaLabel: 'dark theme on', name: 'dark theme', id: 'dark-theme-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'contrast',
|
||||||
|
label: 'contrast',
|
||||||
|
ariaLabel: 'contrast theme on',
|
||||||
|
name: 'contrast theme',
|
||||||
|
id: 'contrast-theme-on'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onSelect: x => setTheme(x) // setTheme
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Autosave',
|
||||||
|
value: autosave,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true, label: 'On', ariaLabel: 'autosave on', name: 'autosave', id: 'autosave-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false, label: 'Off', ariaLabel: 'autosave off', name: 'autosave', id: 'autosave-off'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSelect: x => setAutosave(x) // setAutosave
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Word Wrap',
|
||||||
|
value: linewrap,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true, label: 'On', ariaLabel: 'linewrap on', name: 'linewrap', id: 'linewrap-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false, label: 'Off', ariaLabel: 'linewrap off', name: 'linewrap', id: 'linewrap-off'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSelect: x => setLinewrap(x)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const outputSettings = [
|
||||||
|
{
|
||||||
|
title: 'Plain-text',
|
||||||
|
value: textOutput,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true, label: 'On', ariaLabel: 'text output on', name: 'text output', id: 'text-output-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false, label: 'Off', ariaLabel: 'text output off', name: 'text output', id: 'text-output-off'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSelect: x => setTextOutput(x)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Table-text',
|
||||||
|
value: gridOutput,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true, label: 'On', ariaLabel: 'table output on', name: 'table output', id: 'table-output-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false, label: 'Off', ariaLabel: 'table output off', name: 'table output', id: 'table-output-off'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSelect: x => setGridOutput(x)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Sound',
|
||||||
|
value: soundOutput,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true, label: 'On', ariaLabel: 'sound output on', name: 'sound output', id: 'sound-output-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false, label: 'Off', ariaLabel: 'sound output off', name: 'sound output', id: 'sound-output-off'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSelect: x => setSoundOutput(x)
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const accessibilitySettings = [
|
||||||
|
{
|
||||||
|
title: 'Line Numbers',
|
||||||
|
value: lineNumbers,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true, label: 'On', ariaLabel: 'line numbers on', name: 'line numbers', id: 'line-numbers-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false, label: 'Off', ariaLabel: 'line numbers off', name: 'line numbers', id: 'line-numbers-off'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSelect: x => setLineNumbers(x)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Lint Warning Sound',
|
||||||
|
value: lintWarning,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: true, label: 'On', ariaLabel: 'lint warning on', name: 'lint warning', id: 'lint-warning-on'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false, label: 'Off', ariaLabel: 'lint warning off', name: 'lint warning', id: 'lint-warning-off'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onSelect: x => setLintWarning(x)
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen fullscreen>
|
||||||
|
<section>
|
||||||
|
<SettingsHeader transparent title="Preferences">
|
||||||
|
|
||||||
|
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" />
|
||||||
|
</SettingsHeader>
|
||||||
|
<section className="preferences">
|
||||||
|
<Content>
|
||||||
|
<SectionHeader>General Settings</SectionHeader>
|
||||||
|
{ generalSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||||
|
|
||||||
|
<SectionHeader>Accessibility</SectionHeader>
|
||||||
|
{ accessibilitySettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||||
|
|
||||||
|
<SectionHeader>Accessible Output</SectionHeader>
|
||||||
|
<SectionSubeader>Used with screen reader</SectionSubeader>
|
||||||
|
{ outputSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||||
|
|
||||||
|
</Content>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</Screen>);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
MobilePreferences.propTypes = {
|
||||||
|
fontSize: PropTypes.number.isRequired,
|
||||||
|
lineNumbers: PropTypes.bool.isRequired,
|
||||||
|
autosave: PropTypes.bool.isRequired,
|
||||||
|
linewrap: PropTypes.bool.isRequired,
|
||||||
|
textOutput: PropTypes.bool.isRequired,
|
||||||
|
gridOutput: PropTypes.bool.isRequired,
|
||||||
|
soundOutput: PropTypes.bool.isRequired,
|
||||||
|
lintWarning: PropTypes.bool.isRequired,
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
setLinewrap: PropTypes.func.isRequired,
|
||||||
|
setLintWarning: PropTypes.func.isRequired,
|
||||||
|
setTheme: PropTypes.func.isRequired,
|
||||||
|
setFontSize: PropTypes.func.isRequired,
|
||||||
|
setLineNumbers: PropTypes.func.isRequired,
|
||||||
|
setAutosave: PropTypes.func.isRequired,
|
||||||
|
setTextOutput: PropTypes.func.isRequired,
|
||||||
|
setGridOutput: PropTypes.func.isRequired,
|
||||||
|
setSoundOutput: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
...state.preferences,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => bindActionCreators({
|
||||||
|
...PreferencesActions,
|
||||||
|
...IdeActions
|
||||||
|
}, dispatch);
|
||||||
|
|
||||||
|
|
||||||
|
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobilePreferences));
|
184
client/modules/Mobile/MobileSketchView.jsx
Normal file
184
client/modules/Mobile/MobileSketchView.jsx
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import Header from '../../components/mobile/Header';
|
||||||
|
import IconButton from '../../components/mobile/IconButton';
|
||||||
|
import PreviewFrame from '../IDE/components/PreviewFrame';
|
||||||
|
import Screen from '../../components/mobile/MobileScreen';
|
||||||
|
import * as ProjectActions from '../IDE/actions/project';
|
||||||
|
import * as IDEActions from '../IDE/actions/ide';
|
||||||
|
import * as PreferencesActions from '../IDE/actions/preferences';
|
||||||
|
import * as ConsoleActions from '../IDE/actions/console';
|
||||||
|
import * as FilesActions from '../IDE/actions/files';
|
||||||
|
|
||||||
|
import { getHTMLFile } from '../IDE/reducers/files';
|
||||||
|
|
||||||
|
|
||||||
|
import { ExitIcon } from '../../common/icons';
|
||||||
|
import { remSize } from '../../theme';
|
||||||
|
|
||||||
|
|
||||||
|
const Content = styled.div`
|
||||||
|
z-index: 0;
|
||||||
|
margin-top: ${remSize(68)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MobileSketchView = (props) => {
|
||||||
|
// TODO: useSelector requires react-redux ^7.1.0
|
||||||
|
// const htmlFile = useSelector(state => getHTMLFile(state.files));
|
||||||
|
// const jsFiles = useSelector(state => getJSFiles(state.files));
|
||||||
|
// const cssFiles = useSelector(state => getCSSFiles(state.files));
|
||||||
|
// const files = useSelector(state => state.files);
|
||||||
|
|
||||||
|
const {
|
||||||
|
htmlFile, files, selectedFile, projectName
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const {
|
||||||
|
setTextOutput, setGridOutput, setSoundOutput,
|
||||||
|
endSketchRefresh, stopSketch,
|
||||||
|
dispatchConsoleEvent, expandConsole, clearConsole,
|
||||||
|
setBlobUrl,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { preferences, ide } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Screen fullscreen>
|
||||||
|
<Header
|
||||||
|
leftButton={
|
||||||
|
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
|
||||||
|
}
|
||||||
|
title={projectName}
|
||||||
|
/>
|
||||||
|
<Content>
|
||||||
|
<PreviewFrame
|
||||||
|
htmlFile={htmlFile}
|
||||||
|
files={files}
|
||||||
|
head={<link type="text/css" rel="stylesheet" href="/preview-styles.css" />}
|
||||||
|
|
||||||
|
content={selectedFile.content}
|
||||||
|
|
||||||
|
isPlaying
|
||||||
|
isAccessibleOutputPlaying={ide.isAccessibleOutputPlaying}
|
||||||
|
previewIsRefreshing={ide.previewIsRefreshing}
|
||||||
|
|
||||||
|
textOutput={preferences.textOutput}
|
||||||
|
gridOutput={preferences.gridOutput}
|
||||||
|
soundOutput={preferences.soundOutput}
|
||||||
|
autorefresh={preferences.autorefresh}
|
||||||
|
|
||||||
|
setTextOutput={setTextOutput}
|
||||||
|
setGridOutput={setGridOutput}
|
||||||
|
setSoundOutput={setSoundOutput}
|
||||||
|
dispatchConsoleEvent={dispatchConsoleEvent}
|
||||||
|
endSketchRefresh={endSketchRefresh}
|
||||||
|
stopSketch={stopSketch}
|
||||||
|
setBlobUrl={setBlobUrl}
|
||||||
|
expandConsole={expandConsole}
|
||||||
|
clearConsole={clearConsole}
|
||||||
|
/>
|
||||||
|
</Content>
|
||||||
|
</Screen>);
|
||||||
|
};
|
||||||
|
|
||||||
|
MobileSketchView.propTypes = {
|
||||||
|
params: PropTypes.shape({
|
||||||
|
project_id: PropTypes.string,
|
||||||
|
username: PropTypes.string
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
htmlFile: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
files: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
})).isRequired,
|
||||||
|
|
||||||
|
selectedFile: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
preferences: PropTypes.shape({
|
||||||
|
fontSize: PropTypes.number.isRequired,
|
||||||
|
autosave: PropTypes.bool.isRequired,
|
||||||
|
linewrap: PropTypes.bool.isRequired,
|
||||||
|
lineNumbers: PropTypes.bool.isRequired,
|
||||||
|
lintWarning: PropTypes.bool.isRequired,
|
||||||
|
textOutput: PropTypes.bool.isRequired,
|
||||||
|
gridOutput: PropTypes.bool.isRequired,
|
||||||
|
soundOutput: PropTypes.bool.isRequired,
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
autorefresh: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
ide: PropTypes.shape({
|
||||||
|
isPlaying: PropTypes.bool.isRequired,
|
||||||
|
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||||
|
consoleEvent: PropTypes.array,
|
||||||
|
modalIsVisible: PropTypes.bool.isRequired,
|
||||||
|
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||||
|
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||||
|
preferencesIsVisible: PropTypes.bool.isRequired,
|
||||||
|
projectOptionsVisible: PropTypes.bool.isRequired,
|
||||||
|
newFolderModalVisible: PropTypes.bool.isRequired,
|
||||||
|
shareModalVisible: PropTypes.bool.isRequired,
|
||||||
|
shareModalProjectId: PropTypes.string.isRequired,
|
||||||
|
shareModalProjectName: PropTypes.string.isRequired,
|
||||||
|
shareModalProjectUsername: PropTypes.string.isRequired,
|
||||||
|
editorOptionsVisible: PropTypes.bool.isRequired,
|
||||||
|
keyboardShortcutVisible: PropTypes.bool.isRequired,
|
||||||
|
unsavedChanges: PropTypes.bool.isRequired,
|
||||||
|
infiniteLoop: PropTypes.bool.isRequired,
|
||||||
|
previewIsRefreshing: PropTypes.bool.isRequired,
|
||||||
|
infiniteLoopMessage: PropTypes.string.isRequired,
|
||||||
|
projectSavedTime: PropTypes.string,
|
||||||
|
previousPath: PropTypes.string.isRequired,
|
||||||
|
justOpenedProject: PropTypes.bool.isRequired,
|
||||||
|
errorType: PropTypes.string,
|
||||||
|
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||||
|
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
|
||||||
|
projectName: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
setTextOutput: PropTypes.func.isRequired,
|
||||||
|
setGridOutput: PropTypes.func.isRequired,
|
||||||
|
setSoundOutput: PropTypes.func.isRequired,
|
||||||
|
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||||
|
endSketchRefresh: PropTypes.func.isRequired,
|
||||||
|
stopSketch: PropTypes.func.isRequired,
|
||||||
|
setBlobUrl: PropTypes.func.isRequired,
|
||||||
|
expandConsole: PropTypes.func.isRequired,
|
||||||
|
clearConsole: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
htmlFile: getHTMLFile(state.files),
|
||||||
|
projectName: state.project.name,
|
||||||
|
files: state.files,
|
||||||
|
ide: state.ide,
|
||||||
|
preferences: state.preferences,
|
||||||
|
selectedFile: state.files.find(file => file.isSelectedFile) ||
|
||||||
|
state.files.find(file => file.name === 'sketch.js') ||
|
||||||
|
state.files.find(file => file.name !== 'root'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
...ProjectActions, ...IDEActions, ...PreferencesActions, ...ConsoleActions, ...FilesActions
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(MobileSketchView);
|
|
@ -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,
|
||||||
|
|
|
@ -71,41 +71,50 @@ ShareURL.propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
class CollectionItemRowBase extends React.Component {
|
const CollectionItemRowBase = ({
|
||||||
handleSketchRemove = () => {
|
collection, item, isOwner, removeFromCollection
|
||||||
if (window.confirm(`Are you sure you want to remove "${this.props.item.project.name}" from this collection?`)) {
|
}) => {
|
||||||
this.props.removeFromCollection(this.props.collection.id, this.props.item.project.id);
|
const projectIsDeleted = item.isDeleted;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
const handleSketchRemove = () => {
|
||||||
const { item } = this.props;
|
const name = projectIsDeleted ? 'deleted sketch' : item.project.name;
|
||||||
const sketchOwnerUsername = item.project.user.username;
|
|
||||||
const sketchUrl = `/${item.project.user.username}/sketches/${item.project.id}`;
|
if (window.confirm(`Are you sure you want to remove "${name}" from this collection?`)) {
|
||||||
|
removeFromCollection(collection.id, item.projectId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const name = projectIsDeleted ? <span>Sketch was deleted</span> : (
|
||||||
|
<Link to={`/${item.project.user.username}/sketches/${item.projectId}`}>
|
||||||
|
{item.project.name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sketchOwnerUsername = projectIsDeleted ? null : item.project.user.username;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
className="sketches-table__row"
|
className={`sketches-table__row ${projectIsDeleted ? 'is-deleted' : ''}`}
|
||||||
>
|
>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<Link to={sketchUrl}>
|
{name}
|
||||||
{item.project.name}
|
|
||||||
</Link>
|
|
||||||
</th>
|
</th>
|
||||||
<td>{format(new Date(item.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
<td>{format(new Date(item.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
||||||
<td>{sketchOwnerUsername}</td>
|
<td>{sketchOwnerUsername}</td>
|
||||||
<td className="collection-row__action-column ">
|
<td className="collection-row__action-column ">
|
||||||
|
{isOwner &&
|
||||||
<button
|
<button
|
||||||
className="collection-row__remove-button"
|
className="collection-row__remove-button"
|
||||||
onClick={this.handleSketchRemove}
|
onClick={handleSketchRemove}
|
||||||
aria-label="Remove sketch from collection"
|
aria-label="Remove sketch from collection"
|
||||||
>
|
>
|
||||||
<RemoveIcon focusable="false" aria-hidden="true" />
|
<RemoveIcon focusable="false" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>);
|
</tr>);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
CollectionItemRowBase.propTypes = {
|
CollectionItemRowBase.propTypes = {
|
||||||
collection: PropTypes.shape({
|
collection: PropTypes.shape({
|
||||||
|
@ -114,14 +123,17 @@ CollectionItemRowBase.propTypes = {
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
item: PropTypes.shape({
|
item: PropTypes.shape({
|
||||||
createdAt: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
projectId: PropTypes.string.isRequired,
|
||||||
|
isDeleted: PropTypes.bool.isRequired,
|
||||||
project: PropTypes.shape({
|
project: PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
username: PropTypes.string.isRequired
|
username: PropTypes.string.isRequired
|
||||||
})
|
}),
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
isOwner: PropTypes.bool.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
authenticated: PropTypes.bool.isRequired
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
@ -342,6 +354,7 @@ class Collection extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const title = this.hasCollection() ? this.getCollectionName() : null;
|
const title = this.hasCollection() ? this.getCollectionName() : null;
|
||||||
|
const isOwner = this.isOwner();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="collection-container" data-has-items={this.hasCollectionItems() ? 'true' : 'false'}>
|
<main className="collection-container" data-has-items={this.hasCollectionItems() ? 'true' : 'false'}>
|
||||||
|
@ -372,6 +385,7 @@ class Collection extends React.Component {
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
username={this.getUsername()}
|
username={this.getUsername()}
|
||||||
collection={this.props.collection}
|
collection={this.props.collection}
|
||||||
|
isOwner={isOwner}
|
||||||
/>))}
|
/>))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -2,6 +2,9 @@ import { Route, IndexRoute } from 'react-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import App from './modules/App/App';
|
import App from './modules/App/App';
|
||||||
import IDEView from './modules/IDE/pages/IDEView';
|
import IDEView from './modules/IDE/pages/IDEView';
|
||||||
|
import MobileIDEView from './modules/IDE/pages/MobileIDEView';
|
||||||
|
import MobileSketchView from './modules/Mobile/MobileSketchView';
|
||||||
|
import MobilePreferences from './modules/Mobile/MobilePreferences';
|
||||||
import FullView from './modules/IDE/pages/FullView';
|
import FullView from './modules/IDE/pages/FullView';
|
||||||
import LoginView from './modules/User/pages/LoginView';
|
import LoginView from './modules/User/pages/LoginView';
|
||||||
import SignupView from './modules/User/pages/SignupView';
|
import SignupView from './modules/User/pages/SignupView';
|
||||||
|
@ -20,7 +23,11 @@ const checkAuth = (store) => {
|
||||||
store.dispatch(getUser());
|
store.dispatch(getUser());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: This short-circuit seems unnecessary - using the mobile <Switch /> navigator (future) should prevent this from being called
|
||||||
const onRouteChange = (store) => {
|
const onRouteChange = (store) => {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
if (path.includes('/mobile')) return;
|
||||||
|
|
||||||
store.dispatch(stopSketch());
|
store.dispatch(stopSketch());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -49,6 +56,10 @@ const routes = store => (
|
||||||
<Route path="/:username/collections/create" component={DashboardView} />
|
<Route path="/:username/collections/create" component={DashboardView} />
|
||||||
<Route path="/:username/collections/:collection_id" component={CollectionView} />
|
<Route path="/:username/collections/:collection_id" component={CollectionView} />
|
||||||
<Route path="/about" component={IDEView} />
|
<Route path="/about" component={IDEView} />
|
||||||
|
|
||||||
|
<Route path="/mobile" component={MobileIDEView} />
|
||||||
|
<Route path="/mobile/preview" component={MobileSketchView} />
|
||||||
|
<Route path="/mobile/preferences" component={MobilePreferences} />
|
||||||
</Route>
|
</Route>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: calc(var(--percent) * 100%);
|
width: calc(var(--percent, 1) * 100%);
|
||||||
|
|
||||||
@include themify() {
|
@include themify() {
|
||||||
background-color: getThemifyVariable('progress-bar-active-color');
|
background-color: getThemifyVariable('progress-bar-active-color');
|
||||||
|
|
|
@ -85,6 +85,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sketches-table__row.is-deleted > * {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.sketches-table thead {
|
.sketches-table thead {
|
||||||
font-size: #{12 / $base-font-size}rem;
|
font-size: #{12 / $base-font-size}rem;
|
||||||
@include themify() {
|
@include themify() {
|
||||||
|
|
|
@ -85,6 +85,17 @@ export default {
|
||||||
border: grays.middleLight,
|
border: grays.middleLight,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Icon: {
|
||||||
|
default: grays.middleGray,
|
||||||
|
hover: grays.darker
|
||||||
|
},
|
||||||
|
MobilePanel: {
|
||||||
|
default: {
|
||||||
|
foreground: colors.black,
|
||||||
|
background: grays.light,
|
||||||
|
border: grays.middleLight,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[Theme.dark]: {
|
[Theme.dark]: {
|
||||||
colors,
|
colors,
|
||||||
|
@ -113,6 +124,17 @@ export default {
|
||||||
border: grays.middleDark,
|
border: grays.middleDark,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Icon: {
|
||||||
|
default: grays.middleLight,
|
||||||
|
hover: grays.lightest
|
||||||
|
},
|
||||||
|
MobilePanel: {
|
||||||
|
default: {
|
||||||
|
foreground: grays.light,
|
||||||
|
background: grays.dark,
|
||||||
|
border: grays.middleDark,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[Theme.contrast]: {
|
[Theme.contrast]: {
|
||||||
colors,
|
colors,
|
||||||
|
@ -141,5 +163,16 @@ export default {
|
||||||
border: grays.middleDark,
|
border: grays.middleDark,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Icon: {
|
||||||
|
default: grays.mediumLight,
|
||||||
|
hover: colors.yellow
|
||||||
|
},
|
||||||
|
MobilePanel: {
|
||||||
|
default: {
|
||||||
|
foreground: grays.light,
|
||||||
|
background: grays.dark,
|
||||||
|
border: grays.middleDark,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
17
client/utils/apiClient.js
Normal file
17
client/utils/apiClient.js
Normal 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();
|
|
@ -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
24
client/utils/getConfig.js
Normal 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;
|
28
client/utils/getConfig.test.js
Normal file
28
client/utils/getConfig.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -69,9 +69,8 @@ Note that this is optional, unless you are working on the part of the applicatio
|
||||||
If your S3 bucket is in the US East (N Virginia) region (us-east-1), you'll
|
If your S3 bucket is in the US East (N Virginia) region (us-east-1), you'll
|
||||||
need to set a custom URL base for it, because it does not follow the standard
|
need to set a custom URL base for it, because it does not follow the standard
|
||||||
naming pattern as the rest of the regions. Instead, add the following to your
|
naming pattern as the rest of the regions. Instead, add the following to your
|
||||||
environment/.env file:
|
environment/.env file, changing `BUCKET_NAME` to your bucket name. This is necessary because this override is currently treated as the full path to the bucket rather than as a proper base url:
|
||||||
|
`S3_BUCKET_URL_BASE=https://s3.amazonaws.com/{BUCKET_NAME}/`
|
||||||
`S3_BUCKET_URL_BASE=https://s3.amazonaws.com`
|
|
||||||
|
|
||||||
If you've configured your S3 bucket and DNS records to use a custom domain
|
If you've configured your S3 bucket and DNS records to use a custom domain
|
||||||
name, you can also set it using this variable. I.e.:
|
name, you can also set it using this variable. I.e.:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { configure } from 'enzyme'
|
|
||||||
import Adapter from 'enzyme-adapter-react-16'
|
|
||||||
import '@babel/polyfill'
|
|
||||||
import mongoose from 'mongoose'
|
|
||||||
|
|
||||||
mongoose.Promise = global.Promise;
|
|
||||||
|
|
||||||
configure({ adapter: new Adapter() })
|
|
8725
package-lock.json
generated
8725
package-lock.json
generated
File diff suppressed because it is too large
Load diff
40
package.json
40
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "p5.js-web-editor",
|
"name": "p5.js-web-editor",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"description": "The web editor for p5.js.",
|
"description": "The web editor for p5.js.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
|
@ -36,12 +36,30 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"setupFiles": [
|
"projects": [
|
||||||
"<rootDir>/jest.setup.js"
|
{
|
||||||
|
"displayName": "server",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"<rootDir>/server/jest.setup.js"
|
||||||
|
],
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/server/**/*.test.(js|jsx)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"displayName": "client",
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"<rootDir>/client/jest.setup.js"
|
||||||
],
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/client/__test__/mocks/fileMock.js"
|
"^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/client/__test__/mocks/fileMock.js"
|
||||||
|
},
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/client/**/*.test.(js|jsx)"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "Cassie Tarakajian",
|
"author": "Cassie Tarakajian",
|
||||||
|
@ -78,15 +96,14 @@
|
||||||
"@storybook/addons": "^5.3.6",
|
"@storybook/addons": "^5.3.6",
|
||||||
"@storybook/react": "^5.3.6",
|
"@storybook/react": "^5.3.6",
|
||||||
"@svgr/webpack": "^5.4.0",
|
"@svgr/webpack": "^5.4.0",
|
||||||
|
"@testing-library/jest-dom": "^5.10.1",
|
||||||
|
"@testing-library/react": "^10.2.1",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-eslint": "^9.0.0",
|
"babel-eslint": "^9.0.0",
|
||||||
"babel-jest": "^24.9.0",
|
|
||||||
"babel-loader": "^8.0.0",
|
"babel-loader": "^8.0.0",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.2.12",
|
"babel-plugin-transform-react-remove-prop-types": "^0.2.12",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^3.4.2",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"enzyme": "^3.11.0",
|
|
||||||
"enzyme-adapter-react-16": "^1.15.2",
|
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^4.19.1",
|
||||||
"eslint-config-airbnb": "^16.1.0",
|
"eslint-config-airbnb": "^16.1.0",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
|
@ -94,10 +111,10 @@
|
||||||
"eslint-plugin-react": "^7.18.3",
|
"eslint-plugin-react": "^7.18.3",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^2.0.0",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
"jest": "^24.9.0",
|
"jest": "^26.0.1",
|
||||||
"lint-staged": "^10.1.3",
|
"lint-staged": "^10.1.3",
|
||||||
"mini-css-extract-plugin": "^0.8.2",
|
"mini-css-extract-plugin": "^0.8.2",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^4.14.1",
|
||||||
"nodemon": "^1.19.4",
|
"nodemon": "^1.19.4",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
"postcss-cssnext": "^3.1.0",
|
"postcss-cssnext": "^3.1.0",
|
||||||
|
@ -137,6 +154,7 @@
|
||||||
"connect-mongo": "^1.3.2",
|
"connect-mongo": "^1.3.2",
|
||||||
"console-feed": "^2.8.11",
|
"console-feed": "^2.8.11",
|
||||||
"cookie-parser": "^1.4.3",
|
"cookie-parser": "^1.4.3",
|
||||||
|
"copy-webpack-plugin": "^6.0.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"csslint": "^1.0.5",
|
"csslint": "^1.0.5",
|
||||||
|
@ -151,12 +169,15 @@
|
||||||
"express-session": "^1.17.0",
|
"express-session": "^1.17.0",
|
||||||
"friendly-words": "^1.1.10",
|
"friendly-words": "^1.1.10",
|
||||||
"htmlhint": "^0.10.1",
|
"htmlhint": "^0.10.1",
|
||||||
|
"i18next": "^19.4.5",
|
||||||
|
"i18next-browser-languagedetector": "^4.2.0",
|
||||||
|
"i18next-http-backend": "^1.0.15",
|
||||||
"is-url": "^1.2.4",
|
"is-url": "^1.2.4",
|
||||||
"jest-express": "^1.11.0",
|
"jest-express": "^1.11.0",
|
||||||
"js-beautify": "^1.10.3",
|
"js-beautify": "^1.10.3",
|
||||||
"jsdom": "^9.8.3",
|
"jsdom": "^9.8.3",
|
||||||
"jshint": "^2.11.0",
|
"jshint": "^2.11.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.19",
|
||||||
"loop-protect": "github:catarak/loop-protect",
|
"loop-protect": "github:catarak/loop-protect",
|
||||||
"mime-types": "^2.1.26",
|
"mime-types": "^2.1.26",
|
||||||
"mjml": "^3.3.2",
|
"mjml": "^3.3.2",
|
||||||
|
@ -178,6 +199,7 @@
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-helmet": "^5.1.3",
|
"react-helmet": "^5.1.3",
|
||||||
"react-hot-loader": "^4.12.19",
|
"react-hot-loader": "^4.12.19",
|
||||||
|
"react-i18next": "^11.5.0",
|
||||||
"react-redux": "^5.1.2",
|
"react-redux": "^5.1.2",
|
||||||
"react-router": "^3.2.5",
|
"react-router": "^3.2.5",
|
||||||
"react-split-pane": "^0.1.89",
|
"react-split-pane": "^0.1.89",
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default function addProjectToCollection(req, res) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectInCollection = collection.items.find(p => p.project._id === project._id);
|
const projectInCollection = collection.items.find(p => p.projectId === project._id);
|
||||||
|
|
||||||
if (projectInCollection) {
|
if (projectInCollection) {
|
||||||
sendFailure(404, 'Project already in collection');
|
sendFailure(404, 'Project already in collection');
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default function addProjectToCollection(req, res) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = collection.items.find(p => p.project._id === projectId);
|
const project = collection.items.find(p => p.projectId === projectId);
|
||||||
|
|
||||||
if (project != null) {
|
if (project != null) {
|
||||||
project.remove();
|
project.remove();
|
||||||
|
|
|
@ -34,6 +34,7 @@ export function serveProject(req, res) {
|
||||||
resolveScripts(sketchDoc, files);
|
resolveScripts(sketchDoc, files);
|
||||||
resolveStyles(sketchDoc, files);
|
resolveStyles(sketchDoc, files);
|
||||||
|
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=0');
|
||||||
res.send(serializeDocument(sketchDoc));
|
res.send(serializeDocument(sketchDoc));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
4
server/jest.setup.js
Normal file
4
server/jest.setup.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import '@babel/polyfill';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
mongoose.Promise = global.Promise;
|
|
@ -15,6 +15,14 @@ collectedProjectSchema.virtual('id').get(function getId() {
|
||||||
return this._id.toHexString();
|
return this._id.toHexString();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
collectedProjectSchema.virtual('projectId').get(function projectId() {
|
||||||
|
return this.populated('project');
|
||||||
|
});
|
||||||
|
|
||||||
|
collectedProjectSchema.virtual('isDeleted').get(function isDeleted() {
|
||||||
|
return this.project == null;
|
||||||
|
});
|
||||||
|
|
||||||
collectedProjectSchema.set('toJSON', {
|
collectedProjectSchema.set('toJSON', {
|
||||||
virtuals: true
|
virtuals: true
|
||||||
});
|
});
|
||||||
|
|
|
@ -114,9 +114,19 @@ router.get('/about', (req, res) => {
|
||||||
res.send(renderIndex());
|
res.send(renderIndex());
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/feedback', (req, res) => {
|
if (process.env.MOBILE_ENABLED) {
|
||||||
|
router.get('/mobile', (req, res) => {
|
||||||
res.send(renderIndex());
|
res.send(renderIndex());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/mobile/preview', (req, res) => {
|
||||||
|
res.send(renderIndex());
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/mobile/preferences', (req, res) => {
|
||||||
|
res.send(renderIndex());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
router.get('/:username/collections/create', (req, res) => {
|
router.get('/:username/collections/create', (req, res) => {
|
||||||
userExists(req.params.username, (exists) => {
|
userExists(req.params.username, (exists) => {
|
||||||
|
|
|
@ -73,7 +73,7 @@ function getCategories() {
|
||||||
function getSketchesInCategories(categories) {
|
function getSketchesInCategories(categories) {
|
||||||
return Q.all(categories.map((category) => {
|
return Q.all(categories.map((category) => {
|
||||||
const options = {
|
const options = {
|
||||||
url: `${category.url.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
|
url: `${category.url.replace('?ref=main', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers,
|
headers,
|
||||||
json: true
|
json: true
|
||||||
|
@ -107,7 +107,7 @@ function getSketchesInCategories(categories) {
|
||||||
function getSketchContent(projectsInAllCategories) {
|
function getSketchContent(projectsInAllCategories) {
|
||||||
return Q.all(projectsInAllCategories.map(projectsInOneCategory => Q.all(projectsInOneCategory.map((project) => {
|
return Q.all(projectsInAllCategories.map(projectsInOneCategory => Q.all(projectsInOneCategory.map((project) => {
|
||||||
const options = {
|
const options = {
|
||||||
url: `${project.sketchUrl.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
|
url: `${project.sketchUrl.replace('?ref=main', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers
|
headers
|
||||||
};
|
};
|
||||||
|
@ -264,7 +264,7 @@ function createProjectsInP5user(projectsInAllCategories) {
|
||||||
const fileID = objectID().toHexString();
|
const fileID = objectID().toHexString();
|
||||||
newProject.files.push({
|
newProject.files.push({
|
||||||
name: assetName,
|
name: assetName,
|
||||||
url: `https://cdn.jsdelivr.net/gh/processing/p5.js-website@master/src/data/examples/assets/${assetName}`,
|
url: `https://cdn.jsdelivr.net/gh/processing/p5.js-website@main/src/data/examples/assets/${assetName}`,
|
||||||
id: fileID,
|
id: fileID,
|
||||||
_id: fileID,
|
_id: fileID,
|
||||||
children: [],
|
children: [],
|
||||||
|
|
|
@ -75,10 +75,11 @@ app.use(corsMiddleware);
|
||||||
app.options('*', corsMiddleware);
|
app.options('*', corsMiddleware);
|
||||||
|
|
||||||
// Body parser, cookie parser, sessions, serve public assets
|
// Body parser, cookie parser, sessions, serve public assets
|
||||||
|
app.use('/locales', Express.static(path.resolve(__dirname, '../dist/static/locales'), { cacheControl: false }));
|
||||||
app.use(Express.static(path.resolve(__dirname, '../dist/static'), {
|
app.use(Express.static(path.resolve(__dirname, '../dist/static'), {
|
||||||
maxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === 'production' ? '1d' : '0')
|
maxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === 'production' ? '1d' : '0')
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
|
@ -32,6 +32,9 @@ export function renderIndex() {
|
||||||
window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true};
|
window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true};
|
||||||
window.process.env.UI_COLLECTIONS_ENABLED = ${process.env.UI_COLLECTIONS_ENABLED === 'false' ? false : true};
|
window.process.env.UI_COLLECTIONS_ENABLED = ${process.env.UI_COLLECTIONS_ENABLED === 'false' ? false : true};
|
||||||
window.process.env.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined};
|
window.process.env.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined};
|
||||||
|
window.process.env.MOBILE_ENABLED = ${process.env.MOBILE_ENABLED ? `${process.env.MOBILE_ENABLED}` : undefined};
|
||||||
|
window.process.env.TRANSLATIONS_ENABLED = ${process.env.TRANSLATIONS_ENABLED === 'true' ? true :false};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
115
translations/locales/en-US/translations.json
Normal file
115
translations/locales/en-US/translations.json
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
{
|
||||||
|
"Contribute": "Contribute",
|
||||||
|
"NewP5": "New to p5.js?",
|
||||||
|
"Report": "Report a bug",
|
||||||
|
"Learn": "Learn",
|
||||||
|
"About": "About",
|
||||||
|
"Resources": "Resources",
|
||||||
|
"Libraries": "Libraries",
|
||||||
|
"Forum": "Forum",
|
||||||
|
"File": "File",
|
||||||
|
"New": "New",
|
||||||
|
"Save": "Save",
|
||||||
|
"Share": "Share",
|
||||||
|
"Duplicate": "Duplicate",
|
||||||
|
"Examples": "Examples",
|
||||||
|
"Edit": "Edit",
|
||||||
|
"TidyCode": "Tidy Code",
|
||||||
|
"Find": "Find",
|
||||||
|
"AddToCollection": "Add to Collection",
|
||||||
|
"FindNext": "Find Next",
|
||||||
|
"FindPrevious": "Find Previous",
|
||||||
|
"Sketch": "Sketch",
|
||||||
|
"AddFile": "Add File",
|
||||||
|
"AddFolder": "Add Folder",
|
||||||
|
"Run": "Run",
|
||||||
|
"Stop": "Stop",
|
||||||
|
"Help": "Help",
|
||||||
|
"KeyboardShortcuts": "Keyboard Shortcuts",
|
||||||
|
"Reference": "Reference",
|
||||||
|
"Tidy": "Tidy",
|
||||||
|
"Lang": "Language",
|
||||||
|
"FindNextMatch": "Find Next Match",
|
||||||
|
"FindPrevMatch": "Find Previous Match",
|
||||||
|
"IndentCodeLeft": "Indent Code Left",
|
||||||
|
"IndentCodeRight": "Indent Code Right",
|
||||||
|
"CommentLine": "Comment Line",
|
||||||
|
"StartSketch": "Start Sketch",
|
||||||
|
"StopSketch": "StopSketch",
|
||||||
|
"TurnOnAccessibleOutput": "Turn On Accessible Output",
|
||||||
|
"TurnOffAccessibleOutput": "Turn Off Accessible Output",
|
||||||
|
"ToogleSidebar": "Toogle Sidebar",
|
||||||
|
"ToogleConsole": "Toogle Console",
|
||||||
|
"Preview": "Preview",
|
||||||
|
"Auto-refresh": "Auto-refresh",
|
||||||
|
"Console": "Console",
|
||||||
|
"Settings": "Settings",
|
||||||
|
"GeneralSettings": "General settings",
|
||||||
|
"Theme": "Theme",
|
||||||
|
"Light": "Light",
|
||||||
|
"Dark": "Dark",
|
||||||
|
"HighContrast": "High Contrast",
|
||||||
|
"TextSize": "Text Size",
|
||||||
|
"Decrease": "Decrease",
|
||||||
|
"Increase": "Increase",
|
||||||
|
"IndentationAmount": "Indentation amount",
|
||||||
|
"Autosave": "Autosave",
|
||||||
|
"On": "On",
|
||||||
|
"Off": "Off",
|
||||||
|
"SketchSettings": "Sketch Settings",
|
||||||
|
"SecurityProtocol": "Security Protocol",
|
||||||
|
"ServeOverHTTPS": "Serve over HTTPS",
|
||||||
|
"Accessibility": "Accessibility",
|
||||||
|
"LintWarningSound": "Lint warning sound",
|
||||||
|
"PreviewSound": "Preview sound",
|
||||||
|
"AccessibleTextBasedCanvas": "Accessible text-based canvas",
|
||||||
|
"UsedScreenReader": "Used with screen reader",
|
||||||
|
"PlainText": "Plain-text",
|
||||||
|
"TableText": "Table-text",
|
||||||
|
"Sound": "Sound",
|
||||||
|
"WordWrap": "Word Wrap",
|
||||||
|
"LineNumbers": "Line numbers",
|
||||||
|
"LangChange": "Language changed",
|
||||||
|
"Welcome": "Welcome",
|
||||||
|
"Login": "Log in",
|
||||||
|
"LoginOr": "or",
|
||||||
|
"SignUp": "Sign up",
|
||||||
|
"Email": "email",
|
||||||
|
"Username": "username",
|
||||||
|
"LoginGithub": "Login with Github",
|
||||||
|
"LoginGoogle": "Login with Google",
|
||||||
|
"DontHaveAccount": "Don't have an account?",
|
||||||
|
"ForgotPassword": "Forgot your password?",
|
||||||
|
"ResetPassword": "Reset your password",
|
||||||
|
"BackEditor": "Back to Editor",
|
||||||
|
"UsernameSplit": "User Name",
|
||||||
|
"Password": "Password",
|
||||||
|
"ConfirmPassword": "Confirm Password",
|
||||||
|
"OpenedNewSketch": "Opened new sketch.",
|
||||||
|
"Hello": "Hello",
|
||||||
|
"MyAccount": "My Account",
|
||||||
|
"My":"My",
|
||||||
|
"Sketches": "My sketches",
|
||||||
|
"Collections": "My collections",
|
||||||
|
"Asset": "Asset",
|
||||||
|
"MyAssets": "My assets",
|
||||||
|
"TitleAbout": "p5.js Web Editor | About",
|
||||||
|
"CodeEditing": "Code Editing",
|
||||||
|
"Error": "Error",
|
||||||
|
"In order to save": "In order to save",
|
||||||
|
"you must be logged in": "you must be logged in",
|
||||||
|
"Please": "please",
|
||||||
|
"Find in files": "Find in files",
|
||||||
|
"Create": "Create",
|
||||||
|
"enter a name": "enter a name",
|
||||||
|
"Add": "Add",
|
||||||
|
"Folder": "Folder",
|
||||||
|
"FindText": "Find Text",
|
||||||
|
"FindNextTextMatch": "Find Next Text Match",
|
||||||
|
"FindPreviousTextMatch": "Find Previous Text Match",
|
||||||
|
"Code editing keyboard shortcuts follow": "Code editing keyboard shortcuts follow",
|
||||||
|
"Sublime Text shortcuts": "Sublime Text shortcuts",
|
||||||
|
"WarningUnsavedChanges": "Are you sure you want to leave this page? You have unsaved changes."
|
||||||
|
|
||||||
|
}
|
||||||
|
|
113
translations/locales/es-419/translations.json
Normal file
113
translations/locales/es-419/translations.json
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
{
|
||||||
|
"Contribute": "Contribuir",
|
||||||
|
"NewP5": "¿Empezando con p5.js?",
|
||||||
|
"Report": "Reporta un error",
|
||||||
|
"Learn": "Aprende",
|
||||||
|
"About": "Acerca de",
|
||||||
|
"Resources": "Recursos",
|
||||||
|
"Libraries": "Bibliotecas",
|
||||||
|
"Forum": "Foro",
|
||||||
|
"File": "Archivo",
|
||||||
|
"New": "Nuevo",
|
||||||
|
"Save": "Guardar",
|
||||||
|
"Share": "Compartir",
|
||||||
|
"Duplicate": "Duplicar",
|
||||||
|
"Examples": "Ejemplos",
|
||||||
|
"Edit": "Editar",
|
||||||
|
"TidyCode": "Ordenar código",
|
||||||
|
"Find": "Buscar",
|
||||||
|
"AddToCollection": "Agregar a colección",
|
||||||
|
"FindNext": "Buscar siguiente",
|
||||||
|
"FindPrevious": "Buscar anterior",
|
||||||
|
"Sketch": "Bosquejo",
|
||||||
|
"AddFile": "Agregar archivo",
|
||||||
|
"AddFolder": "Agregar directorio",
|
||||||
|
"Run": "Ejecutar",
|
||||||
|
"Stop": "Detener",
|
||||||
|
"Help": "Ayuda",
|
||||||
|
"KeyboardShortcuts": "Atajos",
|
||||||
|
"Reference": "Referencia",
|
||||||
|
"Tidy": "Ordenar",
|
||||||
|
"Lang": "Lenguaje",
|
||||||
|
"FindNextMatch": "Encontrar siguiente ocurrencia",
|
||||||
|
"FindPrevMatch": "Encontrar ocurrencia previa",
|
||||||
|
"IndentCodeLeft": "Indentar código a la izquierda",
|
||||||
|
"IndentCodeRight": "Indentar código a la derecha",
|
||||||
|
"CommentLine": "Comentar línea de código",
|
||||||
|
"StartSketch": "Iniciar bosquejo",
|
||||||
|
"StopSketch": "Detener bosquejo",
|
||||||
|
"TurnOnAccessibleOutput": "Activar salida accesible",
|
||||||
|
"TurnOffAccessibleOutput": "Desactivar salida accesible",
|
||||||
|
"ToogleSidebar": "Alternar barra de deslizamiento",
|
||||||
|
"ToogleConsole": "Alternar consola",
|
||||||
|
"Preview": "Vista previa",
|
||||||
|
"Auto-refresh": "Auto-refrescar",
|
||||||
|
"Console": "Consola",
|
||||||
|
"Settings": "Configuración",
|
||||||
|
"GeneralSettings": "Configuración general",
|
||||||
|
"Theme": "Modo de visualización",
|
||||||
|
"Light": "Claro",
|
||||||
|
"Dark": "Oscuro",
|
||||||
|
"HighContrast": "Alto contraste",
|
||||||
|
"TextSize": "Tamaño del texto",
|
||||||
|
"Decrease": "Disminuir",
|
||||||
|
"Increase": "Aumentar",
|
||||||
|
"IndentationAmount": "Cantidad de indentación",
|
||||||
|
"Autosave": "Grabar automáticamente",
|
||||||
|
"On": "Activar",
|
||||||
|
"Off": "Desactivar",
|
||||||
|
"SketchSettings": "Configuración del bosquejo",
|
||||||
|
"SecurityProtocol": "Protocolo de seguridad",
|
||||||
|
"ServeOverHTTPS": "Usar HTTPS",
|
||||||
|
"Accessibility": "Accesibilidad",
|
||||||
|
"LintWarningSound": "Sonido de alarma Lint",
|
||||||
|
"PreviewSound": "Probar sonido",
|
||||||
|
"AccessibleTextBasedCanvas": "Lienzo accesible por texto",
|
||||||
|
"UsedScreenReader": "Uso con screen reader",
|
||||||
|
"PlainText": "Texto sin formato",
|
||||||
|
"TableText": "Tablero de texto",
|
||||||
|
"Sound": "Sonido",
|
||||||
|
"WordWrap": "Ajuste automático de línea",
|
||||||
|
"LineNumbers": "Número de línea",
|
||||||
|
"LangChange": "Lenguaje cambiado",
|
||||||
|
"Welcome": "Bienvenida",
|
||||||
|
"Login": "Ingresa",
|
||||||
|
"LoginOr": "o",
|
||||||
|
"SignUp": "registráte",
|
||||||
|
"email": "correo electrónico",
|
||||||
|
"username": "nombre de usuario",
|
||||||
|
"LoginGithub": "Ingresa con Github",
|
||||||
|
"LoginGoogle": "Ingresa con Google",
|
||||||
|
"DontHaveAccount": "No tienes cuenta?",
|
||||||
|
"ForgotPassword": "¿Olvidaste tu contraseña?",
|
||||||
|
"ResetPassword": "Regenera tu contraseña",
|
||||||
|
"BackEditor": "Regresa al editor",
|
||||||
|
"UsernameSplit": "Nombre de usuario",
|
||||||
|
"Password": "Contraseña",
|
||||||
|
"ConfirmPassword": "Confirma la contraseña",
|
||||||
|
"OpenedNewSketch": "Creaste nuevo bosquejo.",
|
||||||
|
"Hello": "Hola",
|
||||||
|
"MyAccount": "Mi Cuenta",
|
||||||
|
"My": "Mi",
|
||||||
|
"MySketches": "Mis bosquejos",
|
||||||
|
"MyCollections":"Mis colecciones",
|
||||||
|
"Asset": "Asset",
|
||||||
|
"MyAssets": "Mis assets",
|
||||||
|
"TitleAbout": "Editor Web p5.js | Acerca de",
|
||||||
|
"CodeEditing": "Editando Código",
|
||||||
|
"Error": "Error",
|
||||||
|
"In order to save": "Para guardar",
|
||||||
|
"you must be logged in": "debes ingresar a tu cuenta",
|
||||||
|
"Please": "Por favor",
|
||||||
|
"Find in files": "Encontrar en archivos",
|
||||||
|
"Create": "Create",
|
||||||
|
"enter a name": "enter a name",
|
||||||
|
"Add": "Add",
|
||||||
|
"Folder": "Directorio",
|
||||||
|
"FindText": "Encontrar texto",
|
||||||
|
"FindNextTextMatch": "Encontrar la siguiente ocurrencia de texto",
|
||||||
|
"FindPreviousTextMatch": "Encontrar la ocurrencia previa de texto",
|
||||||
|
"Code editing keyboard shortcuts follow": "Los atajos para edición son como",
|
||||||
|
"Sublime Text shortcuts": "los atajos de Sublime Text ",
|
||||||
|
"WarningUnsavedChanges": "¿Estás seguro de que quieres salir de la página? Tienes cambios sin guardar."
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
@ -40,7 +41,13 @@ module.exports = {
|
||||||
'process.env': {
|
'process.env': {
|
||||||
NODE_ENV: JSON.stringify('development')
|
NODE_ENV: JSON.stringify('development')
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{from: path.resolve(__dirname, '../translations/locales') , to: path.resolve(__dirname, 'locales')}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|
|
@ -8,6 +8,7 @@ const cssnext = require('postcss-cssnext');
|
||||||
const postcssFocus = require('postcss-focus');
|
const postcssFocus = require('postcss-focus');
|
||||||
const postcssReporter = require('postcss-reporter');
|
const postcssReporter = require('postcss-reporter');
|
||||||
const cssnano = require('cssnano');
|
const cssnano = require('cssnano');
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
}
|
}
|
||||||
|
@ -144,7 +145,13 @@ module.exports = [{
|
||||||
}),
|
}),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'app.[hash].css',
|
filename: 'app.[hash].css',
|
||||||
})
|
}),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{from: path.resolve(__dirname, '../translations/locales') , to: path.resolve(__dirname, '../dist/static/locales')}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue