Added more views and atlas APIs
This commit is contained in:
parent
84ba65f72a
commit
cbe9c0b6ee
|
@ -0,0 +1,52 @@
|
|||
import Router from 'koa-router';
|
||||
import httpErrors from 'http-errors';
|
||||
|
||||
import auth from './authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentAtlas } from '../presenters';
|
||||
import { Team, Atlas } from '../models';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post('atlases.info', auth(), async (ctx) => {
|
||||
let { id } = ctx.request.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
|
||||
const team = await ctx.state.user.getTeam();
|
||||
const atlas = await Atlas.findOne({
|
||||
where: {
|
||||
id: id,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!atlas) throw httpErrors.NotFound();
|
||||
|
||||
ctx.body = {
|
||||
data: presentAtlas(atlas),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
router.post('atlases.list', auth(), pagination(), async (ctx) => {
|
||||
let { teamId } = ctx.request.body;
|
||||
ctx.assertPresent(teamId, 'teamId is required');
|
||||
|
||||
const team = await ctx.state.user.getTeam();
|
||||
const atlases = await Atlas.findAll({
|
||||
where: {
|
||||
teamId: teamId,
|
||||
},
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: atlases.map((atlas) => {
|
||||
return presentAtlas(atlas);
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -6,6 +6,7 @@ import Sequelize from 'sequelize';
|
|||
|
||||
import auth from './auth';
|
||||
import user from './user';
|
||||
import atlases from './atlases';
|
||||
|
||||
import validation from './validation';
|
||||
|
||||
|
@ -42,6 +43,7 @@ api.use(validation());
|
|||
|
||||
router.use('/', auth.routes());
|
||||
router.use('/', user.routes());
|
||||
router.use('/', atlases.routes());
|
||||
|
||||
// Router is embedded in a Koa application wrapper, because koa-router does not
|
||||
// allow middleware to catch any routes which were not explicitly defined.
|
||||
|
|
|
@ -4,9 +4,19 @@ import {
|
|||
} from '../sequelize';
|
||||
import Team from './Team';
|
||||
|
||||
const allowedAtlasTypes = [['atlas', 'journal']];
|
||||
|
||||
const Atlas = sequelize.define('atlas', {
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.STRING,
|
||||
type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes }},
|
||||
}, {
|
||||
instanceMethods: {
|
||||
getRecentDocuments() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Atlas.belongsTo(Team);
|
||||
|
|
|
@ -23,6 +23,9 @@ const User = sequelize.define('user', {
|
|||
getJwtToken() {
|
||||
return JWT.sign({ id: this.id }, this.jwtSecret);
|
||||
},
|
||||
async getTeam() {
|
||||
return this.team;
|
||||
}
|
||||
},
|
||||
indexes: [
|
||||
{
|
||||
|
|
|
@ -14,3 +14,13 @@ export function presentTeam(team) {
|
|||
name: team.name,
|
||||
};
|
||||
}
|
||||
|
||||
export function presentAtlas(atlas) {
|
||||
return {
|
||||
id: atlas.id,
|
||||
name: atlas.name,
|
||||
description: atlas.description,
|
||||
type: atlas.type,
|
||||
recentDocuments: atlas.getRecentDocuments(),
|
||||
}
|
||||
}
|
|
@ -106,6 +106,7 @@ const text = (state = textDefaultState, action) => {
|
|||
|
||||
import team from './team';
|
||||
import user from './user';
|
||||
import atlases from './atlases';
|
||||
|
||||
export default combineReducers({
|
||||
activeEditors,
|
||||
|
@ -113,4 +114,5 @@ export default combineReducers({
|
|||
text,
|
||||
user,
|
||||
team,
|
||||
atlases,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import makeActionCreator from '../utils/actions';
|
||||
import { client } from 'utils/ApiClient';
|
||||
|
||||
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, 'items', 'pagination');
|
||||
const fetchAtlasesFailure = makeActionCreator(FETCH_ATLASES_FAILURE, 'error');
|
||||
|
||||
export function fetchAtlasesAsync(teamId) {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchAtlasesPending());
|
||||
|
||||
client.post('/atlases.list', {
|
||||
teamId: teamId,
|
||||
})
|
||||
.then(data => {
|
||||
dispatch(fetchAtlasesSuccess(data.data, 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 => {
|
||||
dispatch(fetchAtlasSuccess(data.data,));
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch(fetchAtlasFailure(err));
|
||||
})
|
||||
};
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import Link from 'react-router/lib/Link';
|
||||
|
||||
import styles from './AtlasPreview.scss';
|
||||
import classNames from 'classnames/bind';
|
||||
const cx = classNames.bind(styles);
|
||||
|
||||
class AtlasPreview extends React.Component {
|
||||
static propTypes = {
|
||||
data: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<h2><Link to={ `/atlas/${this.props.data.id}` } className={ styles.atlasLink }>{ this.props.data.name }</Link></h2>
|
||||
<div>No documents. Why not <Link to='/new-document'>create one</Link>?</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AtlasPreview;
|
|
@ -0,0 +1,6 @@
|
|||
@import '../../utils/constants.scss';
|
||||
|
||||
.atlasLink {
|
||||
text-decoration: none;
|
||||
color: $textColor;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import AtlasPreview from './AtlasPreview';
|
||||
export default AtlasPreview;
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
|
||||
import styles from './CenteredContent.scss';
|
||||
|
||||
const CenteredContent = (props) => {
|
||||
const style = {
|
||||
maxWidth: props.maxWidth,
|
||||
...props.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ styles.content } style={ style }>
|
||||
{ props.children }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CenteredContent.defaultProps = {
|
||||
maxWidth: '600px',
|
||||
};
|
||||
|
||||
CenteredContent.propTypes = {
|
||||
children: React.PropTypes.node.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
};
|
||||
|
||||
export default CenteredContent;
|
|
@ -0,0 +1,5 @@
|
|||
.content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin: 40px 20px;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import CenteredContent from './CenteredContent';
|
||||
export default CenteredContent;
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Link from 'react-router/lib/Link';
|
||||
|
||||
import HeaderMenu from './components/HeaderMenu';
|
||||
|
||||
|
@ -14,7 +15,9 @@ class Layout extends React.Component {
|
|||
return (
|
||||
<div className={ styles.container }>
|
||||
<div className={ styles.header }>
|
||||
<div className={ styles.teamName }>Coinbase</div>
|
||||
<div className={ styles.teamName }>
|
||||
<Link to="/">{ this.props.teamName }</Link>
|
||||
</div>
|
||||
<HeaderMenu>
|
||||
<img src={ this.props.avatarUrl } />
|
||||
</HeaderMenu>
|
||||
|
@ -29,6 +32,7 @@ class Layout extends React.Component {
|
|||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
teamName: state.team ? state.team.name : null,
|
||||
avatarUrl: state.user ? state.user.avatarUrl : null,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import '../../utils/constants.scss';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -16,9 +18,11 @@
|
|||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.teamName {
|
||||
.teamName a {
|
||||
font-family: 'Atlas Grotesk';
|
||||
font-weight: bold;
|
||||
color: $textColor;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
10
src/index.js
10
src/index.js
|
@ -22,6 +22,7 @@ import 'fonts/atlas/atlas.css';
|
|||
import Home from 'scenes/Home';
|
||||
// import App from 'scenes/App';
|
||||
import Dashboard from 'scenes/Dashboard';
|
||||
import Atlas from 'scenes/Atlas';
|
||||
import SlackAuth from 'scenes/SlackAuth';
|
||||
|
||||
// Redux
|
||||
|
@ -47,12 +48,10 @@ persistStore(store, {
|
|||
<Route path="/">
|
||||
<IndexRoute component={Home} />
|
||||
|
||||
<Route path="/dashboard" component={Dashboard
|
||||
} onEnter={ requireAuth } />
|
||||
<Route path="/atlas/:id" component={Dashboard} onEnter={ requireAuth } />
|
||||
<Route path="/atlas/:id/new" component={Dashboard} onEnter={ requireAuth } />
|
||||
<Route path="/dashboard" component={ Dashboard } onEnter={ requireAuth } />
|
||||
<Route path="/atlas/:id" component={ Atlas } onEnter={ requireAuth } />
|
||||
|
||||
<Route path="/editor" component={Dashboard} />
|
||||
<Route path="/editor" component={Dashboard} onEnter={ requireAuth } />
|
||||
|
||||
<Route path="/auth/slack" component={SlackAuth} />
|
||||
</Route>
|
||||
|
@ -69,4 +68,3 @@ function requireAuth(nextState, replace) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
FETCH_ATLASES_PENDING,
|
||||
FETCH_ATLASES_SUCCESS,
|
||||
FETCH_ATLASES_FAILURE,
|
||||
} from 'actions/AtlasActions';
|
||||
|
||||
const initialState = {
|
||||
items: [],
|
||||
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,
|
||||
items: action.items,
|
||||
pagination: action.pagination,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
case FETCH_ATLASES_FAILURE: {
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
error: action.error,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default atlases;
|
|
@ -0,0 +1,85 @@
|
|||
import React from 'react';
|
||||
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 Layout from 'components/Layout';
|
||||
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
|
||||
import styles from './Atlas.scss';
|
||||
|
||||
class Atlas extends React.Component {
|
||||
static propTypes = {
|
||||
atlas: React.PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
isLoading: true,
|
||||
data: null,
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
const { id } = this.props.params;
|
||||
|
||||
// this.props.fetchAtlasAsync(id);
|
||||
|
||||
// Temp before breaking out redux store
|
||||
client.post('/atlases.info', {
|
||||
id: id,
|
||||
})
|
||||
.then(data => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
data: data.data
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.state.data;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<CenteredContent>
|
||||
{ this.state.isLoading ? (
|
||||
<AtlasPreviewLoading />
|
||||
) : (
|
||||
<div className={ styles.container }>
|
||||
<div className={ styles.atlasDetails }>
|
||||
<h2>{ data.name }</h2>
|
||||
<blockquote>
|
||||
{ data.description }
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<div className={ styles.divider }><span></span></div>
|
||||
</div>
|
||||
) }
|
||||
</CenteredContent>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
isLoading: state.atlases.isLoading,
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return bindActionCreators({
|
||||
replace,
|
||||
fetchAtlasAsync,
|
||||
}, dispatch)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Atlas);
|
|
@ -0,0 +1,31 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.atlasDetails {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
blockquote {
|
||||
padding: 0;
|
||||
margin: 0 0 20px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
margin: 20px 0;
|
||||
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import Atlas from './Atlas';
|
||||
export default Atlas;
|
|
@ -1,37 +1,53 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { replace } from 'react-router-redux';
|
||||
|
||||
import { client } from 'utils/ApiClient';
|
||||
import { fetchAtlasesAsync } from 'actions/AtlasActions';
|
||||
|
||||
import Layout from 'components/Layout';
|
||||
import AtlasPreview from 'components/AtlasPreview';
|
||||
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
|
||||
import styles from './Dashboard.scss';
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
static propTypes = {
|
||||
replace: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.props.fetchAtlasesAsync(this.props.teamId);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout
|
||||
header={<div>header!</div>}
|
||||
>
|
||||
<div className={ styles.content }>
|
||||
<AtlasPreviewLoading />
|
||||
</div>
|
||||
<Layout>
|
||||
<CenteredContent>
|
||||
{ this.props.isLoading ? (
|
||||
<AtlasPreviewLoading />
|
||||
) : this.props.items.map((item) => {
|
||||
return (<AtlasPreview data={ item } />);
|
||||
}) }
|
||||
</CenteredContent>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
teamId: state.team ? state.team.id : null,
|
||||
isLoading: state.atlases.isLoading,
|
||||
items: state.atlases.items,
|
||||
}
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return bindActionCreators({ replace }, dispatch)
|
||||
return bindActionCreators({
|
||||
fetchAtlasesAsync,
|
||||
}, dispatch)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null, mapDispatchToProps
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Dashboard);
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
max-width: 600px;
|
||||
|
||||
margin: 40px 20px;
|
||||
}
|
Reference in New Issue