Merge branch 'develop' into console-scroll-to-bottom

This commit is contained in:
ghsuvr123 2020-08-17 18:51:19 +03:00 committed by GitHub
commit f1b2cb5ee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 114 additions and 34 deletions

View File

@ -9,7 +9,7 @@ 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, setLanguage } from '../modules/IDE/actions/preferences';
import { logoutUser } from '../modules/User/actions'; import { logoutUser } from '../modules/User/actions';
import getConfig from '../utils/getConfig'; import getConfig from '../utils/getConfig';
@ -72,7 +72,6 @@ class Nav extends React.PureComponent {
document.removeEventListener('mousedown', this.handleClick, false); document.removeEventListener('mousedown', this.handleClick, false);
document.removeEventListener('keydown', this.closeDropDown, false); document.removeEventListener('keydown', this.closeDropDown, false);
} }
setDropdown(dropdown) { setDropdown(dropdown) {
this.setState({ this.setState({
dropdownOpen: dropdown dropdownOpen: dropdown
@ -170,7 +169,7 @@ class Nav extends React.PureComponent {
} }
handleLangSelection(event) { handleLangSelection(event) {
i18next.changeLanguage(event.target.value); this.props.setLanguage(event.target.value);
this.props.showToast(1500); this.props.showToast(1500);
this.props.setToastText('Toast.LangChange'); this.props.setToastText('Toast.LangChange');
this.setDropdown('none'); this.setDropdown('none');
@ -808,8 +807,8 @@ Nav.propTypes = {
params: PropTypes.shape({ params: PropTypes.shape({
username: PropTypes.string username: PropTypes.string
}), }),
t: PropTypes.func.isRequired t: PropTypes.func.isRequired,
setLanguage: PropTypes.func.isRequired,
}; };
Nav.defaultProps = { Nav.defaultProps = {
@ -839,7 +838,8 @@ const mapDispatchToProps = {
...projectActions, ...projectActions,
...toastActions, ...toastActions,
logoutUser, logoutUser,
setAllAccessibleOutput setAllAccessibleOutput,
setLanguage
}; };
export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav))); export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)));

View File

@ -45,7 +45,8 @@ describe('Nav', () => {
rootFile: { rootFile: {
id: 'root-file' id: 'root-file'
}, },
t: jest.fn() t: jest.fn(),
setLanguage: jest.fn()
}; };
it('renders correctly', () => { it('renders correctly', () => {

View File

@ -93,6 +93,7 @@ export const SHOW_TOAST = 'SHOW_TOAST';
export const HIDE_TOAST = 'HIDE_TOAST'; export const HIDE_TOAST = 'HIDE_TOAST';
export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; export const SET_TOAST_TEXT = 'SET_TOAST_TEXT';
export const SET_THEME = 'SET_THEME'; export const SET_THEME = 'SET_THEME';
export const SET_LANGUAGE = 'SET_LANGUAGE';
export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES'; export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES';
export const SET_AUTOREFRESH = 'SET_AUTOREFRESH'; export const SET_AUTOREFRESH = 'SET_AUTOREFRESH';

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import getConfig from '../../utils/getConfig'; 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';
import { setLanguage } from '../IDE/actions/preferences';
class App extends React.Component { class App extends React.Component {
constructor(props, context) { constructor(props, context) {
@ -23,6 +24,10 @@ class App extends React.Component {
if (locationWillChange && !shouldSkipRemembering) { if (locationWillChange && !shouldSkipRemembering) {
this.props.setPreviousPath(this.props.location.pathname); this.props.setPreviousPath(this.props.location.pathname);
} }
if (this.props.language !== nextProps.language) {
this.props.setLanguage(nextProps.language, { persistPreference: false });
}
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -50,18 +55,22 @@ App.propTypes = {
}), }),
}).isRequired, }).isRequired,
setPreviousPath: PropTypes.func.isRequired, setPreviousPath: PropTypes.func.isRequired,
setLanguage: PropTypes.func.isRequired,
language: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
}; };
App.defaultProps = { App.defaultProps = {
children: null, children: null,
language: null,
theme: 'light' theme: 'light'
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
theme: state.preferences.theme, theme: state.preferences.theme,
language: state.preferences.language,
}); });
const mapDispatchToProps = { setPreviousPath }; const mapDispatchToProps = { setPreviousPath, setLanguage };
export default connect(mapStateToProps, mapDispatchToProps)(App); export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@ -1,3 +1,4 @@
import i18next from 'i18next';
import apiClient from '../../../utils/apiClient'; import apiClient from '../../../utils/apiClient';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
@ -210,3 +211,22 @@ export function setAllAccessibleOutput(value) {
}; };
} }
export function setLanguage(value, { persistPreference = true } = {}) {
return (dispatch, getState) => {
i18next.changeLanguage(value);
dispatch({
type: ActionTypes.SET_LANGUAGE,
language: value
});
const state = getState();
if (persistPreference && state.user.authenticated) {
const formParams = {
preferences: {
language: value
}
};
updatePreferences(formParams, dispatch);
}
};
}

