refactor(errors, snackbar): do less dispatches, make logic easier but inside SnackBar container and make components dumbier

This commit is contained in:
Kirill Shumilov
2016-07-22 18:00:48 +03:00
parent d4938ae9cd
commit c336008564
6 changed files with 135 additions and 153 deletions

View File

@@ -22,5 +22,7 @@ export const EDITOR_SET_SELECTED_NODETYPE = 'EDITOR_SET_SELECTED_NODETYPE';
export const PROJECT_LOAD_DATA = 'PROJECT_LOAD_DATA';
export const ERROR = 'ERROR';
export const ERROR_ADD = 'ERROR_ADD';
export const ERROR_DELETE = 'ERROR_DELETE';
export const UPLOAD = 'UPLOAD';

View File

@@ -3,41 +3,19 @@ import * as STATUS from './constants/statuses';
import Selectors from './selectors';
import { upload as uploadToEspruino } from 'xod-espruino/upload';
const ERROR_TIMEOUT = 3000;
const getTimestamp = () => new Date().getTime();
const setTimeoutForError = (id, dispatch) => {
setTimeout(() => {
dispatch({
type: ActionType.ERROR,
payload: {
id,
},
meta: {
timestamp: getTimestamp(),
status: STATUS.SUCCEEDED,
},
});
}, ERROR_TIMEOUT);
};
export const addError = (error) => (dispatch, getState) => {
const errors = Selectors.Errors.getErrors(getState());
const newErrorId = Selectors.Errors.getNewId(errors);
dispatch({
type: ActionType.ERROR,
payload: error,
meta: {
timestamp: getTimestamp(),
status: STATUS.STARTED,
},
});
setTimeoutForError(newErrorId, dispatch);
};
export const addError = (error) => ({
type: ActionType.ERROR_ADD,
payload: error,
meta: {
timestamp: getTimestamp(),
status: STATUS.STARTED,
},
});
export const deleteError = (id) => ({
type: ActionType.ERROR,
type: ActionType.ERROR_DELETE,
payload: {
id,
},
@@ -47,10 +25,6 @@ export const deleteError = (id) => ({
},
});
export const keepError = (id) => (dispatch) => {
setTimeoutForError(id, dispatch);
};
export const moveNode = (id, position) => ({
type: ActionType.NODE_MOVE,
payload: {

View File

@@ -1,22 +1,17 @@
import R from 'ramda';
import React from 'react';
import classNames from 'classnames';
import * as STATUS from '../constants/statuses';
import { checkForMouseBubbling } from '../utils/browser';
class SnackBarError extends React.Component {
constructor(props) {
super(props);
this.state = {
mouseOver: false,
hidden: true,
};
this.hide = this.hide.bind(this);
this.onClick = this.onClick.bind(this);
this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
}
componentDidMount() {
@@ -25,54 +20,10 @@ class SnackBarError extends React.Component {
}, 5);
}
componentWillReceiveProps(nextProps) {
if (
nextProps.error.status === STATUS.SUCCEEDED &&
!this.state.mouseOver
) {
this.hide();
}
}
shouldComponentUpdate(nextProps, nextState) {
if (
nextState.hidden === this.state.hidden ||
(
nextState.mouseOver &&
nextProps.error.status === STATUS.SUCCEEDED
)
) { return false; }
return true;
}
onClick() {
this.hide();
}
onMouseOver() {
if (this.state.mouseOver) {
return;
}
this.setMouseOver(true);
}
onMouseOut(event) {
if (
!this.state.mouseOver ||
checkForMouseBubbling(event, this.refs.body)
) {
return;
}
this.setMouseOver(false);
this.props.keepError(this.props.error.id);
}
setMouseOver(val) {
this.setState(
R.assoc('mouseOver', val, this.state)
);
}
setHidden(val) {
this.setState(
R.assoc('hidden', val, this.state)
@@ -80,11 +31,10 @@ class SnackBarError extends React.Component {
}
hide() {
this.setHidden(true);
setTimeout(() => {
this.props.onHide(this.props.error.id);
}, 500);
return new Promise((resolve) => {
this.setHidden(true);
setTimeout(() => resolve(), 500);
});
}
render() {
@@ -97,8 +47,6 @@ class SnackBarError extends React.Component {
ref="body"
className={cls}
onClick={this.onClick}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
dataId={error.id}
>
<small>
@@ -114,7 +62,6 @@ class SnackBarError extends React.Component {
SnackBarError.propTypes = {
error: React.PropTypes.object,
onHide: React.PropTypes.func,
keepError: React.PropTypes.func,
};
export default SnackBarError;

View File

@@ -1,13 +1,19 @@
import React from 'react';
const SnackBarList = ({ children }) => (
<ul className="SnackBarList">
const SnackBarList = ({ onMouseOver, onMouseOut, children }) => (
<ul
className="SnackBarList"
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
>
{children}
</ul>
);
SnackBarList.propTypes = {
children: React.PropTypes.arrayOf(React.PropTypes.element),
onMouseOver: React.PropTypes.func,
onMouseOut: React.PropTypes.func,
};
export default SnackBarList;

View File

@@ -6,35 +6,114 @@ import { connect } from 'react-redux';
import SnackBarList from '../components/SnackBarList';
import SnackBarError from '../components/SnackBarError';
import Selectors from '../selectors';
import { deleteError, keepError } from '../actions';
import { deleteError } from '../actions';
const ERROR_TIMEOUT = 3000;
class SnackBar extends React.Component {
constructor(props) {
super(props);
this.errors = {};
this.state = {
mouseover: false,
};
this.hideError = this.hideError.bind(this);
this.keepError = this.keepError.bind(this);
this.onMouseOver = this.onMouseOver.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
}
componentWillReceiveProps(nextProps) {
this.addErrors(nextProps.errors);
}
onMouseOver() {
if (this.state.mouseover) return;
this.setState(
R.assoc('mouseover', true, this.state)
);
}
onMouseOut() {
if (!this.state.mouseover) return;
this.setState(
R.assoc('mouseover', false, this.state)
);
this.restartTimeouts();
}
setTimeout(id) {
return setTimeout(() => {
if (!this.state.mouseover) {
this.hideError(id);
}
}, ERROR_TIMEOUT);
}
restartTimeouts() {
R.pipe(
R.values,
R.forEach((error) => {
const id = error.data.id;
clearTimeout(error.timeout);
this.errors[id].timeout = this.setTimeout(id);
})
)(this.errors);
}
hideError(id) {
this.props.deleteError(id);
const element = this.errors[id].ref;
if (!element) return;
element
.hide()
.then(() => delete this.errors[id])
.then(() => this.props.deleteError(id));
}
keepError(id) {
this.props.keepError(id);
addErrors(errors) {
R.pipe(
R.values,
R.forEach((error) => {
if (this.errors.hasOwnProperty(error.id)) {
return;
}
const assignRef = (el) => {
if (this.errors.hasOwnProperty(error.id)) {
this.errors[error.id].ref = el;
}
};
this.errors[error.id] = {
timeout: this.setTimeout(error.id),
data: error,
element: (
<SnackBarError
ref={assignRef}
key={error.id}
onHide={this.hideError}
error={error}
/>
),
};
})
)(errors);
}
render() {
const errors = R.values(this.props.errors);
const errors = R.values(this.errors);
return (
<SnackBarList>
{errors.map((error) =>
<SnackBarError
key={error.id}
onHide={this.hideError}
keepError={this.keepError}
error={error}
/>
)}
<SnackBarList
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
{errors.map(error => error.element)}
</SnackBarList>
);
}
@@ -44,7 +123,6 @@ class SnackBar extends React.Component {
SnackBar.propTypes = {
errors: React.PropTypes.object,
deleteError: React.PropTypes.func,
keepError: React.PropTypes.func,
};
const mapStateToProps = (state) => ({
@@ -53,7 +131,6 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) => (bindActionCreators({
deleteError,
keepError,
}, dispatch));
export default connect(mapStateToProps, mapDispatchToProps)(SnackBar);

View File

@@ -1,48 +1,24 @@
import R from 'ramda';
import { ERROR } from '../actionTypes';
import * as STATUS from '../constants/statuses';
import { ERROR_ADD, ERROR_DELETE } from '../actionTypes';
import { getNewId } from '../selectors/errors';
export const errorsReducer = (errors = {}, action) => {
if (
action.type === ERROR &&
action.meta &&
action.meta.status
) {
switch (action.meta.status) {
case STATUS.STARTED: {
const newId = getNewId(errors);
return R.assoc(
newId,
{
id: newId,
timestamp: action.meta.timestamp,
status: action.meta.status,
payload: action.payload,
},
errors
);
}
case STATUS.SUCCEEDED: {
if (!errors.hasOwnProperty(action.payload.id)) { return errors; }
return R.assoc(
action.payload.id,
R.merge(
errors[action.payload.id],
{
timestamp: action.meta.timestamp,
status: action.meta.status,
}
),
errors
);
}
case STATUS.DELETED:
return R.omit([action.payload.id.toString()], errors);
default: break;
switch (action.type) {
case ERROR_ADD: {
const newId = getNewId(errors);
return R.assoc(
newId,
{
id: newId,
timestamp: action.meta.timestamp,
payload: action.payload,
},
errors
);
}
case ERROR_DELETE:
return R.omit([action.payload.id.toString()], errors);
default:
return errors;
}
return errors;
};