From 4c6964ad073ffbbd3264d3ddc58257e59c65a0f9 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Wed, 25 May 2016 21:26:06 -0700 Subject: [PATCH] Document editing --- package.json | 1 + server/api/documents.js | 30 ++++ server/models/Document.js | 4 + src/actions/DocumentActions.js | 24 ++-- src/actions/EditorActions.js | 12 +- .../MarkdownEditor/MarkdownEditor.js | 14 +- src/index.js | 3 + src/reducers/document.js | 5 + src/reducers/editor.js | 31 ++-- src/scenes/Atlas/Atlas.js | 2 +- src/scenes/DocumentEdit/DocumentEdit.js | 134 ++++++++++++++++++ .../DocumentEdit/components/SaveAction.js | 23 +++ src/scenes/DocumentEdit/index.js | 2 + src/scenes/DocumentScene/DocumentScene.js | 4 + src/scenes/Editor/Editor.js | 11 +- src/scenes/Editor/Editor.scss | 13 -- 16 files changed, 261 insertions(+), 52 deletions(-) create mode 100644 src/scenes/DocumentEdit/DocumentEdit.js create mode 100644 src/scenes/DocumentEdit/components/SaveAction.js create mode 100644 src/scenes/DocumentEdit/index.js delete mode 100644 src/scenes/Editor/Editor.scss diff --git a/package.json b/package.json index 09f274fee..bc215ea05 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/api/documents.js b/server/api/documents.js index 85077f66c..b96c45149 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -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; \ No newline at end of file diff --git a/server/models/Document.js b/server/models/Document.js index c6995e6d9..9e8544a9b 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -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); + }, } }); diff --git a/src/actions/DocumentActions.js b/src/actions/DocumentActions.js index 877985382..54a93a940 100644 --- a/src/actions/DocumentActions.js +++ b/src/actions/DocumentActions.js @@ -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}`)); diff --git a/src/actions/EditorActions.js b/src/actions/EditorActions.js index 2f5726e35..329230094 100644 --- a/src/actions/EditorActions.js +++ b/src/actions/EditorActions.js @@ -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'); diff --git a/src/components/MarkdownEditor/MarkdownEditor.js b/src/components/MarkdownEditor/MarkdownEditor.js index b0e730e71..de222b591 100644 --- a/src/components/MarkdownEditor/MarkdownEditor.js +++ b/src/components/MarkdownEditor/MarkdownEditor.js @@ -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, `![${file.name}](${data.asset.url})`); + this.props.replaceText({ + original: pendingUploadTag, + new: `![${file.name}](${data.asset.url})` + }); editor.setCursor(newCursorPositionLine, 0); }) .catch(err => { - this.props.replaceText(pendingUploadTag, ''); + this.props.replaceText({ + original: pendingUploadTag, + new: '', + }); editor.setCursor(newCursorPositionLine, 0); }); }); diff --git a/src/index.js b/src/index.js index 5f121fc25..7629245f5 100644 --- a/src/index.js +++ b/src/index.js @@ -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, { + diff --git a/src/reducers/document.js b/src/reducers/document.js index e81293cfe..4a2fb04c6 100644 --- a/src/reducers/document.js +++ b/src/reducers/document.js @@ -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, diff --git a/src/reducers/editor.js b/src/reducers/editor.js index dd1ae2dc5..2c1075097 100644 --- a/src/reducers/editor.js +++ b/src/reducers/editor.js @@ -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, diff --git a/src/scenes/Atlas/Atlas.js b/src/scenes/Atlas/Atlas.js index 2c174c660..307228317 100644 --- a/src/scenes/Atlas/Atlas.js +++ b/src/scenes/Atlas/Atlas.js @@ -34,7 +34,7 @@ class Atlas extends React.Component { let actions; let title; - if (this.props.isLoading === false) { + if (!this.props.isLoading) { actions = New document; title = { atlas.name }; } diff --git a/src/scenes/DocumentEdit/DocumentEdit.js b/src/scenes/DocumentEdit/DocumentEdit.js new file mode 100644 index 000000000..5f0016ce8 --- /dev/null +++ b/src/scenes/DocumentEdit/DocumentEdit.js @@ -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 = ( + + { this.props.title } + + ); + + return ( + + + + )} + title={ title } + fixed={ true } + > + { (this.props.isLoading && !this.props.document) ? ( + + + + ) : ( + + ) } + + ); + } +} + +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; diff --git a/src/scenes/DocumentEdit/components/SaveAction.js b/src/scenes/DocumentEdit/components/SaveAction.js new file mode 100644 index 000000000..cba36ddbc --- /dev/null +++ b/src/scenes/DocumentEdit/components/SaveAction.js @@ -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 ( +
+ Save +
+ ); + } +}; + +export default SaveAction; \ No newline at end of file diff --git a/src/scenes/DocumentEdit/index.js b/src/scenes/DocumentEdit/index.js new file mode 100644 index 000000000..548ef8a56 --- /dev/null +++ b/src/scenes/DocumentEdit/index.js @@ -0,0 +1,2 @@ +import DocumentEdit from './DocumentEdit'; +export default DocumentEdit; diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js index 8e156f4a3..4c4a16584 100644 --- a/src/scenes/DocumentScene/DocumentScene.js +++ b/src/scenes/DocumentScene/DocumentScene.js @@ -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 = Edit; title = `${document.atlas.name} - ${document.title}`; } return ( { this.props.isLoading || !document ? ( diff --git a/src/scenes/Editor/Editor.js b/src/scenes/Editor/Editor.js index 591a1d5e7..d7d8fb548 100644 --- a/src/scenes/Editor/Editor.js +++ b/src/scenes/Editor/Editor.js @@ -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, diff --git a/src/scenes/Editor/Editor.scss b/src/scenes/Editor/Editor.scss deleted file mode 100644 index ba81774a1..000000000 --- a/src/scenes/Editor/Editor.scss +++ /dev/null @@ -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 { -} \ No newline at end of file