diff --git a/server/api/atlases.js b/server/api/atlases.js index b34e871c..3234d4d0 100644 --- a/server/api/atlases.js +++ b/server/api/atlases.js @@ -56,4 +56,26 @@ router.post('atlases.list', auth(), pagination(), async (ctx) => { }; }); +router.post('atlases.updateNavigationTree', auth(), async (ctx) => { + let { id, tree } = ctx.request.body; + ctx.assertPresent(id, 'id is required'); + + const user = ctx.state.user; + const atlas = await Atlas.findOne({ + where: { + id: id, + teamId: user.teamId, + }, + }); + + if (!atlas) throw httpErrors.NotFound(); + + const newTree = await atlas.updateNavigationTree(tree); + + ctx.body = { + data: await presentAtlas(atlas, true), + tree: newTree, + }; +}); + export default router; diff --git a/server/api/documents.js b/server/api/documents.js index 2fef9508..9d8e0f94 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -46,6 +46,7 @@ router.post('documents.create', auth(), async (ctx) => { atlas, title, text, + parentDocument, } = ctx.request.body; ctx.assertPresent(atlas, 'atlas is required'); ctx.assertPresent(title, 'title is required'); @@ -61,7 +62,18 @@ router.post('documents.create', auth(), async (ctx) => { if (!ownerAtlas) throw httpErrors.BadRequest(); + let parentDocumentObj; + if (parentDocument && ownerAtlas.type === 'atlas') { + parentDocumentObj = await Document.findOne({ + where: { + id: parentDocument, + atlasId: ownerAtlas.id, + }, + }); + } + const document = await Document.create({ + parentDocumentId: parentDocumentObj.id, atlasId: ownerAtlas.id, teamId: user.teamId, userId: user.id, @@ -69,6 +81,10 @@ router.post('documents.create', auth(), async (ctx) => { text: text, }); + // TODO: Move to afterSave hook if possible with imports + ownerAtlas.addNodeToNavigationTree(document); + await ownerAtlas.save(); + ctx.body = { data: await presentDocument(document, true), }; diff --git a/server/models/Atlas.js b/server/models/Atlas.js index a0bcb6f6..f8af3815 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -2,6 +2,7 @@ import { DataTypes, sequelize, } from '../sequelize'; +import _isEqual from 'lodash/isEqual'; import Document from './Document'; const allowedAtlasTypes = [['atlas', 'journal']]; @@ -30,7 +31,11 @@ const Atlas = sequelize.define('atlas', { // }, }, instanceMethods: { - async buildStructure() { + async getStructure() { + if (this.atlasStructure) { + return this.atlasStructure; + } + const getNodeForDocument = async (document) => { const children = await Document.findAll({ where: { parentDocumentId: document.id, @@ -39,28 +44,110 @@ const Atlas = sequelize.define('atlas', { let childNodes = [] await Promise.all(children.map(async (child) => { - console.log(child.id) childNodes.push(await getNodeForDocument(child)); })); return { - name: document.title, + title: document.title, id: document.id, url: document.getUrl(), children: childNodes, }; } - const rootDocument = await Document.findOne({ where: { - parentDocumentId: null, - atlasId: this.id, - }}); + const rootDocument = await Document.findOne({ + where: { + parentDocumentId: null, + atlasId: this.id, + } + }); if (rootDocument) { return await getNodeForDocument(rootDocument); } else { return; // TODO should create a root doc } + }, + async updateNavigationTree(tree) { + let nodeIds = []; + nodeIds.push(tree.id); + + const rootDocument = await Document.findOne({ + where: { + id: tree.id, + atlasId: this.id, + }, + }); + if (!rootDocument) throw new Error; + + let newTree = { + id: tree.id, + title: rootDocument.title, + url: rootDocument.getUrl(), + children: [], + }; + + const getIdsForChildren = async (children) => { + const childNodes = []; + for (const child of children) { + const childDocument = await Document.findOne({ + where: { + id: child.id, + atlasId: this.id, + }, + }); + if (!childDocument) throw new Error; + + childNodes.push({ + id: childDocument.id, + title: childDocument.title, + url: childDocument.getUrl(), + children: await getIdsForChildren(child.children), + }) + nodeIds.push(child.id); + } + return childNodes; + }; + newTree.children = await getIdsForChildren(tree.children); + + const documents = await Document.findAll({ + attributes: ['id'], + where: { + atlasId: this.id, + } + }); + const documentIds = documents.map(doc => doc.id); + + if (!_isEqual(nodeIds.sort(), documentIds.sort())) { + throw new Error('Invalid navigation tree'); + } + + this.atlasStructure = newTree; + await this.save(); + + return newTree; + }, + async addNodeToNavigationTree(document) { + const newNode = { + id: document.id, + title: document.title, + url: document.getUrl(), + children: [], + } + + const insertNode = (node) => { + if (document.parentDocumentId === node.id) { + node.children.push(newNode); + } else { + node.children = node.children.map(childNode => { + return insertNode(childNode); + }) + } + + return node; + }; + + this.atlasStructure = insertNode(this.atlasStructure); } } }); diff --git a/server/presenters.js b/server/presenters.js index 05f27c18..2441dcc0 100644 --- a/server/presenters.js +++ b/server/presenters.js @@ -33,7 +33,7 @@ export function presentAtlas(atlas, includeRecentDocuments=false) { if (atlas.type === 'atlas') { // Todo replace with `.atlasStructure` - data.structure = await atlas.buildStructure(); + data.structure = await atlas.getStructure(); } if (includeRecentDocuments) { diff --git a/src/components/Tree/Node.js b/src/components/Tree/Node.js index fc5bc6c0..ab980dcf 100644 --- a/src/components/Tree/Node.js +++ b/src/components/Tree/Node.js @@ -1,4 +1,5 @@ var React = require('react'); +import history from 'utils/History'; import styles from './Tree.scss'; import classNames from 'classnames/bind'; @@ -79,10 +80,10 @@ var Node = React.createClass({ {!this.props.rootNode && this.renderCollapse()} {}} + onClick={() => { history.push(node.url) }} onMouseDown={this.props.rootNode ? function(e){e.stopPropagation()} : undefined} > - {node.name} + { node.title } {this.renderChildren()} diff --git a/src/index.js b/src/index.js index 3b1a83ff..38cca8e3 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,7 @@ render(( + diff --git a/src/scenes/DocumentEdit/DocumentEdit.js b/src/scenes/DocumentEdit/DocumentEdit.js index fd011a50..ceacce94 100644 --- a/src/scenes/DocumentEdit/DocumentEdit.js +++ b/src/scenes/DocumentEdit/DocumentEdit.js @@ -26,6 +26,10 @@ class DocumentEdit extends Component { if (this.props.route.newDocument) { store.atlasId = this.props.params.id; store.newDocument = true; + } else if (this.props.route.newChildDocument) { + store.documentId = this.props.params.id; + store.newChildDocument = true; + store.fetchDocument(); } else { store.documentId = this.props.params.id; store.newDocument = false; @@ -44,7 +48,7 @@ class DocumentEdit extends Component { // alert("Please add a title before saving (hint: Write a markdown header)"); // return // } - if (store.newDocument) { + if (store.newDocument || store.newChildDocument) { store.saveDocument(); } else { store.updateDocument(); diff --git a/src/scenes/DocumentEdit/DocumentEditStore.js b/src/scenes/DocumentEdit/DocumentEditStore.js index a2648ffa..155d778b 100644 --- a/src/scenes/DocumentEdit/DocumentEditStore.js +++ b/src/scenes/DocumentEdit/DocumentEditStore.js @@ -18,9 +18,11 @@ const parseHeader = (text) => { const documentEditStore = new class DocumentEditStore { @observable documentId = null; @observable atlasId = null; + @observable parentDocument; @observable title; @observable text; @observable newDocument; + @observable newChildDocument; @observable preview; @observable isFetching; @@ -35,9 +37,13 @@ const documentEditStore = new class DocumentEditStore { const data = await client.post('/documents.info', { id: this.documentId, }) - const { title, text } = data.data; - this.title = title; - this.text = text; + if (this.newDocument) { + const { title, text } = data.data; + this.title = title; + this.text = text; + } else { + this.parentDocument = data.data; + } } catch (e) { console.error("Something went wrong"); } @@ -51,7 +57,8 @@ const documentEditStore = new class DocumentEditStore { try { const data = await client.post('/documents.create', { - atlas: this.atlasId, + parentDocument: this.parentDocument && this.parentDocument.id, + atlas: this.atlasId || this.parentDocument.atlas.id, title: this.title, text: this.text, }) diff --git a/src/scenes/DocumentEdit/components/SaveAction.js b/src/scenes/DocumentEdit/components/SaveAction.js index 948cb59e..9f509d79 100644 --- a/src/scenes/DocumentEdit/components/SaveAction.js +++ b/src/scenes/DocumentEdit/components/SaveAction.js @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; @observer class SaveAction extends React.Component { - propTypes = { + static propTypes = { onClick: React.PropTypes.func.isRequired, disabled: React.PropTypes.bool, } diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js index 46778849..e9ac2937 100644 --- a/src/scenes/DocumentScene/DocumentScene.js +++ b/src/scenes/DocumentScene/DocumentScene.js @@ -30,6 +30,13 @@ class DocumentScene extends React.Component { } componentWillReceiveProps = (nextProps) => { + // Reload on url change + const oldId = this.props.params.id; + const newId = nextProps.params.id; + if (oldId !== newId) { + store.fetchDocument(newId); + } + // Scroll to anchor after loading, and only once const { hash } = this.props.location; @@ -57,17 +64,10 @@ class DocumentScene extends React.Component { ); } - // onClickNode = (node) => { - // this.setState({ - // active: node - // }); - // } - - // handleChange = (tree) => { - // this.setState({ - // tree: tree - // }); - // } + handleChange = (tree) => { + console.log(tree); + store.updateNavigationTree(tree); + } render() { const doc = store.document; @@ -114,7 +114,7 @@ class DocumentScene extends React.Component { { store.isAtlas ? (
{ + this.isFetching = true; + + try { + const res = await client.post('/atlases.updateNavigationTree', { + id: this.document.atlas.id, + tree: tree, + }); + } catch (e) { + console.error("Something went wrong"); + } + this.isFetching = false; + } }(); export default store; \ No newline at end of file