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 80d5086d4..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",
@@ -53,6 +54,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",
@@ -96,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/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/atlases.js b/server/api/collections.js
similarity index 64%
rename from server/api/atlases.js
rename to server/api/collections.js
index 44a6921e9..3234d4d01 100644
--- a/server/api/atlases.js
+++ b/server/api/collections.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'],
@@ -56,4 +56,26 @@ router.post('atlases.list', auth(), pagination(), async (ctx) => {
};
});
-export default router;
\ No newline at end of file
+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 9a02389b5..9d8e0f946 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();
}
@@ -46,30 +46,45 @@ 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');
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,
},
});
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: team.id,
+ teamId: user.teamId,
userId: user.id,
title: title,
text: text,
});
+ // TODO: Move to afterSave hook if possible with imports
+ ownerAtlas.addNodeToNavigationTree(document);
+ await ownerAtlas.save();
+
ctx.body = {
data: await presentDocument(document, true),
};
@@ -86,11 +101,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,16 +126,18 @@ 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,
},
});
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/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/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..bc3c6373a
--- /dev/null
+++ b/server/migrations/20160619080644-initial.js
@@ -0,0 +1,199 @@
+'use strict';
+
+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',
+ 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",
+ // }
+ }
+ });
+
+ queryInterface.createTable('users', {
+ 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",
+ // }
+ }
+ });
+
+ 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",
+ // }
+ }
+ });
+ },
+
+ down: function (queryInterface, Sequelize) {
+ queryInterface.dropAllTables();
+ }
+};
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 175bd1ba1..f6f778d22 100644
--- a/server/models/Atlas.js
+++ b/server/models/Atlas.js
@@ -2,7 +2,8 @@ import {
DataTypes,
sequelize,
} from '../sequelize';
-import Team from './Team';
+import _isEqual from 'lodash/isEqual';
+import Document from './Document';
const allowedAtlasTypes = [['atlas', 'journal']];
@@ -11,8 +12,146 @@ const Atlas = sequelize.define('atlas', {
name: DataTypes.STRING,
description: DataTypes.STRING,
type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes }},
+
+ /* type: atlas */
+ navigationTree: 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: {
+ async getStructure() {
+ if (this.navigationTree) {
+ return this.navigationTree;
+ }
+
+ 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) => {
+ childNodes.push(await getNodeForDocument(child));
+ }));
+
+ return {
+ title: document.title,
+ id: document.id,
+ url: document.getUrl(),
+ children: childNodes,
+ };
+ }
+
+ 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.navigationTree = 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.navigationTree = insertNode(this.navigationTree);
+ }
+ }
});
-Atlas.belongsTo(Team);
+Atlas.hasMany(Document, { as: 'documents', foreignKey: 'atlasId' });
export default Atlas;
diff --git a/server/models/Document.js b/server/models/Document.js
index d0413afc1..db3e323bd 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';
@@ -29,6 +27,8 @@ const Document = sequelize.define('document', {
text: DataTypes.TEXT,
html: DataTypes.TEXT,
preview: DataTypes.TEXT,
+
+ parentDocumentId: DataTypes.UUID,
}, {
hooks: {
beforeValidate: (doc) => {
@@ -47,12 +47,13 @@ const Document = sequelize.define('document', {
buildUrl() {
const slugifiedTitle = slug(this.title);
return `${slugifiedTitle}-${this.urlId}`;
- }
+ },
+ getUrl() {
+ return `/documents/${ this.id }`;
+ },
}
});
-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..49218b1d5 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) => {
@@ -31,6 +31,10 @@ export function presentAtlas(atlas, includeRecentDocuments=false) {
type: atlas.type,
}
+ if (atlas.type === 'atlas') {
+ data.navigationTree = await atlas.getStructure();
+ }
+
if (includeRecentDocuments) {
const documents = await Document.findAll({
where: {
@@ -65,12 +69,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);
}
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..d5ad2a10d 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;
}
}
@@ -28,9 +22,10 @@
}
ul {
+ padding-left: 1.5em;
+
ul {
margin: 0;
- padding-left: 1.5em;
}
}
}
\ No newline at end of file
diff --git a/src/components/Tree/Node.js b/src/components/Tree/Node.js
new file mode 100644
index 000000000..ab980dcff
--- /dev/null
+++ b/src/components/Tree/Node.js
@@ -0,0 +1,110 @@
+var React = require('react');
+import history from 'utils/History';
+
+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 (
+