View File

@ -35,6 +35,7 @@ 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) { function getTitle(props) {
const { id } = props.project; const { id } = props.project;
return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor'; return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor';
@ -144,13 +145,11 @@ class IDEView extends React.Component {
this.props.router.setRouteLeaveHook(this.props.route, () => warnIfUnsavedChanges(this.props)); this.props.router.setRouteLeaveHook(this.props.route, () => warnIfUnsavedChanges(this.props));
} }
} }
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener('keydown', this.handleGlobalKeydown, false); document.removeEventListener('keydown', this.handleGlobalKeydown, false);
clearTimeout(this.autosaveInterval); clearTimeout(this.autosaveInterval);
this.autosaveInterval = null; this.autosaveInterval = null;
} }
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))) {
@ -367,6 +366,7 @@ class IDEView extends React.Component {
expandConsole={this.props.expandConsole} expandConsole={this.props.expandConsole}
clearConsole={this.props.clearConsole} clearConsole={this.props.clearConsole}
cmController={this.cmController} cmController={this.cmController}
language={this.props.preferences.language}
/> />
</div> </div>
</section> </section>
@ -527,7 +527,8 @@ IDEView.propTypes = {
gridOutput: PropTypes.bool.isRequired, gridOutput: PropTypes.bool.isRequired,
soundOutput: PropTypes.bool.isRequired, soundOutput: PropTypes.bool.isRequired,
theme: PropTypes.string.isRequired, theme: PropTypes.string.isRequired,
autorefresh: PropTypes.bool.isRequired autorefresh: PropTypes.bool.isRequired,
language: PropTypes.string.isRequired
}).isRequired, }).isRequired,
closePreferences: PropTypes.func.isRequired, closePreferences: PropTypes.func.isRequired,
setFontSize: PropTypes.func.isRequired, setFontSize: PropTypes.func.isRequired,

View File

