Eradicated Redux for MobX

This commit is contained in:
Jori Lallo 2016-06-04 18:28:14 -07:00
parent 19712a41f9
commit c2f1ea22b9
20 changed files with 177 additions and 505 deletions

View File

@ -1,59 +0,0 @@
import makeActionCreator from '../utils/actions';
import { client } from 'utils/ApiClient';
import { normalize, Schema, arrayOf } from 'normalizr';
const atlas = new Schema('atlases');
export const FETCH_ATLASES_PENDING = 'FETCH_ATLASES_PENDING';
export const FETCH_ATLASES_SUCCESS = 'FETCH_ATLASES_SUCCESS';
export const FETCH_ATLASES_FAILURE = 'FETCH_ATLASES_FAILURE';
const fetchAtlasesPending = makeActionCreator(FETCH_ATLASES_PENDING);
const fetchAtlasesSuccess = makeActionCreator(FETCH_ATLASES_SUCCESS, 'data', 'pagination');
const fetchAtlasesFailure = makeActionCreator(FETCH_ATLASES_FAILURE, 'error');
export function fetchAtlasesAsync(teamId) {
return (dispatch) => {
dispatch(fetchAtlasesPending());
client.post('/atlases.list', {
teamId: teamId,
})
.then(data => {
const response = normalize(data.data, arrayOf(atlas));
dispatch(fetchAtlasesSuccess(response, data.pagination));
})
.catch((err) => {
dispatch(fetchAtlasesFailure(err));
})
};
};
export const FETCH_ATLAS_PENDING = 'FETCH_ATLAS_PENDING';
export const FETCH_ATLAS_SUCCESS = 'FETCH_ATLAS_SUCCESS';
export const FETCH_ATLAS_FAILURE = 'FETCH_ATLAS_FAILURE';
const fetchAtlasPending = makeActionCreator(FETCH_ATLAS_PENDING);
const fetchAtlasSuccess = makeActionCreator(FETCH_ATLAS_SUCCESS, 'data');
const fetchAtlasFailure = makeActionCreator(FETCH_ATLAS_FAILURE, 'error');
export function fetchAtlasAsync(atlasId) {
return (dispatch) => {
dispatch(fetchAtlasPending());
client.post('/atlases.info', {
id: atlasId,
})
.then(data => {
const response = normalize(data.data, atlas);
dispatch(fetchAtlasSuccess(response));
})
.catch((err) => {
dispatch(fetchAtlasFailure(err));
})
};
};

View File

@ -1,91 +0,0 @@
import makeActionCreator from '../utils/actions';
import { replace } from 'react-router-redux';
import { client } from 'utils/ApiClient';
import { createAction } from 'redux-actions';
export const resetDocument = createAction('RESET_DOCUMENT');
// GET
export const FETCH_DOCUMENT_PENDING = 'FETCH_DOCUMENT_PENDING';
export const FETCH_DOCUMENT_SUCCESS = 'FETCH_DOCUMENT_SUCCESS';
export const FETCH_DOCUMENT_FAILURE = 'FETCH_DOCUMENT_FAILURE';
const fetchDocumentPending = makeActionCreator(FETCH_DOCUMENT_PENDING);
const fetchDocumentSuccess = makeActionCreator(FETCH_DOCUMENT_SUCCESS, 'data');
const fetchDocumentFailure = makeActionCreator(FETCH_DOCUMENT_FAILURE, 'error');
export function fetchDocumentAsync(documentId) {
return (dispatch) => {
dispatch(fetchDocumentPending());
client.post('/documents.info', {
id: documentId,
})
.then(data => {
dispatch(fetchDocumentSuccess(data.data));
})
.catch((err) => {
dispatch(fetchDocumentFailure(err));
})
};
};
// POST/UPDATE
export const SAVE_DOCUMENT_PENDING = 'SAVE_DOCUMENT_PENDING';
export const SAVE_DOCUMENT_SUCCESS = 'SAVE_DOCUMENT_SUCCESS';
export const SAVE_DOCUMENT_FAILURE = 'SAVE_DOCUMENT_FAILURE';
const saveDocumentPending = makeActionCreator(SAVE_DOCUMENT_PENDING);
const saveDocumentSuccess = makeActionCreator(SAVE_DOCUMENT_SUCCESS, 'data');
const saveDocumentFailure = makeActionCreator(SAVE_DOCUMENT_FAILURE, 'error');
export function saveDocumentAsync(atlasId, documentId, title, text) {
return (dispatch) => {
dispatch(saveDocumentPending());
let url;
let data = {
title,
text,
};
if (documentId) {
url = '/documents.update';
data.id = documentId;
} else {
url = '/documents.create';
data.atlas = atlasId;
}
client.post(url, data)
.then(data => {
dispatch(saveDocumentSuccess(data.data, data.pagination));
dispatch(replace(`/documents/${data.data.id}`));
})
.catch((err) => {
dispatch(saveDocumentFailure(err));
})
};
};
// documents.delete
export const deleteDocumentPending = createAction('DELETE_DOCUMENT_PENDING');
export const deleteDocumentSuccess = createAction('DELETE_DOCUMENT_SUCCESS');
export const deleteDocumentFailure = createAction('DELETE_DOCUMENT_FAILURE');
export const deleteDocument = (documentId, returnPath) => {
return (dispatch) => {
dispatch(deleteDocumentPending());
client.post('/documents.delete', { id: documentId })
.then(data => {
dispatch(deleteDocumentSuccess(documentId));
dispatch(replace(returnPath));
})
.catch((err) => {
dispatch(deleteDocumentFailure(err));
})
};
};

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions';
export const resetEditor = createAction('EDITOR_RESET');
export const updateText = createAction('EDITOR_UPDATE_TEXT');
export const updateTitle = createAction('EDITOR_UPDATE_TITLE');
export const replaceText = createAction('EDITOR_REPLACE_TEXT');

