move login/registration functions and view state to session reducer, pass to gui (#2078)

* move login/registration functions and view state to session reducer, pass to gui

* navigation reducer handles login; gui passed renderLogin function

* put back in join class to make smoke tests keep working
This commit is contained in:
Benjamin Wheeler 2018-09-24 11:04:30 -04:00 committed by GitHub
parent c047612d68
commit 935eb0b15f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 549 additions and 349 deletions

View file

@ -33,8 +33,8 @@
}
input {
// 100% minus border and padding
margin-bottom: 12px;
// 100% minus border and padding
width: calc(100% - 30px);
}

View file

@ -3,7 +3,7 @@ const connect = require('react-redux').connect;
const PropTypes = require('prop-types');
const React = require('react');
const sessionActions = require('../../redux/session.js');
const navigationActions = require('../../redux/navigation.js');
const IframeModal = require('../modal/iframe/modal.jsx');
const Registration = require('../registration/registration.jsx');
@ -15,10 +15,7 @@ class Intro extends React.Component {
super(props);
bindAll(this, [
'handleShowVideo',
'handleCloseVideo',
'handleJoinClick',
'handleCloseRegistration',
'handleCompleteRegistration'
'handleCloseVideo'
]);
this.state = {
videoOpen: false
@ -30,17 +27,6 @@ class Intro extends React.Component {
handleCloseVideo () {
this.setState({videoOpen: false});
}
handleJoinClick (e) {
e.preventDefault();
this.setState({registrationOpen: true});
}
handleCloseRegistration () {
this.setState({registrationOpen: false});
}
handleCompleteRegistration () {
this.props.dispatch(sessionActions.refreshSession());
this.closeRegistration();
}
render () {
return (
<div className="intro">
@ -92,7 +78,7 @@ class Intro extends React.Component {
<a
className="sprite sprite-3"
href="#"
onClick={this.handleJoinClick}
onClick={this.props.handleOpenRegistration}
>
<img
alt="Gobo"
@ -111,10 +97,7 @@ class Intro extends React.Component {
<div className="text subtext">{this.props.messages['intro.itsFree']}</div>
</a>
<Registration
isOpen={this.state.registrationOpen}
key="registration"
onRegistrationDone={this.handleCompleteRegistration}
onRequestClose={this.handleCloseRegistration}
/>
</div>
<div
@ -160,7 +143,7 @@ class Intro extends React.Component {
}
Intro.propTypes = {
dispatch: PropTypes.func.isRequired,
handleOpenRegistration: PropTypes.func,
messages: PropTypes.shape({
'intro.aboutScratch': PropTypes.string,
'intro.forEducators': PropTypes.string,
@ -194,6 +177,17 @@ const mapStateToProps = state => ({
session: state.session
});
const ConnectedIntro = connect(mapStateToProps)(Intro);
const mapDispatchToProps = dispatch => ({
handleOpenRegistration: event => {
event.preventDefault();
dispatch(navigationActions.handleOpenRegistration());
}
});
const ConnectedIntro = connect(
mapStateToProps,
mapDispatchToProps
)(Intro);
module.exports = ConnectedIntro;

View file

@ -0,0 +1,60 @@
const React = require('react');
const connect = require('react-redux').connect;
const FormattedMessage = require('react-intl').FormattedMessage;
const PropTypes = require('prop-types');
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const navigationActions = require('../../redux/navigation.js');
const Modal = require('../modal/base/modal.jsx');
const CanceledDeletionModal = ({
canceledDeletionOpen,
handleCloseCanceledDeletion,
intl
}) => (
<Modal
isOpen={canceledDeletionOpen}
style={{
content: {
padding: 15
}
}}
onRequestClose={handleCloseCanceledDeletion}
>
<h4><FormattedMessage id="general.noDeletionTitle" /></h4>
<p>
<FormattedMessage
id="general.noDeletionDescription"
values={{
resetLink: <a href="/accounts/password_reset/">
{intl.formatMessage({id: 'general.noDeletionLink'})}
</a>
}}
/>
</p>
</Modal>
);
CanceledDeletionModal.propTypes = {
canceledDeletionOpen: PropTypes.bool,
handleCloseCanceledDeletion: PropTypes.func,
intl: intlShape
};
const mapStateToProps = state => ({
canceledDeletionOpen: state.navigation && state.navigation.canceledDeletionOpen
});
const mapDispatchToProps = dispatch => ({
handleCloseCanceledDeletion: () => {
dispatch(navigationActions.setCanceledDeletionOpen(false));
}
});
const ConnectedCanceledDeletionModal = connect(
mapStateToProps,
mapDispatchToProps
)(CanceledDeletionModal);
module.exports = injectIntl(ConnectedCanceledDeletionModal);

View file

@ -0,0 +1,34 @@
const PropTypes = require('prop-types');
const React = require('react');
const connect = require('react-redux').connect;
const Login = require('./login.jsx');
require('./login-dropdown.scss');
const ConnectedLogin = ({
error,
onLogIn
}) => (
<Login
error={error}
key="login-dropdown-presentation"
onLogIn={onLogIn}
/>
);
ConnectedLogin.propTypes = {
error: PropTypes.string,
onLogIn: PropTypes.func
};
const mapStateToProps = state => ({
error: state.navigation && state.navigation.loginError
});
const mapDispatchToProps = () => ({});
module.exports = connect(
mapStateToProps,
mapDispatchToProps
)(ConnectedLogin);

View file

@ -0,0 +1,50 @@
const PropTypes = require('prop-types');
const React = require('react');
const connect = require('react-redux').connect;
const navigationActions = require('../../redux/navigation.js');
const Dropdown = require('../dropdown/dropdown.jsx');
const ConnectedLogin = require('./connected-login.jsx');
require('./login-dropdown.scss');
const LoginDropdown = ({
isOpen,
onClose,
onLogIn
}) => (
<Dropdown
className={'with-arrow'}
isOpen={isOpen}
key="login-dropdown"
onRequestClose={onClose}
>
<ConnectedLogin
onLogIn={onLogIn}
/>
</Dropdown>
);
LoginDropdown.propTypes = {
isOpen: PropTypes.bool,
onClose: PropTypes.func,
onLogIn: PropTypes.func
};
const mapStateToProps = state => ({
isOpen: state.navigation && state.navigation.loginOpen
});
const mapDispatchToProps = dispatch => ({
onClose: () => {
dispatch(navigationActions.setLoginOpen(false));
},
onLogIn: (formData, callback) => {
dispatch(navigationActions.handleLogIn(formData, callback));
}
});
module.exports = connect(
mapStateToProps,
mapDispatchToProps
)(LoginDropdown);

View file

View file

@ -3,8 +3,6 @@ const FormattedMessage = require('react-intl').FormattedMessage;
const PropTypes = require('prop-types');
const React = require('react');
const log = require('../../lib/log.js');
const Form = require('../forms/form.jsx');
const Input = require('../forms/input.jsx');
const Button = require('../forms/button.jsx');
@ -24,8 +22,7 @@ class Login extends React.Component {
}
handleSubmit (formData) {
this.setState({waiting: true});
this.props.onLogIn(formData, err => {
if (err) log.error(err);
this.props.onLogIn(formData, () => {
this.setState({waiting: false});
});
}
@ -48,9 +45,6 @@ class Login extends React.Component {
key="usernameInput"
maxLength="30"
name="username"
ref={input => {
this.username = input;
}}
type="text"
/>
<label
@ -63,9 +57,6 @@ class Login extends React.Component {
required
key="passwordInput"
name="password"
ref={input => {
this.password = input;
}}
type="password"
/>
{this.state.waiting ? [

View file

@ -2,6 +2,26 @@
.login {
padding: 10px;
width: 200px;
line-height: 1.5rem;
white-space: normal; // override any parent, such as in gui, who sets nowrap
color: $type-white;
font-size: .8125rem;
.button {
padding: .75em;
}
.row {
margin-bottom: 1.25rem;
}
.input {
margin-bottom: 12px;
// 100% minus border and padding
width: calc(100% - 30px);
height: 2.25rem;
}
label {
padding-top: 5px;
@ -24,13 +44,19 @@
a {
margin-top: 15px;
color: $ui-white;
&:link,
&:visited,
&:active {
color: $ui-white;
}
&:hover {
background-color: transparent;
}
}
.error {
border: 1px solid $active-dark-gray;
border-radius: 5px;

View file

@ -7,7 +7,7 @@ const ReactModal = require('react-modal');
require('./modal.scss');
ReactModal.setAppElement(document.getElementById('view'));
ReactModal.setAppElement(document.getElementById('app'));
/**
* Container for pop up windows (See: registration window)
@ -25,7 +25,7 @@ class Modal extends React.Component {
render () {
return (
<ReactModal
appElement={document.getElementById('view')}
appElement={document.getElementById('app')}
className={{
base: classNames('modal-content', this.props.className),
afterOpen: classNames('modal-content', this.props.className),

View file

@ -23,10 +23,11 @@ const AccountNav = ({
}) => (
<div className="account-nav">
<a
className={classNames({
'user-info': true,
'open': isOpen
})}
className={classNames([
'ignore-react-onclickoutside',
'user-info',
{open: isOpen}
])}
href="#"
onClick={onClick}
>

View file

@ -8,16 +8,14 @@ const PropTypes = require('prop-types');
const React = require('react');
const messageCountActions = require('../../../redux/message-count.js');
const navigationActions = require('../../../redux/navigation.js');
const sessionActions = require('../../../redux/session.js');
const api = require('../../../lib/api');
const Button = require('../../forms/button.jsx');
const Dropdown = require('../../dropdown/dropdown.jsx');
const Form = require('../../forms/form.jsx');
const Input = require('../../forms/input.jsx');
const log = require('../../../lib/log.js');
const Login = require('../../login/login.jsx');
const Modal = require('../../modal/base/modal.jsx');
const LoginDropdown = require('../../login/login-dropdown.jsx');
const CanceledDeletionModal = require('../../login/canceled-deletion-modal.jsx');
const NavigationBox = require('../base/navigation.jsx');
const Registration = require('../../registration/registration.jsx');
const AccountNav = require('./accountnav.jsx');
@ -29,32 +27,16 @@ class Navigation extends React.Component {
super(props);
bindAll(this, [
'getProfileUrl',
'handleJoinClick',
'handleLoginClick',
'handleCloseLogin',
'handleLogIn',
'handleLogOut',
'handleAccountNavClick',
'handleCloseAccountNav',
'showCanceledDeletion',
'handleCloseCanceledDeletion',
'handleCloseRegistration',
'handleCompleteRegistration',
'handleSearchSubmit'
]);
this.state = {
accountNavOpen: false,
canceledDeletionOpen: false,
loginOpen: false,
loginError: null,
registrationOpen: false,
messageCountIntervalId: -1 // javascript method interval id for getting messsage count.
};
}
componentDidMount () {
if (this.props.session.session.user) {
if (this.props.user) {
const intervalId = setInterval(() => {
this.props.getMessageCount(this.props.session.session.user.username);
this.props.getMessageCount(this.props.user.username);
}, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-mount-set-state
messageCountIntervalId: intervalId
@ -62,14 +44,11 @@ class Navigation extends React.Component {
}
}
componentDidUpdate (prevProps) {
if (prevProps.session.session.user !== this.props.session.session.user) {
this.setState({ // eslint-disable-line react/no-did-update-set-state
loginOpen: false,
accountNavOpen: false
});
if (this.props.session.session.user) {
if (prevProps.user !== this.props.user) {
this.props.closeAccountMenus();
if (this.props.user) {
const intervalId = setInterval(() => {
this.props.getMessageCount(this.props.session.session.user.username);
this.props.getMessageCount(this.props.user.username);
}, 120000); // check for new messages every 2 mins.
this.setState({ // eslint-disable-line react/no-did-update-set-state
messageCountIntervalId: intervalId
@ -88,104 +67,25 @@ class Navigation extends React.Component {
// clear message interval if it exists
if (this.state.messageCountIntervalId !== -1) {
clearInterval(this.state.messageCountIntervalId);
this.props.dispatch(messageCountActions.setCount(0));
this.props.setMessageCount(0);
this.setState({
messageCountIntervalId: -1
});
}
}
getProfileUrl () {
if (!this.props.session.session.user) return;
return `/users/${this.props.session.session.user.username}/`;
}
handleJoinClick (e) {
e.preventDefault();
this.setState({registrationOpen: true});
}
handleLoginClick (e) {
e.preventDefault();
this.setState({loginOpen: !this.state.loginOpen});
}
handleCloseLogin () {
this.setState({loginOpen: false});
}
// NOTE: TODO: continue here. Should move these two functions up to a redux level,
// maybe into session...
handleLogIn (formData, callback) {
this.setState({loginError: null});
formData.useMessages = true;
api({
method: 'post',
host: '',
uri: '/accounts/login/',
json: formData,
useCsrf: true
}, (err, body) => {
if (err) this.setState({loginError: err.message});
if (body) {
body = body[0];
if (body.success) {
this.handleCloseLogin();
body.messages.map(message => { // eslint-disable-line array-callback-return
if (message.message === 'canceled-deletion') {
this.showCanceledDeletion();
}
});
this.props.refreshSession();
} else {
if (body.redirect) {
window.location = body.redirect;
}
// Update login error message to a friendlier one if it exists
this.setState({loginError: body.msg});
}
}
// JS error already logged by api mixin
callback();
});
}
handleLogOut (e) {
e.preventDefault();
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) log.error(err);
this.handleCloseLogin();
window.location = '/';
});
}
handleAccountNavClick (e) {
e.preventDefault();
this.setState({accountNavOpen: true});
}
handleCloseAccountNav () {
this.setState({accountNavOpen: false});
}
showCanceledDeletion () {
this.setState({canceledDeletionOpen: true});
}
handleCloseCanceledDeletion () {
this.setState({canceledDeletionOpen: false});
}
handleCloseRegistration () {
this.setState({registrationOpen: false});
}
handleCompleteRegistration () {
this.props.dispatch(sessionActions.refreshSession());
this.handleCloseRegistration();
if (!this.props.user) return;
return `/users/${this.props.user.username}/`;
}
handleSearchSubmit (formData) {
window.location.href = `/search/projects?q=${encodeURIComponent(formData.q)}`;
}
render () {
const createLink = this.props.session.session.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
const createLink = this.props.user ? '/projects/editor/' : '/projects/editor/?tip_bar=home';
return (
<NavigationBox
className={classNames({
'logged-in': this.props.session.session.user
'logged-in': this.props.user
})}
>
<ul>
@ -233,7 +133,7 @@ class Navigation extends React.Component {
</Form>
</li>
{this.props.session.status === sessionActions.Status.FETCHED ? (
this.props.session.session.user ? [
this.props.user ? [
<li
className="link right messages"
key="messages"
@ -267,16 +167,16 @@ class Navigation extends React.Component {
key="account-nav"
>
<AccountNav
classroomId={this.props.session.session.user.classroomId}
classroomId={this.props.user.classroomId}
isEducator={this.props.permissions.educator}
isOpen={this.state.accountNavOpen}
isOpen={this.props.accountNavOpen}
isStudent={this.props.permissions.student}
profileUrl={this.getProfileUrl()}
thumbnailUrl={this.props.session.session.user.thumbnailUrl}
username={this.props.session.session.user.username}
onClick={this.handleAccountNavClick}
onClickLogout={this.handleLogOut}
onClose={this.handleCloseAccountNav}
thumbnailUrl={this.props.user.thumbnailUrl}
username={this.props.user.username}
onClick={this.props.handleToggleAccountNav}
onClickLogout={this.props.handleLogOut}
onClose={this.props.handleCloseAccountNav}
/>
</li>
] : [
@ -286,16 +186,13 @@ class Navigation extends React.Component {
>
<a
href="#"
onClick={this.handleJoinClick}
onClick={this.props.handleOpenRegistration}
>
<FormattedMessage id="general.joinScratch" />
</a>
</li>,
<Registration
isOpen={this.state.registrationOpen}
key="registration"
onRegistrationDone={this.handleCompleteRegistration}
onRequestClose={this.handleCloseRegistration}
/>,
<li
className="link right login-item"
@ -305,54 +202,31 @@ class Navigation extends React.Component {
className="ignore-react-onclickoutside"
href="#"
key="login-link"
onClick={this.handleLoginClick}
onClick={this.props.handleToggleLoginOpen}
>
<FormattedMessage id="general.signIn" />
</a>
<Dropdown
className="login-dropdown with-arrow"
isOpen={this.state.loginOpen}
<LoginDropdown
key="login-dropdown"
onRequestClose={this.handleCloseLogin}
>
<Login
error={this.state.loginError}
onLogIn={this.handleLogIn}
/>
</Dropdown>
/>
</li>
]) : []}
</ul>
<Modal
isOpen={this.state.canceledDeletionOpen}
style={{
content: {
padding: 15
}
}}
onRequestClose={this.handleCloseCanceledDeletion}
>
<h4>Your Account Will Not Be Deleted</h4>
<h4><FormattedMessage id="general.noDeletionTitle" /></h4>
<p>
<FormattedMessage
id="general.noDeletionDescription"
values={{
resetLink: <a href="/accounts/password_reset/">
{this.props.intl.formatMessage({id: 'general.noDeletionLink'})}
</a>
}}
/>
</p>
</Modal>
<CanceledDeletionModal />
</NavigationBox>
);
}
}
Navigation.propTypes = {
dispatch: PropTypes.func,
accountNavOpen: PropTypes.bool,
closeAccountMenus: PropTypes.func,
getMessageCount: PropTypes.func,
handleCloseAccountNav: PropTypes.func,
handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func,
handleToggleAccountNav: PropTypes.func,
handleToggleLoginOpen: PropTypes.func,
intl: intlShape,
permissions: PropTypes.shape({
admin: PropTypes.bool,
@ -361,20 +235,17 @@ Navigation.propTypes = {
educator_invitee: PropTypes.bool,
student: PropTypes.bool
}),
refreshSession: PropTypes.func,
searchTerm: PropTypes.string,
session: PropTypes.shape({
session: PropTypes.shape({
user: PropTypes.shape({
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
thumbnailUrl: PropTypes.string,
username: PropTypes.string
})
}),
status: PropTypes.string
}),
setMessageCount: PropTypes.func,
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
unreadMessageCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
user: PropTypes.shape({
classroomId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
thumbnailUrl: PropTypes.string,
username: PropTypes.string
})
};
Navigation.defaultProps = {
@ -384,18 +255,39 @@ Navigation.defaultProps = {
};
const mapStateToProps = state => ({
accountNavOpen: state.navigation && state.navigation.accountNavOpen,
session: state.session,
permissions: state.permissions,
searchTerm: state.navigation.searchTerm,
unreadMessageCount: state.messageCount.messageCount,
searchTerm: state.navigation
user: state.session && state.session.session && state.session.session.user
});
const mapDispatchToProps = dispatch => ({
closeAccountMenus: () => {
dispatch(navigationActions.closeAccountMenus());
},
getMessageCount: username => {
dispatch(messageCountActions.getCount(username));
},
refreshSession: () => {
dispatch(sessionActions.refreshSession());
handleToggleAccountNav: event => {
event.preventDefault();
dispatch(navigationActions.handleToggleAccountNav());
},
handleCloseAccountNav: () => {
dispatch(navigationActions.setAccountNavOpen(false));
},
handleOpenRegistration: event => {
event.preventDefault();
dispatch(navigationActions.setRegistrationOpen(true));
},
handleLogOut: event => {
event.preventDefault();
dispatch(navigationActions.handleLogOut());
},
handleToggleLoginOpen: event => {
event.preventDefault();
dispatch(navigationActions.toggleLoginOpen());
},
setMessageCount: newCount => {
dispatch(messageCountActions.setCount(newCount));

View file

@ -163,25 +163,6 @@
background-image: url("/images/mystuff.png");
}
}
.login-dropdown {
width: 200px;
.button {
padding: .75em;
}
}
.dropdown {
.row {
margin-bottom: 1.25rem;
input {
margin: 0;
height: 2.25rem;
}
}
}
}
//4 columns

View file

@ -1,8 +1,10 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const connect = require('react-redux').connect;
const IframeModal = require('../modal/iframe/modal.jsx');
const navigationActions = require('../../redux/navigation.js');
require('./registration.scss');
@ -26,7 +28,7 @@ class Registration extends React.Component {
handleMessage (e) {
if (e.origin !== window.location.origin) return;
if (e.source !== this.registrationIframe.contentWindow) return;
if (e.data === 'registration-done') this.props.onRegistrationDone();
if (e.data === 'registration-done') this.props.handleCompleteRegistration();
if (e.data === 'registration-relaunch') {
this.registrationIframe.contentWindow.location.reload();
}
@ -47,16 +49,32 @@ class Registration extends React.Component {
}}
isOpen={this.props.isOpen}
src="/accounts/standalone-registration/"
onRequestClose={this.props.onRequestClose}
onRequestClose={this.props.handleCloseRegistration}
/>
);
}
}
Registration.propTypes = {
isOpen: PropTypes.bool,
onRegistrationDone: PropTypes.func,
onRequestClose: PropTypes.func
handleCloseRegistration: PropTypes.func,
handleCompleteRegistration: PropTypes.func,
isOpen: PropTypes.bool
};
module.exports = Registration;
const mapStateToProps = state => ({
isOpen: state.navigation.registrationOpen
});
const mapDispatchToProps = dispatch => ({
handleCloseRegistration: () => {
dispatch(navigationActions.setRegistrationOpen(false));
},
handleCompleteRegistration: () => {
dispatch(navigationActions.handleCompleteRegistration());
}
});
module.exports = connect(
mapStateToProps,
mapDispatchToProps
)(Registration);

View file

@ -1,22 +1,153 @@
const keyMirror = require('keymirror');
const defaults = require('lodash.defaults');
const api = require('../lib/api');
const log = require('../lib/log.js');
const sessionActions = require('./session.js');
const Types = keyMirror({
SET_SEARCH_TERM: null
SET_SEARCH_TERM: null,
SET_ACCOUNT_NAV_OPEN: null,
TOGGLE_ACCOUNT_NAV_OPEN: null,
SET_LOGIN_ERROR: null,
SET_LOGIN_OPEN: null,
TOGGLE_LOGIN_OPEN: null,
SET_CANCELED_DELETION_OPEN: null,
SET_REGISTRATION_OPEN: null
});
module.exports.getInitialState = () => ({
accountNavOpen: false,
canceledDeletionOpen: false,
loginError: null,
loginOpen: false,
registrationOpen: false,
searchTerm: ''
});
module.exports.navigationReducer = (state, action) => {
if (typeof state === 'undefined') {
state = '';
state = module.exports.getInitialState();
}
switch (action.type) {
case Types.SET_SEARCH_TERM:
return action.searchTerm;
return defaults({searchTerm: action.searchTerm}, state);
case Types.SET_ACCOUNT_NAV_OPEN:
return defaults({accountNavOpen: action.isOpen}, state);
case Types.TOGGLE_ACCOUNT_NAV_OPEN:
return defaults({accountNavOpen: !state.accountNavOpen}, state);
case Types.SET_LOGIN_ERROR:
return defaults({loginError: action.loginError}, state);
case Types.SET_LOGIN_OPEN:
return defaults({loginOpen: action.isOpen}, state);
case Types.TOGGLE_LOGIN_OPEN:
return defaults({loginOpen: !state.loginOpen}, state);
case Types.SET_CANCELED_DELETION_OPEN:
return defaults({canceledDeletionOpen: action.isOpen}, state);
case Types.SET_REGISTRATION_OPEN:
return defaults({registrationOpen: action.isOpen}, state);
default:
return state;
}
};
module.exports.setAccountNavOpen = isOpen => ({
type: Types.SET_ACCOUNT_NAV_OPEN,
isOpen: isOpen
});
module.exports.handleToggleAccountNav = () => ({
type: Types.TOGGLE_ACCOUNT_NAV_OPEN
});
module.exports.setCanceledDeletionOpen = isOpen => ({
type: Types.SET_CANCELED_DELETION_OPEN,
isOpen: isOpen
});
module.exports.setLoginError = loginError => ({
type: Types.SET_LOGIN_ERROR,
loginError: loginError
});
module.exports.setLoginOpen = isOpen => ({
type: Types.SET_LOGIN_OPEN,
isOpen: isOpen
});
module.exports.toggleLoginOpen = () => ({
type: Types.TOGGLE_LOGIN_OPEN
});
module.exports.setRegistrationOpen = isOpen => ({
type: Types.SET_REGISTRATION_OPEN,
isOpen: isOpen
});
module.exports.setSearchTerm = searchTerm => ({
type: Types.SET_SEARCH_TERM,
searchTerm: searchTerm
});
module.exports.handleCompleteRegistration = () => (dispatch => {
dispatch(sessionActions.refreshSession());
dispatch(module.exports.setRegistrationOpen(false));
});
module.exports.closeAccountMenus = () => (dispatch => {
dispatch(module.exports.setAccountNavOpen(false));
dispatch(module.exports.setRegistrationOpen(false));
});
module.exports.handleLogIn = (formData, callback) => (dispatch => {
dispatch(module.exports.setLoginError(null));
formData.useMessages = true; // NOTE: this may or may not be being used anywhere else
api({
method: 'post',
host: '',
uri: '/accounts/login/',
json: formData,
useCsrf: true
}, (err, body) => {
if (err) dispatch(module.exports.setLoginError(err.message));
if (body) {
body = body[0];
if (body.success) {
dispatch(module.exports.setLoginOpen(false));
body.messages.forEach(message => {
if (message.message === 'canceled-deletion') {
dispatch(module.exports.setCanceledDeletionOpen(true));
}
});
dispatch(sessionActions.refreshSession());
callback({success: true});
} else {
if (body.redirect) {
window.location = body.redirect;
}
// Update login error message to a friendlier one if it exists
dispatch(module.exports.setLoginError(body.msg));
// JS error already logged by api mixin
callback({success: false});
}
} else {
// JS error already logged by api mixin
callback({success: false});
}
});
});
module.exports.handleLogOut = () => (dispatch => {
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) log.error(err);
dispatch(module.exports.setLoginOpen(false));
dispatch(module.exports.setAccountNavOpen(false));
window.location = '/';
});
});

View file

@ -4,6 +4,7 @@ const defaults = require('lodash.defaults');
const messageCountReducer = require('./message-count.js').messageCountReducer;
const permissionsReducer = require('./permissions.js').permissionsReducer;
const sessionReducer = require('./session.js').sessionReducer;
const navigationReducer = require('./navigation.js').navigationReducer;
/**
* Returns a combined reducer to be used for a page in `render.jsx`.
@ -18,8 +19,9 @@ const sessionReducer = require('./session.js').sessionReducer;
module.exports = opts => {
opts = opts || {};
return combineReducers(defaults(opts, {
session: sessionReducer,
messageCount: messageCountReducer,
navigation: navigationReducer,
permissions: permissionsReducer,
messageCount: messageCountReducer
session: sessionReducer
}));
};

View file

@ -8,7 +8,6 @@ const connect = require('react-redux').connect;
const injectIntl = require('react-intl').injectIntl;
const parser = require('scratch-parser');
const Page = require('../../components/page/www/page.jsx');
const api = require('../../lib/api');
const render = require('../../lib/render.jsx');
const storage = require('../../lib/storage.js').default;
const log = require('../../lib/log');
@ -16,8 +15,12 @@ const EXTENSION_INFO = require('../../lib/extensions.js').default;
const PreviewPresentation = require('./presentation.jsx');
const projectShape = require('./projectshape.jsx').projectShape;
const Registration = require('../../components/registration/registration.jsx');
const ConnectedLogin = require('../../components/login/connected-login.jsx');
const CanceledDeletionModal = require('../../components/login/canceled-deletion-modal.jsx');
const sessionActions = require('../../redux/session.js');
const navigationActions = require('../../redux/navigation.js');
const previewActions = require('../../redux/preview.js');
const GUI = require('scratch-gui');
@ -31,10 +34,7 @@ class Preview extends React.Component {
'handleToggleStudio',
'handleFavoriteToggle',
'handleLoadMore',
// temporary, to pass to GUI. Remove when nav bar components are shared between www and gui.
'handleLogout',
'handleLoveToggle',
'handlePermissions',
'handlePopState',
'handleReportClick',
'handleReportClose',
@ -45,9 +45,8 @@ class Preview extends React.Component {
'handleUpdateProjectTitle',
'handleUpdate',
'initCounts',
'isShared',
'pushHistory',
'userOwnsProject'
'renderLogin'
]);
const pathname = window.location.pathname.toLowerCase();
const parts = pathname.split('/').filter(Boolean);
@ -55,7 +54,6 @@ class Preview extends React.Component {
// parts[1]: either :id or 'editor'
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
this.state = {
editable: false,
extensions: [],
favoriteCount: 0,
loveCount: 0,
@ -90,7 +88,6 @@ class Preview extends React.Component {
if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
this.getExtensions(this.state.projectId);
this.initCounts(this.props.projectInfo.stats.favorites, this.props.projectInfo.stats.loves);
this.handlePermissions();
if (this.props.projectInfo.remix.parent !== null) {
this.props.getParentInfo(this.props.projectInfo.remix.parent);
}
@ -145,21 +142,6 @@ class Preview extends React.Component {
});
});
}
// Temporarily duplicated this function from navigation.jsx here.
// Should move handling of login/logout into session.js, and handle them
// from here as well as navigation.jsx.
handleLogout (e) {
e.preventDefault();
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) log.error(err);
window.location = '/';
});
}
handleReportClick () {
this.setState({reportOpen: true});
}
@ -261,12 +243,6 @@ class Preview extends React.Component {
}));
}
}
handlePermissions () {
// TODO: handle admins and mods
if (this.props.projectInfo.author.username === this.props.user.username) {
this.setState({editable: true});
}
}
handleSeeInside () {
this.props.setPlayer(false);
}
@ -289,26 +265,21 @@ class Preview extends React.Component {
loveCount: loves
});
}
isShared () {
renderLogin ({onClose}) {
return (
// if we don't have projectInfo assume shared until we know otherwise
Object.keys(this.props.projectInfo).length === 0 || (
this.props.projectInfo.history &&
this.props.projectInfo.history.shared.length > 0
)
);
}
isLoggedIn () {
return (
this.props.sessionStatus === sessionActions.Status.FETCHED &&
Object.keys(this.props.user).length > 0
);
}
userOwnsProject () {
return (
this.isLoggedIn() &&
Object.keys(this.props.projectInfo).length > 0 &&
this.props.user.id === this.props.projectInfo.author.id
<ConnectedLogin
key="login-dropdown-presentation"
/* eslint-disable react/jsx-no-bind */
onLogIn={(formData, callback) => {
this.props.handleLogIn(formData, result => {
if (result.success === true) {
onClose();
}
callback(result);
});
}}
/* eslint-ensable react/jsx-no-bind */
/>
);
}
render () {
@ -320,13 +291,13 @@ class Preview extends React.Component {
assetHost={this.props.assetHost}
backpackOptions={this.props.backpackOptions}
comments={this.props.comments}
editable={this.state.editable}
editable={this.props.isEditable}
extensions={this.state.extensions}
faved={this.props.faved}
favoriteCount={this.state.favoriteCount}
isFullScreen={this.state.isFullScreen}
isLoggedIn={this.isLoggedIn()}
isShared={this.isShared()}
isLoggedIn={this.props.isLoggedIn}
isShared={this.props.isShared}
loveCount={this.state.loveCount}
loved={this.props.loved}
originalInfo={this.props.original}
@ -339,7 +310,7 @@ class Preview extends React.Component {
replies={this.props.replies}
reportOpen={this.state.reportOpen}
studios={this.props.studios}
userOwnsProject={this.userOwnsProject()}
userOwnsProject={this.props.userOwnsProject}
onAddToStudioClicked={this.handleAddToStudioClick}
onAddToStudioClosed={this.handleAddToStudioClose}
onFavoriteClicked={this.handleFavoriteToggle}
@ -353,19 +324,27 @@ class Preview extends React.Component {
onUpdate={this.handleUpdate}
/>
</Page> :
<IntlGUI
enableCommunity
hideIntro
assetHost={this.props.assetHost}
backpackOptions={this.props.backpackOptions}
basePath="/"
className="gui"
projectHost={this.props.projectHost}
projectId={this.state.projectId}
projectTitle={this.props.projectInfo.title}
onClickLogout={this.handleLogout}
onUpdateProjectTitle={this.handleUpdateProjectTitle}
/>
<React.Fragment>
<IntlGUI
enableCommunity
hideIntro
assetHost={this.props.assetHost}
backpackOptions={this.props.backpackOptions}
basePath="/"
className="gui"
projectHost={this.props.projectHost}
projectId={this.state.projectId}
projectTitle={this.props.projectInfo.title}
renderLogin={this.renderLogin}
onLogOut={this.props.handleLogOut}
onOpenRegistration={this.props.handleOpenRegistration}
onToggleLoginOpen={this.props.handleToggleLoginOpen}
onUpdateProjectTitle={this.handleUpdateProjectTitle}
/>
<Registration />
<CanceledDeletionModal />
</React.Fragment>
);
}
}
@ -388,6 +367,13 @@ Preview.propTypes = {
getProjectStudios: PropTypes.func.isRequired,
getRemixes: PropTypes.func.isRequired,
getTopLevelComments: PropTypes.func.isRequired,
handleLogIn: PropTypes.func,
handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func,
handleToggleLoginOpen: PropTypes.func,
isEditable: PropTypes.bool,
isLoggedIn: PropTypes.bool,
isShared: PropTypes.bool,
loved: PropTypes.bool,
original: projectShape,
parent: projectShape,
@ -415,7 +401,8 @@ Preview.propTypes = {
dateJoined: PropTypes.string,
email: PropTypes.string,
classroomId: PropTypes.string
})
}),
userOwnsProject: PropTypes.bool
};
Preview.defaultProps = {
@ -463,26 +450,64 @@ const consolidateStudiosInfo = (curatedStudios, projectStudios, currentStudioIds
return consolidatedStudios;
};
const mapStateToProps = state => ({
comments: state.preview.comments,
faved: state.preview.faved,
loved: state.preview.loved,
original: state.preview.original,
parent: state.preview.parent,
remixes: state.preview.remixes,
replies: state.preview.replies,
sessionStatus: state.session.status,
projectInfo: state.preview.projectInfo,
projectStudios: state.preview.projectStudios,
studios: consolidateStudiosInfo(state.preview.curatedStudios,
state.preview.projectStudios, state.preview.currentStudioIds,
state.preview.status.studioRequests),
user: state.session.session.user,
playerMode: state.scratchGui.mode.isPlayerOnly,
fullScreen: state.scratchGui.mode.isFullScreen
});
const mapStateToProps = state => {
const projectInfoPresent = Object.keys(state.preview.projectInfo).length > 0;
const userPresent = state.session.session.user &&
Object.keys(state.session.session.user).length > 0;
const isLoggedIn = state.session.status === sessionActions.Status.FETCHED &&
userPresent;
const authorPresent = projectInfoPresent && state.preview.projectInfo.author &&
Object.keys(state.preview.projectInfo.author).length > 0;
return {
comments: state.preview.comments,
faved: state.preview.faved,
fullScreen: state.scratchGui.mode.isFullScreen,
// project is editable iff logged in user is the author of the project, or
// logged in user is an admin.
isEditable: isLoggedIn &&
((authorPresent && state.preview.projectInfo.author.username === state.session.session.user.username) ||
state.permissions.admin === true),
isLoggedIn: isLoggedIn,
// if we don't have projectInfo, assume it's shared until we know otherwise
isShared: !projectInfoPresent || (
state.preview.projectInfo.history &&
state.preview.projectInfo.history.shared &&
state.preview.projectInfo.history.shared.length > 0),
loved: state.preview.loved,
original: state.preview.original,
parent: state.preview.parent,
playerMode: state.scratchGui.mode.isPlayerOnly,
projectInfo: state.preview.projectInfo,
projectStudios: state.preview.projectStudios,
remixes: state.preview.remixes,
replies: state.preview.replies,
sessionStatus: state.session.status, // check if used
studios: consolidateStudiosInfo(state.preview.curatedStudios,
state.preview.projectStudios, state.preview.currentStudioIds,
state.preview.status.studioRequests),
user: state.session.session.user,
userOwnsProject: isLoggedIn && authorPresent &&
state.session.session.user.id === state.preview.projectInfo.author.id
};
};
const mapDispatchToProps = dispatch => ({
handleOpenRegistration: event => {
event.preventDefault();
dispatch(navigationActions.setRegistrationOpen(true));
},
handleLogIn: (formData, callback) => {
dispatch(navigationActions.handleLogIn(formData, callback));
},
handleLogOut: event => {
event.preventDefault();
dispatch(navigationActions.handleLogOut());
},
handleToggleLoginOpen: event => {
event.preventDefault();
dispatch(navigationActions.toggleLoginOpen());
},
getOriginalInfo: id => {
dispatch(previewActions.getOriginalInfo(id));
},
@ -523,9 +548,6 @@ const mapDispatchToProps = dispatch => ({
setLovedStatus: (loved, id, username, token) => {
dispatch(previewActions.setLovedStatus(loved, id, username, token));
},
refreshSession: () => {
dispatch(sessionActions.refreshSession());
},
reportProject: (id, formData) => {
dispatch(previewActions.reportProject(id, formData));
},

View file

@ -232,7 +232,7 @@ Search.propTypes = {
};
const mapStateToProps = state => ({
searchTerm: state.navigation
searchTerm: state.navigation.searchTerm
});
const WrappedSearch = injectIntl(Search);

View file

@ -7,8 +7,8 @@ const React = require('react');
const api = require('../../lib/api');
const injectIntl = require('../../lib/intl.jsx').injectIntl;
const intlShape = require('../../lib/intl.jsx').intlShape;
const log = require('../../lib/log.js');
const sessionStatus = require('../../redux/session').Status;
const navigationActions = require('../../redux/navigation.js');
const Deck = require('../../components/deck/deck.jsx');
const Progression = require('../../components/progression/progression.jsx');
@ -24,7 +24,6 @@ class StudentCompleteRegistration extends React.Component {
super(props);
bindAll(this, [
'handleAdvanceStep',
'handleLogOut',
'handleRegister',
'handleGoToClass'
]);
@ -61,18 +60,6 @@ class StudentCompleteRegistration extends React.Component {
formData: defaults({}, formData, this.state.formData)
});
}
handleLogOut (e) {
e.preventDefault();
api({
host: '',
method: 'post',
uri: '/accounts/logout/',
useCsrf: true
}, err => {
if (err) return log.error(err);
window.location = '/';
});
}
handleRegister (formData) {
this.setState({waiting: true});
@ -147,7 +134,7 @@ class StudentCompleteRegistration extends React.Component {
classroom={this.state.classroom}
studentUsername={this.props.studentUsername}
waiting={this.state.waiting}
onHandleLogOut={this.handleLogOut}
onHandleLogOut={this.props.handleLogOut}
onNextStep={this.handleAdvanceStep}
/>
{this.props.must_reset_password ?
@ -178,6 +165,7 @@ class StudentCompleteRegistration extends React.Component {
StudentCompleteRegistration.propTypes = {
classroomId: PropTypes.number.isRequired,
handleLogOut: PropTypes.func,
intl: intlShape,
must_reset_password: PropTypes.bool.isRequired,
newStudent: PropTypes.bool.isRequired,
@ -199,6 +187,16 @@ const mapStateToProps = state => ({
studentUsername: state.session.session.user && state.session.session.user.username
});
const ConnectedStudentCompleteRegistration = connect(mapStateToProps)(IntlStudentCompleteRegistration);
const mapDispatchToProps = dispatch => ({
handleLogOut: event => {
event.preventDefault();
dispatch(navigationActions.handleLogOut());
}
});
const ConnectedStudentCompleteRegistration = connect(
mapStateToProps,
mapDispatchToProps
)(IntlStudentCompleteRegistration);
render(<ConnectedStudentCompleteRegistration />, document.getElementById('app'));