@ -1,5 +1,7 @@
import i18next from 'i18next';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
const initialState = { const initialState = {
fontSize: 18, fontSize: 18,
autosave: true, autosave: true,
@ -10,7 +12,8 @@ const initialState = {
gridOutput: false, gridOutput: false,
soundOutput: false, soundOutput: false,
theme: 'light', theme: 'light',
autorefresh: false autorefresh: false,
language: 'en-US'
}; };
const preferences = (state = initialState, action) => { const preferences = (state = initialState, action) => {
@ -37,6 +40,8 @@ const preferences = (state = initialState, action) => {
return Object.assign({}, state, { autorefresh: action.value }); return Object.assign({}, state, { autorefresh: action.value });
case ActionTypes.SET_LINE_NUMBERS: case ActionTypes.SET_LINE_NUMBERS:
return Object.assign({}, state, { lineNumbers: action.value }); return Object.assign({}, state, { lineNumbers: action.value });
case ActionTypes.SET_LANGUAGE:
return Object.assign({}, state, { language: action.language });
default: default:
return state; return state;
} }

View File

@ -2,6 +2,7 @@ import { browserHistory } from 'react-router';
import * as ActionTypes from '../../constants'; import * as ActionTypes from '../../constants';
import apiClient from '../../utils/apiClient'; import apiClient from '../../utils/apiClient';
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
import { setLanguage } from '../IDE/actions/preferences';
import { showToast, setToastText } from '../IDE/actions/toast'; import { showToast, setToastText } from '../IDE/actions/toast';
export function authError(error) { export function authError(error) {
@ -59,6 +60,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
type: ActionTypes.SET_PREFERENCES, type: ActionTypes.SET_PREFERENCES,
preferences: response.data.preferences preferences: response.data.preferences
}); });
setLanguage(response.data.preferences.language, { persistPreference: false });
dispatch(justOpenedProject()); dispatch(justOpenedProject());
browserHistory.push(previousPath); browserHistory.push(previousPath);
resolve(); resolve();
@ -80,8 +82,8 @@ export function getUser() {
type: ActionTypes.SET_PREFERENCES, type: ActionTypes.SET_PREFERENCES,
preferences: response.data.preferences preferences: response.data.preferences
}); });
}) setLanguage(response.data.preferences.language, { persistPreference: false });
.catch((error) => { }).catch((error) => {
const { response } = error; const { response } = error;
const message = response.message || response.data.error; const message = response.message || response.data.error;
dispatch(authError(message)); dispatch(authError(message));

View File

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { withTranslation } from 'react-i18next';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button'; import Button from '../../../common/Button';
@ -13,10 +14,10 @@ function SignupForm(props) {
return ( return (
<form className="form" onSubmit={handleSubmit(props.signUpUser.bind(this, props.previousPath))}> <form className="form" onSubmit={handleSubmit(props.signUpUser.bind(this, props.previousPath))}>
<p className="form__field"> <p className="form__field">
<label htmlFor="username" className="form__label">User Name</label> <label htmlFor="username" className="form__label">{props.t('SignupForm.Title')}</label>
<input <input
className="form__input" className="form__input"
aria-label="username" aria-label={props.t('SignupForm.TitleARIA')}
type="text" type="text"
id="username" id="username"
{...domOnlyProps(username)} {...domOnlyProps(username)}
@ -24,10 +25,10 @@ function SignupForm(props) {
{username.touched && username.error && <span className="form-error">{username.error}</span>} {username.touched && username.error && <span className="form-error">{username.error}</span>}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="email" className="form__label">Email</label> <label htmlFor="email" className="form__label">{props.t('SignupForm.Email')}</label>
<input <input
className="form__input" className="form__input"
aria-label="email" aria-label={props.t('SignupForm.EmailARIA')}
type="text" type="text"
id="email" id="email"
{...domOnlyProps(email)} {...domOnlyProps(email)}
@ -35,10 +36,10 @@ function SignupForm(props) {
{email.touched && email.error && <span className="form-error">{email.error}</span>} {email.touched && email.error && <span className="form-error">{email.error}</span>}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="password" className="form__label">Password</label> <label htmlFor="password" className="form__label">{props.t('SignupForm.Password')}</label>
<input <input
className="form__input" className="form__input"
aria-label="password" aria-label={props.t('SignupForm.PasswordARIA')}
type="password" type="password"
id="password" id="password"
{...domOnlyProps(password)} {...domOnlyProps(password)}
@ -46,11 +47,11 @@ function SignupForm(props) {
{password.touched && password.error && <span className="form-error">{password.error}</span>} {password.touched && password.error && <span className="form-error">{password.error}</span>}
</p> </p>
<p className="form__field"> <p className="form__field">
<label htmlFor="confirm password" className="form__label">Confirm Password</label> <label htmlFor="confirm password" className="form__label">{props.t('SignupForm.ConfirmPassword')}</label>
<input <input
className="form__input" className="form__input"
type="password" type="password"
aria-label="confirm password" aria-label={props.t('SignupForm.ConfirmPasswordARIA')}
id="confirm password" id="confirm password"
{...domOnlyProps(confirmPassword)} {...domOnlyProps(confirmPassword)}
/> />
@ -63,7 +64,7 @@ function SignupForm(props) {
<Button <Button
type="submit" type="submit"
disabled={submitting || invalid || pristine} disabled={submitting || invalid || pristine}
>Sign Up >{props.t('SignupForm.SubmitSignup')}
</Button> </Button>
</form> </form>
); );
@ -81,7 +82,8 @@ SignupForm.propTypes = {
submitting: PropTypes.bool, submitting: PropTypes.bool,
invalid: PropTypes.bool, invalid: PropTypes.bool,
pristine: PropTypes.bool, pristine: PropTypes.bool,
previousPath: PropTypes.string.isRequired previousPath: PropTypes.string.isRequired,
t: PropTypes.func.isRequired
}; };
SignupForm.defaultProps = { SignupForm.defaultProps = {
@ -90,4 +92,4 @@ SignupForm.defaultProps = {
invalid: false invalid: false
}; };
export default SignupForm; export default withTranslation()(SignupForm);

View File

@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux';
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 { withTranslation } from 'react-i18next';
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 apiClient from '../../../utils/apiClient';
@ -26,19 +27,19 @@ class SignupView extends React.Component {
<Nav layout="dashboard" /> <Nav layout="dashboard" />
<main className="form-container"> <main className="form-container">
<Helmet> <Helmet>
<title>p5.js Web Editor | Signup</title> <title>{this.props.t('SignupView.Title')}</title>
</Helmet> </Helmet>
<div className="form-container__content"> <div className="form-container__content">
<h2 className="form-container__title">Sign Up</h2> <h2 className="form-container__title">{this.props.t('SignupView.Description')}</h2>
<SignupForm {...this.props} /> <SignupForm {...this.props} />
<h2 className="form-container__divider">Or</h2> <h2 className="form-container__divider">{this.props.t('SignupView.Or')}</h2>
<div className="form-container__stack"> <div className="form-container__stack">
<SocialAuthButton service={SocialAuthButton.services.github} /> <SocialAuthButton service={SocialAuthButton.services.github} />
<SocialAuthButton service={SocialAuthButton.services.google} /> <SocialAuthButton service={SocialAuthButton.services.google} />
</div> </div>
<p className="form__navigation-options"> <p className="form__navigation-options">
Already have an account?&nbsp; {this.props.t('SignupView.AlreadyHave')}
<Link className="form__login-button" to="/login">Log In</Link> <Link className="form__login-button" to="/login">{this.props.t('SignupView.Login')}</Link>
</p> </p>
</div> </div>
</main> </main>
@ -108,7 +109,8 @@ SignupView.propTypes = {
previousPath: PropTypes.string.isRequired, previousPath: PropTypes.string.isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
authenticated: PropTypes.bool authenticated: PropTypes.bool
}) }),
t: PropTypes.func.isRequired
}; };
SignupView.defaultProps = { SignupView.defaultProps = {
@ -117,11 +119,11 @@ SignupView.defaultProps = {
} }
}; };
export default reduxForm({ export default withTranslation()(reduxForm({
form: 'signup', form: 'signup',
fields: ['username', 'email', 'password', 'confirmPassword'], fields: ['username', 'email', 'password', 'confirmPassword'],
onSubmitFail, onSubmitFail,
validate: validateSignup, validate: validateSignup,
asyncValidate, asyncValidate,
asyncBlurFields: ['username', 'email'] asyncBlurFields: ['username', 'email']
}, mapStateToProps, mapDispatchToProps)(SignupView); }, mapStateToProps, mapDispatchToProps)(SignupView));

View File

@ -65,7 +65,8 @@ const userSchema = new Schema({
gridOutput: { type: Boolean, default: false }, gridOutput: { type: Boolean, default: false },
soundOutput: { type: Boolean, default: false }, soundOutput: { type: Boolean, default: false },
theme: { type: String, default: 'light' }, theme: { type: String, default: 'light' },
autorefresh: { type: Boolean, default: false } autorefresh: { type: Boolean, default: false },
language: { type: String, default: 'en-US' }
}, },
totalSize: { type: Number, default: 0 } totalSize: { type: Number, default: 0 }
}, { timestamps: true, usePushEach: true }); }, { timestamps: true, usePushEach: true });