View File

@ -1,5 +0,0 @@
import makeActionCreator from '../utils/actions';
export const UPDATE_TEAM = 'UPDATE_TEAM';
export const updateTeam = makeActionCreator(UPDATE_TEAM, 'team');

View File

@ -1,4 +1,5 @@
import React from 'react';
import { observer } from 'mobx-react';
import Link from 'react-router/lib/Link';
import DocumentLink from './components/DocumentLink';
@ -7,6 +8,7 @@ import styles from './AtlasPreview.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
@observer
class AtlasPreview extends React.Component {
static propTypes = {
data: React.PropTypes.object.isRequired,

View File

@ -1,17 +1,18 @@
import React from 'react';
import { observer } from "mobx-react"
import moment from 'moment';
import Link from 'react-router/lib/Link';
import styles from './DocumentLink.scss';
const DocumentLink = (props) => {
const DocumentLink = observer((props) => {
return (
<Link to={ `/documents/${props.document.id}` } className={ styles.link }>
<h3 className={ styles.title }>{ props.document.title }</h3>
<span className={ styles.timestamp }>{ moment(props.document.updatedAt).fromNow() }</span>
</Link>
);
};
});
export default DocumentLink;

View File

@ -98,7 +98,6 @@ class MarkdownAtlas extends React.Component {
onPaddingTopClick = () => {
const cm = this.getEditorInstance();
console.log(cm)
cm.setCursor(0, 0);
cm.focus();
}

View File

@ -11,9 +11,7 @@ import createLogger from 'redux-logger';
import History from 'utils/History';
import DevTools from 'mobx-react-devtools';
import auth from 'utils/auth';
import reducers from 'reducers';
import userStore from 'stores/UserStore';
import 'normalize.css/normalize.css';
import 'utils/base-styles.scss';
@ -31,49 +29,30 @@ import DocumentScene from 'scenes/DocumentScene';
import DocumentEdit from 'scenes/DocumentEdit';
import SlackAuth from 'scenes/SlackAuth';
// Redux
let store;
const routerMiddlewareWithHistory = routerMiddleware(History);
if (__DEV__) {
const loggerMiddleware = createLogger();
store = createStore(reducers, applyMiddleware(
thunkMiddleware,
routerMiddlewareWithHistory,
loggerMiddleware,
));
} else {
store = createStore(reducers, applyMiddleware(
thunkMiddleware,
routerMiddlewareWithHistory,
));
}
render((
<div style={{ display: 'flex', flex: 1, }}>
<Provider store={store}>
<Router history={History}>
<Route path="/" component={ Application }>
<IndexRoute component={Home} />
<Route path="/dashboard" component={ Dashboard } onEnter={ requireAuth } />
<Route path="/atlas/:id" component={ Atlas } onEnter={ requireAuth } />
<Route path="/atlas/:id/new" component={ Editor } onEnter={ requireAuth } />
<Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } />
<Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } />
<Route path="/auth/slack" component={SlackAuth} />
</Route>
</Router>
</Provider>
{ __DEV__ ? <DevTools position={{ bottom: 0, right: 0 }} /> : null }
</div>
), document.getElementById('root'));
function requireAuth(nextState, replace) {
if (!auth.loggedIn()) {
if (!userStore.authenticated) {
replace({
pathname: '/',
state: { nextPathname: nextState.location.pathname },
});
}
}
render((
<div style={{ display: 'flex', flex: 1, }}>
<Router history={History}>
<Route path="/" component={ Application }>
<IndexRoute component={Home} />
<Route path="/dashboard" component={ Dashboard } onEnter={ requireAuth } />
<Route path="/atlas/:id" component={ Atlas } onEnter={ requireAuth } />
<Route path="/atlas/:id/new" component={ Editor } onEnter={ requireAuth } />
<Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } />
<Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } />
<Route path="/auth/slack" component={SlackAuth} />
</Route>
</Router>
{ __DEV__ ? <DevTools position={{ bottom: 0, right: 0 }} /> : null }
</div>
), document.getElementById('root'));

