Saving and fetching of documents

This commit is contained in:
Jori Lallo 2016-05-19 20:46:34 -07:00
parent 58e588a6fd
commit 4430a3129e
14 changed files with 332 additions and 16 deletions

63
server/api/documents.js Normal file
View File

@ -0,0 +1,63 @@
import Router from 'koa-router';
import httpErrors from 'http-errors';
import auth from './authentication';
import pagination from './middlewares/pagination';
import { presentDocument } from '../presenters';
import { Document, Atlas } from '../models';
const router = new Router();
router.post('documents.info', auth(), async (ctx) => {
let { id } = ctx.request.body;
ctx.assertPresent(id, 'id is required');
const team = await ctx.state.user.getTeam();
const document = await Document.findOne({
where: {
id: id,
teamId: team.id,
},
});
if (!document) throw httpErrors.NotFound();
ctx.body = {
data: await presentDocument(document, true),
};
});
router.post('documents.create', auth(), async (ctx) => {
let {
atlas,
title,
text,
} = ctx.request.body;
ctx.assertPresent(atlas, 'atlas is required');
ctx.assertPresent(title, 'title is required');
ctx.assertPresent(text, 'text is required');
const team = await ctx.state.user.getTeam();
const ownerAtlas = await Atlas.findOne({
where: {
id: atlas,
teamId: team.id,
},
});
if (!ownerAtlas) throw httpErrors.BadRequest();
const document = await Document.create({
atlasId: ownerAtlas.id,
teamId: team.id,
title: title,
text: text,
});
ctx.body = {
data: await presentDocument(document, true),
};
});
export default router;

View File

@ -7,6 +7,7 @@ import Sequelize from 'sequelize';
import auth from './auth';
import user from './user';
import atlases from './atlases';
import documents from './documents';
import validation from './validation';
@ -44,6 +45,7 @@ api.use(validation());
router.use('/', auth.routes());
router.use('/', user.routes());
router.use('/', atlases.routes());
router.use('/', documents.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.

View File

@ -7,11 +7,11 @@ import Team from './Team';
const Document = sequelize.define('document', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
name: DataTypes.STRING,
content: DataTypes.STRING,
title: DataTypes.STRING,
text: DataTypes.TEXT,
});
Document.belongsTo(Atlas);
Document.belongsTo(Atlas, { as: 'atlas' });
Document.belongsTo(Team);
export default Atlas;
export default Document;

View File

@ -3,4 +3,9 @@ import Team from './Team';
import Atlas from './Atlas';
import Document from './Document';
export { User, Team, Atlas, Document };
export {
User,
Team,
Atlas,
Document,
};

View File

@ -1,3 +1,5 @@
var marked = require('marked');
export function presentUser(user) {
return {
id: user.id,
@ -15,7 +17,7 @@ export function presentTeam(team) {
};
}
export function presentAtlas(atlas) {
export function presentAtlas(atlas, includeRecentDocuments=true) {
return {
id: atlas.id,
name: atlas.name,
@ -23,4 +25,24 @@ export function presentAtlas(atlas) {
type: atlas.type,
recentDocuments: atlas.getRecentDocuments(),
}
}
}
export async function presentDocument(document, includeAtlas=false) {
const data = {
id: document.id,
title: document.title,
text: document.text,
html: marked(document.text),
createdAt: document.createdAt,
updatedAt: document.updatedAt,
atlas: document.atlaId,
team: document.teamId,
}
if (includeAtlas) {
const atlas = await document.getAtlas();
data.atlas = presentAtlas(atlas, false);
}
return data;
}

View File

@ -1,12 +1,14 @@
import { combineReducers } from 'redux';
import atlases from './atlases';
import document from './document';
import team from './team';
import editor from './editor';
import user from './user';
export default combineReducers({
atlases,
document,
team,
editor,
user,

View File

@ -0,0 +1,63 @@
import makeActionCreator from '../utils/actions';
import { replace } from 'react-router-redux';
import { client } from 'utils/ApiClient';
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));
})
};
};
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;
if (documentId) {
url = '/documents.update'
} else {
url = '/documents.create'
}
client.post(url, {
atlas: atlasId,
document: documentId,
title,
text,
})
.then(data => {
dispatch(saveDocumentSuccess(data.data, data.pagination));
dispatch(replace(`/documents/${data.data.id}`));
})
.catch((err) => {
dispatch(saveDocumentFailure(err));
})
};
};

