Document editing
This commit is contained in:
@@ -89,6 +89,7 @@
|
||||
"react-router-redux": "^4.0.4",
|
||||
"rebass": "^0.2.6",
|
||||
"redux": "^3.3.1",
|
||||
"redux-actions": "^0.9.1",
|
||||
"redux-logger": "^2.6.1",
|
||||
"redux-persist": "3.0.3",
|
||||
"redux-thunk": "^2.0.1",
|
||||
|
||||
@@ -62,4 +62,34 @@ router.post('documents.create', auth(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.update', auth(), async (ctx) => {
|
||||
let {
|
||||
id,
|
||||
title,
|
||||
text,
|
||||
} = ctx.request.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
ctx.assertPresent(title, 'title is required');
|
||||
ctx.assertPresent(text, 'text is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const team = await user.getTeam();
|
||||
let document = await Document.findOne({
|
||||
where: {
|
||||
id: id,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) throw httpErrors.BadRequest();
|
||||
|
||||
document.title = title;
|
||||
document.text = text;
|
||||
await document.save();
|
||||
|
||||
ctx.body = {
|
||||
data: await presentDocument(document, true),
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -22,6 +22,10 @@ const Document = sequelize.define('document', {
|
||||
doc.html = convertToMarkdown(doc.text);
|
||||
doc.preview = truncateMarkdown(doc.text, 160);
|
||||
},
|
||||
beforeUpdate: (doc) => {
|
||||
doc.html = convertToMarkdown(doc.text);
|
||||
doc.preview = truncateMarkdown(doc.text, 160);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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');
|
||||
|
||||
export const FETCH_DOCUMENT_PENDING = 'FETCH_DOCUMENT_PENDING';
|
||||
export const FETCH_DOCUMENT_SUCCESS = 'FETCH_DOCUMENT_SUCCESS';
|
||||
@@ -39,18 +42,19 @@ export function saveDocumentAsync(atlasId, documentId, title, text) {
|
||||
dispatch(saveDocumentPending());
|
||||
|
||||
let url;
|
||||
if (documentId) {
|
||||
url = '/documents.update'
|
||||
} else {
|
||||
url = '/documents.create'
|
||||
}
|
||||
|
||||
client.post(url, {
|
||||
atlas: atlasId,
|
||||
document: documentId,
|
||||
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}`));
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import makeActionCreator from '../utils/actions';
|
||||
import { createAction } from 'redux-actions';
|
||||
|
||||
// export const TOGGLE_PREVIEW = 'TOGGLE_PREVIEW';
|
||||
export const UPDATE_TEXT = 'UPDATE_TEXT';
|
||||
export const REPLACE_TEXT = 'REPLACE_TEXT';
|
||||
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');
|
||||
|
||||
// export const togglePreview = makeActionCreator(TOGGLE_PREVIEW);
|
||||
export const updateText = makeActionCreator(UPDATE_TEXT, 'text');
|
||||
export const replaceText = makeActionCreator(REPLACE_TEXT, 'originalText', 'replacedText');
|
||||
|
||||
@@ -30,10 +30,6 @@ class MarkdownAtlas extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
console.log(this.props);
|
||||
}
|
||||
|
||||
onDropAccepted = (files) => {
|
||||
const file = files[0];
|
||||
const editor = this.getEditorInstance();
|
||||
@@ -78,11 +74,17 @@ class MarkdownAtlas extends React.Component {
|
||||
body: formData
|
||||
})
|
||||
.then(s3Response => {
|
||||
this.props.replaceText(pendingUploadTag, ``);
|
||||
this.props.replaceText({
|
||||
original: pendingUploadTag,
|
||||
new: ``
|
||||
});
|
||||
editor.setCursor(newCursorPositionLine, 0);
|
||||
})
|
||||
.catch(err => {
|
||||
this.props.replaceText(pendingUploadTag, '');
|
||||
this.props.replaceText({
|
||||
original: pendingUploadTag,
|
||||
new: '',
|
||||
});
|
||||
editor.setCursor(newCursorPositionLine, 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'normalize.css/normalize.css';
|
||||
import 'utils/base-styles.scss';
|
||||
import 'fonts/atlas/atlas.css';
|
||||
import 'assets/styles/github-gist.scss';
|
||||
import 'assets/styles/codemirror.css';
|
||||
|
||||
import Application from 'scenes/Application';
|
||||
|
||||
@@ -27,6 +28,7 @@ import Editor from 'scenes/Editor';
|
||||
import Dashboard from 'scenes/Dashboard';
|
||||
import Atlas from 'scenes/Atlas';
|
||||
import DocumentScene from 'scenes/DocumentScene';
|
||||
import DocumentEdit from 'scenes/DocumentEdit';
|
||||
import SlackAuth from 'scenes/SlackAuth';
|
||||
|
||||
// Redux
|
||||
@@ -63,6 +65,7 @@ persistStore(store, {
|
||||
<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>
|
||||
|
||||
@@ -16,6 +16,11 @@ const initialState = {
|
||||
|
||||
const doc = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'RESET_DOCUMENT': {
|
||||
return {
|
||||
...initialState,
|
||||
}
|
||||
}
|
||||
case FETCH_DOCUMENT_PENDING: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
UPDATE_TEXT,
|
||||
REPLACE_TEXT,
|
||||
} from 'actions/EditorActions';
|
||||
|
||||
const initialState = {
|
||||
originalText: null,
|
||||
text: null,
|
||||
@@ -21,24 +16,38 @@ const parseHeader = (text) => {
|
||||
|
||||
const editor = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case UPDATE_TEXT: {
|
||||
const title = parseHeader(action.text);
|
||||
case 'EDITOR_RESET': {
|
||||
return {
|
||||
...initialState,
|
||||
}
|
||||
}
|
||||
case 'EDITOR_UPDATE_TITLE': {
|
||||
return {
|
||||
...state,
|
||||
title: action.payload,
|
||||
}
|
||||
}
|
||||
case 'EDITOR_UPDATE_TEXT': {
|
||||
const title = parseHeader(action.payload);
|
||||
|
||||
console.log(title);
|
||||
|
||||
let unsavedChanges = false;
|
||||
if (state.originalText !== action.text) {
|
||||
if (state.originalText !== action.payload) {
|
||||
unsavedChanges = true;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
unsavedChanges,
|
||||
text: action.text,
|
||||
text: action.payload,
|
||||
title: title || state.title,
|
||||
};
|
||||
}
|
||||
case REPLACE_TEXT: {
|
||||
const newText = state.text.replace(action.originalText, action.replacedText);
|
||||
case 'EDITOR_REPLACE_TEXT': {
|
||||
const newText = state.text.replace(
|
||||
action.payload.original,
|
||||
action.payload.new
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -34,7 +34,7 @@ class Atlas extends React.Component {
|
||||
let actions;
|
||||
let title;
|
||||
|
||||
if (this.props.isLoading === false) {
|
||||
if (!this.props.isLoading) {
|
||||
actions = <Link to={ `/atlas/${atlas.id}/new` }>New document</Link>;
|
||||
title = <Title>{ atlas.name }</Title>;
|
||||
}
|
||||
|
||||
134
src/scenes/DocumentEdit/DocumentEdit.js
Normal file
134
src/scenes/DocumentEdit/DocumentEdit.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import {
|
||||
resetEditor,
|
||||
updateText,
|
||||
updateTitle,
|
||||
replaceText,
|
||||
} from 'actions/EditorActions';
|
||||
import {
|
||||
resetDocument,
|
||||
fetchDocumentAsync,
|
||||
saveDocumentAsync,
|
||||
} from 'actions/DocumentActions';
|
||||
|
||||
import Layout, { Title } from 'components/Layout';
|
||||
import Flex from 'components/Flex';
|
||||
import MarkdownEditor from 'components/MarkdownEditor';
|
||||
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
|
||||
import SaveAction from './components/SaveAction';
|
||||
|
||||
class DocumentEdit extends Component {
|
||||
static propTypes = {
|
||||
updateText: React.PropTypes.func.isRequired,
|
||||
updateTitle: React.PropTypes.func.isRequired,
|
||||
replaceText: React.PropTypes.func.isRequired,
|
||||
resetDocument: React.PropTypes.func.isRequired,
|
||||
saveDocumentAsync: React.PropTypes.func.isRequired,
|
||||
text: React.PropTypes.string,
|
||||
title: React.PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
loadingDocument: false,
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
this.props.resetEditor();
|
||||
this.props.resetDocument();
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
const id = this.props.routeParams.id;
|
||||
this.props.fetchDocumentAsync(id);
|
||||
}
|
||||
|
||||
componentWillReceiveProps = (nextProps) => {
|
||||
if (!this.props.document && nextProps.document) {
|
||||
const doc = nextProps.document;
|
||||
this.props.updateText(doc.text);
|
||||
this.props.updateTitle(doc.title);
|
||||
}
|
||||
}
|
||||
|
||||
onSave = () => {
|
||||
if (this.props.title.length === 0) {
|
||||
alert("Please add a title before saving (hint: Write a markdown header)");
|
||||
return
|
||||
}
|
||||
|
||||
this.props.saveDocumentAsync(
|
||||
null,
|
||||
this.props.document.id,
|
||||
this.props.title,
|
||||
this.props.text,
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = (
|
||||
<Title
|
||||
truncate={ 60 }
|
||||
placeholder={ "Untitle document" }
|
||||
>
|
||||
{ this.props.title }
|
||||
</Title>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
actions={(
|
||||
<Flex direction="row" align="center">
|
||||
<SaveAction onClick={ this.onSave } />
|
||||
</Flex>
|
||||
)}
|
||||
title={ title }
|
||||
fixed={ true }
|
||||
>
|
||||
{ (this.props.isLoading && !this.props.document) ? (
|
||||
<CenteredContent>
|
||||
<AtlasPreviewLoading />
|
||||
</CenteredContent>
|
||||
) : (
|
||||
<MarkdownEditor
|
||||
onChange={ this.props.updateText }
|
||||
text={ this.props.text }
|
||||
replaceText={this.props.replaceText}
|
||||
/>
|
||||
) }
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
document: state.document.data,
|
||||
text: state.editor.text,
|
||||
title: state.editor.title,
|
||||
isLoading: state.document.isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return bindActionCreators({
|
||||
resetEditor,
|
||||
updateText,
|
||||
updateTitle,
|
||||
replaceText,
|
||||
resetDocument,
|
||||
fetchDocumentAsync,
|
||||
saveDocumentAsync,
|
||||
}, dispatch)
|
||||
};
|
||||
|
||||
DocumentEdit = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(DocumentEdit);
|
||||
|
||||
export default DocumentEdit;
|
||||
23
src/scenes/DocumentEdit/components/SaveAction.js
Normal file
23
src/scenes/DocumentEdit/components/SaveAction.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Arrow } from 'rebass';
|
||||
|
||||
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;
|
||||
2
src/scenes/DocumentEdit/index.js
Normal file
2
src/scenes/DocumentEdit/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import DocumentEdit from './DocumentEdit';
|
||||
export default DocumentEdit;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Link from 'react-router/lib/Link';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { fetchDocumentAsync } from 'actions/DocumentActions';
|
||||
|
||||
@@ -19,13 +20,16 @@ class DocumentScene extends React.Component {
|
||||
render() {
|
||||
const document = this.props.document;
|
||||
let title;
|
||||
let actions;
|
||||
if (document) {
|
||||
actions = <Link to={ `/documents/${document.id}/edit` }>Edit</Link>;
|
||||
title = `${document.atlas.name} - ${document.title}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={ title }
|
||||
actions={ actions }
|
||||
>
|
||||
<CenteredContent>
|
||||
{ this.props.isLoading || !document ? (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import {
|
||||
resetEditor,
|
||||
updateText,
|
||||
replaceText,
|
||||
} from 'actions/EditorActions';
|
||||
@@ -10,12 +11,11 @@ import {
|
||||
saveDocumentAsync,
|
||||
} from 'actions/DocumentActions';
|
||||
|
||||
import styles from './Editor.scss';
|
||||
import 'assets/styles/codemirror.css';
|
||||
|
||||
import Layout, { Title } from 'components/Layout';
|
||||
import Flex from 'components/Flex';
|
||||
import MarkdownEditor from 'components/MarkdownEditor';
|
||||
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
|
||||
import SaveAction from './components/SaveAction';
|
||||
import MoreAction from './components/MoreAction';
|
||||
@@ -31,7 +31,9 @@ class Editor extends Component {
|
||||
|
||||
componentDidMount = () => {
|
||||
const atlasId = this.props.routeParams.id;
|
||||
this.setState({ atlasId: atlasId });
|
||||
this.setState({
|
||||
atlasId: atlasId,
|
||||
});
|
||||
}
|
||||
|
||||
onSave = () => {
|
||||
@@ -88,6 +90,7 @@ const mapStateToProps = (state) => {
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return bindActionCreators({
|
||||
resetEditor,
|
||||
updateText,
|
||||
replaceText,
|
||||
saveDocumentAsync,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: #fff;
|
||||
font-family: -apple-system, "Helvetica Neue", "Lucida Grande";
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.content {
|
||||
}
|
||||
Reference in New Issue
Block a user