View File

@ -1,66 +0,0 @@
import {
FETCH_ATLASES_PENDING,
FETCH_ATLASES_SUCCESS,
FETCH_ATLASES_FAILURE,
FETCH_ATLAS_PENDING,
FETCH_ATLAS_SUCCESS,
FETCH_ATLAS_FAILURE,
} from 'actions/AtlasActions';
const initialState = {
pagination: null,
isLoading: false,
}
const atlases = (state = initialState, action) => {
switch (action.type) {
case FETCH_ATLASES_PENDING: {
return {
...state,
isLoading: true,
};
}
case FETCH_ATLASES_SUCCESS: {
return {
...state,
...action.data,
pagination: action.pagination,
isLoading: false,
};
}
case FETCH_ATLASES_FAILURE: {
return {
...state,
isLoading: false,
error: action.error,
};
}
case FETCH_ATLAS_PENDING: {
return {
...state,
isLoading: true,
};
}
case FETCH_ATLAS_SUCCESS: {
return {
...state,
...action.data,
isLoading: false,
};
}
case FETCH_ATLAS_FAILURE: {
return {
...state,
isLoading: false,
error: action.error,
};
}
default:
return state;
}
};
export default atlases;

View File

@ -1,69 +0,0 @@
import {
FETCH_DOCUMENT_PENDING,
FETCH_DOCUMENT_SUCCESS,
FETCH_DOCUMENT_FAILURE,
SAVE_DOCUMENT_PENDING,
SAVE_DOCUMENT_SUCCESS,
SAVE_DOCUMENT_FAILURE,
} from 'actions/DocumentActions';
const initialState = {
data: null,
error: null,
isLoading: false,
isSaving: false,
}
const doc = (state = initialState, action) => {
switch (action.type) {
case 'RESET_DOCUMENT': {
return {
...initialState,
}
}
case FETCH_DOCUMENT_PENDING: {
return {
...state,
isLoading: true,
};
}
case FETCH_DOCUMENT_SUCCESS: {
return {
data: action.data,
isLoading: false,
};
}
case FETCH_DOCUMENT_FAILURE: {
return {
...state,
error: action.error,
isLoading: false,
};
}
case SAVE_DOCUMENT_PENDING: {
return {
...state,
isSaving: true,
};
}
case SAVE_DOCUMENT_SUCCESS: {
return {
data: action.date,
isSaving: false,
};
}
case SAVE_DOCUMENT_FAILURE: {
return {
...state,
error: action.error,
isSaving: false,
};
}
default:
return state;
}
};
export default doc;

View File

@ -1,9 +0,0 @@
import { combineReducers } from 'redux';
import atlases from './atlases';
import document from './document';
export default combineReducers({
atlases,
document,
});

View File