View File

@ -289,5 +289,23 @@
"ConfirmPassword": "Confirm Password", "ConfirmPassword": "Confirm Password",
"ConfirmPasswordARIA": "Confirm Password", "ConfirmPasswordARIA": "Confirm Password",
"SubmitSetNewPassword": "Set New Password" "SubmitSetNewPassword": "Set New Password"
},
"SignupForm": {
"Title": "User Name",
"TitleARIA": "username",
"Email": "Email",
"EmailARIA": "email",
"Password": "Password",
"PasswordARIA": "password",
"ConfirmPassword": "Confirm Password",
"ConfirmPasswordARIA": "Confirm password",
"SubmitSignup": "Sign Up"
},
"SignupView": {
"Title": "p5.js Web Editor | Signup",
"Description": "Sign Up",
"Or": "Or",
"AlreadyHave": "Already have an account?",
"Login": "Log In"
} }
} }

View File

@ -289,5 +289,23 @@
"ConfirmPassword": "Confirmar Contraseña", "ConfirmPassword": "Confirmar Contraseña",
"ConfirmPasswordARIA": "Confirmar contraseña", "ConfirmPasswordARIA": "Confirmar contraseña",
"SubmitSetNewPassword": "Crear Nueva Contraseña" "SubmitSetNewPassword": "Crear Nueva Contraseña"
},
"SignupForm": {
"Title": "Identificación",
"TitleARIA": "Identificación",
"Email": "Correo electrónico",
"EmailARIA": "correo electrónico",
"Password": "Contraseña",
"PasswordARIA": "contraseña",
"ConfirmPassword": "Confirma tu contraseña",
"ConfirmPasswordARIA": "Confirma tu contraseña",
"SubmitSignup": "Registráte"
},
"SignupView": {
"Title": " Editor Web p5.js | Registráte",
"Description": "Registráte",
"Or": "o",
"AlreadyHave": "¿Ya tienes cuenta? ",
"Login": "Ingresa"
} }
} }