From 591050a1e3d8271c92ee363a7284ff98db48a9a7 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sat, 18 Jun 2016 11:40:11 -0700 Subject: [PATCH 01/14] Initial navigation tree --- package.json | 1 + src/components/Tree/Node.js | 108 ++++++++ src/components/Tree/Tree.js | 68 +++++ src/components/Tree/Tree.scss | 79 ++++++ src/components/Tree/UiTree.js | 266 ++++++++++++++++++++ src/components/Tree/assets/chevron.svg | 1 + src/components/Tree/index.js | 2 + src/scenes/DocumentScene/DocumentScene.js | 82 +++++- src/scenes/DocumentScene/DocumentScene.scss | 7 +- 9 files changed, 607 insertions(+), 7 deletions(-) create mode 100644 src/components/Tree/Node.js create mode 100644 src/components/Tree/Tree.js create mode 100644 src/components/Tree/Tree.scss create mode 100644 src/components/Tree/UiTree.js create mode 100644 src/components/Tree/assets/chevron.svg create mode 100644 src/components/Tree/index.js diff --git a/package.json b/package.json index 80d5086d4..ae29becb1 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "http-errors": "^1.4.0", "imports-loader": "^0.6.5", "isomorphic-fetch": "^2.2.1", + "js-tree": "^1.1.0", "json-loader": "^0.5.4", "jsonwebtoken": "^5.7.0", "koa": "^2.0.0", diff --git a/src/components/Tree/Node.js b/src/components/Tree/Node.js new file mode 100644 index 000000000..ab58dcc44 --- /dev/null +++ b/src/components/Tree/Node.js @@ -0,0 +1,108 @@ +var React = require('react'); + +import styles from './Tree.scss'; +import classNames from 'classnames/bind'; +const cx = classNames.bind(styles); + +var Node = React.createClass({ + displayName: 'UITreeNode', + + renderCollapse() { + var index = this.props.index; + + if(index.children && index.children.length) { + var collapsed = index.node.collapsed; + + return ( + + + + ); + } + + return null; + }, + + renderChildren() { + var index = this.props.index; + var tree = this.props.tree; + var dragging = this.props.dragging; + + if(index.children && index.children.length) { + var childrenStyles = {}; + + if (!this.props.rootNode) { + if(index.node.collapsed) childrenStyles.display = 'none'; + childrenStyles['paddingLeft'] = this.props.paddingLeft + 'px'; + } + + return ( +
+ {index.children.map((child) => { + var childIndex = tree.getIndex(child); + return ( + + ); + })} +
+ ); + } + + return null; + }, + + render() { + var tree = this.props.tree; + var index = this.props.index; + var dragging = this.props.dragging; + var node = index.node; + var style = {}; + + return ( +
+
+ {!this.props.rootNode && this.renderCollapse()} + {}} + > + {node.module.name} + +
+ {this.renderChildren()} +
+ ); + }, + + handleCollapse(e) { + e.stopPropagation(); + var nodeId = this.props.index.id; + if(this.props.onCollapse) this.props.onCollapse(nodeId); + }, + + handleMouseDown(e) { + var nodeId = this.props.index.id; + var dom = this.refs.inner; + + if(this.props.onDragStart) { + this.props.onDragStart(nodeId, dom, e); + } + } +}); + +module.exports = Node; \ No newline at end of file diff --git a/src/components/Tree/Tree.js b/src/components/Tree/Tree.js new file mode 100644 index 000000000..f5d4b8e3f --- /dev/null +++ b/src/components/Tree/Tree.js @@ -0,0 +1,68 @@ +var Tree = require('js-tree'); +var proto = Tree.prototype; + +proto.updateNodesPosition = function() { + var top = 1; + var left = 1; + var root = this.getIndex(1); + var self = this; + + root.top = top++; + root.left = left++; + + if(root.children && root.children.length) { + walk(root.children, root, left, root.node.collapsed); + } + + function walk(children, parent, left, collapsed) { + var height = 1; + children.forEach(function(id) { + var node = self.getIndex(id); + if(collapsed) { + node.top = null; + node.left = null; + } else { + node.top = top++; + node.left = left; + } + + if(node.children && node.children.length) { + height += walk(node.children, node, left+1, collapsed || node.node.collapsed); + } else { + node.height = 1; + height += 1; + } + }); + + if(parent.node.collapsed) parent.height = 1; + else parent.height = height; + return parent.height; + } +}; + +proto.move = function(fromId, toId, placement) { + if(fromId === toId || toId === 1) return; + + var obj = this.remove(fromId); + var index = null; + + if(placement === 'before') index = this.insertBefore(obj, toId); + else if(placement === 'after') index = this.insertAfter(obj, toId); + else if(placement === 'prepend') index = this.prepend(obj, toId); + else if(placement === 'append') index = this.append(obj, toId); + + // todo: perf + this.updateNodesPosition(); + return index; +}; + +proto.getNodeByTop = function(top) { + var indexes = this.indexes; + for(var id in indexes) { + if(indexes.hasOwnProperty(id)) { + if(indexes[id].top === top) return indexes[id]; + } + } +}; + +module.exports = Tree; \ No newline at end of file diff --git a/src/components/Tree/Tree.scss b/src/components/Tree/Tree.scss new file mode 100644 index 000000000..219c4e8e7 --- /dev/null +++ b/src/components/Tree/Tree.scss @@ -0,0 +1,79 @@ +@mixin no-select { + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.tree { + position: relative; + overflow: hidden; + @include no-select; +} + +.draggable { + position: absolute; + opacity: 0.8; + @include no-select; +} + +.node { + &.placeholder > * { + visibility: hidden; + } + + &.placeholder { + border: 1px dashed #ccc; + } + + .inner { + position: relative; + cursor: pointer; + padding-left: 10px; + } + + .collapse { + position: absolute; + left: 0; + cursor: pointer; + + width: 20px; + height: 25px; + } + + .caretRight { + margin-top: 3px; + margin-left: -3px; + } + + .caretDown { + transform: rotate(90deg); + margin-left: -4px; + margin-top: 2px; + } +} + +.node { + &.placeholder { + border: 1px dashed #1385e5; + } + + .inner { + font-size: 14px; + } + + .nodeLabel { + display: inline-block; + width: 100%; + padding: 4px 5px; + + &.isActive { + background-color: #31363F; + } + } + + .rootLabel { + color: #ccc; + } +} \ No newline at end of file diff --git a/src/components/Tree/UiTree.js b/src/components/Tree/UiTree.js new file mode 100644 index 000000000..640be2c79 --- /dev/null +++ b/src/components/Tree/UiTree.js @@ -0,0 +1,266 @@ +var React = require('react'); +var Tree = require('./tree'); +var Node = require('./node'); + +import styles from './Tree.scss'; + +module.exports = React.createClass({ + displayName: 'UITree', + + propTypes: { + tree: React.PropTypes.object.isRequired, + paddingLeft: React.PropTypes.number, + renderNode: React.PropTypes.func.isRequired + }, + + getDefaultProps() { + return { + paddingLeft: 20 + }; + }, + + getInitialState() { + return this.init(this.props); + }, + + componentWillReceiveProps(nextProps) { + if(!this._updated) this.setState(this.init(nextProps)); + else this._updated = false; + }, + + init(props) { + var tree = new Tree(props.tree); + tree.isNodeCollapsed = props.isNodeCollapsed; + tree.renderNode = props.renderNode; + tree.changeNodeCollapsed = props.changeNodeCollapsed; + tree.updateNodesPosition(); + + return { + tree: tree, + dragging: { + id: null, + x: null, + y: null, + w: null, + h: null + } + }; + }, + + getDraggingDom() { + var tree = this.state.tree; + var dragging = this.state.dragging; + + if(dragging && dragging.id) { + var draggingIndex = tree.getIndex(dragging.id); + var draggingStyles = { + top: dragging.y, + left: dragging.x, + width: dragging.w + }; + + return ( +
+ +
+ ); + } + + return null; + }, + + render() { + var tree = this.state.tree; + var dragging = this.state.dragging; + var draggingDom = this.getDraggingDom(); + + return ( +
+ {draggingDom} + +
+ ); + }, + + dragStart(id, dom, e) { + this.dragging = { + id: id, + w: dom.offsetWidth, + h: dom.offsetHeight, + x: dom.offsetLeft, + y: dom.offsetTop + }; + + this._startX = dom.offsetLeft; + this._startY = dom.offsetTop; + this._offsetX = e.clientX; + this._offsetY = e.clientY; + this._start = true; + + window.addEventListener('mousemove', this.drag); + window.addEventListener('mouseup', this.dragEnd); + }, + + // oh + drag(e) { + if(this._start) { + this.setState({ + dragging: this.dragging + }); + this._start = false; + } + + var tree = this.state.tree; + var dragging = this.state.dragging; + var paddingLeft = this.props.paddingLeft; + var newIndex = null; + var index = tree.getIndex(dragging.id); + var collapsed = index.node.collapsed; + + var _startX = this._startX; + var _startY = this._startY; + var _offsetX = this._offsetX; + var _offsetY = this._offsetY; + + var pos = { + x: _startX + e.clientX - _offsetX, + y: _startY + e.clientY - _offsetY + }; + dragging.x = pos.x; + dragging.y = pos.y; + + var diffX = dragging.x - paddingLeft/2 - (index.left-2) * paddingLeft; + var diffY = dragging.y - dragging.h/2 - (index.top-2) * dragging.h; + + if(diffX < 0) { // left + if(index.parent && !index.next) { + newIndex = tree.move(index.id, index.parent, 'after'); + } + } else if(diffX > paddingLeft) { // right + if(index.prev) { + var prevNode = tree.getIndex(index.prev).node; + if(!prevNode.collapsed && !prevNode.leaf) { + newIndex = tree.move(index.id, index.prev, 'append'); + } + } + } + + if(newIndex) { + index = newIndex; + newIndex.node.collapsed = collapsed; + dragging.id = newIndex.id; + } + + if(diffY < 0) { // up + var above = tree.getNodeByTop(index.top-1); + newIndex = tree.move(index.id, above.id, 'before'); + } else if(diffY > dragging.h) { // down + if(index.next) { + var below = tree.getIndex(index.next); + if(below.children && below.children.length && !below.node.collapsed) { + newIndex = tree.move(index.id, index.next, 'prepend'); + } else { + newIndex = tree.move(index.id, index.next, 'after'); + } + } else { + var below = tree.getNodeByTop(index.top+index.height); + if(below && below.parent !== index.id) { + if(below.children && below.children.length) { + newIndex = tree.move(index.id, below.id, 'prepend'); + } else { + newIndex = tree.move(index.id, below.id, 'after'); + } + } + } + } + + if(newIndex) { + newIndex.node.collapsed = collapsed; + dragging.id = newIndex.id; + } + + this.setState({ + tree: tree, + dragging: dragging + }); + }, + + dragEnd() { + this.setState({ + dragging: { + id: null, + x: null, + y: null, + w: null, + h: null + } + }); + + this.change(this.state.tree); + window.removeEventListener('mousemove', this.drag); + window.removeEventListener('mouseup', this.dragEnd); + }, + + change(tree) { + this._updated = true; + if(this.props.onChange) this.props.onChange(tree.obj); + }, + + toggleCollapse(nodeId) { + var tree = this.state.tree; + var index = tree.getIndex(nodeId); + var node = index.node; + node.collapsed = !node.collapsed; + tree.updateNodesPosition(); + + this.setState({ + tree: tree + }); + + this.change(tree); + }, + + // buildTreeNumbering(tree) { + // const numberBuilder = (index, node, parentNumbering) => { + // let numbering = parentNumbering ? `${parentNumbering}.${index}` : index; + // let children; + // if (node.children) { + // children = node.children.map((child, childIndex) => { + // return numberBuilder(childIndex+1, child, numbering); + // }); + // } + + // const data = { + // module: { + // ...node.module, + // index: numbering, + // } + // } + // if (children) { + // data.children = children; + // } + + // return data; + // }; + + // const newTree = {...tree}; + // newTree.children = []; + // tree.children.forEach((child, index) => { + // newTree.children.push(numberBuilder(index+1, child)); + // }) + // return newTree; + // } +}); diff --git a/src/components/Tree/assets/chevron.svg b/src/components/Tree/assets/chevron.svg new file mode 100644 index 000000000..4daab5920 --- /dev/null +++ b/src/components/Tree/assets/chevron.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Tree/index.js b/src/components/Tree/index.js new file mode 100644 index 000000000..282ed6e1b --- /dev/null +++ b/src/components/Tree/index.js @@ -0,0 +1,2 @@ +import UiTree from './UiTree'; +export default UiTree; \ No newline at end of file diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js index b9bf84956..e0f0f5b47 100644 --- a/src/scenes/DocumentScene/DocumentScene.js +++ b/src/scenes/DocumentScene/DocumentScene.js @@ -9,13 +9,48 @@ import AtlasPreviewLoading from 'components/AtlasPreviewLoading'; import CenteredContent from 'components/CenteredContent'; import Document from 'components/Document'; import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; +import Flex from 'components/Flex'; +import Tree from 'components/Tree'; import styles from './DocumentScene.scss'; +import classNames from 'classnames/bind'; +const cx = classNames.bind(styles); + +import treeStyles from 'components/Tree/Tree.scss'; + +const tree = { + module: { + name: "Introduction", + id: "1", + }, + children: [{ + collapsed: false, + module: { + name: "dist", + id: "2" + }, + children: [ + { + module: { + name: "Details", + id: "21", + }, + }, + { + module: { + name: "Distribution", + id: "22", + }, + } + ] + }] +}; @observer class DocumentScene extends React.Component { state = { didScroll: false, + tree: tree, } componentDidMount = () => { @@ -43,6 +78,26 @@ class DocumentScene extends React.Component { }; } + renderNode = (node) => { + return ( + + {node.module.name} + + ); + } + + onClickNode = (node) => { + this.setState({ + active: node + }); + } + + handleChange = (tree) => { + this.setState({ + tree: tree + }); + } + render() { const doc = store.document; let title; @@ -74,13 +129,28 @@ class DocumentScene extends React.Component { titleText={ titleText } actions={ actions } > - - { store.isFetching ? ( + { store.isFetching ? ( + - ) : ( - - ) } - + + ) : ( + +
+ +
+ + + + + +
+ ) } ); } diff --git a/src/scenes/DocumentScene/DocumentScene.scss b/src/scenes/DocumentScene/DocumentScene.scss index c7d614611..2a9b34235 100644 --- a/src/scenes/DocumentScene/DocumentScene.scss +++ b/src/scenes/DocumentScene/DocumentScene.scss @@ -1,4 +1,9 @@ .actions { display: flex; flex-direction: row; -} \ No newline at end of file +} + +.sidebar { + width: 250px; + padding: 40px 20px; +} From dafd97572ebe452080328d5268cdaeb71bf15d42 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sat, 18 Jun 2016 12:12:36 -0700 Subject: [PATCH 02/14] simplified tree structure --- src/components/Tree/Node.js | 2 +- src/scenes/DocumentScene/DocumentScene.js | 24 ++++++++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/components/Tree/Node.js b/src/components/Tree/Node.js index ab58dcc44..62a1f34bd 100644 --- a/src/components/Tree/Node.js +++ b/src/components/Tree/Node.js @@ -81,7 +81,7 @@ var Node = React.createClass({ className={ cx(styles.nodeLabel, { rootLabel: this.props.rootNode }) } onClick={() => {}} > - {node.module.name} + {node.name} {this.renderChildren()} diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js index e0f0f5b47..e6486abb1 100644 --- a/src/scenes/DocumentScene/DocumentScene.js +++ b/src/scenes/DocumentScene/DocumentScene.js @@ -19,28 +19,20 @@ const cx = classNames.bind(styles); import treeStyles from 'components/Tree/Tree.scss'; const tree = { - module: { - name: "Introduction", - id: "1", - }, + name: "Introduction", + id: "1", children: [{ collapsed: false, - module: { - name: "dist", - id: "2" - }, + name: "dist", + id: "2", children: [ { - module: { - name: "Details", - id: "21", - }, + name: "Details", + id: "21", }, { - module: { - name: "Distribution", - id: "22", - }, + name: "Distribution", + id: "22", } ] }] From b8341611ac18d4fbd9348a7b3a06ca28d6e8edf0 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sat, 18 Jun 2016 12:13:03 -0700 Subject: [PATCH 03/14] Disable root element moving --- src/components/Tree/Node.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Tree/Node.js b/src/components/Tree/Node.js index 62a1f34bd..fc5bc6c0d 100644 --- a/src/components/Tree/Node.js +++ b/src/components/Tree/Node.js @@ -80,6 +80,7 @@ var Node = React.createClass({ {}} + onMouseDown={this.props.rootNode ? function(e){e.stopPropagation()} : undefined} > {node.name} From f2732aacab976071ccd882b0a020afb09f392217 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 19 Jun 2016 14:18:24 -0700 Subject: [PATCH 04/14] Show sidebar if type: atlas --- src/scenes/DocumentScene/DocumentScene.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js index e6486abb1..ac754573b 100644 --- a/src/scenes/DocumentScene/DocumentScene.js +++ b/src/scenes/DocumentScene/DocumentScene.js @@ -127,15 +127,17 @@ class DocumentScene extends React.Component { ) : ( -
- -
+ { doc.atlas.type === 'atlas' ? ( +
+ +
+ ) : null } From 24e02bfdc49c7d44b569d1fdef22853154c571ad Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 20 Jun 2016 00:18:03 -0700 Subject: [PATCH 05/14] New database with migrations --- .sequelizerc | 10 + package.json | 5 +- server/api/atlases.js | 8 +- server/api/auth.js | 37 ++- server/api/documents.js | 15 +- server/config/database.json | 14 ++ server/migrations/20160619080644-initial.js | 258 ++++++++++++++++++++ server/models/Atlas.js | 29 ++- server/models/Document.js | 4 - server/models/Team.js | 18 ++ server/models/User.js | 4 - server/presenters.js | 8 +- 12 files changed, 361 insertions(+), 49 deletions(-) create mode 100644 .sequelizerc create mode 100644 server/config/database.json create mode 100644 server/migrations/20160619080644-initial.js diff --git a/.sequelizerc b/.sequelizerc new file mode 100644 index 000000000..0f26a4e15 --- /dev/null +++ b/.sequelizerc @@ -0,0 +1,10 @@ +require('localenv'); + +var path = require('path'); + +module.exports = { + 'config': path.resolve('server/config', 'database.json'), + 'migrations-path': path.resolve('server', 'migrations'), + 'models-path': path.resolve('server', 'models'), + 'seeders-path': path.resolve('server/models', 'fixtures'), +} diff --git a/package.json b/package.json index ae29becb1..d43dfba2a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "start": "cross-env NODE_ENV=development DEBUG=1 ./node_modules/.bin/nodemon --watch server index.js", "lint": "eslint src", "deploy": "git push heroku master", - "heroku-postbuild": "npm run build" + "heroku-postbuild": "npm run build && npm run sequelize db:migrate", + "sequelize": "./node_modules/.bin/sequelize" }, "repository": { "type": "git", @@ -97,7 +98,7 @@ "safestart": "^0.8.0", "sass-loader": "^3.2.0", "sequelize": "^3.21.0", - "sequelize-cli": "^2.3.1", + "sequelize-cli": "^2.4.0", "sequelize-encrypted": "^0.1.0", "slug": "^0.9.1", "style-loader": "^0.13.0", diff --git a/server/api/atlases.js b/server/api/atlases.js index 44a6921e9..c1fca939e 100644 --- a/server/api/atlases.js +++ b/server/api/atlases.js @@ -13,11 +13,11 @@ router.post('atlases.info', auth(), async (ctx) => { let { id } = ctx.request.body; ctx.assertPresent(id, 'id is required'); - const team = await ctx.state.user.getTeam(); + const user = ctx.state.user; const atlas = await Atlas.findOne({ where: { id: id, - teamId: team.id, + teamId: user.teamId, }, }); @@ -30,10 +30,10 @@ router.post('atlases.info', auth(), async (ctx) => { router.post('atlases.list', auth(), pagination(), async (ctx) => { - const team = await ctx.state.user.getTeam(); + const user = ctx.state.user; const atlases = await Atlas.findAll({ where: { - teamId: team.id, + teamId: user.teamId, }, order: [ ['updatedAt', 'DESC'], diff --git a/server/api/auth.js b/server/api/auth.js index 882637a6f..1f00e0663 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -40,13 +40,27 @@ router.post('auth.slack', async (ctx) => { const authResponse = await fetch(`https://slack.com/api/auth.test?token=${data.access_token}`); const authData = await authResponse.json(); + // Team + let team = await Team.findOne({ where: { slackId: data.team.id } }); + if (!team) { + team = await Team.create({ + name: data.team.name, + slackId: data.team.id, + slackData: data.team, + }); + const atlas = await team.createFirstAtlas(); + } else { + team.name = data.team.name; + team.slackData = data.team; + team = await team.save(); + } + if (user) { user.slackAccessToken = data.access_token; user.slackData = data.user; user = await user.save(); } else { - // Existing user - user = await User.create({ + user = await team.createUser({ slackId: data.user.id, username: authData.user, name: data.user.name, @@ -56,30 +70,11 @@ router.post('auth.slack', async (ctx) => { }); } - // Team - let team = await Team.findOne({ where: { slackId: data.team.id } }); - if (!team) { - team = await Team.create({ - name: data.team.name, - slackId: data.team.id, - slackData: data.team, - }); - } else { - // Update data - team.name = data.team.name; - team.slackData = data.team; - team = await team.save(); - } - - // Add to correct team - user.setTeam(team); - ctx.body = { data: { user: await presentUser(user), team: await presentTeam(team), accessToken: user.getJwtToken(), }}; - console.log("enf") }); export default router; diff --git a/server/api/documents.js b/server/api/documents.js index 9a02389b5..4aef31d13 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -23,8 +23,8 @@ router.post('documents.info', auth({ require: false }), async (ctx) => { if (document.private) { if (!ctx.state.user) throw httpErrors.NotFound(); - const team = await ctx.state.user.getTeam(); - if (document.teamId !== team.id) { + const user = await ctx.state.user; + if (document.teamId !== user.teamId) { throw httpErrors.NotFound(); } @@ -52,11 +52,10 @@ router.post('documents.create', auth(), async (ctx) => { ctx.assertPresent(text, 'text is required'); const user = ctx.state.user; - const team = await user.getTeam(); const ownerAtlas = await Atlas.findOne({ where: { id: atlas, - teamId: team.id, + teamId: user.teamId, }, }); @@ -64,7 +63,7 @@ router.post('documents.create', auth(), async (ctx) => { const document = await Document.create({ atlasId: ownerAtlas.id, - teamId: team.id, + teamId: user.teamId, userId: user.id, title: title, text: text, @@ -86,11 +85,10 @@ router.post('documents.update', auth(), async (ctx) => { 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, + teamId: user.teamId, }, }); @@ -112,11 +110,10 @@ router.post('documents.delete', auth(), async (ctx) => { ctx.assertPresent(id, 'id is required'); const user = ctx.state.user; - const team = await user.getTeam(); let document = await Document.findOne({ where: { id: id, - teamId: team.id, + teamId: user.teamId, }, }); diff --git a/server/config/database.json b/server/config/database.json new file mode 100644 index 000000000..7d739b971 --- /dev/null +++ b/server/config/database.json @@ -0,0 +1,14 @@ +{ + "development": { + "use_env_variable": "DATABASE_URL", + "dialect": "postgres" + }, + "test": { + "use_env_variable": "DATABASE_URL", + "dialect": "postgres" + }, + "production": { + "use_env_variable": "DATABASE_URL", + "dialect": "postgres" + } +} diff --git a/server/migrations/20160619080644-initial.js b/server/migrations/20160619080644-initial.js new file mode 100644 index 000000000..2407ae40c --- /dev/null +++ b/server/migrations/20160619080644-initial.js @@ -0,0 +1,258 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.createTable('atlases', { + id: + { type: 'UUID', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: true }, + name: + { type: 'CHARACTER VARYING', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + description: + { type: 'CHARACTER VARYING', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + type: + { type: 'CHARACTER VARYING', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + atlasStructure: + { type: 'JSONB', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + createdAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + updatedAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + teamId: + { type: 'UUID', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false } } + ); + + // documents + queryInterface.createTable('documents', { + id: + { type: 'UUID', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: true }, + urlId: + { type: 'CHARACTER VARYING', + allowNull: false, + unique: true, + defaultValue: null, + special: [], + primaryKey: false }, + private: + { type: 'BOOLEAN', + allowNull: false, + defaultValue: true, + special: [], + primaryKey: false }, + title: + { type: 'CHARACTER VARYING', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + text: + { type: 'TEXT', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + html: + { type: 'TEXT', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + preview: + { type: 'TEXT', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + createdAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + updatedAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + userId: + { type: 'UUID', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + atlasId: + { type: 'UUID', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + rootDocumentForId: + { type: 'UUID', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + teamId: + { type: 'UUID', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false } + }); + + queryInterface.createTable('teams', { + id: + { type: 'UUID', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: true }, + name: + { type: 'CHARACTER VARYING', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + slackId: + { type: 'CHARACTER VARYING', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: true }, + slackData: + { type: 'JSONB', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + createdAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + updatedAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false } } + ); + + queryInterface.createTable('users', { + id: + { type: 'UUID', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: true }, + email: + { type: 'CHARACTER VARYING', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + username: + { type: 'CHARACTER VARYING', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + name: + { type: 'CHARACTER VARYING', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + isAdmin: + { type: 'BOOLEAN', + allowNull: true, + defaultValue: false, + special: [], + primaryKey: false }, + slackAccessToken: + { type: 'bytea', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + slackId: + { type: 'CHARACTER VARYING', + allowNull: false, + defaultValue: null, + unique: true, + special: [], + primaryKey: false }, + slackData: + { type: 'JSONB', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + jwtSecret: + { type: 'bytea', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false }, + createdAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + updatedAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + defaultValue: null, + special: [], + primaryKey: false }, + teamId: + { type: 'UUID', + allowNull: true, + defaultValue: null, + special: [], + primaryKey: false } + }); + }, + + down: function (queryInterface, Sequelize) { + queryInterface.dropAllTables(); + } +}; diff --git a/server/models/Atlas.js b/server/models/Atlas.js index 175bd1ba1..5c358618e 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -2,7 +2,7 @@ import { DataTypes, sequelize, } from '../sequelize'; -import Team from './Team'; +import Document from './Document'; const allowedAtlasTypes = [['atlas', 'journal']]; @@ -11,8 +11,33 @@ const Atlas = sequelize.define('atlas', { name: DataTypes.STRING, description: DataTypes.STRING, type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes }}, + + /* type: atlas */ + atlasStructure: DataTypes.JSONB, +}, { + tableName: 'atlases', + hooks: { + // beforeValidate: (doc) => { + // doc.urlId = randomstring.generate(15); + // }, + // beforeCreate: (doc) => { + // doc.html = convertToMarkdown(doc.text); + // doc.preview = truncateMarkdown(doc.text, 160); + // }, + // beforeUpdate: (doc) => { + // doc.html = convertToMarkdown(doc.text); + // doc.preview = truncateMarkdown(doc.text, 160); + // }, + }, + instanceMethods: { + // buildUrl() { + // const slugifiedTitle = slug(this.title); + // return `${slugifiedTitle}-${this.urlId}`; + // } + } }); -Atlas.belongsTo(Team); +Atlas.hasMany(Document, { as: 'documents', foreignKey: 'atlasId' }); +Atlas.hasOne(Document, { as: 'rootDocument', foreignKey: 'rootDocumentForId', constraints: false }); export default Atlas; diff --git a/server/models/Document.js b/server/models/Document.js index d0413afc1..904ba7ade 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -10,8 +10,6 @@ import { import { truncateMarkdown, } from '../utils/truncate'; -import Atlas from './Atlas'; -import Team from './Team'; import User from './User'; slug.defaults.mode ='rfc3986'; @@ -51,8 +49,6 @@ const Document = sequelize.define('document', { } }); -Document.belongsTo(Atlas, { as: 'atlas' }); -Document.belongsTo(Team); Document.belongsTo(User); export default Document; diff --git a/server/models/Team.js b/server/models/Team.js index 9c933d259..bd6e4e1e5 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -2,6 +2,9 @@ import { DataTypes, sequelize, } from '../sequelize'; +import Atlas from './Atlas'; +import Document from './Document'; +import User from './User'; const Team = sequelize.define('team', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, @@ -9,6 +12,17 @@ const Team = sequelize.define('team', { slackId: { type: DataTypes.STRING, unique: true }, slackData: DataTypes.JSONB, }, { + instanceMethods: { + async createFirstAtlas() { + const atlas = await Atlas.create({ + name: this.name, + description: 'Your first Atlas', + type: 'journal', + teamId: this.id, + }); + return atlas; + } + }, indexes: [ { unique: true, @@ -17,4 +31,8 @@ const Team = sequelize.define('team', { ], }); +Team.hasMany(Atlas, { as: 'atlases' }); +Team.hasMany(Document, { as: 'documents' }); +Team.hasMany(User, { as: 'users' }); + export default Team; diff --git a/server/models/User.js b/server/models/User.js index 64452ab8e..6eab90862 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -4,7 +4,6 @@ import { sequelize, encryptedFields } from '../sequelize'; -import Team from './Team'; import JWT from 'jsonwebtoken'; @@ -39,8 +38,5 @@ const setRandomJwtSecret = (model) => { }; User.beforeCreate(setRandomJwtSecret); -User.belongsTo(Team); - -sequelize.sync(); export default User; diff --git a/server/presenters.js b/server/presenters.js index 7c3977ec2..e00b4fcff 100644 --- a/server/presenters.js +++ b/server/presenters.js @@ -1,5 +1,5 @@ import _orderBy from 'lodash.orderby'; -import Document from './models/Document'; +import { Document, Atlas } from './models'; export function presentUser(user) { return new Promise(async (resolve, reject) => { @@ -65,12 +65,14 @@ export async function presentDocument(document, includeAtlas=false) { private: document.private, createdAt: document.createdAt, updatedAt: document.updatedAt, - atlas: document.atlaId, + atlas: document.atlasId, team: document.teamId, } if (includeAtlas) { - const atlas = await document.getAtlas(); + const atlas = await Atlas.findOne({ where: { + id: document.atlasId, + }}); data.atlas = await presentAtlas(atlas, false); } From 2c07d43f46f7ae8f9535c4de0b4ffb56ff3858f6 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 20 Jun 2016 23:12:56 -0700 Subject: [PATCH 06/14] More tree --- server/migrations/20160619080644-initial.js | 2 +- server/models/Atlas.js | 16 +++++++- server/models/Document.js | 5 ++- server/presenters.js | 5 +++ src/scenes/Atlas/Atlas.js | 9 ++++- src/scenes/Atlas/AtlasStore.js | 3 +- src/scenes/DocumentScene/DocumentScene.js | 43 ++++++--------------- 7 files changed, 46 insertions(+), 37 deletions(-) diff --git a/server/migrations/20160619080644-initial.js b/server/migrations/20160619080644-initial.js index 2407ae40c..cc6902ade 100644 --- a/server/migrations/20160619080644-initial.js +++ b/server/migrations/20160619080644-initial.js @@ -122,7 +122,7 @@ module.exports = { defaultValue: null, special: [], primaryKey: false }, - rootDocumentForId: + parentDocumentForId: { type: 'UUID', allowNull: true, defaultValue: null, diff --git a/server/models/Atlas.js b/server/models/Atlas.js index 5c358618e..bc7784b8b 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -34,10 +34,24 @@ const Atlas = sequelize.define('atlas', { // const slugifiedTitle = slug(this.title); // return `${slugifiedTitle}-${this.urlId}`; // } + async buildStructure() { + // TODO + const rootDocument = await Document.findOne({ where: { + parentDocumentForId: null, + atlasId: this.id, + }}); + + return { + name: rootDocument.title, + id: rootDocument.id, + url: rootDocument.getUrl(), + children: null, + } + } } }); Atlas.hasMany(Document, { as: 'documents', foreignKey: 'atlasId' }); -Atlas.hasOne(Document, { as: 'rootDocument', foreignKey: 'rootDocumentForId', constraints: false }); +Atlas.hasOne(Document, { as: 'parentDocument', foreignKey: 'parentDocumentForId', constraints: false }); export default Atlas; diff --git a/server/models/Document.js b/server/models/Document.js index 904ba7ade..e0829927b 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -45,7 +45,10 @@ const Document = sequelize.define('document', { buildUrl() { const slugifiedTitle = slug(this.title); return `${slugifiedTitle}-${this.urlId}`; - } + }, + getUrl() { + return `/documents/${ this.id }`; + }, } }); diff --git a/server/presenters.js b/server/presenters.js index e00b4fcff..05f27c180 100644 --- a/server/presenters.js +++ b/server/presenters.js @@ -31,6 +31,11 @@ export function presentAtlas(atlas, includeRecentDocuments=false) { type: atlas.type, } + if (atlas.type === 'atlas') { + // Todo replace with `.atlasStructure` + data.structure = await atlas.buildStructure(); + } + if (includeRecentDocuments) { const documents = await Document.findAll({ where: { diff --git a/src/scenes/Atlas/Atlas.js b/src/scenes/Atlas/Atlas.js index 521426139..4310ecc7d 100644 --- a/src/scenes/Atlas/Atlas.js +++ b/src/scenes/Atlas/Atlas.js @@ -1,6 +1,7 @@ import React from 'react'; import { observer } from 'mobx-react'; import Link from 'react-router/lib/Link'; +import History from 'utils/History'; import store from './AtlasStore'; @@ -16,7 +17,13 @@ import styles from './Atlas.scss'; class Atlas extends React.Component { componentDidMount = () => { const { id } = this.props.params; - store.fetchAtlas(id); + store.fetchAtlas(id, data => { + + // Forward directly to root document + if (data.type === 'atlas') { + History.replace(data.structure.url); + } + }) } render() { diff --git a/src/scenes/Atlas/AtlasStore.js b/src/scenes/Atlas/AtlasStore.js index d103de558..2eb026d33 100644 --- a/src/scenes/Atlas/AtlasStore.js +++ b/src/scenes/Atlas/AtlasStore.js @@ -8,7 +8,7 @@ const store = new class AtlasStore { /* Actions */ - @action fetchAtlas = async (id) => { + @action fetchAtlas = async (id, successCallback) => { this.isFetching = true; this.atlas = null; @@ -16,6 +16,7 @@ const store = new class AtlasStore { const res = await client.post('/atlases.info', { id: id }); const { data } = res; this.atlas = data; + successCallback(data); } catch (e) { console.error("Something went wrong"); } diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js index ac754573b..61862c5e5 100644 --- a/src/scenes/DocumentScene/DocumentScene.js +++ b/src/scenes/DocumentScene/DocumentScene.js @@ -18,31 +18,10 @@ const cx = classNames.bind(styles); import treeStyles from 'components/Tree/Tree.scss'; -const tree = { - name: "Introduction", - id: "1", - children: [{ - collapsed: false, - name: "dist", - id: "2", - children: [ - { - name: "Details", - id: "21", - }, - { - name: "Distribution", - id: "22", - } - ] - }] -}; - @observer class DocumentScene extends React.Component { state = { didScroll: false, - tree: tree, } componentDidMount = () => { @@ -78,17 +57,17 @@ class DocumentScene extends React.Component { ); } - onClickNode = (node) => { - this.setState({ - active: node - }); - } + // onClickNode = (node) => { + // this.setState({ + // active: node + // }); + // } - handleChange = (tree) => { - this.setState({ - tree: tree - }); - } + // handleChange = (tree) => { + // this.setState({ + // tree: tree + // }); + // } render() { const doc = store.document; @@ -131,7 +110,7 @@ class DocumentScene extends React.Component {
Date: Mon, 20 Jun 2016 23:33:40 -0700 Subject: [PATCH 07/14] Escaping and better anchor tags --- src/assets/icons/anchor.svg | 1 - src/components/Document/Document.scss | 8 +------- src/utils/markdown.js | 4 +--- 3 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 src/assets/icons/anchor.svg diff --git a/src/assets/icons/anchor.svg b/src/assets/icons/anchor.svg deleted file mode 100644 index f98164372..000000000 --- a/src/assets/icons/anchor.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Document/Document.scss b/src/components/Document/Document.scss index 7aff8e271..5442e7e7e 100644 --- a/src/components/Document/Document.scss +++ b/src/components/Document/Document.scss @@ -8,13 +8,7 @@ :global { .anchor { visibility: hidden; - background-image: url('../../assets/icons/anchor.svg'); - background-repeat: no-repeat; - background-size: 100%; - background-position: 0 center; - margin-left: -26px; - width: 20px; - display: inline-block; + color: #ccc; } } diff --git a/src/utils/markdown.js b/src/utils/markdown.js index 475e70a10..f6bc68493 100644 --- a/src/utils/markdown.js +++ b/src/utils/markdown.js @@ -16,10 +16,8 @@ renderer.heading = (text, level) => { const headingSlug = slug(text); return ` - -   - ${text} + # `; }, From db16e71c5e48c48aa2344ceff6eb32eedc0c002b Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Wed, 22 Jun 2016 00:01:57 -0700 Subject: [PATCH 08/14] Structures for atlases --- server/api/atlases.js | 2 +- server/migrations/20160619080644-initial.js | 361 ++++++++---------- .../20160622043741-add-parent-document.js | 22 ++ server/models/Atlas.js | 35 +- server/models/Document.js | 2 + 5 files changed, 198 insertions(+), 224 deletions(-) create mode 100644 server/migrations/20160622043741-add-parent-document.js diff --git a/server/api/atlases.js b/server/api/atlases.js index c1fca939e..b34e871ca 100644 --- a/server/api/atlases.js +++ b/server/api/atlases.js @@ -56,4 +56,4 @@ router.post('atlases.list', auth(), pagination(), async (ctx) => { }; }); -export default router; \ No newline at end of file +export default router; diff --git a/server/migrations/20160619080644-initial.js b/server/migrations/20160619080644-initial.js index cc6902ade..5e5592830 100644 --- a/server/migrations/20160619080644-initial.js +++ b/server/migrations/20160619080644-initial.js @@ -3,252 +3,193 @@ module.exports = { up: function (queryInterface, Sequelize) { queryInterface.createTable('atlases', { - id: - { type: 'UUID', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: true }, - name: - { type: 'CHARACTER VARYING', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - description: - { type: 'CHARACTER VARYING', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - type: - { type: 'CHARACTER VARYING', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - atlasStructure: - { type: 'JSONB', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - createdAt: - { type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - updatedAt: - { type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - teamId: - { type: 'UUID', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false } } - ); + id: { + type: 'UUID', + allowNull: false, + primaryKey: true + }, + name: { + type: 'CHARACTER VARYING', + allowNull: true, + }, + description: { + type: 'CHARACTER VARYING', + allowNull: true, + }, + type: { + type: 'CHARACTER VARYING', + allowNull: true, + }, + atlasStructure: { + type: 'JSONB', + allowNull: true, + }, + createdAt: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + }, + updatedAt: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + }, + teamId: { + type: 'UUID', + allowNull: false, + references: { + model: "teams", + key: "id", + } + } + }); - // documents queryInterface.createTable('documents', { id: { type: 'UUID', allowNull: false, - defaultValue: null, - special: [], primaryKey: true }, urlId: { type: 'CHARACTER VARYING', allowNull: false, - unique: true, - defaultValue: null, - special: [], - primaryKey: false }, + unique: true, }, private: { type: 'BOOLEAN', allowNull: false, defaultValue: true, - special: [], - primaryKey: false }, + }, title: { type: 'CHARACTER VARYING', allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, + }, text: { type: 'TEXT', allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, + }, html: { type: 'TEXT', allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, + }, preview: { type: 'TEXT', allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, + }, createdAt: { type: 'TIMESTAMP WITH TIME ZONE', allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, + }, updatedAt: { type: 'TIMESTAMP WITH TIME ZONE', allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - userId: - { type: 'UUID', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - atlasId: - { type: 'UUID', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - parentDocumentForId: - { type: 'UUID', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - teamId: - { type: 'UUID', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false } + }, + userId: { + type: 'UUID', + allowNull: true, + references: { + model: "users", + key: "id", + } + }, + atlasId: { + type: 'UUID', + allowNull: true, + references: { + model: "atlases", + key: "id", + } + }, + teamId: { + type: 'UUID', + allowNull: true, + references: { + model: "teams", + key: "id", + } + } }); queryInterface.createTable('teams', { - id: - { type: 'UUID', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: true }, - name: - { type: 'CHARACTER VARYING', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - slackId: - { type: 'CHARACTER VARYING', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: true }, - slackData: - { type: 'JSONB', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - createdAt: - { type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - updatedAt: - { type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false } } - ); + id: { + type: 'UUID', + allowNull: false, + primaryKey: true + }, + name: { + type: 'CHARACTER VARYING', + allowNull: true, + }, + slackId: { + type: 'CHARACTER VARYING', + allowNull: true, + unique: true + }, + slackData: { + type: 'JSONB', + allowNull: true, + }, + createdAt: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + }, + updatedAt: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + } + }); queryInterface.createTable('users', { - id: - { type: 'UUID', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: true }, - email: - { type: 'CHARACTER VARYING', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - username: - { type: 'CHARACTER VARYING', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - name: - { type: 'CHARACTER VARYING', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - isAdmin: - { type: 'BOOLEAN', - allowNull: true, - defaultValue: false, - special: [], - primaryKey: false }, - slackAccessToken: - { type: 'bytea', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - slackId: - { type: 'CHARACTER VARYING', - allowNull: false, - defaultValue: null, - unique: true, - special: [], - primaryKey: false }, - slackData: - { type: 'JSONB', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - jwtSecret: - { type: 'bytea', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false }, - createdAt: - { type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - updatedAt: - { type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - defaultValue: null, - special: [], - primaryKey: false }, - teamId: - { type: 'UUID', - allowNull: true, - defaultValue: null, - special: [], - primaryKey: false } + id: { + type: 'UUID', + allowNull: false, + primaryKey: true + }, + email: { + type: 'CHARACTER VARYING', + allowNull: false, + }, + username: { + type: 'CHARACTER VARYING', + allowNull: false, + }, + name: { + type: 'CHARACTER VARYING', + allowNull: false, + }, + isAdmin: { + type: 'BOOLEAN', + allowNull: true, + defaultValue: false, + }, + slackAccessToken: { + type: 'bytea', + allowNull: true, }, + slackId: { + type: 'CHARACTER VARYING', + unique: true, + allowNull: false, + }, + slackData: { + type: 'JSONB', + allowNull: true, + }, + jwtSecret: { + type: 'bytea', + allowNull: true, + }, + createdAt: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + }, + updatedAt: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + }, + teamId: { + type: 'UUID', + allowNull: true, + references: { + model: "teams", + key: "id", + } + } }); }, diff --git a/server/migrations/20160622043741-add-parent-document.js b/server/migrations/20160622043741-add-parent-document.js new file mode 100644 index 000000000..eda6ae28d --- /dev/null +++ b/server/migrations/20160622043741-add-parent-document.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.addColumn( + 'documents', + 'parentDocumentId', + { + type: Sequelize.UUID, + allowNull: true, + references: { + model: "documents", + key: "id", + } + } + ); + }, + + down: function (queryInterface, Sequelize) { + queryInterface.removeColumn('documents', 'parentDocumentId'); + } +}; diff --git a/server/models/Atlas.js b/server/models/Atlas.js index bc7784b8b..1550a1e69 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -30,28 +30,37 @@ const Atlas = sequelize.define('atlas', { // }, }, instanceMethods: { - // buildUrl() { - // const slugifiedTitle = slug(this.title); - // return `${slugifiedTitle}-${this.urlId}`; - // } async buildStructure() { - // TODO + const getNodeForDocument = async (document) => { + const children = await Document.findAll({ where: { + parentDocumentId: document.id, + atlasId: this.id, + }}); + + let childNodes = [] + await Promise.all(children.map(async (child) => { + console.log(child.id) + childNodes.push(await getNodeForDocument(child)); + })); + + return { + name: document.title, + id: document.id, + url: document.getUrl(), + children: childNodes, + }; + } + const rootDocument = await Document.findOne({ where: { - parentDocumentForId: null, + parentDocumentId: null, atlasId: this.id, }}); - return { - name: rootDocument.title, - id: rootDocument.id, - url: rootDocument.getUrl(), - children: null, - } + return await getNodeForDocument(rootDocument); } } }); Atlas.hasMany(Document, { as: 'documents', foreignKey: 'atlasId' }); -Atlas.hasOne(Document, { as: 'parentDocument', foreignKey: 'parentDocumentForId', constraints: false }); export default Atlas; diff --git a/server/models/Document.js b/server/models/Document.js index e0829927b..db3e323bd 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -27,6 +27,8 @@ const Document = sequelize.define('document', { text: DataTypes.TEXT, html: DataTypes.TEXT, preview: DataTypes.TEXT, + + parentDocumentId: DataTypes.UUID, }, { hooks: { beforeValidate: (doc) => { From 4c262500c158557fba3ef805f516db7b99970e60 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Wed, 22 Jun 2016 00:03:53 -0700 Subject: [PATCH 09/14] Better list styles --- src/components/Document/Document.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Document/Document.scss b/src/components/Document/Document.scss index 5442e7e7e..d5ad2a10d 100644 --- a/src/components/Document/Document.scss +++ b/src/components/Document/Document.scss @@ -22,9 +22,10 @@ } ul { + padding-left: 1.5em; + ul { margin: 0; - padding-left: 1.5em; } } } \ No newline at end of file From 496e9214d5617d08783862977b1563d16ccb2308 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Wed, 22 Jun 2016 22:57:39 -0700 Subject: [PATCH 10/14] Use computed for atlas detection --- src/scenes/DocumentScene/DocumentScene.js | 7 ++++++- src/scenes/DocumentScene/DocumentSceneStore.js | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js index 61862c5e5..467788493 100644 --- a/src/scenes/DocumentScene/DocumentScene.js +++ b/src/scenes/DocumentScene/DocumentScene.js @@ -77,6 +77,11 @@ class DocumentScene extends React.Component { if (doc) { actions = (
+ { store.isAtlas ? ( + + New document + + ) : null } Edit @@ -106,7 +111,7 @@ class DocumentScene extends React.Component { ) : ( - { doc.atlas.type === 'atlas' ? ( + { store.isAtlas ? (
{ From 7f4095b77f5eb10da0af4ffbef22444255c90d94 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Thu, 23 Jun 2016 00:12:41 -0700 Subject: [PATCH 11/14] moved migrations around --- server/migrations/20160619080644-initial.js | 202 ++++++++++---------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/server/migrations/20160619080644-initial.js b/server/migrations/20160619080644-initial.js index 5e5592830..bc3c6373a 100644 --- a/server/migrations/20160619080644-initial.js +++ b/server/migrations/20160619080644-initial.js @@ -2,6 +2,35 @@ module.exports = { up: function (queryInterface, Sequelize) { + queryInterface.createTable('teams', { + id: { + type: 'UUID', + allowNull: false, + primaryKey: true + }, + name: { + type: 'CHARACTER VARYING', + allowNull: true, + }, + slackId: { + type: 'CHARACTER VARYING', + allowNull: true, + unique: true + }, + slackData: { + type: 'JSONB', + allowNull: true, + }, + createdAt: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + }, + updatedAt: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + } + }); + queryInterface.createTable('atlases', { id: { type: 'UUID', @@ -35,103 +64,10 @@ module.exports = { teamId: { type: 'UUID', allowNull: false, - references: { - model: "teams", - key: "id", - } - } - }); - - queryInterface.createTable('documents', { - id: - { type: 'UUID', - allowNull: false, - primaryKey: true }, - urlId: - { type: 'CHARACTER VARYING', - allowNull: false, - unique: true, }, - private: - { type: 'BOOLEAN', - allowNull: false, - defaultValue: true, - }, - title: - { type: 'CHARACTER VARYING', - allowNull: false, - }, - text: - { type: 'TEXT', - allowNull: true, - }, - html: - { type: 'TEXT', - allowNull: true, - }, - preview: - { type: 'TEXT', - allowNull: true, - }, - createdAt: - { type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - }, - updatedAt: - { type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - }, - userId: { - type: 'UUID', - allowNull: true, - references: { - model: "users", - key: "id", - } - }, - atlasId: { - type: 'UUID', - allowNull: true, - references: { - model: "atlases", - key: "id", - } - }, - teamId: { - type: 'UUID', - allowNull: true, - references: { - model: "teams", - key: "id", - } - } - }); - - queryInterface.createTable('teams', { - id: { - type: 'UUID', - allowNull: false, - primaryKey: true - }, - name: { - type: 'CHARACTER VARYING', - allowNull: true, - }, - slackId: { - type: 'CHARACTER VARYING', - allowNull: true, - unique: true - }, - slackData: { - type: 'JSONB', - allowNull: true, - }, - createdAt: { - type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, - }, - updatedAt: { - type: 'TIMESTAMP WITH TIME ZONE', - allowNull: false, + // references: { + // model: "teams", + // key: "id", + // } } }); @@ -185,10 +121,74 @@ module.exports = { teamId: { type: 'UUID', allowNull: true, - references: { - model: "teams", - key: "id", - } + // references: { + // model: "teams", + // key: "id", + // } + } + }); + + queryInterface.createTable('documents', { + id: + { type: 'UUID', + allowNull: false, + primaryKey: true }, + urlId: + { type: 'CHARACTER VARYING', + allowNull: false, + unique: true, }, + private: + { type: 'BOOLEAN', + allowNull: false, + defaultValue: true, + }, + title: + { type: 'CHARACTER VARYING', + allowNull: false, + }, + text: + { type: 'TEXT', + allowNull: true, + }, + html: + { type: 'TEXT', + allowNull: true, + }, + preview: + { type: 'TEXT', + allowNull: true, + }, + createdAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + }, + updatedAt: + { type: 'TIMESTAMP WITH TIME ZONE', + allowNull: false, + }, + userId: { + type: 'UUID', + allowNull: true, + // references: { + // model: "users", + // key: "id", + // } + }, + atlasId: { + type: 'UUID', + allowNull: true, + // references: { + // model: "atlases", + // key: "id", + // } + }, + teamId: { + type: 'UUID', + allowNull: true, + // references: { + // model: "teams", + // key: "id", + // } } }); }, From 7ea6eb5d2876aff4d117fa7dd5b366ac3b73a862 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Thu, 23 Jun 2016 00:19:45 -0700 Subject: [PATCH 12/14] Fixes --- server/api/documents.js | 3 +++ server/models/Atlas.js | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/api/documents.js b/server/api/documents.js index 4aef31d13..2fef95088 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -119,6 +119,9 @@ router.post('documents.delete', auth(), async (ctx) => { if (!document) throw httpErrors.BadRequest(); + // TODO: Don't allow to destroy root docs + // TODO: handle sub documents + try { await document.destroy(); } catch (e) { diff --git a/server/models/Atlas.js b/server/models/Atlas.js index 1550a1e69..a0bcb6f66 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -56,7 +56,11 @@ const Atlas = sequelize.define('atlas', { atlasId: this.id, }}); - return await getNodeForDocument(rootDocument); + if (rootDocument) { + return await getNodeForDocument(rootDocument); + } else { + return; // TODO should create a root doc + } } } }); From a0649e6fd3f8e4b9a5fe0ade1757d5315c12fb32 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sat, 25 Jun 2016 22:27:44 -0700 Subject: [PATCH 13/14] Added API to update document tree --- server/api/atlases.js | 22 ++++ server/api/documents.js | 16 +++ server/models/Atlas.js | 101 ++++++++++++++++-- server/presenters.js | 2 +- src/components/Tree/Node.js | 5 +- src/index.js | 1 + src/scenes/DocumentEdit/DocumentEdit.js | 6 +- src/scenes/DocumentEdit/DocumentEditStore.js | 15 ++- .../DocumentEdit/components/SaveAction.js | 2 +- src/scenes/DocumentScene/DocumentScene.js | 24 ++--- .../DocumentScene/DocumentSceneStore.js | 18 +++- 11 files changed, 182 insertions(+), 30 deletions(-) diff --git a/server/api/atlases.js b/server/api/atlases.js index b34e871ca..3234d4d01 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 2fef95088..9d8e0f946 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 a0bcb6f66..f8af38151 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 05f27c180..2441dcc0a 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 fc5bc6c0d..ab980dcff 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 3b1a83ff8..38cca8e34 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 fd011a507..ceacce94b 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 a2648ffa1..155d778b1 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 948cb59e0..9f509d797 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 467788493..e9ac29378 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 From 4beb84f4bcbe92bc8b9bf7d790235c07a041d294 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sat, 25 Jun 2016 22:36:16 -0700 Subject: [PATCH 14/14] Renamed --- server/api/{atlases.js => collections.js} | 0 server/api/index.js | 4 ++-- server/models/Atlas.js | 10 +++++----- server/presenters.js | 3 +-- src/scenes/Atlas/Atlas.js | 2 +- src/scenes/DocumentScene/DocumentScene.js | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) rename server/api/{atlases.js => collections.js} (100%) diff --git a/server/api/atlases.js b/server/api/collections.js similarity index 100% rename from server/api/atlases.js rename to server/api/collections.js diff --git a/server/api/index.js b/server/api/index.js index 302eb1953..11f8fc09d 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -6,7 +6,7 @@ import Sequelize from 'sequelize'; import auth from './auth'; import user from './user'; -import atlases from './atlases'; +import collections from './collections'; import documents from './documents'; import validation from './validation'; @@ -44,7 +44,7 @@ api.use(validation()); router.use('/', auth.routes()); router.use('/', user.routes()); -router.use('/', atlases.routes()); +router.use('/', collections.routes()); router.use('/', documents.routes()); // Router is embedded in a Koa application wrapper, because koa-router does not diff --git a/server/models/Atlas.js b/server/models/Atlas.js index f8af38151..f6f778d22 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -14,7 +14,7 @@ const Atlas = sequelize.define('atlas', { type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes }}, /* type: atlas */ - atlasStructure: DataTypes.JSONB, + navigationTree: DataTypes.JSONB, }, { tableName: 'atlases', hooks: { @@ -32,8 +32,8 @@ const Atlas = sequelize.define('atlas', { }, instanceMethods: { async getStructure() { - if (this.atlasStructure) { - return this.atlasStructure; + if (this.navigationTree) { + return this.navigationTree; } const getNodeForDocument = async (document) => { @@ -122,7 +122,7 @@ const Atlas = sequelize.define('atlas', { throw new Error('Invalid navigation tree'); } - this.atlasStructure = newTree; + this.navigationTree = newTree; await this.save(); return newTree; @@ -147,7 +147,7 @@ const Atlas = sequelize.define('atlas', { return node; }; - this.atlasStructure = insertNode(this.atlasStructure); + this.navigationTree = insertNode(this.navigationTree); } } }); diff --git a/server/presenters.js b/server/presenters.js index 2441dcc0a..49218b1d5 100644 --- a/server/presenters.js +++ b/server/presenters.js @@ -32,8 +32,7 @@ export function presentAtlas(atlas, includeRecentDocuments=false) { } if (atlas.type === 'atlas') { - // Todo replace with `.atlasStructure` - data.structure = await atlas.getStructure(); + data.navigationTree = await atlas.getStructure(); } if (includeRecentDocuments) { diff --git a/src/scenes/Atlas/Atlas.js b/src/scenes/Atlas/Atlas.js index 4310ecc7d..d689841a6 100644 --- a/src/scenes/Atlas/Atlas.js +++ b/src/scenes/Atlas/Atlas.js @@ -21,7 +21,7 @@ class Atlas extends React.Component { // Forward directly to root document if (data.type === 'atlas') { - History.replace(data.structure.url); + History.replace(data.navigationTree.url); } }) } diff --git a/src/scenes/DocumentScene/DocumentScene.js b/src/scenes/DocumentScene/DocumentScene.js index e9ac29378..e8883207a 100644 --- a/src/scenes/DocumentScene/DocumentScene.js +++ b/src/scenes/DocumentScene/DocumentScene.js @@ -115,7 +115,7 @@ class DocumentScene extends React.Component {