@ -1,12 +1,8 @@
import React from 'react';
import { observer } from 'mobx-react';
import Link from 'react-router/lib/Link';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { replace } from 'react-router-redux';
import { fetchAtlasAsync } from 'actions/AtlasActions';
// Temp
import { client } from 'utils/ApiClient';
import store from './AtlasStore';
import Layout, { Title, HeaderAction } from 'components/Layout';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
@ -16,25 +12,20 @@ import Divider from 'components/Divider';
import styles from './Atlas.scss';
@observer
class Atlas extends React.Component {
static propTypes = {
isLoading: React.PropTypes.bool,
atlas: React.PropTypes.object,
}
componentDidMount = () => {
const { id } = this.props.params;
this.props.fetchAtlasAsync(id);
store.fetchAtlas(id);
}
render() {
const atlas = this.props.atlas;
const atlas = store.atlas;
let actions;
let title;
if (!this.props.isLoading) {
if (atlas) {
actions = <HeaderAction>
<Link to={ `/atlas/${atlas.id}/new` }>New document</Link>
</HeaderAction>;
@ -47,7 +38,7 @@ class Atlas extends React.Component {
title={ title }
>
<CenteredContent>
{ this.props.isLoading ? (
{ store.isFetching ? (
<AtlasPreviewLoading />
) : (
<div className={ styles.container }>
@ -68,24 +59,4 @@ class Atlas extends React.Component {
);
}
}
const mapStateToProps = (state, currentProps) => {
const id = currentProps.params.id;
return {
isLoading: state.atlases.isLoading,
atlas: state.atlases.entities ? state.atlases.entities.atlases[id] : null, // reselect
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
replace,
fetchAtlasAsync,
}, dispatch)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Atlas);
export default Atlas;

View File

@ -0,0 +1,25 @@
import { observable, action } from 'mobx';
import { client } from 'utils/ApiClient';
const store = new class AtlasStore {
@observable atlas;
@observable isFetching;
/* Actions */
@action fetchAtlas = async (id) => {
this.isFetching = true;
try {
const res = await client.post('/atlases.info', { id: id });
const { data } = res;
this.atlas = data;
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
}();
export default store;

View File

@ -1,7 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchAtlasesAsync } from 'actions/AtlasActions';
import { observer } from 'mobx-react';
import userStore from 'stores/UserStore';
import store from './DashboardStore';
import Flex from 'components/Flex';
import Layout from 'components/Layout';
@ -11,12 +12,10 @@ import CenteredContent from 'components/CenteredContent';
import styles from './Dashboard.scss';
@observer
class Dashboard extends React.Component {
static propTypes = {
}
componentDidMount = () => {
this.props.fetchAtlasesAsync(this.props.teamId);
store.fetchAtlases(userStore.team.id);
}
render() {
@ -24,10 +23,10 @@ class Dashboard extends React.Component {
<Layout>
<CenteredContent>
<Flex direction="column" flex={ true }>
{ this.props.isLoading ? (
{ store.isFetching ? (
<AtlasPreviewLoading />
) : this.props.items.map((item) => {
return (<AtlasPreview key={ item.id } data={ item } />);
) : store.atlases.map((atlas) => {
return (<AtlasPreview key={ atlas.id } data={ atlas } />);
}) }
</Flex>
</CenteredContent>
@ -36,21 +35,4 @@ class Dashboard extends React.Component {
}
}
const mapStateToProps = (state) => {
return {
teamId: state.team ? state.team.id : null,
isLoading: state.atlases.isLoading,
items: Array.isArray(state.atlases.result) ? state.atlases.result.map((id) => state.atlases.entities.atlases[id]) : [], // reselect
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
fetchAtlasesAsync,
}, dispatch)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Dashboard);
export default Dashboard;

View File

@ -0,0 +1,27 @@
import { observable, action } from 'mobx';
import { client } from 'utils/ApiClient';
const store = new class DashboardStore {
@observable atlases;
@observable pagination;
@observable isFetching;
/* Actions */
@action fetchAtlases = async (teamId) => {
this.isFetching = true;
try {
const res = await client.post('/atlases.list', { id: teamId });
const { data, pagination } = res;
this.atlases = data;
this.pagination = pagination;
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
}();
export default store;

View File

@ -1,11 +1,8 @@
import React from 'react';
import { connect } from 'react-redux';
import Link from 'react-router/lib/Link';
import { bindActionCreators } from 'redux';
import {
fetchDocumentAsync,
deleteDocument,
} from 'actions/DocumentActions';
import { Link } from 'react-router';
import { observer } from 'mobx-react';
import store from './DocumentSceneStore';
import Layout, { HeaderAction } from 'components/Layout';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
@ -15,25 +12,26 @@ import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import styles from './DocumentScene.scss';
@observer
class DocumentScene extends React.Component {
state = {
didScroll: false,
}
componentDidMount = () => {
const documentId = this.props.routeParams.id;
this.props.fetchDocumentAsync(documentId);
const { id } = this.props.routeParams;
store.fetchDocument(id);
}
componentWillReceiveProps = (nextProps) => {
// Scroll to anchor after loading, and only once
const hash = this.props.location.hash;
const { hash } = this.props.location;
if (nextProps.document && hash && !this.state.didScroll) {
if (nextProps.doc && hash && !this.state.didScroll) {
const name = hash.split('#')[1];
setTimeout(() => {
this.setState({ didScroll: true });
const element = document.getElementsByName(name)[0];
const element = doc.getElementsByName(name)[0];
if (element) element.scrollIntoView()
}, 0);
}
@ -41,29 +39,26 @@ class DocumentScene extends React.Component {
onDelete = () => {
if (confirm("Are you sure you want to delete this document?")) {
this.props.deleteDocument(
this.props.document.id,
`/atlas/${ this.props.document.atlas.id }`,
);
store.deleteDocument();
};
}
render() {
const document = this.props.document;
const doc = store.document;
let title;
let actions;
if (document) {
if (doc) {
actions = (
<div className={ styles.actions }>
<HeaderAction>
<Link to={ `/documents/${document.id}/edit` }>Edit</Link>
<Link to={ `/documents/${doc.id}/edit` }>Edit</Link>
</HeaderAction>
<DropdownMenu label="More">
<MenuItem onClick={ this.onDelete }>Delete</MenuItem>
</DropdownMenu>
</div>
);
title = `${document.atlas.name} - ${document.title}`;
title = `${doc.atlas.name} - ${doc.title}`;
}
return (
@ -72,10 +67,10 @@ class DocumentScene extends React.Component {
actions={ actions }
>
<CenteredContent>
{ this.props.isLoading || !document ? (
{ store.isFetching ? (
<AtlasPreviewLoading />
) : (
<Document document={ document } />
<Document document={ doc } />
) }
</CenteredContent>
</Layout>
@ -83,22 +78,4 @@ class DocumentScene extends React.Component {
}
};
const mapStateToProps = (state) => {
return {
isLoading: state.document.isLoading,
document: state.document.data,
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
fetchDocumentAsync,
deleteDocument,
}, dispatch)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(DocumentScene);
export default DocumentScene;

View File

@ -0,0 +1,39 @@
import { observable, action } from 'mobx';
import { client } from 'utils/ApiClient';
import { browserHistory } from 'react-router';
const store = new class DocumentSceneStore {
@observable document;
@observable isFetching = true;
@observable isDeleting;
/* Actions */
@action fetchDocument = async (id) => {
this.isFetching = true;
try {
const res = await client.post('/documents.info', { id: id });
const { data } = res;
this.document = data;
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
@action deleteDocument = async () => {
this.isFetching = true;
try {
const res = await client.post('/documents.delete', { id: this.document.id });
browserHistory.push(`/atlas/${this.document.atlas.id}`);
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
}();
export default store;

View File

@ -2,14 +2,14 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
resetEditor,
updateText,
replaceText,
} from 'actions/EditorActions';
import {
saveDocumentAsync,
} from 'actions/DocumentActions';
// import {
// resetEditor,
// updateText,
// replaceText,
// } from 'actions/EditorActions';
// import {
// saveDocumentAsync,
// } from 'actions/DocumentActions';
import Layout, { Title, HeaderAction } from 'components/Layout';
import Flex from 'components/Flex';
@ -81,26 +81,21 @@ class Editor extends Component {
}
}
const mapStateToProps = (state) => {
return {
text: state.editor.text,
title: state.editor.title,
isSaving: state.document.isSaving,
};
};
// const mapStateToProps = (state) => {
// return {
// text: state.editor.text,
// title: state.editor.title,
// isSaving: state.document.isSaving,
// };
// };
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
resetEditor,
updateText,
replaceText,
saveDocumentAsync,
}, dispatch)
};
Editor = connect(
mapStateToProps,
mapDispatchToProps,
)(Editor);
// const mapDispatchToProps = (dispatch) => {
// return bindActionCreators({
// resetEditor,
// updateText,
// replaceText,
// saveDocumentAsync,
// }, dispatch)
// };
export default Editor;

View File

@ -1,19 +0,0 @@
import constants from '../constants';
export default {
setToken(token) {
localStorage.setItem(constants.JWT_STORE_KEY, token);
},
getToken() {
return localStorage.getItem(constants.JWT_STORE_KEY);
},
logout() {
localStorage.removeItem(constants.JWT_STORE_KEY);
},
loggedIn() {
return !!localStorage.getItem(constants.JWT_STORE_KEY);
},
};

View File

@ -54,5 +54,5 @@ module.exports = {
'fetch': 'imports?this=>global!exports?global.fetch!isomorphic-fetch'
}),
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/)
]
],
};