10
.sequelizerc
Normal file
10
.sequelizerc
Normal file
@@ -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'),
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
"start": "cross-env NODE_ENV=development DEBUG=1 ./node_modules/.bin/nodemon --watch server index.js",
|
"start": "cross-env NODE_ENV=development DEBUG=1 ./node_modules/.bin/nodemon --watch server index.js",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"deploy": "git push heroku master",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"http-errors": "^1.4.0",
|
"http-errors": "^1.4.0",
|
||||||
"imports-loader": "^0.6.5",
|
"imports-loader": "^0.6.5",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
|
"js-tree": "^1.1.0",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"jsonwebtoken": "^5.7.0",
|
"jsonwebtoken": "^5.7.0",
|
||||||
"koa": "^2.0.0",
|
"koa": "^2.0.0",
|
||||||
@@ -96,7 +98,7 @@
|
|||||||
"safestart": "^0.8.0",
|
"safestart": "^0.8.0",
|
||||||
"sass-loader": "^3.2.0",
|
"sass-loader": "^3.2.0",
|
||||||
"sequelize": "^3.21.0",
|
"sequelize": "^3.21.0",
|
||||||
"sequelize-cli": "^2.3.1",
|
"sequelize-cli": "^2.4.0",
|
||||||
"sequelize-encrypted": "^0.1.0",
|
"sequelize-encrypted": "^0.1.0",
|
||||||
"slug": "^0.9.1",
|
"slug": "^0.9.1",
|
||||||
"style-loader": "^0.13.0",
|
"style-loader": "^0.13.0",
|
||||||
|
|||||||
@@ -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 authResponse = await fetch(`https://slack.com/api/auth.test?token=${data.access_token}`);
|
||||||
const authData = await authResponse.json();
|
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) {
|
if (user) {
|
||||||
user.slackAccessToken = data.access_token;
|
user.slackAccessToken = data.access_token;
|
||||||
user.slackData = data.user;
|
user.slackData = data.user;
|
||||||
user = await user.save();
|
user = await user.save();
|
||||||
} else {
|
} else {
|
||||||
// Existing user
|
user = await team.createUser({
|
||||||
user = await User.create({
|
|
||||||
slackId: data.user.id,
|
slackId: data.user.id,
|
||||||
username: authData.user,
|
username: authData.user,
|
||||||
name: data.user.name,
|
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: {
|
ctx.body = { data: {
|
||||||
user: await presentUser(user),
|
user: await presentUser(user),
|
||||||
team: await presentTeam(team),
|
team: await presentTeam(team),
|
||||||
accessToken: user.getJwtToken(),
|
accessToken: user.getJwtToken(),
|
||||||
}};
|
}};
|
||||||
console.log("enf")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ router.post('atlases.info', auth(), async (ctx) => {
|
|||||||
let { id } = ctx.request.body;
|
let { id } = ctx.request.body;
|
||||||
ctx.assertPresent(id, 'id is required');
|
ctx.assertPresent(id, 'id is required');
|
||||||
|
|
||||||
const team = await ctx.state.user.getTeam();
|
const user = ctx.state.user;
|
||||||
const atlas = await Atlas.findOne({
|
const atlas = await Atlas.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
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) => {
|
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({
|
const atlases = await Atlas.findAll({
|
||||||
where: {
|
where: {
|
||||||
teamId: team.id,
|
teamId: user.teamId,
|
||||||
},
|
},
|
||||||
order: [
|
order: [
|
||||||
['updatedAt', 'DESC'],
|
['updatedAt', 'DESC'],
|
||||||
@@ -56,4 +56,26 @@ router.post('atlases.list', auth(), pagination(), async (ctx) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
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;
|
||||||
@@ -23,8 +23,8 @@ router.post('documents.info', auth({ require: false }), async (ctx) => {
|
|||||||
if (document.private) {
|
if (document.private) {
|
||||||
if (!ctx.state.user) throw httpErrors.NotFound();
|
if (!ctx.state.user) throw httpErrors.NotFound();
|
||||||
|
|
||||||
const team = await ctx.state.user.getTeam();
|
const user = await ctx.state.user;
|
||||||
if (document.teamId !== team.id) {
|
if (document.teamId !== user.teamId) {
|
||||||
throw httpErrors.NotFound();
|
throw httpErrors.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,30 +46,45 @@ router.post('documents.create', auth(), async (ctx) => {
|
|||||||
atlas,
|
atlas,
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
|
parentDocument,
|
||||||
} = ctx.request.body;
|
} = ctx.request.body;
|
||||||
ctx.assertPresent(atlas, 'atlas is required');
|
ctx.assertPresent(atlas, 'atlas is required');
|
||||||
ctx.assertPresent(title, 'title is required');
|
ctx.assertPresent(title, 'title is required');
|
||||||
ctx.assertPresent(text, 'text is required');
|
ctx.assertPresent(text, 'text is required');
|
||||||
|
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
const team = await user.getTeam();
|
|
||||||
const ownerAtlas = await Atlas.findOne({
|
const ownerAtlas = await Atlas.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: atlas,
|
id: atlas,
|
||||||
teamId: team.id,
|
teamId: user.teamId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ownerAtlas) throw httpErrors.BadRequest();
|
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({
|
const document = await Document.create({
|
||||||
|
parentDocumentId: parentDocumentObj.id,
|
||||||
atlasId: ownerAtlas.id,
|
atlasId: ownerAtlas.id,
|
||||||
teamId: team.id,
|
teamId: user.teamId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
title: title,
|
title: title,
|
||||||
text: text,
|
text: text,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Move to afterSave hook if possible with imports
|
||||||
|
ownerAtlas.addNodeToNavigationTree(document);
|
||||||
|
await ownerAtlas.save();
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: await presentDocument(document, true),
|
data: await presentDocument(document, true),
|
||||||
};
|
};
|
||||||
@@ -86,11 +101,10 @@ router.post('documents.update', auth(), async (ctx) => {
|
|||||||
ctx.assertPresent(text, 'text is required');
|
ctx.assertPresent(text, 'text is required');
|
||||||
|
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
const team = await user.getTeam();
|
|
||||||
let document = await Document.findOne({
|
let document = await Document.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
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');
|
ctx.assertPresent(id, 'id is required');
|
||||||
|
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
const team = await user.getTeam();
|
|
||||||
let document = await Document.findOne({
|
let document = await Document.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
teamId: team.id,
|
teamId: user.teamId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!document) throw httpErrors.BadRequest();
|
if (!document) throw httpErrors.BadRequest();
|
||||||
|
|
||||||
|
// TODO: Don't allow to destroy root docs
|
||||||
|
// TODO: handle sub documents
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await document.destroy();
|
await document.destroy();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Sequelize from 'sequelize';
|
|||||||
|
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import user from './user';
|
import user from './user';
|
||||||
import atlases from './atlases';
|
import collections from './collections';
|
||||||
import documents from './documents';
|
import documents from './documents';
|
||||||
|
|
||||||
import validation from './validation';
|
import validation from './validation';
|
||||||
@@ -44,7 +44,7 @@ api.use(validation());
|
|||||||
|
|
||||||
router.use('/', auth.routes());
|
router.use('/', auth.routes());
|
||||||
router.use('/', user.routes());
|
router.use('/', user.routes());
|
||||||
router.use('/', atlases.routes());
|
router.use('/', collections.routes());
|
||||||
router.use('/', documents.routes());
|
router.use('/', documents.routes());
|
||||||
|
|
||||||
// Router is embedded in a Koa application wrapper, because koa-router does not
|
// Router is embedded in a Koa application wrapper, because koa-router does not
|
||||||
|
|||||||
14
server/config/database.json
Normal file
14
server/config/database.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
199
server/migrations/20160619080644-initial.js
Normal file
199
server/migrations/20160619080644-initial.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
22
server/migrations/20160622043741-add-parent-document.js
Normal file
22
server/migrations/20160622043741-add-parent-document.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,7 +2,8 @@ import {
|
|||||||
DataTypes,
|
DataTypes,
|
||||||
sequelize,
|
sequelize,
|
||||||
} from '../sequelize';
|
} from '../sequelize';
|
||||||
import Team from './Team';
|
import _isEqual from 'lodash/isEqual';
|
||||||
|
import Document from './Document';
|
||||||
|
|
||||||
const allowedAtlasTypes = [['atlas', 'journal']];
|
const allowedAtlasTypes = [['atlas', 'journal']];
|
||||||
|
|
||||||
@@ -11,8 +12,146 @@ const Atlas = sequelize.define('atlas', {
|
|||||||
name: DataTypes.STRING,
|
name: DataTypes.STRING,
|
||||||
description: DataTypes.STRING,
|
description: DataTypes.STRING,
|
||||||
type: { type: DataTypes.STRING, validate: { isIn: allowedAtlasTypes }},
|
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;
|
export default Atlas;
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
truncateMarkdown,
|
truncateMarkdown,
|
||||||
} from '../utils/truncate';
|
} from '../utils/truncate';
|
||||||
import Atlas from './Atlas';
|
|
||||||
import Team from './Team';
|
|
||||||
import User from './User';
|
import User from './User';
|
||||||
|
|
||||||
slug.defaults.mode ='rfc3986';
|
slug.defaults.mode ='rfc3986';
|
||||||
@@ -29,6 +27,8 @@ const Document = sequelize.define('document', {
|
|||||||
text: DataTypes.TEXT,
|
text: DataTypes.TEXT,
|
||||||
html: DataTypes.TEXT,
|
html: DataTypes.TEXT,
|
||||||
preview: DataTypes.TEXT,
|
preview: DataTypes.TEXT,
|
||||||
|
|
||||||
|
parentDocumentId: DataTypes.UUID,
|
||||||
}, {
|
}, {
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeValidate: (doc) => {
|
beforeValidate: (doc) => {
|
||||||
@@ -47,12 +47,13 @@ const Document = sequelize.define('document', {
|
|||||||
buildUrl() {
|
buildUrl() {
|
||||||
const slugifiedTitle = slug(this.title);
|
const slugifiedTitle = slug(this.title);
|
||||||
return `${slugifiedTitle}-${this.urlId}`;
|
return `${slugifiedTitle}-${this.urlId}`;
|
||||||
}
|
},
|
||||||
|
getUrl() {
|
||||||
|
return `/documents/${ this.id }`;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Document.belongsTo(Atlas, { as: 'atlas' });
|
|
||||||
Document.belongsTo(Team);
|
|
||||||
Document.belongsTo(User);
|
Document.belongsTo(User);
|
||||||
|
|
||||||
export default Document;
|
export default Document;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import {
|
|||||||
DataTypes,
|
DataTypes,
|
||||||
sequelize,
|
sequelize,
|
||||||
} from '../sequelize';
|
} from '../sequelize';
|
||||||
|
import Atlas from './Atlas';
|
||||||
|
import Document from './Document';
|
||||||
|
import User from './User';
|
||||||
|
|
||||||
const Team = sequelize.define('team', {
|
const Team = sequelize.define('team', {
|
||||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
||||||
@@ -9,6 +12,17 @@ const Team = sequelize.define('team', {
|
|||||||
slackId: { type: DataTypes.STRING, unique: true },
|
slackId: { type: DataTypes.STRING, unique: true },
|
||||||
slackData: DataTypes.JSONB,
|
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: [
|
indexes: [
|
||||||
{
|
{
|
||||||
unique: true,
|
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;
|
export default Team;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
sequelize,
|
sequelize,
|
||||||
encryptedFields
|
encryptedFields
|
||||||
} from '../sequelize';
|
} from '../sequelize';
|
||||||
import Team from './Team';
|
|
||||||
|
|
||||||
import JWT from 'jsonwebtoken';
|
import JWT from 'jsonwebtoken';
|
||||||
|
|
||||||
@@ -39,8 +38,5 @@ const setRandomJwtSecret = (model) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
User.beforeCreate(setRandomJwtSecret);
|
User.beforeCreate(setRandomJwtSecret);
|
||||||
User.belongsTo(Team);
|
|
||||||
|
|
||||||
sequelize.sync();
|
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import _orderBy from 'lodash.orderby';
|
import _orderBy from 'lodash.orderby';
|
||||||
import Document from './models/Document';
|
import { Document, Atlas } from './models';
|
||||||
|
|
||||||
export function presentUser(user) {
|
export function presentUser(user) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
@@ -31,6 +31,10 @@ export function presentAtlas(atlas, includeRecentDocuments=false) {
|
|||||||
type: atlas.type,
|
type: atlas.type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (atlas.type === 'atlas') {
|
||||||
|
data.navigationTree = await atlas.getStructure();
|
||||||
|
}
|
||||||
|
|
||||||
if (includeRecentDocuments) {
|
if (includeRecentDocuments) {
|
||||||
const documents = await Document.findAll({
|
const documents = await Document.findAll({
|
||||||
where: {
|
where: {
|
||||||
@@ -65,12 +69,14 @@ export async function presentDocument(document, includeAtlas=false) {
|
|||||||
private: document.private,
|
private: document.private,
|
||||||
createdAt: document.createdAt,
|
createdAt: document.createdAt,
|
||||||
updatedAt: document.updatedAt,
|
updatedAt: document.updatedAt,
|
||||||
atlas: document.atlaId,
|
atlas: document.atlasId,
|
||||||
team: document.teamId,
|
team: document.teamId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeAtlas) {
|
if (includeAtlas) {
|
||||||
const atlas = await document.getAtlas();
|
const atlas = await Atlas.findOne({ where: {
|
||||||
|
id: document.atlasId,
|
||||||
|
}});
|
||||||
data.atlas = await presentAtlas(atlas, false);
|
data.atlas = await presentAtlas(atlas, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 125" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M82.641,40.452L69.984,53.109c-1.164,1.164-3.054,1.164-4.218,0s-1.164-3.054,0-4.218l12.657-12.657 c3.489-3.489,3.489-9.165,0-12.657s-9.165-3.489-12.657,0L48.891,40.452c-1.035,1.059-5.955,6.702,0,12.657 c1.164,1.164,1.164,3.054,0,4.218s-3.054,1.164-4.218,0c-8.343-8.343-3.648-17.445,0-21.093l16.875-16.875 c5.814-5.814,15.279-5.814,21.093,0C88.455,25.173,88.452,34.638,82.641,40.452z M57.327,44.673c-1.164-1.164-3.054-1.164-4.218,0 c-1.164,1.164-1.164,3.054,0,4.218c5.955,5.955,1.035,11.595,0,12.657L36.234,78.423c-3.489,3.489-9.165,3.489-12.657,0 c-3.492-3.489-3.489-9.165,0-12.657l12.657-12.657c1.164-1.164,1.164-3.054,0-4.218s-3.054-1.164-4.218,0L19.359,61.548 c-5.814,5.814-5.814,15.279,0,21.093c5.814,5.814,15.279,5.814,21.093,0l16.875-16.875C60.975,62.118,65.67,53.013,57.327,44.673z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1003 B |
@@ -8,13 +8,7 @@
|
|||||||
:global {
|
:global {
|
||||||
.anchor {
|
.anchor {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
background-image: url('../../assets/icons/anchor.svg');
|
color: #ccc;
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 100%;
|
|
||||||
background-position: 0 center;
|
|
||||||
margin-left: -26px;
|
|
||||||
width: 20px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,9 +22,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 1.5em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
110
src/components/Tree/Node.js
Normal file
110
src/components/Tree/Node.js
Normal file
@@ -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 (
|
||||||
|
<span
|
||||||
|
className={cx(styles.collapse, collapsed ? styles.caretRight : styles.caretDown)}
|
||||||
|
onMouseDown={function(e) {e.stopPropagation()}}
|
||||||
|
onClick={this.handleCollapse}
|
||||||
|
>
|
||||||
|
<img src={ require("./assets/chevron.svg") } />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={ styles.children } style={childrenStyles}>
|
||||||
|
{index.children.map((child) => {
|
||||||
|
var childIndex = tree.getIndex(child);
|
||||||
|
return (
|
||||||
|
<Node
|
||||||
|
tree={tree}
|
||||||
|
index={childIndex}
|
||||||
|
key={childIndex.id}
|
||||||
|
dragging={dragging}
|
||||||
|
paddingLeft={this.props.paddingLeft}
|
||||||
|
onCollapse={this.props.onCollapse}
|
||||||
|
onDragStart={this.props.onDragStart}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={cx(styles.node, {
|
||||||
|
placeholder: index.id === dragging,
|
||||||
|
rootNode: this.props.rootNode,
|
||||||
|
})} style={style}>
|
||||||
|
<div className={ styles.inner } ref="inner" onMouseDown={this.handleMouseDown}>
|
||||||
|
{!this.props.rootNode && this.renderCollapse()}
|
||||||
|
<span
|
||||||
|
className={ cx(styles.nodeLabel, { rootLabel: this.props.rootNode }) }
|
||||||
|
onClick={() => { history.push(node.url) }}
|
||||||
|
onMouseDown={this.props.rootNode ? function(e){e.stopPropagation()} : undefined}
|
||||||
|
>
|
||||||
|
{ node.title }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{this.renderChildren()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
68
src/components/Tree/Tree.js
Normal file
68
src/components/Tree/Tree.js
Normal file
@@ -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;
|
||||||
79
src/components/Tree/Tree.scss
Normal file
79
src/components/Tree/Tree.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
266
src/components/Tree/UiTree.js
Normal file
266
src/components/Tree/UiTree.js
Normal file
@@ -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 (
|
||||||
|
<div className={ styles.draggable } style={draggingStyles}>
|
||||||
|
<Node
|
||||||
|
tree={tree}
|
||||||
|
index={draggingIndex}
|
||||||
|
paddingLeft={this.props.paddingLeft}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
var tree = this.state.tree;
|
||||||
|
var dragging = this.state.dragging;
|
||||||
|
var draggingDom = this.getDraggingDom();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ styles.tree }>
|
||||||
|
{draggingDom}
|
||||||
|
<Node
|
||||||
|
rootNode={ true }
|
||||||
|
tree={tree}
|
||||||
|
index={tree.getIndex(1)}
|
||||||
|
key={1}
|
||||||
|
paddingLeft={this.props.paddingLeft}
|
||||||
|
onDragStart={this.dragStart}
|
||||||
|
onCollapse={this.toggleCollapse}
|
||||||
|
dragging={dragging && dragging.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
// }
|
||||||
|
});
|
||||||
1
src/components/Tree/assets/chevron.svg
Normal file
1
src/components/Tree/assets/chevron.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 30" x="0px" y="0px"><path d="M8.59,18.16L14.25,12.5L8.59,6.84L7.89,7.55L12.84,12.5L7.89,17.45L8.59,18.16Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 227 B |
2
src/components/Tree/index.js
Normal file
2
src/components/Tree/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import UiTree from './UiTree';
|
||||||
|
export default UiTree;
|
||||||
@@ -44,6 +44,7 @@ render((
|
|||||||
<Route path="/atlas/:id/new" component={ DocumentEdit } onEnter={ requireAuth } newDocument={ true } />
|
<Route path="/atlas/:id/new" component={ DocumentEdit } onEnter={ requireAuth } newDocument={ true } />
|
||||||
<Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } />
|
<Route path="/documents/:id" component={ DocumentScene } onEnter={ requireAuth } />
|
||||||
<Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } />
|
<Route path="/documents/:id/edit" component={ DocumentEdit } onEnter={ requireAuth } />
|
||||||
|
<Route path="/documents/:id/new" component={ DocumentEdit } onEnter={ requireAuth } newChildDocument={ true } />
|
||||||
|
|
||||||
<Route path="/auth/slack" component={SlackAuth} />
|
<Route path="/auth/slack" component={SlackAuth} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import Link from 'react-router/lib/Link';
|
import Link from 'react-router/lib/Link';
|
||||||
|
import History from 'utils/History';
|
||||||
|
|
||||||
import store from './AtlasStore';
|
import store from './AtlasStore';
|
||||||
|
|
||||||
@@ -16,7 +17,13 @@ import styles from './Atlas.scss';
|
|||||||
class Atlas extends React.Component {
|
class Atlas extends React.Component {
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
store.fetchAtlas(id);
|
store.fetchAtlas(id, data => {
|
||||||
|
|
||||||
|
// Forward directly to root document
|
||||||
|
if (data.type === 'atlas') {
|
||||||
|
History.replace(data.navigationTree.url);
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const store = new class AtlasStore {
|
|||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
@action fetchAtlas = async (id) => {
|
@action fetchAtlas = async (id, successCallback) => {
|
||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
this.atlas = null;
|
this.atlas = null;
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ const store = new class AtlasStore {
|
|||||||
const res = await client.post('/atlases.info', { id: id });
|
const res = await client.post('/atlases.info', { id: id });
|
||||||
const { data } = res;
|
const { data } = res;
|
||||||
this.atlas = data;
|
this.atlas = data;
|
||||||
|
successCallback(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Something went wrong");
|
console.error("Something went wrong");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class DocumentEdit extends Component {
|
|||||||
if (this.props.route.newDocument) {
|
if (this.props.route.newDocument) {
|
||||||
store.atlasId = this.props.params.id;
|
store.atlasId = this.props.params.id;
|
||||||
store.newDocument = true;
|
store.newDocument = true;
|
||||||
|
} else if (this.props.route.newChildDocument) {
|
||||||
|
store.documentId = this.props.params.id;
|
||||||
|
store.newChildDocument = true;
|
||||||
|
store.fetchDocument();
|
||||||
} else {
|
} else {
|
||||||
store.documentId = this.props.params.id;
|
store.documentId = this.props.params.id;
|
||||||
store.newDocument = false;
|
store.newDocument = false;
|
||||||
@@ -44,7 +48,7 @@ class DocumentEdit extends Component {
|
|||||||
// alert("Please add a title before saving (hint: Write a markdown header)");
|
// alert("Please add a title before saving (hint: Write a markdown header)");
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
if (store.newDocument) {
|
if (store.newDocument || store.newChildDocument) {
|
||||||
store.saveDocument();
|
store.saveDocument();
|
||||||
} else {
|
} else {
|
||||||
store.updateDocument();
|
store.updateDocument();
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ const parseHeader = (text) => {
|
|||||||
const documentEditStore = new class DocumentEditStore {
|
const documentEditStore = new class DocumentEditStore {
|
||||||
@observable documentId = null;
|
@observable documentId = null;
|
||||||
@observable atlasId = null;
|
@observable atlasId = null;
|
||||||
|
@observable parentDocument;
|
||||||
@observable title;
|
@observable title;
|
||||||
@observable text;
|
@observable text;
|
||||||
@observable newDocument;
|
@observable newDocument;
|
||||||
|
@observable newChildDocument;
|
||||||
|
|
||||||
@observable preview;
|
@observable preview;
|
||||||
@observable isFetching;
|
@observable isFetching;
|
||||||
@@ -35,9 +37,13 @@ const documentEditStore = new class DocumentEditStore {
|
|||||||
const data = await client.post('/documents.info', {
|
const data = await client.post('/documents.info', {
|
||||||
id: this.documentId,
|
id: this.documentId,
|
||||||
})
|
})
|
||||||
const { title, text } = data.data;
|
if (this.newDocument) {
|
||||||
this.title = title;
|
const { title, text } = data.data;
|
||||||
this.text = text;
|
this.title = title;
|
||||||
|
this.text = text;
|
||||||
|
} else {
|
||||||
|
this.parentDocument = data.data;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Something went wrong");
|
console.error("Something went wrong");
|
||||||
}
|
}
|
||||||
@@ -51,7 +57,8 @@ const documentEditStore = new class DocumentEditStore {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await client.post('/documents.create', {
|
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,
|
title: this.title,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
class SaveAction extends React.Component {
|
class SaveAction extends React.Component {
|
||||||
propTypes = {
|
static propTypes = {
|
||||||
onClick: React.PropTypes.func.isRequired,
|
onClick: React.PropTypes.func.isRequired,
|
||||||
disabled: React.PropTypes.bool,
|
disabled: React.PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
|
|||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
import Document from 'components/Document';
|
import Document from 'components/Document';
|
||||||
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
|
||||||
|
import Flex from 'components/Flex';
|
||||||
|
import Tree from 'components/Tree';
|
||||||
|
|
||||||
import styles from './DocumentScene.scss';
|
import styles from './DocumentScene.scss';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
const cx = classNames.bind(styles);
|
||||||
|
|
||||||
|
import treeStyles from 'components/Tree/Tree.scss';
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class DocumentScene extends React.Component {
|
class DocumentScene extends React.Component {
|
||||||
@@ -24,6 +30,13 @@ class DocumentScene extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps = (nextProps) => {
|
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
|
// Scroll to anchor after loading, and only once
|
||||||
const { hash } = this.props.location;
|
const { hash } = this.props.location;
|
||||||
|
|
||||||
@@ -43,6 +56,19 @@ class DocumentScene extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderNode = (node) => {
|
||||||
|
return (
|
||||||
|
<span className={ treeStyles.nodeLabel } onClick={this.onClickNode.bind(null, node)}>
|
||||||
|
{node.module.name}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (tree) => {
|
||||||
|
console.log(tree);
|
||||||
|
store.updateNavigationTree(tree);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const doc = store.document;
|
const doc = store.document;
|
||||||
let title;
|
let title;
|
||||||
@@ -51,6 +77,11 @@ class DocumentScene extends React.Component {
|
|||||||
if (doc) {
|
if (doc) {
|
||||||
actions = (
|
actions = (
|
||||||
<div className={ styles.actions }>
|
<div className={ styles.actions }>
|
||||||
|
{ store.isAtlas ? (
|
||||||
|
<HeaderAction>
|
||||||
|
<Link to={ `/documents/${doc.id}/new` }>New document</Link>
|
||||||
|
</HeaderAction>
|
||||||
|
) : null }
|
||||||
<HeaderAction>
|
<HeaderAction>
|
||||||
<Link to={ `/documents/${doc.id}/edit` }>Edit</Link>
|
<Link to={ `/documents/${doc.id}/edit` }>Edit</Link>
|
||||||
</HeaderAction>
|
</HeaderAction>
|
||||||
@@ -74,13 +105,30 @@ class DocumentScene extends React.Component {
|
|||||||
titleText={ titleText }
|
titleText={ titleText }
|
||||||
actions={ actions }
|
actions={ actions }
|
||||||
>
|
>
|
||||||
<CenteredContent>
|
{ store.isFetching ? (
|
||||||
{ store.isFetching ? (
|
<CenteredContent>
|
||||||
<AtlasPreviewLoading />
|
<AtlasPreviewLoading />
|
||||||
) : (
|
</CenteredContent>
|
||||||
<Document document={ doc } />
|
) : (
|
||||||
) }
|
<Flex flex={ true }>
|
||||||
</CenteredContent>
|
{ store.isAtlas ? (
|
||||||
|
<div className={ styles.sidebar }>
|
||||||
|
<Tree
|
||||||
|
paddingLeft={10}
|
||||||
|
tree={ doc.atlas.navigationTree }
|
||||||
|
onChange={this.handleChange}
|
||||||
|
isNodeCollapsed={this.isNodeCollapsed}
|
||||||
|
renderNode={this.renderNode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
<Flex flex={ true } justify={ 'center' }>
|
||||||
|
<CenteredContent>
|
||||||
|
<Document document={ doc } />
|
||||||
|
</CenteredContent>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
) }
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { observable, action } from 'mobx';
|
import { observable, action, computed } from 'mobx';
|
||||||
import { client } from 'utils/ApiClient';
|
import { client } from 'utils/ApiClient';
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
|
|
||||||
@@ -8,6 +8,13 @@ const store = new class DocumentSceneStore {
|
|||||||
@observable isFetching = true;
|
@observable isFetching = true;
|
||||||
@observable isDeleting;
|
@observable isDeleting;
|
||||||
|
|
||||||
|
/* Computed */
|
||||||
|
|
||||||
|
@computed get isAtlas() {
|
||||||
|
return this.document &&
|
||||||
|
this.document.atlas.type === 'atlas';
|
||||||
|
}
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
@action fetchDocument = async (id) => {
|
@action fetchDocument = async (id) => {
|
||||||
@@ -35,6 +42,20 @@ const store = new class DocumentSceneStore {
|
|||||||
}
|
}
|
||||||
this.isFetching = false;
|
this.isFetching = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action updateNavigationTree = async (tree) => {
|
||||||
|
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;
|
export default store;
|
||||||
@@ -16,10 +16,8 @@ renderer.heading = (text, level) => {
|
|||||||
const headingSlug = slug(text);
|
const headingSlug = slug(text);
|
||||||
return `
|
return `
|
||||||
<h${level}>
|
<h${level}>
|
||||||
<a name="${headingSlug}" class="anchor" href="#${headingSlug}">
|
|
||||||
<span class="header-link"> </span>
|
|
||||||
</a>
|
|
||||||
${text}
|
${text}
|
||||||
|
<a name="${headingSlug}" class="anchor" href="#${headingSlug}">#</a>
|
||||||
</h${level}>
|
</h${level}>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user