Saving and fetching of documents
This commit is contained in:
parent
58e588a6fd
commit
4430a3129e
|
@ -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;
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
})
|
||||
};
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
|
@ -0,0 +1,2 @@
|
|||
import Document from './Document';
|
||||
export default Document;
|
|
@ -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)
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
Reference in New Issue