View File

@ -23,6 +23,7 @@ import Home from 'scenes/Home';
import Editor from 'scenes/Editor';
import Dashboard from 'scenes/Dashboard';
import Atlas from 'scenes/Atlas';
import Document from 'scenes/Document';
import SlackAuth from 'scenes/SlackAuth';
// Redux
@ -51,8 +52,7 @@ persistStore(store, {
<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="/editor" component={Dashboard} onEnter={ requireAuth } />
<Route path="/documents/:id" component={ Document } onEnter={ requireAuth } />
<Route path="/auth/slack" component={SlackAuth} />
</Route>

63
src/reducers/document.js Normal file
View File

@ -0,0 +1,63 @@
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,
}
const doc = (state = initialState, action) => {
switch (action.type) {
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,
isLoading: true,
};
}
case SAVE_DOCUMENT_SUCCESS: {
return {
data: action.date,
isLoading: false,
};
}
case SAVE_DOCUMENT_FAILURE: {
return {
...state,
error: action.error,
isLoading: false,
};
}
default:
return state;
}
};
export default doc;

View File

@ -0,0 +1,58 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchDocumentAsync } from 'actions/DocumentActions';
import Layout from 'components/Layout';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
import styles from './Document.scss';
class Document extends React.Component {
componentDidMount = () => {
const documentId = this.props.routeParams.id;
this.props.fetchDocumentAsync(documentId);
}
render() {
const document = this.props.document;
let title;
if (document) {
title = `${document.atlas.name} - ${document.title}`;
}
return (
<Layout
title={ title }
>
<CenteredContent>
{ this.props.isLoading || !document ? (
<AtlasPreviewLoading />
) : (
<div dangerouslySetInnerHTML={{ __html: document.html }} />
) }
</CenteredContent>
</Layout>
);
}
};
const mapStateToProps = (state) => {
return {
isLoading: state.document.isLoading,
document: state.document.data,
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
fetchDocumentAsync,
}, dispatch)
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Document);

View File

View File

@ -0,0 +1,2 @@
import Document from './Document';
export default Document;

View File

@ -6,6 +6,9 @@ import {
updateText,
replaceText,
} from 'actions/EditorActions';
import {
saveDocumentAsync,
} from 'actions/DocumentActions';
import styles from './Editor.scss';
import 'assets/styles/codemirror.css';
@ -21,10 +24,30 @@ class Editor extends Component {
static propTypes = {
updateText: React.PropTypes.func.isRequired,
replaceText: React.PropTypes.func.isRequired,
saveDocumentAsync: React.PropTypes.func.isRequired,
text: React.PropTypes.string,
title: React.PropTypes.string,
}
componentDidMount = () => {
const atlasId = this.props.routeParams.id;
this.setState({ atlasId: atlasId });
}
onSave = () => {
if (this.props.title.length === 0) {
alert("Please add a title before saving (hint: Write a markdown header)");
return
}
this.props.saveDocumentAsync(
this.state.atlasId,
null,
this.props.title,
this.props.text,
)
}
render() {
let title = (
<Title
@ -39,7 +62,7 @@ class Editor extends Component {
<Layout
actions={(
<Flex direction="row" align="center">
<SaveAction />
<SaveAction onClick={ this.onSave } />
<MoreAction />
</Flex>
)}
@ -60,6 +83,7 @@ const mapStateToProps = (state) => {
return {
text: state.editor.text,
title: state.editor.title,
isSaving: state.document.isLoading,
};
};
@ -67,6 +91,7 @@ const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
updateText,
replaceText,
saveDocumentAsync,
}, dispatch)
};

View File

@ -1,12 +1,23 @@
import React from 'react';
import { Arrow } from 'rebass';
const SaveAction = (props) => {
return (
<div>
Save
</div>
);
class SaveAction extends React.Component {
propTypes = {
onClick: React.PropTypes.func.isRequired,
}
onClick = (event) => {
event.preventDefault();
this.props.onClick();
}
render() {
return (
<div>
<a href onClick={ this.onClick }>Save</a>
</div>
);
}
};
export default SaveAction;