1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-08 06:55:26 +02:00

Fixed comments,added extra solutions for the first task

This commit is contained in:
timakasucces 2019-03-05 21:36:18 +03:00
commit df10d46198
62 changed files with 2797 additions and 2709 deletions

View file

@ -1,9 +1,10 @@
{ {
"title": "CodeX Editor   🤩🧦🤨", "title": "CodeX Docs",
"menu": [ "menu": [
"Guides", "Guides",
"API", "API",
"Plugins", "Plugins",
{"title": "Support Project", "uri": "/support"} {"title": "Support Project", "uri": "/support"}
] ],
"landingFrameSrc": "https://codex.so/editor?frame=1"
} }

2
.gitignore vendored
View file

@ -70,3 +70,5 @@ typings/
# Cache of babel and others # Cache of babel and others
.cache/ .cache/
.eslintcache .eslintcache
.DS_Store
.codexdocsrc

11
nodemon.json Normal file
View file

@ -0,0 +1,11 @@
{
"ignore": [
"node_modules",
"src/frontend",
"public/dist"
],
"events": {
"restart": "echo \"App restarted due to: '$FILENAME'\""
},
"ext": "js,twig"
}

View file

@ -16,17 +16,23 @@
"@babel/polyfill": "^7.0.0", "@babel/polyfill": "^7.0.0",
"bcrypt": "^3.0.3", "bcrypt": "^3.0.3",
"body-parser": "latest", "body-parser": "latest",
"codex.editor": "^2.1.3", "codex.editor": "^2.8.1",
"codex.editor.delimiter": "^1.0.2",
"codex.editor.embed": "^2.1.2",
"codex.editor.header": "^2.0.5", "codex.editor.header": "^2.0.5",
"commander": "^2.19.0", "commander": "^2.19.0",
"codex.editor.image": "^2.0.3",
"codex.editor.quote": "^2.1.5",
"codex.editor.raw": "^2.0.2",
"cookie-parser": "~1.4.3", "cookie-parser": "~1.4.3",
"csurf": "^1.9.0", "csurf": "^1.9.0",
"debug": "~4.1.0", "debug": "~4.1.0",
"dotenv": "^6.2.0", "dotenv": "^6.2.0",
"jsonwebtoken": "^8.4.0",
"eslint-plugin-standard": "^4.0.0",
"express": "~4.16.0", "express": "~4.16.0",
"http-errors": "~1.7.1", "http-errors": "~1.7.1",
"jsonwebtoken": "^8.4.0", "module-dispatcher": "^2.0.0",
"module-dispatcher": "^1.0.2",
"morgan": "~1.9.0", "morgan": "~1.9.0",
"multer": "^1.3.1", "multer": "^1.3.1",
"nedb": "^1.8.0", "nedb": "^1.8.0",
@ -45,9 +51,9 @@
"babel-loader": "^8.0.2", "babel-loader": "^8.0.2",
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-http": "^4.0.0", "chai-http": "^4.0.0",
"codex.editor.code": "^2.0.0", "codex.editor.code": "^2.3.1",
"codex.editor.inline-code": "^1.0.1", "codex.editor.inline-code": "^1.2.0",
"codex.editor.list": "^1.0.2", "codex.editor.list": "^1.2.3",
"codex.editor.marker": "^1.0.1", "codex.editor.marker": "^1.0.1",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
@ -55,6 +61,8 @@
"eslint": "^5.3.0", "eslint": "^5.3.0",
"eslint-config-codex": "github:codex-team/eslint-config", "eslint-config-codex": "github:codex-team/eslint-config",
"eslint-plugin-chai-friendly": "^0.4.1", "eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.1",
"highlight.js": "^9.13.1", "highlight.js": "^9.13.1",
"husky": "^1.1.2", "husky": "^1.1.2",
"mini-css-extract-plugin": "^0.4.3", "mini-css-extract-plugin": "^0.4.3",

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
.hljs{display:block;background:#fff;padding:.5em;color:#333;overflow-x:auto}.hljs-comment,.hljs-meta{color:#969896}.hljs-emphasis,.hljs-quote,.hljs-string,.hljs-strong,.hljs-template-variable,.hljs-variable{color:#df5000}.hljs-keyword,.hljs-selector-tag,.hljs-type{color:#a71d5d}.hljs-attribute,.hljs-bullet,.hljs-literal,.hljs-symbol{color:#0086b3}.hljs-name,.hljs-section{color:#63a35c}.hljs-tag{color:#333}.hljs-attr,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-title{color:#795da3}.hljs-addition{color:#55a532;background-color:#eaffea}.hljs-deletion{color:#bd2c00;background-color:#ffecec}.hljs-link{text-decoration:underline} .hljs{display:block;overflow-x:auto;padding:.5em;color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-built_in,.hljs-class .hljs-title{color:#e6c07b}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -17,9 +17,9 @@ app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'twig'); app.set('view engine', 'twig');
require('./utils/twig'); require('./utils/twig');
app.use(logger('dev')); app.use(logger('dev '));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({extended: true})); app.use(express.urlencoded({ extended: true }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../public'))); app.use(express.static(path.join(__dirname, '../public')));

View file

@ -0,0 +1,26 @@
const Alias = require('../models/alias');
/**
* @class Aliases
* @classdesc Aliases controller
*/
class Aliases {
/**
* @static
* Find and return entity with given alias
*
* @param {string} aliasName - alias name of entity
* @returns {Promise<Alias>}
*/
static async get(aliasName) {
const alias = await Alias.get(aliasName);
if (!alias.id) {
throw new Error('Entity with given alias does not exist');
}
return alias;
}
}
module.exports = Aliases;

View file

@ -1,4 +1,5 @@
const Model = require('../models/page'); const Model = require('../models/page');
const Alias = require('../models/alias');
/** /**
* @class Pages * @class Pages
@ -85,7 +86,18 @@ class Pages {
const page = new Model(data); const page = new Model(data);
return page.save(); const insertedPage = await page.save();
if (insertedPage.uri) {
const alias = new Alias({
id: insertedPage._id,
type: Alias.types.PAGE
}, insertedPage.uri);
alias.save();
}
return insertedPage;
} catch (validationError) { } catch (validationError) {
throw new Error(validationError); throw new Error(validationError);
} }
@ -132,14 +144,33 @@ class Pages {
*/ */
static async update(id, data) { static async update(id, data) {
const page = await Model.get(id); const page = await Model.get(id);
const previousUri = page.uri;
if (!page._id) { if (!page._id) {
throw new Error('Page with given id does not exist'); throw new Error('Page with given id does not exist');
} }
page.data = data; if (data.uri && !data.uri.match(/^[a-z0-9'-]+$/i)) {
throw new Error('Uri has unexpected characters');
}
return page.save(); page.data = data;
const updatedPage = await page.save();
if (updatedPage.uri !== previousUri) {
if (updatedPage.uri) {
const alias = new Alias({
id: updatedPage._id,
type: Alias.types.PAGE
}, updatedPage.uri);
alias.save();
}
Alias.markAsDeprecated(previousUri);
}
return updatedPage;
} }
/** /**
@ -155,6 +186,10 @@ class Pages {
throw new Error('Page with given id does not exist'); throw new Error('Page with given id does not exist');
} }
const alias = await Alias.get(page.uri);
await alias.destroy();
return page.destroy(); return page.destroy();
} }
} }

View file

@ -0,0 +1,126 @@
const Model = require('../models/pageOrder');
/**
* @class PagesOrder
* @classdesc PagesOrder controller
*
* Manipulates with Pages: changes the order, deletes, updates and so on...
*/
class PagesOrder {
/**
* Returns Page's order
*
* @param {string} parentId - of which page we want to get children order
* @returns {Promise<PageOrder>}
*/
static async get(parentId) {
const order = await Model.get(parentId);
if (!order._id) {
throw new Error('Page with given id does not contain order');
}
return order;
}
/**
* Returns all records about page's order
*
* @returns {Promise<PagesOrder[]>}
*/
static async getAll() {
return Model.getAll();
}
/**
* Pushes the child page to the parent's order list
*
* @param {string} parentId - parent page's id
* @param {string} childId - new page pushed to the order
*/
static async push(parentId, childId) {
const order = await Model.get(parentId);
order.push(childId);
await order.save();
}
/**
* Move one page to another Page's order
*
* @param {string} oldParentId - old parent page's id
* @param {string} newParentId - new parent page's id
* @param {string} targetPageId - page's id which is changing the parent page
*/
static async move(oldParentId, newParentId, targetPageId) {
const oldParentOrder = await Model.get(oldParentId);
oldParentOrder.remove(targetPageId);
await oldParentOrder.save();
const newParentOrder = await Model.get(newParentId);
newParentOrder.push(targetPageId);
await newParentOrder.save();
}
/**
* Returns new array with ordered pages
*
* @param {Page[]} pages - list of all available pages
* @param {string} currentPageId - page's id around which we are ordering
* @param {string} parentPageId - parent page's id that contains page above
* @param {Boolean} ignoreSelf - should we ignore current page in list or not
* @return {Page[]}
*/
static async getOrderedChildren(pages, currentPageId, parentPageId, ignoreSelf = false) {
const children = await Model.get(parentPageId);
const unordered = pages.filter(page => page._parent === parentPageId).map(page => page._id);
// Create unique array with ordered and unordered pages id
const ordered = [ ...new Set([...children.order, ...unordered]) ];
const result = [];
ordered.forEach(pageId => {
pages.forEach(page => {
if (page._id === pageId && (pageId !== currentPageId || !ignoreSelf)) {
result.push(page);
}
});
});
return result;
}
/**
* @param {string[]} unordered
* @param {string} currentPageId - page's id that changes the order
* @param {string} parentPageId - parent page's id that contains both two pages
* @param {string} putAbovePageId - page's id above which we put the target page
*/
static async update(unordered, currentPageId, parentPageId, putAbovePageId) {
const pageOrder = await Model.get(parentPageId);
// Create unique array with ordered and unordered pages id
pageOrder.order = [ ...new Set([...pageOrder.order, ...unordered]) ];
pageOrder.putAbove(currentPageId, putAbovePageId);
await pageOrder.save();
}
/**
* @param parentId
* @returns {Promise<void>}
*/
static async remove(parentId) {
const order = await Model.get(parentId);
if (!order._id) {
throw new Error('Page with given id does not contain order');
}
return order.destroy();
}
}
module.exports = PagesOrder;

View file

@ -23,14 +23,14 @@ class Docs {
* @constructor * @constructor
*/ */
constructor() { constructor() {
console.log('CodeX Docs initialized');
this.writing = new Writing(); this.writing = new Writing();
this.page = new Page(); this.page = new Page();
document.addEventListener('DOMContentLoaded', (event) => { document.addEventListener('DOMContentLoaded', (event) => {
this.docReady(); this.docReady();
}); });
console.log('CodeX Docs initialized');
} }
/** /**

View file

@ -1,6 +1,9 @@
import hljs from 'highlight.js/lib/highlight'; import hljs from 'highlight.js/lib/highlight';
import javascript from 'highlight.js/lib/languages/javascript'; import javascript from 'highlight.js/lib/languages/javascript';
import style from 'highlight.js/styles/github-gist.css'; import xml from 'highlight.js/lib/languages/xml';
import json from 'highlight.js/lib/languages/json';
// eslint-disable-next-line
import style from 'highlight.js/styles/atom-one-dark.css';
/** /**
* @class CodeStyles * @class CodeStyles
@ -11,11 +14,13 @@ export default class CodeStyler {
* @param {string} selector - CSS selector for code blocks * @param {string} selector - CSS selector for code blocks
* @param {string[]} languages - list of languages to highlight, see hljs.listLanguages() * @param {string[]} languages - list of languages to highlight, see hljs.listLanguages()
*/ */
constructor({selector, languages = [ 'javascript' ]}) { constructor({ selector, languages = ['javascript', 'xml', 'json'] }) {
this.codeBlocksSelector = selector; this.codeBlocksSelector = selector;
this.languages = languages; this.languages = languages;
this.langsAvailable = { this.langsAvailable = {
javascript javascript,
xml,
json
}; };
this.init(); this.init();

View file

@ -1,9 +1,17 @@
import CodeXEditor from 'codex.editor'; import CodeXEditor from 'codex.editor';
/**
* Tools for the Editor
*/
import Header from 'codex.editor.header'; import Header from 'codex.editor.header';
import CodeTool from 'codex.editor.code'; import Quote from 'codex.editor.quote';
import InlineCode from 'codex.editor.inline-code';
import Marker from 'codex.editor.marker'; import Marker from 'codex.editor.marker';
import ListTool from 'codex.editor.list'; import CodeTool from 'codex.editor.code';
import Delimiter from 'codex.editor.delimiter';
import InlineCode from 'codex.editor.inline-code';
import List from 'codex.editor.list';
import RawTool from 'codex.editor.raw';
import Embed from 'codex.editor.embed';
/** /**
* Class for working with Editor.js * Class for working with Editor.js
@ -11,32 +19,59 @@ import ListTool from 'codex.editor.list';
export default class Editor { export default class Editor {
/** /**
* Creates Editor instance * Creates Editor instance
* @property {object} initialData - data to start with * @param {object} editorConfig - configuration object for Editor.js
* @param {object} data.blocks - data to start with
* @param {object} options
* @param {string} options.headerPlaceholder - placeholder for Header tool
*/ */
constructor({initialData}) { constructor(editorConfig = {}, options = {}) {
this.editor = new CodeXEditor({ const defaultConfig = {
tools: { tools: {
header: { header: {
class: Header, class: Header,
inlineToolbar: ['link', 'marker'],
config: { config: {
placeholder: 'Enter a title' placeholder: options.headerPlaceholder || ''
} }
}, },
code: CodeTool, // image: {
// class: ImageTool,
// inlineToolbar: true,
// config: {
// endpoints: {
// byFile: '/editor/transport',
// byUrl: '/editor/transport'
// }
// }
// },
list: {
class: List,
inlineToolbar: true
},
quote: {
class: Quote,
inlineToolbar: true
},
code: {
class: CodeTool,
shortcut: 'CMD+SHIFT+D'
},
rawTool: {
class: RawTool,
shortcut: 'CMD+SHIFT+R'
},
delimiter: Delimiter,
embed: Embed,
inlineCode: { inlineCode: {
class: InlineCode, class: InlineCode,
shortcut: 'CMD+SHIFT+I' shortcut: 'CMD+SHIFT+C'
}, },
Marker: { marker: {
class: Marker, class: Marker,
shortcut: 'CMD+SHIFT+M' shortcut: 'CMD+SHIFT+M'
},
list: {
class: ListTool,
inlineToolbar: true
} }
}, },
data: initialData || { data: {
blocks: [ blocks: [
{ {
type: 'header', type: 'header',
@ -47,7 +82,9 @@ export default class Editor {
} }
] ]
} }
}); };
this.editor = new CodeXEditor(Object.assign(defaultConfig, editorConfig));
} }
/** /**

View file

@ -27,7 +27,7 @@ export default class Writing {
* Init code highlighting * Init code highlighting
*/ */
async createCodeStyling() { async createCodeStyling() {
const {default: CodeStyler} = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler'); const { default: CodeStyler } = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
return new CodeStyler({ return new CodeStyler({
selector: '.block-code' selector: '.block-code'

View file

@ -27,7 +27,10 @@ export default class Writing {
this.nodes = { this.nodes = {
editorWrapper: null, editorWrapper: null,
saveButton: null, saveButton: null,
parentIdSelector: null removeButton: null,
parentIdSelector: null,
putAboveIdSelector: null,
uriInput: null
}; };
} }
@ -40,11 +43,7 @@ export default class Writing {
/** /**
* Create Editor * Create Editor
*/ */
this.nodes.editorWrapper = document.createElement('div'); this.nodes.editorWrapper = document.getElementById('codex-editor');
this.nodes.editorWrapper.id = 'codex-editor';
moduleEl.appendChild(this.nodes.editorWrapper);
if (settings.page) { if (settings.page) {
this.page = settings.page; this.page = settings.page;
} }
@ -56,11 +55,28 @@ export default class Writing {
/** /**
* Activate form elements * Activate form elements
*/ */
this.nodes.saveButton = moduleEl.querySelector('[name="js-submit"]'); this.nodes.saveButton = moduleEl.querySelector('[name="js-submit-save"]');
this.nodes.saveButton.addEventListener('click', () => { this.nodes.saveButton.addEventListener('click', () => {
this.saveButtonClicked(); this.saveButtonClicked();
}); });
this.nodes.removeButton = moduleEl.querySelector('[name="js-submit-remove"]');
if (this.nodes.removeButton) {
this.nodes.removeButton.addEventListener('click', () => {
const isUserAgree = window.confirm('Are you sure?');
if (!isUserAgree) {
return;
}
this.removeButtonClicked();
});
}
this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]'); this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]');
this.nodes.putAboveIdSelector = moduleEl.querySelector('[name="above"]');
this.nodes.uriInput = moduleEl.querySelector('[name="uri-input"]');
}; };
/** /**
@ -68,10 +84,14 @@ export default class Writing {
* @return {Promise<Editor>} * @return {Promise<Editor>}
*/ */
async loadEditor() { async loadEditor() {
const {default: Editor} = await import(/* webpackChunkName: "editor" */ './../classes/editor'); const { default: Editor } = await import(/* webpackChunkName: "editor" */ './../classes/editor');
return new Editor({ const editorConfig = this.page ? {
initialData: this.page ? this.page.body : null data: this.page.body
} : {};
return new Editor(editorConfig, {
headerPlaceholder: 'Enter a title'
}); });
} }
@ -84,13 +104,31 @@ export default class Writing {
const editorData = await this.editor.save(); const editorData = await this.editor.save();
const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null; const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null;
const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null; const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null;
let uri = '';
if (this.nodes.uriInput && this.nodes.uriInput.value) {
if (this.nodes.uriInput.value.match(/^[a-z0-9'-]+$/i)) {
uri = this.nodes.uriInput.value;
} else {
throw new Error('Uri has unexpected characters');
}
}
if (!title) { if (!title) {
throw new Error('Entry should start with Header'); throw new Error('Entry should start with Header');
} }
/** get ordering selector value */
let putAbovePageId = null;
if (this.nodes.putAboveIdSelector) {
putAbovePageId = this.nodes.putAboveIdSelector.value;
}
return { return {
parent: this.nodes.parentIdSelector.value, parent: this.nodes.parentIdSelector.value,
putAbovePageId: putAbovePageId,
uri: uri,
body: editorData body: editorData
}; };
} }
@ -115,7 +153,7 @@ export default class Writing {
response = await response.json(); response = await response.json();
if (response.success) { if (response.success) {
document.location = '/page/' + response.result._id; window.location.pathname = response.result.uri ? response.result.uri : '/page/' + response.result._id;
} else { } else {
alert(response.error); alert(response.error);
console.log('Validation failed:', response.error); console.log('Validation failed:', response.error);
@ -128,4 +166,31 @@ export default class Writing {
console.log('Saving error: ', savingError); console.log('Saving error: ', savingError);
} }
} }
/**
* @returns {Promise<void>}
*/
async removeButtonClicked() {
try {
const endpoint = this.page ? '/api/page/' + this.page._id : '';
let response = await fetch(endpoint, {
method: 'DELETE'
});
response = await response.json();
if (response.success) {
if (response.result && response.result._id) {
document.location = '/page/' + response.result._id;
} else {
document.location = '/';
}
} else {
alert(response.error);
console.log('Server fetch failed:', response.error);
}
} catch (e) {
console.log('Server fetch failed due to the:', e);
}
}
} }

View file

@ -2,6 +2,16 @@
font-size: 15px; font-size: 15px;
color: var(--color-text-second); color: var(--color-text-second);
@media (--mobile) {
font-size: 13px;
display: none;
margin-top: 20px;
}
&--toggled {
display: block !important;
}
a { a {
text-decoration: none; text-decoration: none;
} }
@ -9,9 +19,21 @@
&__section { &__section {
margin-bottom: 30px; margin-bottom: 30px;
@media (--mobile) {
margin-bottom: 20px;
}
&:last-of-type {
margin-bottom: 0;
}
&-title { &-title {
margin-bottom: 15px; margin-bottom: 15px;
color: var(--color-link-active); color: var(--color-link-active);
@media (--mobile) {
margin-bottom: 10px;
}
} }
&-list { &-list {
@ -26,3 +48,18 @@
} }
} }
} }
.docs-aside-toggler {
display: none;
font-size: 13px;
cursor: pointer;
color: var(--color-text-second);
@media (--mobile) {
display: block;
}
svg {
margin-right: 10px;
}
}

View file

@ -1,13 +1,20 @@
.docs-header { .docs-header {
font-size: 15.8px;
border-bottom: 1px solid var(--color-line-gray);
line-height: 50px;
display: flex; display: flex;
flex-shrink: 0;
padding: 0 var(--layout-padding-horisontal); padding: 0 var(--layout-padding-horisontal);
border-bottom: 1px solid var(--color-line-gray);
font-size: 15.8px;
line-height: 50px;
flex-wrap: wrap;
position: relative;
@media (--mobile){
line-height: 40px;
}
a { a {
text-decoration: none;
display: inline-block; display: inline-block;
text-decoration: none;
} }
&__logo { &__logo {
@ -18,10 +25,40 @@
&__menu { &__menu {
display: flex; display: flex;
margin: 0 0 0 auto; margin: 0 0 0 auto;
padding-left: 0;
@media (--mobile) {
flex-basis: 100%;
font-size: 12px;
}
li { li {
display: inline-flex;
align-items: center;
list-style: none; list-style: none;
margin-left: 20px; margin-left: 20px;
@media (--mobile) {
margin-left: 0;
margin-right: 15px;
}
}
&-add {
@media (--mobile) {
position: absolute;
right: 15px;
top: 15px;
line-height: 1em;
margin: 0 !important;
}
a {
@media (--mobile) {
font-size: 0;
padding: 8px;
margin-right: 0;
}
}
} }
a:not(.docs-header__button) { a:not(.docs-header__button) {
@ -35,6 +72,7 @@
&__button { &__button {
@apply --button; @apply --button;
@apply --button-primary;
margin: auto 30px auto auto; margin: auto 30px auto auto;
} }
} }

View file

@ -0,0 +1,30 @@
/**
* Index page landing iframe
*/
.landing-body {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.landing-loader {
position: absolute;
left: 50%;
top: 50%;
z-index: -1;
transform: translate(-50%, -50%);
& > svg {
width: 80px;
height: 80px;
}
}
.landing-frame {
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 500ms ease;
will-change: opacity;
}

View file

@ -6,10 +6,18 @@
display: flex; display: flex;
color: var(--color-text-second); color: var(--color-text-second);
@media (--mobile) {
font-size: 13px;
}
&-nav { &-nav {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
@media (--mobile) {
display: none;
}
&:hover { &:hover {
color: var(--color-link-active); color: var(--color-link-active);
} }
@ -24,10 +32,15 @@
&-time { &-time {
margin-left: auto; margin-left: auto;
@media (--mobile) {
margin-left: 0;
}
} }
&-button { &-button {
@apply --button; @apply --button;
@apply --button-primary;
padding: 5px 10px; padding: 5px 10px;
font-size: 13px; font-size: 13px;
margin-left: 10px; margin-left: 10px;
@ -40,6 +53,26 @@
letter-spacing: -0.04px; letter-spacing: -0.04px;
margin-bottom: -0.2em; margin-bottom: -0.2em;
} }
.cdx-marker {
background: rgba(245,235,111,0.33);
padding: 3px 0;
}
&__content {
a {
text-decoration: none;
border-bottom: 1px solid #000;
padding-bottom: 1px;
color: inherit;
&:hover {
color: var(--color-link-active);
border-bottom-color: var(--color-link-active);
}
}
}
} }
/** /**
@ -66,9 +99,9 @@
*/ */
.block-code { .block-code {
padding: 20px !important; padding: 20px !important;
border-radius: 5px;
font-size: 13px; font-size: 13px;
border-radius: 3px; //border: 1px solid var(--color-line-gray);
border: 1px solid var(--color-line-gray);
font-family: Menlo,Monaco,Consolas,Courier New,monospace; font-family: Menlo,Monaco,Consolas,Courier New,monospace;
line-height: 1.7em; line-height: 1.7em;
} }

View file

@ -1,20 +1,57 @@
.writing-header { .writing-header {
display: flex; display: flex;
padding: 15px 0;
margin-top: calc(-1 * var(--layout-padding-vertical));
position: sticky; position: sticky;
top: 0; top: 0;
padding: 15px 0;
margin-top: calc(-1 * var(--layout-padding-vertical));
background: #fff; background: #fff;
z-index: 2;
box-shadow: 0 3px 10px #fff; box-shadow: 0 3px 10px #fff;
z-index: 2;
font-size: 14px;
&__save { &__save {
@apply --button; @apply --button;
margin-left: auto; @apply --button-primary;
margin: auto;
} }
&__left { &__left {
margin: auto 0; margin: auto 0;
color: var(--color-text-second); color: var(--color-text-second);
& span {
margin-right: 10px;
}
}
select {
max-width: 100px
} }
} }
.writing-buttons {
&__remove {
@apply --button;
@apply --button-danger;
}
}
.uri-input {
box-sizing: border-box;
width: 100%;
padding: 10px 12px;
border-radius: 3px;
border: 1px solid rgba(201, 201, 204, 0.48);
box-shadow: inset 0 1px 2px 0 rgba(35, 44, 72, 0.06);
outline: none;
}
.writing-editor {
@media (--desktop) {
margin: 0 -100px;
}
}
.codex-editor__redactor .ce-block:first-of-type .ce-header {
font-size: 32px;
}

View file

@ -2,14 +2,31 @@
display: flex; display: flex;
padding: 0 var(--layout-padding-horisontal); padding: 0 var(--layout-padding-horisontal);
@media (--mobile) {
flex-wrap: wrap;
}
&__aside { &__aside {
width: var(--layout-width-aside); width: var(--layout-width-aside);
@media (--mobile) {
width: 100%;
flex-basis: 100%;
padding: 20px var(--layout-padding-horisontal) !important;
margin: 0 calc(-1 * var(--layout-padding-horisontal));
border-bottom: 1px solid var(--color-line-gray);
}
} }
&__content { &__content {
flex-grow: 2; flex-grow: 2;
word-wrap: break-word; word-wrap: break-word;
@media (--mobile) {
width: 100%;
flex-basis: 100%;
}
&-inner { &-inner {
max-width: var(--layout-width-main-col); max-width: var(--layout-width-main-col);
margin: 0 auto; margin: 0 auto;
@ -19,5 +36,9 @@
&__aside, &__aside,
&__content { &__content {
padding: var(--layout-padding-vertical) 0; padding: var(--layout-padding-vertical) 0;
@media (--mobile) {
padding: 20px 0;
}
} }
} }

View file

@ -5,6 +5,7 @@
@import url('components/aside.pcss'); @import url('components/aside.pcss');
@import url('components/writing.pcss'); @import url('components/writing.pcss');
@import url('components/page.pcss'); @import url('components/page.pcss');
@import url('components/landing.pcss');
body { body {
font-family: system-ui, Helvetica, Arial, Verdana; font-family: system-ui, Helvetica, Arial, Verdana;

View file

@ -3,6 +3,8 @@
--color-text-second: #7B7E89; --color-text-second: #7B7E89;
--color-line-gray: #E8E8EB; --color-line-gray: #E8E8EB;
--color-link-active: #388AE5; --color-link-active: #388AE5;
--color-button-danger: #ff1629;
--color-gray-border: rgba(var(--color-line-gray), 0.48);
/** /**
* Site layout sizes * Site layout sizes
@ -12,22 +14,54 @@
--layout-width-aside: 200px; --layout-width-aside: 200px;
--layout-width-main-col: 650px; --layout-width-main-col: 650px;
@media (--mobile) {
--layout-padding-horisontal: 15px;
--layout-padding-vertical: 15px;
}
--button { --button {
display: inline-block; display: inline-block;
background: var(--color-link-active);
color: #fff;
border-radius: 3px;
padding: 9px 15px; padding: 9px 15px;
border-radius: 3px;
color: #6c6375;
background: #fcfcff;
box-shadow: inset 0 0 0 1px rgba(184, 189, 206, 0.2);
font-size: 14px; font-size: 14px;
line-height: 1em; line-height: 1em;
text-decoration: none; text-decoration: none;
cursor: pointer;
white-space: nowrap;
svg { svg {
margin: 0 0.3em 0 -0.05em; margin: 0 0.3em 0 -0.05em;
} }
}
--button-danger {
background: var(--color-button-danger);
color: #fff;
box-shadow: none;
&:hover {
background: color-mod(var(--color-button-danger) blackness(+10%));
}
}
--button-primary {
background: var(--color-link-active);
color: #fff;
box-shadow: none;
&:hover { &:hover {
background: color-mod(var(--color-link-active) blackness(+10%)); background: color-mod(var(--color-link-active) blackness(+10%));
} }
} }
} }
/**
* Custom media queries
*/
@custom-media --desktop all and (min-width: 1050px);
@custom-media --tablet all and (max-width: 1050px);
@custom-media --mobile all and (max-width: 980px);
@custom-media --retina all and (-webkit-min-device-pixel-ratio: 1.5);

View file

@ -0,0 +1,60 @@
<svg class="ldi-zbvw97" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 80 80" preserveAspectRatio="xMidYMid">
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" style="transform-origin:50px 50px 0">
<g style="transform-origin:50px 50px 0">
<g style="transform-origin:50px 50px 0;transform:scale(.6)">
<g style="transform-origin:50px 50px 0">
<style>
.st3{fill:#dff0fc}
.st5{fill:#69cdff}
</style>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.346153846153846s;animation-direction:normal">
<path class="st3" d="M69.4 28.7c-3-2.8-6.5-4.8-10.3-6-4.6-1.5-7.7-5.8-7.7-10.6V7.5c4.9.2 9.7 1.1 14.2 2.9 7.4 2.9 9.5 12.6 3.8 18.3z" fill="#dff0fc"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.2923076923076924s;animation-direction:normal">
<path class="st5" d="M33 11c-4.4 1.9-8.5 4.6-12.1 8l3.2 3.2c3.4 3.4 8.6 4.2 12.9 2.1 3.6-1.8 7.5-2.8 11.6-3 0-8.1-8.2-13.5-15.6-10.3z" fill="#69cdff"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.2384615384615383s;animation-direction:normal">
<path class="st3" d="M10.4 34.4c-1.8 4.5-2.7 9.3-2.9 14.2H12c4.8 0 9.1-3.1 10.6-7.7 1.3-3.8 3.3-7.3 6.1-10.3-5.7-5.7-15.4-3.6-18.3 3.8z" fill="#dff0fc"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.1846153846153846s;animation-direction:normal">
<path class="st5" d="M11 67c1.9 4.4 4.6 8.5 8 12.1l3.2-3.2c3.4-3.4 4.2-8.6 2.1-12.9-1.8-3.6-2.8-7.5-3-11.6-8.1 0-13.5 8.2-10.3 15.6z" fill="#69cdff"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.1307692307692307s;animation-direction:normal">
<path class="st5" d="M89 33c-1.9-4.4-4.6-8.5-8-12.1l-3.2 3.2c-3.4 3.4-4.2 8.6-2.1 12.9 1.8 3.6 2.8 7.5 3 11.6 8.1 0 13.5-8.2 10.3-15.6z" fill="#69cdff"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.0769230769230769s;animation-direction:normal">
<path class="st3" d="M89.6 65.6c1.8-4.5 2.7-9.3 2.9-14.2H88c-4.8 0-9.1 3.1-10.6 7.7-1.3 3.8-3.3 7.3-6.1 10.3 5.7 5.7 15.4 3.6 18.3-3.8z" fill="#dff0fc"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-1.023076923076923s;animation-direction:normal">
<path class="st5" d="M67 89c4.4-1.9 8.5-4.6 12.1-8l-3.2-3.2c-3.4-3.4-8.6-4.2-12.9-2.1-3.6 1.8-7.5 2.8-11.6 3 0 8.1 8.2 13.5 15.6 10.3z" fill="#69cdff"/>
</g>
</g>
<g style="transform-origin:50px 50px 0">
<g class="ld ld-spin" style="transform-origin:50px 50px 0;animation-duration:1.4s;animation-delay:-.9692307692307691s;animation-direction:normal">
<path class="st3" d="M34.4 89.6c4.5 1.8 9.3 2.7 14.2 2.9V88c0-4.8-3.1-9.1-7.7-10.6-3.8-1.3-7.3-3.3-10.3-6.1-5.7 5.7-3.6 15.4 3.8 18.3z" fill="#dff0fc"/>
</g>
</g>
</g>
</g>
</g>
<style>
.ld-spin {
will-change: transform;
}
@keyframes ld-spin{0%{-webkit-transform:rotate(0);transform:rotate(0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg);animation-timing-function:cubic-bezier(.215,.61,.355,1)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes ld-spin{0%{-webkit-transform:rotate(0);transform:rotate(0);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}50%{-webkit-transform:rotate(180deg);transform:rotate(180deg);animation-timing-function:cubic-bezier(.215,.61,.355,1)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}path{stroke-width:0}.ld.ld-spin{-webkit-animation:ld-spin 1s infinite;animation:ld-spin 1s infinite}
</style>
</svg>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -0,0 +1 @@
<svg width="13" height="10" viewBox="0 0 13 10" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h13v2H0V0zm0 4h13v2H0V4zm0 4h13v2H0V8z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 164 B

142
src/models/alias.js Normal file
View file

@ -0,0 +1,142 @@
const { aliases: aliasesDb } = require('../utils/database/index');
const binaryMD5 = require('../utils/crypto');
/**
* @typedef {Object} AliasData
* @property {string} _id - alias id
* @property {string} hash - alias binary hash
* @property {string} type - entity type
* @property {boolean} deprecated - indicate if alias deprecated
* @property {string} id - entity id
*
*/
/**
* @class Alias
* @classdesc Alias model
*
* @property {string} _id - alias id
* @property {string} hash - alias binary hash
* @property {string} type - entity type
* @property {boolean} deprecated - indicate if alias deprecated
* @property {string} id - entity title
*/
class Alias {
/**
* Return Alias types
*
* @returns {Object}
*/
static get types() {
return {
PAGE: 'page'
};
};
/**
* Find and return alias with given alias
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/
static async get(aliasName) {
const hash = binaryMD5(aliasName);
let data = await aliasesDb.findOne({ hash: hash, deprecated: false });
if (!data) {
data = await aliasesDb.findOne({ hash: hash });
}
return new Alias(data);
}
/**
* @constructor
*
* @param {AliasData} data
* @param {string} aliasName - alias of entity
*/
constructor(data = {}, aliasName = '') {
if (data === null) {
data = {};
}
if (data._id) {
this._id = data._id;
}
if (aliasName) {
this.hash = binaryMD5(aliasName);
}
this.data = data;
}
/**
* Save or update alias data in the database
*
* @returns {Promise<Alias>}
*/
async save() {
if (!this._id) {
const insertedRow = await aliasesDb.insert(this.data);
this._id = insertedRow._id;
} else {
await aliasesDb.update({ _id: this._id }, this.data);
}
return this;
}
/**
* Set AliasData object fields to internal model fields
*
* @param {AliasData} aliasData
*/
set data(aliasData) {
const { id, type, hash, deprecated } = aliasData;
this.id = id || this.id;
this.type = type || this.type;
this.hash = hash || this.hash;
this.deprecated = deprecated || false;
}
/**
* Return AliasData object
*
* @returns {AliasData}
*/
get data() {
return {
_id: this._id,
id: this.id,
type: this.type,
hash: this.hash,
deprecated: this.deprecated
};
}
/**
* Mark alias as deprecated
* @param {string} aliasName - alias of entity
* @returns {Promise<Alias>}
*/
static async markAsDeprecated(aliasName) {
const alias = await Alias.get(aliasName);
alias.deprecated = true;
return alias.save();
}
/**
* @returns {Promise<Alias>}
*/
async destroy() {
await aliasesDb.remove({ _id: this._id });
delete this._id;
return this;
}
}
module.exports = Alias;

View file

@ -1,12 +1,13 @@
const {pages: db} = require('../utils/database/index'); const { pages: pagesDb } = require('../utils/database/index');
const translateString = require('../utils/translation');
/** /**
* @typedef {Object} PageData * @typedef {Object} PageData
* @property {string} _id - page id * @property {string} _id - page id
* @property {string} title - page title * @property {string} title - page title
* @property {string} uri - page uri
* @property {*} body - page body * @property {*} body - page body
* @property {string} parent - id of parent page * @property {string} parent - id of parent page
*
*/ */
/** /**
@ -15,6 +16,7 @@ const {pages: db} = require('../utils/database/index');
* *
* @property {string} _id - page id * @property {string} _id - page id
* @property {string} title - page title * @property {string} title - page title
* @property {string} uri - page uri
* @property {*} body - page body * @property {*} body - page body
* @property {string} _parent - id of parent page * @property {string} _parent - id of parent page
*/ */
@ -25,7 +27,18 @@ class Page {
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
static async get(_id) { static async get(_id) {
const data = await db.findOne({_id}); const data = await pagesDb.findOne({ _id });
return new Page(data);
}
/**
* Find and return model of page with given uri
* @param {string} uri - page uri
* @returns {Promise<Page>}
*/
static async getByUri(uri) {
const data = await pagesDb.findOne({ uri });
return new Page(data); return new Page(data);
} }
@ -37,7 +50,7 @@ class Page {
* @returns {Promise<Page[]>} * @returns {Promise<Page[]>}
*/ */
static async getAll(query = {}) { static async getAll(query = {}) {
const docs = await db.find(query); const docs = await pagesDb.find(query);
return Promise.all(docs.map(doc => new Page(doc))); return Promise.all(docs.map(doc => new Page(doc)));
} }
@ -65,11 +78,12 @@ class Page {
* @param {PageData} pageData * @param {PageData} pageData
*/ */
set data(pageData) { set data(pageData) {
const {body, parent} = pageData; const { body, parent, uri } = pageData;
this.body = body || this.body; this.body = body || this.body;
this.title = this.extractTitleFromBody(); this.title = this.extractTitleFromBody();
this._parent = parent || this._parent; this.uri = uri || '';
this._parent = parent || this._parent || '0';
} }
/** /**
@ -81,6 +95,7 @@ class Page {
return { return {
_id: this._id, _id: this._id,
title: this.title, title: this.title,
uri: this.uri,
body: this.body, body: this.body,
parent: this._parent parent: this._parent
}; };
@ -96,6 +111,21 @@ class Page {
return headerBlock ? headerBlock.data.text : ''; return headerBlock ? headerBlock.data.text : '';
} }
/**
* Transform title for uri
* @return {string}
*/
transformTitleToUri() {
return translateString(this.title
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
}
/** /**
* Link given page as parent * Link given page as parent
* *
@ -111,7 +141,7 @@ class Page {
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
get parent() { get parent() {
return db.findOne({_id: this._parent}) return pagesDb.findOne({ _id: this._parent })
.then(data => new Page(data)); .then(data => new Page(data));
} }
@ -121,7 +151,7 @@ class Page {
* @returns {Promise<Page[]>} * @returns {Promise<Page[]>}
*/ */
get children() { get children() {
return db.find({parent: this._id}) return pagesDb.find({ parent: this._id })
.then(data => data.map(page => new Page(page))); .then(data => data.map(page => new Page(page)));
} }
@ -131,12 +161,14 @@ class Page {
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
async save() { async save() {
this.uri = await this.composeUri(this.uri);
if (!this._id) { if (!this._id) {
const insertedRow = await db.insert(this.data); const insertedRow = await pagesDb.insert(this.data);
this._id = insertedRow._id; this._id = insertedRow._id;
} else { } else {
await db.update({_id: this._id}, this.data); await pagesDb.update({ _id: this._id }, this.data);
} }
return this; return this;
@ -148,13 +180,37 @@ class Page {
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
async destroy() { async destroy() {
await db.remove({_id: this._id}); await pagesDb.remove({ _id: this._id });
delete this._id; delete this._id;
return this; return this;
} }
/**
* Find and return available uri
*
* @returns {Promise<string>}
*/
async composeUri(uri) {
let pageWithSameUriCount = 0;
if (!this._id) {
uri = this.transformTitleToUri();
}
if (uri) {
let pageWithSameUri = await Page.getByUri(uri);
while (pageWithSameUri._id && pageWithSameUri._id !== this._id) {
pageWithSameUriCount++;
pageWithSameUri = await Page.getByUri(uri + `-${pageWithSameUriCount}`);
}
}
return pageWithSameUriCount ? uri + `-${pageWithSameUriCount}` : uri;
}
/** /**
* Return readable page data * Return readable page data
* *

212
src/models/pageOrder.js Normal file
View file

@ -0,0 +1,212 @@
const { pagesOrder: db } = require('../utils/database/index');
/**
* @typedef {Object} PageOrderData
* @property {string} _id - row unique id
* @property {string} page - page id
* @property {Array<string>} order - list of ordered pages
*/
/**
* @class PageOrder
* @classdesc PageOrder
*
* Creates order for Pages with children
*/
class PageOrder {
/**
* Returns current Page's children order
*
* @param {string} pageId - page's id
* @returns {PageOrder}
*/
static async get(pageId) {
const order = await db.findOne({ page: pageId });
let data = {};
if (!order) {
data.page = pageId;
} else {
data = order;
}
return new PageOrder(data);
}
/**
* Find all pages which match passed query object
*
* @param {Object} query
* @returns {Promise<Page[]>}
*/
static async getAll(query = {}) {
const docs = await db.find(query);
return Promise.all(docs.map(doc => new PageOrder(doc)));
}
/**
* @constructor
*
* @param {PageOrderData} data
*/
constructor(data = {}) {
if (data === null) {
data = {};
}
if (data._id) {
this._id = data._id;
}
this.data = data;
}
/**
* constructor data setter
* @param {PageOrderData} pageOrderData
*/
set data(pageOrderData) {
this._page = pageOrderData.page || 0;
this._order = pageOrderData.order || [];
}
/**
* Return Page Children order
* @returns {PageOrderData}
*/
get data() {
return {
_id: this._id,
page: '' + this._page,
order: this._order
};
}
/**
* Pushes page id to the orders array
*
* @param {string} pageId - page's id
*/
push(pageId) {
if (typeof pageId === 'string') {
this._order.push(pageId);
} else {
throw new Error('given id is not string');
}
}
/**
* Removes page id from orders array
*
* @param {string} pageId - page's id
*/
remove(pageId) {
const found = this._order.indexOf(pageId);
if (found >= 0) {
this._order.splice(found, 1);
}
}
/**
* @param {string} currentPageId - page's id that changes the order
* @param {string} putAbovePageId - page's id above which we put the target page
*
* @returns void
*/
putAbove(currentPageId, putAbovePageId) {
const found1 = this.order.indexOf(putAbovePageId);
const found2 = this.order.indexOf(currentPageId);
if (found1 === -1 || found2 === -1) {
return;
}
const margin = found1 < found2 ? 1 : 0;
this.order.splice(found1, 0, currentPageId);
this.order.splice(found2 + margin, 1);
}
/**
* Returns page before passed page with id
*
* @param {string} pageId
*/
getPageBefore(pageId) {
const currentPageInOrder = this.order.indexOf(pageId);
/**
* If page not found or first return nothing
*/
if (currentPageInOrder <= 0) {
return;
}
return this.order[currentPageInOrder - 1];
}
/**
* Returns page before passed page with id
*
* @param pageId
*/
getPageAfter(pageId) {
const currentPageInOrder = this.order.indexOf(pageId);
/**
* If page not found or is last
*/
if (currentPageInOrder === -1 || currentPageInOrder === this.order.length - 1) {
return;
}
return this.order[currentPageInOrder + 1];
}
/**
* @param {string[]} order - define new order
*/
set order(order) {
this._order = order;
}
/**
* Returns ordered list
*
* @return {string[]}
*/
get order() {
return this._order;
}
/**
* Save or update page data in the database
*/
async save() {
if (!this._id) {
const insertedRow = await db.insert(this.data);
this._id = insertedRow._id;
} else {
await db.update({ _id: this._id }, this.data);
}
return this;
}
/**
* Remove page data from the database
*/
async destroy() {
await db.remove({ _id: this._id });
delete this._id;
return this;
}
}
module.exports = PageOrder;

View file

@ -1,4 +1,4 @@
const {password: db} = require('../utils/database/index'); const { password: db } = require('../utils/database/index');
/** /**
* @class User * @class User

35
src/routes/aliases.js Normal file
View file

@ -0,0 +1,35 @@
const express = require('express');
const router = express.Router();
const Aliases = require('../controllers/aliases');
const Pages = require('../controllers/pages');
const Alias = require('../models/alias');
/**
* GET /*
*
* Return document with given alias
*/
router.get('*', async (req, res) => {
try {
const alias = await Aliases.get(req.originalUrl.slice(1)); // Cuts first '/' character
switch (alias.type) {
case Alias.types.PAGE: {
let page = await Pages.get(alias.id);
let pageParent = await page.parent;
res.render('pages/page', {
page, pageParent
});
}
}
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
module.exports = router;

View file

@ -2,12 +2,13 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const multer = require('multer')(); const multer = require('multer')();
const Pages = require('../../controllers/pages'); const Pages = require('../../controllers/pages');
const PagesOrder = require('../../controllers/pagesOrder');
/** /**
* GET /page/:id * GET /page/:id
* *
* Return PageData of page with given id * Return PageData of page with given id
*/ */
router.get('/page/:id', async (req, res) => { router.get('/page/:id', async (req, res) => {
try { try {
const page = await Pages.get(req.params.id); const page = await Pages.get(req.params.id);
@ -52,8 +53,11 @@ router.get('/pages', async (req, res) => {
*/ */
router.put('/page', multer.any(), async (req, res) => { router.put('/page', multer.any(), async (req, res) => {
try { try {
const {title, body, parent} = req.body; const { title, body, parent } = req.body;
const page = await Pages.insert({title, body, parent}); const page = await Pages.insert({ title, body, parent });
/** push to the orders array */
await PagesOrder.push(parent, page._id);
res.json({ res.json({
success: true, success: true,
@ -73,12 +77,24 @@ router.put('/page', multer.any(), async (req, res) => {
* Update page data in the database * Update page data in the database
*/ */
router.post('/page/:id', multer.any(), async (req, res) => { router.post('/page/:id', multer.any(), async (req, res) => {
const {id} = req.params; const { id } = req.params;
try { try {
const {title, body, parent} = req.body; const { title, body, parent, putAbovePageId, uri } = req.body;
const page = await Pages.update(id, {title, body, parent}); const pages = await Pages.getAll();
let page = await Pages.get(id);
if (page._parent !== parent) {
await PagesOrder.move(page._parent, parent, id);
} else {
if (putAbovePageId && putAbovePageId !== '0') {
const unordered = pages.filter(_page => _page._parent === page._parent).map(_page => _page._id);
await PagesOrder.update(unordered, page._id, page._parent, putAbovePageId);
}
}
page = await Pages.update(id, { title, body, parent, uri });
res.json({ res.json({
success: true, success: true,
result: page result: page
@ -98,11 +114,56 @@ router.post('/page/:id', multer.any(), async (req, res) => {
*/ */
router.delete('/page/:id', async (req, res) => { router.delete('/page/:id', async (req, res) => {
try { try {
const page = await Pages.remove(req.params.id); const pageId = req.params.id;
const page = await Pages.get(pageId);
const parentPageOrder = await PagesOrder.get(page._parent);
const pageBeforeId = parentPageOrder.getPageBefore(page._id);
const pageAfterId = parentPageOrder.getPageAfter(page._id);
let pageToRedirect;
if (pageBeforeId) {
pageToRedirect = await Pages.get(pageBeforeId);
} else if (pageAfterId) {
pageToRedirect = await Pages.get(pageAfterId);
} else {
pageToRedirect = page._parent !== '0' ? await Pages.get(page._parent) : null;
}
/**
* remove current page and go deeper to remove children with orders
*
* @param startFrom
* @returns {Promise<void>}
*/
const deleteRecursively = async function (startFrom) {
let order = [];
try {
const children = await PagesOrder.get(startFrom);
order = children.order;
} catch (e) {}
order.forEach(async id => {
await deleteRecursively(id);
});
await Pages.remove(startFrom);
try {
await PagesOrder.remove(startFrom);
} catch (e) {}
};
await deleteRecursively(req.params.id);
// remove also from parent's order
parentPageOrder.remove(req.params.id);
await parentPageOrder.save();
res.json({ res.json({
success: true, success: true,
result: page result: pageToRedirect
}); });
} catch (err) { } catch (err) {
res.status(400).json({ res.status(400).json({

View file

@ -4,7 +4,7 @@ const router = express.Router();
/* GET home page. */ /* GET home page. */
router.get('/', verifyToken, async (req, res) => { router.get('/', verifyToken, async (req, res) => {
res.render('index', { title: 'Express', isAuthorized: res.locals.isAuthorized }); res.render('pages/index', { isAuthorized: res.locals.isAuthorized });
}); });
module.exports = router; module.exports = router;

View file

@ -4,6 +4,7 @@ const router = express.Router();
const home = require('./home'); const home = require('./home');
const pages = require('./pages'); const pages = require('./pages');
const auth = require('./auth'); const auth = require('./auth');
const aliases = require('./aliases');
const api = require('./api'); const api = require('./api');
const pagesMiddleware = require('./middlewares/pages'); const pagesMiddleware = require('./middlewares/pages');
@ -12,5 +13,6 @@ router.use('/', pagesMiddleware, home);
router.use('/', pagesMiddleware, pages); router.use('/', pagesMiddleware, pages);
router.use('/', pagesMiddleware, auth); router.use('/', pagesMiddleware, auth);
router.use('/api', api); router.use('/api', api);
router.use('/', aliases);
module.exports = router; module.exports = router;

View file

@ -1,15 +1,50 @@
const Pages = require('../../controllers/pages'); const Pages = require('../../controllers/pages');
const PagesOrder = require('../../controllers/pagesOrder');
const asyncMiddleware = require('../../utils/asyncMiddleware'); const asyncMiddleware = require('../../utils/asyncMiddleware');
/** /**
* Process one-level pages list to parent-childrens list * Process one-level pages list to parent-children list
*
* @param {string} parentPageId - parent page id
* @param {Page[]} pages - list of all available pages * @param {Page[]} pages - list of all available pages
* @param {PagesOrder[]} pagesOrder - list of pages order
* @param {number} level
* @param {number} currentLevel
*
* @return {Page[]} * @return {Page[]}
*/ */
function createMenuTree(pages) { function createMenuTree(parentPageId, pages, pagesOrder, level = 1, currentLevel = 1) {
return pages.filter(page => page._parent === '0').map(page => { const childrenOrder = pagesOrder.find(order => order.data.page === parentPageId);
/**
* branch is a page children in tree
* if we got some children order on parents tree, then we push found pages in order sequence
* otherwise just find all pages includes parent tree
*/
let ordered = [];
if (childrenOrder) {
ordered = childrenOrder.order.map(pageId => {
return pages.find(page => page._id === pageId);
});
}
const unordered = pages.filter(page => page._parent === parentPageId);
const branch = [ ...new Set([...ordered, ...unordered]) ];
/**
* stop recursion when we got the passed max level
*/
if (currentLevel === level + 1) {
return [];
}
/**
* Each parents children can have subbranches
*/
return branch.filter(page => page && page._id).map(page => {
return Object.assign({ return Object.assign({
children: pages.filter(child => child._parent === page._id).reverse() children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1)
}, page.data); }, page.data);
}); });
} }
@ -21,13 +56,19 @@ function createMenuTree(pages) {
* @param next * @param next
*/ */
module.exports = asyncMiddleware(async function (req, res, next) { module.exports = asyncMiddleware(async function (req, res, next) {
try { /**
const menu = await Pages.getAll(); * Pages without parent
* @type {string}
*/
const parentIdOfRootPages = '0';
res.locals.menu = createMenuTree(menu); try {
const pages = await Pages.getAll();
const pagesOrder = await PagesOrder.getAll();
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
} catch (error) { } catch (error) {
console.log('Can not load menu:', error); console.log('Can not load menu:', error);
} }
next(); next();
}); });

View file

@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const Pages = require('../controllers/pages'); const Pages = require('../controllers/pages');
const PagesOrder = require('../controllers/pagesOrder');
const verifyToken = require('./middlewares/token'); const verifyToken = require('./middlewares/token');
const allowEdit = require('./middlewares/locals'); const allowEdit = require('./middlewares/locals');
@ -24,12 +25,14 @@ router.get('/page/edit/:id', verifyToken, allowEdit, async (req, res, next) => {
const pageId = req.params.id; const pageId = req.params.id;
try { try {
let page = await Pages.get(pageId); const page = await Pages.get(pageId);
let pagesAvailable = await Pages.getAllExceptChildren(pageId); const pagesAvailable = await Pages.getAllExceptChildrens(pageId);
const parentsChildrenOrdered = await PagesOrder.getOrderedChildren(pagesAvailable, pageId, page._parent, true);
res.render('pages/form', { res.render('pages/form', {
pagesAvailable, page,
page parentsChildrenOrdered,
pagesAvailable
}); });
} catch (error) { } catch (error) {
res.status(404); res.status(404);

12
src/utils/crypto.js Normal file
View file

@ -0,0 +1,12 @@
const crypto = require('crypto');
/**
* Create binary md5
* @param stringToHash - string to hash
* @returns {string} - binary hash of argument
*/
module.exports = function binaryMD5(stringToHash) {
return crypto.createHash('md5')
.update(stringToHash)
.digest('binary');
};

View file

@ -0,0 +1,6 @@
const Datastore = require('nedb');
const config = require('../../../config');
const db = new Datastore({ filename: `./${config.database}/aliases.db`, autoload: true });
module.exports = db;

View file

@ -1,5 +1,7 @@
const pages = require('./pages'); const pages = require('./pages');
const password = require('./password'); const password = require('./password');
const aliases = require('./aliases');
const pagesOrder = require('./pagesOrder');
/** /**
* @class Database * @class Database
@ -144,5 +146,7 @@ class Database {
module.exports = { module.exports = {
class: Database, class: Database,
pages: new Database(pages), pages: new Database(pages),
password: new Database(password) password: new Database(password),
aliases: new Database(aliases),
pagesOrder: new Database(pagesOrder)
}; };

View file

@ -1,6 +1,6 @@
const Datastore = require('nedb'); const Datastore = require('nedb');
const config = require('../../../config'); const config = require('../../../config');
const db = new Datastore({filename: `./${config.database}/pages.db`, autoload: true}); const db = new Datastore({ filename: `./${config.database}/pages.db`, autoload: true });
module.exports = db; module.exports = db;

View file

@ -0,0 +1,5 @@
const Datastore = require('nedb');
const config = require('../../../config');
const db = new Datastore({ filename: `./${config.database}/pagesOrder.db`, autoload: true });
module.exports = db;

View file

@ -1,6 +1,6 @@
const Datastore = require('nedb'); const Datastore = require('nedb');
const config = require('../../../config'); const config = require('../../../config');
const db = new Datastore({filename: `./${config.database}/password.db`, autoload: true}); const db = new Datastore({ filename: `./${config.database}/password.db`, autoload: true });
module.exports = db; module.exports = db;

View file

@ -40,8 +40,8 @@ module.exports = class RCParser {
return RCParser.DEFAULTS; return RCParser.DEFAULTS;
} }
const file = fs.readFileSync(rcPath, {encoding: 'UTF-8'}); const file = fs.readFileSync(rcPath, { encoding: 'UTF-8' });
const rConfig = {}; const rConfig = RCParser.DEFAULTS;
let userConfig; let userConfig;
try { try {
@ -51,8 +51,11 @@ module.exports = class RCParser {
return RCParser.DEFAULTS; return RCParser.DEFAULTS;
} }
rConfig.title = userConfig.title || RCParser.DEFAULTS.title; for (let option in userConfig) {
rConfig.menu = userConfig.menu || RCParser.DEFAULTS.menu; if (userConfig.hasOwnProperty(option)) {
rConfig[option] = userConfig[option] || RCParser.DEFAULTS[option] || undefined;
}
}
if (!(rConfig.menu instanceof Array)) { if (!(rConfig.menu instanceof Array)) {
console.log('Menu section in the rc file must be an array.'); console.log('Menu section in the rc file must be an array.');
@ -70,7 +73,7 @@ module.exports = class RCParser {
return false; return false;
} }
const {title, uri} = option; const { title, uri } = option;
if (!title || typeof title !== 'string') { if (!title || typeof title !== 'string') {
console.log(`Menu option #${i} title must be a string.`); console.log(`Menu option #${i} title must be a string.`);

78
src/utils/translation.js Normal file
View file

@ -0,0 +1,78 @@
const translationTable = {
'а': 'a',
'б': 'b',
'в': 'v',
'г': 'g',
'д': 'd',
'е': 'e',
'ж': 'g',
'з': 'z',
'и': 'i',
'й': 'y',
'к': 'k',
'л': 'l',
'м': 'm',
'н': 'n',
'о': 'o',
'п': 'p',
'р': 'r',
'с': 's',
'т': 't',
'у': 'u',
'ф': 'f',
'ы': 'i',
'э': 'e',
'А': 'A',
'Б': 'B',
'В': 'V',
'Г': 'G',
'Д': 'D',
'Е': 'E',
'Ж': 'G',
'З': 'Z',
'И': 'I',
'Й': 'Y',
'К': 'K',
'Л': 'L',
'М': 'M',
'Н': 'N',
'О': 'O',
'П': 'P',
'Р': 'R',
'С': 'S',
'Т': 'T',
'У': 'U',
'Ф': 'F',
'Ы': 'I',
'Э': 'E',
'ё': 'yo',
'х': 'h',
'ц': 'ts',
'ч': 'ch',
'ш': 'sh',
'щ': 'shch',
'ъ': "''",
'ь': "'",
'ю': 'yu',
'я': 'ya',
'Ё': 'YO',
'Х': 'H',
'Ц': 'TS',
'Ч': 'CH',
'Ш': 'SH',
'Щ': 'SHCH',
'Ъ': "''",
'Ь': "'",
'Ю': 'YU',
'Я': 'YA'
};
/**
* Function to translate string
*
* @param string - string to translate
* @returns {string} - translated string
*/
module.exports = function translateString(string) {
return string.replace(/[А-яёЁ]/g, (char) => translationTable[char] || char);
};

View file

@ -1,14 +1,27 @@
<div class="docs-aside-toggler" onclick="document.querySelector('.docs-aside').classList.toggle('docs-aside--toggled')">
{{ svg('menu') }} Table of contents
</div>
<div class="docs-aside"> <div class="docs-aside">
{% for firstLevelPage in menu %} {% for firstLevelPage in menu %}
<section class="docs-aside__section"> <section class="docs-aside__section">
<a class="docs-aside__section-title" href="/page/{{ firstLevelPage._id }}"> <a class="docs-aside__section-title"
{% if firstLevelPage.uri %}
href="/{{ firstLevelPage.uri }}"
{% else %}
href="/page/{{ firstLevelPage._id }}"
{% endif %}>
{{ firstLevelPage.title }} {{ firstLevelPage.title }}
</a> </a>
{% if firstLevelPage.children is not empty %} {% if firstLevelPage.children is not empty %}
<ul class="docs-aside__section-list"> <ul class="docs-aside__section-list">
{% for child in firstLevelPage.children %} {% for child in firstLevelPage.children %}
<li> <li>
<a href="/page/{{ child._id }}"> <a
{% if child.uri %}
href="/{{ child.uri }}"
{% else %}
href="/page/{{ child._id }}"
{% endif %}>
{{ child.title }} {{ child.title }}
</a> </a>
</li> </li>

View file

@ -2,7 +2,7 @@
<a href="/" class="docs-header__logo"> <a href="/" class="docs-header__logo">
{{ config.title }} {{ config.title }}
</a> </a>
<ul class="docs-header__menu"> <ul class="docs-header__menu-add">
<li> <li>
{% if isAuthorized == true %} {% if isAuthorized == true %}
<a class="docs-header__button" href="/page/new"> <a class="docs-header__button" href="/page/new">
@ -13,7 +13,12 @@
</li> </li>
{% for option in config.menu %} {% for option in config.menu %}
<li> <li>
<a href="{{option.uri}}"> <a
{% if child.uri %}
href="{{ option.uri }}"
{% else %}
href="/page/{{ option._id }}"
{% endif %}>
{{ option.title }} {{ option.title }}
</a> </a>
</li> </li>

View file

@ -1,6 +0,0 @@
{% extends 'layout.twig' %}
{% block body %}
{{title}}
<p>Welcome to {{title}}</p>
{% endblock %}

View file

@ -1,11 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>{{ title }}</title> <title>{{ config.title }}</title>
<link rel="stylesheet" href="/dist/main.css" /> <link rel="stylesheet" href="/dist/main.css" />
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ page.title }}" />
<meta property="article:modified_time" content="{{ (page.body.time / 1000) | date("c") }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head> </head>
<body> <body>
{% include "components/header.twig" with isAuthorized %} {% include "components/header.twig" with res.locals.isAuthorized %}
<div class="docs"> <div class="docs">
<aside class="docs__aside"> <aside class="docs__aside">
{% include "components/aside.twig" %} {% include "components/aside.twig" %}

View file

@ -1 +1,2 @@
<pre class="block-code">{{ code }}</pre> <pre class="block-code">{{ code|escape }}</pre>

View file

@ -1,9 +1,10 @@
{% set tag = 'ul' %} {% set tag = 'ul' %}
{% if type == 'orderd' %} {% if style == 'ordered' %}
{% set tag = 'ol' %} {% set tag = 'ol' %}
{% endif %} {% endif %}
<{{ tag }} class="block-list block-list--{{ type }}">
<{{ tag }} class="block-list block-list--{{ style }}">
{% for item in items %} {% for item in items %}
<li> <li>
{{ item }} {{ item }}

View file

@ -7,13 +7,14 @@
} }
</style> </style>
<section data-module="writing"> <section data-module="writing">
<module-settings hidden> <textarea name="module-settings" hidden>
{ {
"page": {{ page | json_encode }} "page": {{ page | json_encode }}
} }
</module-settings> </textarea>
<header class="writing-header"> <header class="writing-header">
<span class="writing-header__left"> <span class="writing-header__left">
<span>
New Page at the New Page at the
{% set currentPageId = 0 %} {% set currentPageId = 0 %}
{% if page is not empty %} {% if page is not empty %}
@ -30,9 +31,31 @@
{% endfor %} {% endfor %}
</select> </select>
</span> </span>
<span class="writing-header__save" name="js-submit">
Save {% if parentsChildrenOrdered is not empty %}
<span>
Put Above
<select name="above">
<option value="0">—</option>
{% for _page in parentsChildrenOrdered %}
<option value="{{ _page._id }}">{{ _page.title }}</option>
{% endfor %}
</select>
</span> </span>
{% endif %}
</span>
<span class="writing-header__save" name="js-submit-save">Save</span>
</header> </header>
<div class="writing-editor">
<div id="codex-editor"></div>
</div>
<div class="writing-buttons">
{% if page._id is not empty %}
<span class="writing-buttons__remove" name="js-submit-remove">Remove</span>
{% endif %}
</div>
{% if page is not empty %}
<p><input type="text" class="uri-input" name="uri-input" placeholder="URI(Optional)" value="{{ page.uri }}"></p>
{% endif %}
</section> </section>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html style="height: 100%">
<head>
<title>{{ config.title }}</title>
<link rel="stylesheet" href="/dist/main.css" />
<link rel="preload" href="{{ config.landingFrameSrc }}" as="document">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body class="landing-body">
{% include "components/header.twig" %}
<div class="landing-loader" id="frame-loader">
{{ svg('loader') }}
</div>
<iframe class="landing-frame" src="{{ config.landingFrameSrc }}" seamless frameborder="0" onload="this.style.opacity = 1; setTimeout(document.getElementById('frame-loader').remove(), 500)"></iframe>
</body>
</html>

View file

@ -7,8 +7,13 @@
Documentation Documentation
</a> </a>
{% if page._parent %} {% if page._parent %}
<a href="/page/{{ page._parent }}" class="page__header-nav"> <a class="page__header-nav"
Parent {{ pageParent.title }} {% if pageParent.uri %}
href="/{{ pageParent.uri }}"
{% else %}
href="/page/{{ pageParent._id }}"
{% endif %}>
{{ pageParent.title }}
</a> </a>
{% endif %} {% endif %}
<time class="page__header-time"> <time class="page__header-time">

View file

@ -1,17 +1,18 @@
const {app} = require('../bin/www'); const { app } = require('../bin/www');
const chai = require('chai'); const chai = require('chai');
const chaiHTTP = require('chai-http'); const chaiHTTP = require('chai-http');
const {expect} = chai; const { expect } = chai;
chai.use(chaiHTTP); chai.use(chaiHTTP);
describe('Express app', () => { describe('Express app', () => {
it('App is available', async () => { it('App is available', async (done) => {
let agent = chai.request.agent(app); let agent = chai.request.agent(app);
const result = await agent const result = await agent
.get('/'); .get('/');
expect(result).to.have.status(200); expect(result).to.have.status(200);
done();
}); });
}); });

139
test/models/alias.js Normal file
View file

@ -0,0 +1,139 @@
const {expect} = require('chai');
const fs = require('fs');
const path = require('path');
const config = require('../../config');
const Alias = require('../../src/models/alias');
const binaryMD5 = require('../../src/utils/crypto');
const {aliases} = require('../../src/utils/database');
describe('Alias model', () => {
after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB);
}
});
it('Working with empty model', async () => {
let alias = new Alias();
expect(alias.data).to.be.a('object');
let {data} = alias;
expect(data._id).to.be.undefined;
expect(data.hash).to.be.undefined;
expect(data.type).to.be.undefined;
expect(data.deprecated).to.be.false;
expect(data.id).to.be.undefined;
alias = new Alias();
data = alias.data;
expect(data._id).to.be.undefined;
expect(data.hash).to.be.undefined;
expect(data.type).to.be.undefined;
expect(data.deprecated).to.be.false;
expect(data.id).to.be.undefined;
const initialData = {
_id: 'alias_id',
type: Alias.types.PAGE,
id: 'page_id'
};
const aliasName = 'alias name';
alias = new Alias(initialData, aliasName);
data = alias.data;
expect(data._id).to.equal(initialData._id);
expect(data.hash).to.equal(binaryMD5(aliasName));
expect(data.type).to.equal(initialData.type);
expect(data.deprecated).to.equal(false);
const update = {
type: Alias.types.PAGE,
id: 'page_id',
hash: binaryMD5('another test hash'),
deprecated: true
};
alias.data = update;
data = alias.data;
expect(data._id).to.equal(initialData._id);
expect(data.type).to.equal(update.type);
expect(data.hash).to.equal(update.hash);
expect(data.deprecated).to.equal(update.deprecated);
});
it('Static get method', async () => {
const initialData = {
type: Alias.types.PAGE,
id: 'page_id'
};
const aliasName = 'alias name';
const alias = new Alias(initialData, aliasName);
const savedAlias = await alias.save();
const foundAlias = await Alias.get(aliasName);
const {data} = foundAlias;
expect(data._id).to.equal(savedAlias._id);
expect(data.hash).to.equal(binaryMD5(aliasName));
expect(data.type).to.equal(initialData.type);
expect(data.deprecated).to.equal(false);
});
it('Saving, updating and deleting model in the database', async () => {
const initialData = {
type: Alias.types.PAGE,
id: 'page_id'
};
const aliasName = 'alias name';
const alias = new Alias(initialData, aliasName);
const savedAlias = await alias.save();
expect(savedAlias._id).not.be.undefined;
expect(savedAlias.hash).to.equal(binaryMD5(aliasName));
expect(savedAlias.type).to.equal(initialData.type);
expect(savedAlias.id).to.equal(initialData.id);
expect(savedAlias.deprecated).to.equal(false);
const insertedAlias = await aliases.findOne({_id: savedAlias._id});
expect(insertedAlias._id).to.equal(savedAlias._id);
expect(insertedAlias.hash).to.equal(savedAlias.hash);
expect(insertedAlias.type).to.equal(savedAlias.type);
expect(insertedAlias.id).to.equal(savedAlias.id);
expect(insertedAlias.deprecated).to.equal(savedAlias.deprecated);
const updateData = {
type: Alias.types.PAGE,
id: 'page_id',
hash: binaryMD5('another test hash'),
deprecated: true
};
alias.data = updateData;
await alias.save();
expect(alias._id).to.equal(insertedAlias._id);
const updatedAlias = await aliases.findOne({_id: alias._id});
expect(updatedAlias._id).to.equal(savedAlias._id);
expect(updatedAlias.hash).to.equal(updateData.hash);
expect(updatedAlias.type).to.equal(updateData.type);
expect(updatedAlias.id).to.equal(updateData.id);
expect(updatedAlias.deprecated).to.equal(updateData.deprecated);
});
});

View file

@ -4,8 +4,21 @@ const path = require('path');
const config = require('../../config'); const config = require('../../config');
const Page = require('../../src/models/page'); const Page = require('../../src/models/page');
const {pages} = require('../../src/utils/database'); const {pages} = require('../../src/utils/database');
const translateString = require('../../src/utils/translation');
describe('Page model', () => { describe('Page model', () => {
const transformToUri = (string) => {
return translateString(string
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
};
after(() => { after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db'); const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
@ -23,8 +36,9 @@ describe('Page model', () => {
expect(data._id).to.be.undefined; expect(data._id).to.be.undefined;
expect(data.title).to.be.empty; expect(data.title).to.be.empty;
expect(data.uri).to.be.empty;
expect(data.body).to.be.undefined; expect(data.body).to.be.undefined;
expect(data.parent).to.be.undefined; expect(data.parent).to.be.equal('0');
page = new Page(null); page = new Page(null);
@ -32,8 +46,9 @@ describe('Page model', () => {
expect(data._id).to.be.undefined; expect(data._id).to.be.undefined;
expect(data.title).to.be.empty; expect(data.title).to.be.empty;
expect(data.uri).to.be.empty;
expect(data.body).to.be.undefined; expect(data.body).to.be.undefined;
expect(data.parent).to.be.undefined; expect(data.parent).to.be.equal('0');
const initialData = { const initialData = {
_id: 'page_id', _id: 'page_id',
@ -57,13 +72,15 @@ describe('Page model', () => {
expect(data._id).to.equal(initialData._id); expect(data._id).to.equal(initialData._id);
expect(data.title).to.equal(initialData.body.blocks[0].data.text); expect(data.title).to.equal(initialData.body.blocks[0].data.text);
expect(data.uri).to.be.empty;
expect(data.body).to.deep.equal(initialData.body); expect(data.body).to.deep.equal(initialData.body);
expect(data.parent).to.be.undefined; expect(data.parent).to.be.equal('0');
expect(json._id).to.equal(initialData._id); expect(json._id).to.equal(initialData._id);
expect(json.title).to.equal(initialData.body.blocks[0].data.text); expect(json.title).to.equal(initialData.body.blocks[0].data.text);
expect(json.title).to.equal(initialData.body.blocks[0].data.text);
expect(json.body).to.deep.equal(initialData.body); expect(json.body).to.deep.equal(initialData.body);
expect(json.parent).to.be.undefined; expect(json.parent).to.be.equal('0');
const update = { const update = {
_id: 12345, _id: 12345,
@ -85,8 +102,9 @@ describe('Page model', () => {
expect(data._id).to.equal(initialData._id); expect(data._id).to.equal(initialData._id);
expect(data.title).to.equal(update.body.blocks[0].data.text); expect(data.title).to.equal(update.body.blocks[0].data.text);
expect(data.uri).to.be.empty;
expect(data.body).to.equal(update.body); expect(data.body).to.equal(update.body);
expect(data.parent).to.be.undefined; expect(data.parent).to.be.equal('0');
}); });
it('Saving, updating and deleting model in the database', async () => { it('Saving, updating and deleting model in the database', async () => {
@ -96,7 +114,7 @@ describe('Page model', () => {
{ {
type: 'header', type: 'header',
data: { data: {
text: 'Page header' text: 'New page header'
} }
} }
] ]
@ -108,6 +126,7 @@ describe('Page model', () => {
expect(savedPage._id).not.be.undefined; expect(savedPage._id).not.be.undefined;
expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text); expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text);
expect(savedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
expect(savedPage.body).to.equal(initialData.body); expect(savedPage.body).to.equal(initialData.body);
expect(page._id).not.be.undefined; expect(page._id).not.be.undefined;
@ -115,6 +134,7 @@ describe('Page model', () => {
expect(insertedPage._id).to.equal(page._id); expect(insertedPage._id).to.equal(page._id);
expect(insertedPage.title).to.equal(page.title); expect(insertedPage.title).to.equal(page.title);
expect(insertedPage.uri).to.equal(page.uri);
expect(insertedPage.body).to.deep.equal(page.body); expect(insertedPage.body).to.deep.equal(page.body);
const updateData = { const updateData = {
@ -127,7 +147,8 @@ describe('Page model', () => {
} }
} }
] ]
} },
uri: 'updated-uri'
}; };
page.data = updateData; page.data = updateData;
@ -139,6 +160,7 @@ describe('Page model', () => {
expect(updatedPage._id).to.equal(savedPage._id); expect(updatedPage._id).to.equal(savedPage._id);
expect(updatedPage.title).to.equal(updateData.body.blocks[0].data.text); expect(updatedPage.title).to.equal(updateData.body.blocks[0].data.text);
expect(updatedPage.uri).to.equal(updateData.uri);
expect(updatedPage.body).to.deep.equal(updateData.body); expect(updatedPage.body).to.deep.equal(updateData.body);
await page.destroy(); await page.destroy();
@ -150,6 +172,39 @@ describe('Page model', () => {
expect(removedPage).to.be.null; expect(removedPage).to.be.null;
}); });
it('Handle multiple page creation with the same uri', async () => {
const initialData = {
body: {
blocks: [
{
type: 'header',
data: {
text: 'New page header'
}
}
]
}
};
const firstPage = new Page(initialData);
let firstSavedPage = await firstPage.save();
const secondPage = new Page(initialData);
let secondSavedPage = await secondPage.save();
expect(secondSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text) + '-1');
const newUri = 'new-uri';
firstPage.data = {...firstPage.data, uri: newUri};
firstSavedPage = await firstPage.save();
expect(firstSavedPage.uri).to.equal(newUri);
const thirdPage = new Page(initialData);
let thirdSavedPage = await thirdPage.save();
expect(thirdSavedPage.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
});
it('Static get method', async () => { it('Static get method', async () => {
const initialData = { const initialData = {
body: { body: {
@ -173,6 +228,7 @@ describe('Page model', () => {
expect(data._id).to.equal(savedPage._id); expect(data._id).to.equal(savedPage._id);
expect(data.title).to.equal(initialData.body.blocks[0].data.text); expect(data.title).to.equal(initialData.body.blocks[0].data.text);
expect(data.uri).to.equal(transformToUri(initialData.body.blocks[0].data.text));
expect(data.body).to.deep.equal(initialData.body); expect(data.body).to.deep.equal(initialData.body);
await page.destroy(); await page.destroy();
@ -213,6 +269,7 @@ describe('Page model', () => {
expect(foundPages.length).to.equal(2); expect(foundPages.length).to.equal(2);
foundPages.forEach((page, i) => { foundPages.forEach((page, i) => {
expect(page.title).to.equal(pagesToSave[i].body.blocks[0].data.text); expect(page.title).to.equal(pagesToSave[i].body.blocks[0].data.text);
expect(page.uri).to.equal(transformToUri(pagesToSave[i].body.blocks[0].data.text));
expect(page.body).to.deep.equal(pagesToSave[i].body); expect(page.body).to.deep.equal(pagesToSave[i].body);
}); });
}); });
@ -257,6 +314,7 @@ describe('Page model', () => {
expect(testedParent._id).to.equal(parentId); expect(testedParent._id).to.equal(parentId);
expect(testedParent.title).to.equal(parent.body.blocks[0].data.text); expect(testedParent.title).to.equal(parent.body.blocks[0].data.text);
expect(testedParent.uri).to.equal(transformToUri(parent.body.blocks[0].data.text));
expect(testedParent.body).to.deep.equal(parent.body); expect(testedParent.body).to.deep.equal(parent.body);
const children = await parent.children; const children = await parent.children;
@ -267,6 +325,7 @@ describe('Page model', () => {
expect(testedChild._id).to.equal(childId); expect(testedChild._id).to.equal(childId);
expect(testedChild.title).to.equal(child.body.blocks[0].data.text); expect(testedChild.title).to.equal(child.body.blocks[0].data.text);
expect(testedChild.uri).to.equal(transformToUri(child.body.blocks[0].data.text));
expect(testedChild.body).to.deep.equal(child.body); expect(testedChild.body).to.deep.equal(child.body);
expect(testedChild._parent).to.equal(child._parent); expect(testedChild._parent).to.equal(child._parent);
expect(testedChild._parent).to.equal(parent._id); expect(testedChild._parent).to.equal(parent._id);
@ -293,4 +352,31 @@ describe('Page model', () => {
expect(page.title).to.equal(pageData.body.blocks[0].data.text); expect(page.title).to.equal(pageData.body.blocks[0].data.text);
}); });
it('test deletion', async () => {
const pages = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const orders = {
'0' : ['1', '2', '3'],
'1' : ['4', '5'],
'5' : ['6', '7', '8'],
'3' : ['9']
};
function deleteRecursively(startFrom) {
const order = orders[startFrom];
if (!order) {
const found = pages.indexOf(startFrom);
pages.splice(found, 1);
return;
}
order.forEach(id => {
deleteRecursively(id);
});
const found = pages.indexOf(startFrom);
pages.splice(found, 1);
}
});
}); });

143
test/models/pageOrder.js Normal file
View file

@ -0,0 +1,143 @@
const {expect} = require('chai');
const fs = require('fs');
const path = require('path');
const config = require('../../config');
const PageOrder = require('../../src/models/pageOrder');
const {pagesOrder} = require('../../src/utils/database');
describe('PageOrder model', () => {
after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pagesOrder.db');
if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB);
}
});
it('Empty Model', async () => {
let pageOrder = new PageOrder();
expect(pageOrder.data).to.be.a('object');
let {data} = pageOrder;
expect(data._id).to.be.undefined;
expect(data.page).to.be.to.equal('0');
expect(data.order).to.be.an('array').that.is.empty;
page = new PageOrder(null);
data = page.data;
expect(data._id).to.be.undefined;
expect(data.page).to.be.to.equal('0');
expect(data.order).to.be.an('array').that.is.empty;
const testData = {
_id: 'order_id',
page: 'page_id',
order: []
};
page = new PageOrder(testData);
data = page.data;
expect(data._id).to.equal(testData._id);
expect(data.page).to.equal(testData.page);
expect(data.order).to.be.an('array').that.is.empty;
});
it('Testing Model methods', async () => {
const testData = {
page: 'page_id',
order: ['1', '2']
};
const pageOrder = new PageOrder(testData);
let {data} = await pageOrder.save();
expect(data._id).not.be.undefined;
expect(data.page).to.equal(testData.page);
expect(data.order).to.deep.equals(testData.order);
const insertedPageOrder = await pagesOrder.findOne({_id: data._id});
expect(insertedPageOrder._id).to.equal(data._id);
expect(insertedPageOrder.page).to.equal(data.page);
expect(insertedPageOrder.order).to.deep.equal(data.order);
const updateData = {
page: 'page_id_2',
order: ['3']
};
pageOrder.data = updateData;
await pageOrder.save();
expect(pageOrder.data._id).to.equal(insertedPageOrder._id);
const updatedData = await pagesOrder.findOne({_id: insertedPageOrder._id});
expect(updatedData.page).to.equal(updateData.page);
expect(updatedData.order).to.deep.equal(updateData.order);
await pageOrder.destroy();
expect(pageOrder.data._id).to.be.undefined;
const removedPage = await pagesOrder.findOne({_id: updatedData._id});
expect(removedPage).to.be.null;
});
it('Testing push and remove order methods', async () => {
const testData = {
page: 'page_id',
order: ['1', '2']
};
const pageOrder = new PageOrder(testData);
await pageOrder.save();
pageOrder.push('3');
expect(pageOrder.data.order).to.be.an('array').that.is.not.empty;
pageOrder.data.order.forEach((el) => {
expect(el).to.be.an('string')
});
expect(pageOrder.data.order).to.deep.equals(['1', '2', '3']);
pageOrder.remove('2');
expect(pageOrder.data.order).to.deep.equals(['1', '3']);
expect(() => {
pageOrder.push(3);
}).to.throw('given id is not string');
pageOrder.push('4');
pageOrder.push('5');
pageOrder.push('2');
pageOrder.putAbove('2', '3');
expect(pageOrder.data.order).to.deep.equals(['1', '2', '3', '4', '5']);
pageOrder.putAbove('2', '10');
expect(pageOrder.data.order).to.deep.equals(['1', '2', '3', '4', '5']);
await pageOrder.destroy();
});
it('Testing static methods', async () => {
const testData = {
page: 'page_id',
order: ['1', '2']
};
const pageOrder = new PageOrder(testData);
const insertedData = await pageOrder.save();
const insertedPageOrder = await PageOrder.get(insertedData.data.page);
expect(insertedPageOrder).to.instanceOf(PageOrder);
expect(insertedPageOrder.data._id).to.be.equal(insertedData.data._id);
const emptyInstance = await PageOrder.get(null);
expect(emptyInstance.data.page).to.be.equal('0');
expect(emptyInstance.data.order).to.be.an('array').that.is.empty;
await pageOrder.destroy();
});
});

59
test/rest/aliases.js Normal file
View file

@ -0,0 +1,59 @@
const {app} = require('../../bin/www');
const fs = require('fs');
const path = require('path');
const config = require('../../config');
const chai = require('chai');
const chaiHTTP = require('chai-http');
const {expect} = chai;
chai.use(chaiHTTP);
describe('Aliases REST: ', () => {
let agent;
before(async () => {
agent = chai.request.agent(app);
});
after(async () => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db');
if (fs.existsSync(pathToDB)) {
fs.unlinkSync(pathToDB);
}
const pathToAliasDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
if (fs.existsSync(pathToAliasDB)) {
fs.unlinkSync(pathToAliasDB);
}
});
it('Finding page with alias', async () => {
const body = {
time: 1548375408533,
blocks: [
{
type: 'header',
data: {
text: 'Test header'
}
}
]
};
const put = await agent
.put('/api/page')
.send({body});
expect(put).to.have.status(200);
expect(put).to.be.json;
const {result: {uri}} = put.body;
const get = await agent.get('/' + uri);
expect(get).to.have.status(200);
});
});

View file

@ -1,5 +1,8 @@
const {app} = require('../../bin/www'); const {app} = require('../../bin/www');
const model = require('../../src/models/page'); const model = require('../../src/models/page');
const Page = require('../../src/models/page');
const PageOrder = require('../../src/models/pageOrder');
const translateString = require('../../src/utils/translation');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -12,16 +15,36 @@ chai.use(chaiHTTP);
describe('Pages REST: ', () => { describe('Pages REST: ', () => {
let agent; let agent;
const transformToUri = (string) => {
return translateString(string
.replace(/&nbsp;/g, ' ')
.replace(/[^a-zA-Z0-9А-Яа-яЁё ]/g, ' ')
.replace(/ +/g, ' ')
.trim()
.toLowerCase()
.split(' ')
.join('-'));
};
before(async () => { before(async () => {
agent = chai.request.agent(app); agent = chai.request.agent(app);
}); });
after(async () => { after(async () => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.db'); const pathToPagesDB = path.resolve(__dirname, '../../', config.database, './pages.db');
const pathToPagesOrderDB = path.resolve(__dirname, '../../', config.database, './pagesOrder.db');
const pathToAliasesDB = path.resolve(__dirname, '../../', config.database, './aliases.db');
if (fs.existsSync(pathToDB)) { if (fs.existsSync(pathToPagesDB)) {
fs.unlinkSync(pathToDB); fs.unlinkSync(pathToPagesDB);
}
if (fs.existsSync(pathToPagesOrderDB)) {
fs.unlinkSync(pathToPagesOrderDB);
}
if (fs.existsSync(pathToAliasesDB)) {
fs.unlinkSync(pathToAliasesDB);
} }
}); });
@ -36,10 +59,10 @@ describe('Pages REST: ', () => {
} }
] ]
}; };
const parent = 0;
const res = await agent const res = await agent
.put('/api/page') .put('/api/page')
.send({body}); .send({body, parent});
expect(res).to.have.status(200); expect(res).to.have.status(200);
expect(res).to.be.json; expect(res).to.be.json;
@ -49,6 +72,7 @@ describe('Pages REST: ', () => {
expect(success).to.be.true; expect(success).to.be.true;
expect(result._id).to.be.a('string'); expect(result._id).to.be.a('string');
expect(result.title).to.equal(body.blocks[0].data.text); expect(result.title).to.equal(body.blocks[0].data.text);
expect(result.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(result.body).to.deep.equal(body); expect(result.body).to.deep.equal(body);
const createdPage = await model.get(result._id); const createdPage = await model.get(result._id);
@ -56,9 +80,14 @@ describe('Pages REST: ', () => {
expect(createdPage).not.be.null; expect(createdPage).not.be.null;
expect(createdPage._id).to.equal(result._id); expect(createdPage._id).to.equal(result._id);
expect(createdPage.title).to.equal(body.blocks[0].data.text); expect(createdPage.title).to.equal(body.blocks[0].data.text);
expect(createdPage.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(createdPage.body).to.deep.equal(body); expect(createdPage.body).to.deep.equal(body);
createdPage.destroy(); const pageOrder = await PageOrder.get('' + (createdPage.data.parent || 0));
expect(pageOrder.order).to.be.an('array');
await createdPage.destroy();
await pageOrder.destroy();
}); });
it('Page data validation on create', async () => { it('Page data validation on create', async () => {
@ -106,12 +135,15 @@ describe('Pages REST: ', () => {
expect(success).to.be.true; expect(success).to.be.true;
const foundPage = await model.get(_id); const foundPage = await model.get(_id);
const pageOrder = await PageOrder.get('' + foundPage._parent);
expect(foundPage._id).to.equal(_id); expect(foundPage._id).to.equal(_id);
expect(foundPage.title).to.equal(body.blocks[0].data.text); expect(foundPage.title).to.equal(body.blocks[0].data.text);
expect(foundPage.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(foundPage.body).to.deep.equal(body); expect(foundPage.body).to.deep.equal(body);
foundPage.destroy(); await pageOrder.destroy();
await foundPage.destroy();
}); });
it('Finding page with not existing id', async () => { it('Finding page with not existing id', async () => {
@ -157,10 +189,11 @@ describe('Pages REST: ', () => {
} }
] ]
}; };
const updatedUri = 'updated-uri';
res = await agent res = await agent
.post(`/api/page/${_id}`) .post(`/api/page/${_id}`)
.send({body: updatedBody}); .send({body: updatedBody, uri: updatedUri});
expect(res).to.have.status(200); expect(res).to.have.status(200);
expect(res).to.be.json; expect(res).to.be.json;
@ -171,18 +204,83 @@ describe('Pages REST: ', () => {
expect(result._id).to.equal(_id); expect(result._id).to.equal(_id);
expect(result.title).not.equal(body.blocks[0].data.text); expect(result.title).not.equal(body.blocks[0].data.text);
expect(result.title).to.equal(updatedBody.blocks[0].data.text); expect(result.title).to.equal(updatedBody.blocks[0].data.text);
expect(result.uri).not.equal(transformToUri(body.blocks[0].data.text));
expect(result.uri).to.equal(updatedUri);
expect(result.body).not.equal(body); expect(result.body).not.equal(body);
expect(result.body).to.deep.equal(updatedBody); expect(result.body).to.deep.equal(updatedBody);
const updatedPage = await model.get(_id); const updatedPage = await model.get(_id);
const pageOrder = await PageOrder.get('' + updatedPage._parent);
expect(updatedPage._id).to.equal(_id); expect(updatedPage._id).to.equal(_id);
expect(updatedPage.title).not.equal(body.blocks[0].data.text); expect(updatedPage.title).not.equal(body.blocks[0].data.text);
expect(updatedPage.title).to.equal(updatedBody.blocks[0].data.text); expect(updatedPage.title).to.equal(updatedBody.blocks[0].data.text);
expect(updatedPage.uri).not.equal(transformToUri(body.blocks[0].data.text));
expect(updatedPage.uri).to.equal(updatedUri);
expect(updatedPage.body).not.equal(body); expect(updatedPage.body).not.equal(body);
expect(updatedPage.body).to.deep.equal(updatedBody); expect(updatedPage.body).to.deep.equal(updatedBody);
updatedPage.destroy(); await pageOrder.destroy();
await updatedPage.destroy();
});
it('Handle multiple page creation with the same uri', async () => {
const body = {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
};
let res = await agent
.put('/api/page')
.send({body});
expect(res).to.have.status(200);
expect(res).to.be.json;
const {result: {_id}} = res.body;
res = await agent
.put('/api/page')
.send({body: body});
expect(res).to.have.status(200);
expect(res).to.be.json;
const {success: secondPageSuccess, result: secondPageResult} = res.body;
expect(secondPageSuccess).to.be.true;
expect(secondPageResult.title).to.equal(body.blocks[0].data.text);
expect(secondPageResult.uri).to.equal(transformToUri(body.blocks[0].data.text) + '-1');
expect(secondPageResult.body).to.deep.equal(body);
const newFirstPageUri = 'New-uri';
res = await agent
.post(`/api/page/${_id}`)
.send({body: body, uri: newFirstPageUri});
expect(res).to.have.status(200);
expect(res).to.be.json;
res = await agent
.put('/api/page')
.send({body: body});
expect(res).to.have.status(200);
expect(res).to.be.json;
const {success: thirdPageSuccess, result: thirdPageResult} = res.body;
expect(thirdPageSuccess).to.be.true;
expect(thirdPageResult.title).to.equal(body.blocks[0].data.text);
expect(thirdPageResult.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(thirdPageResult.body).to.deep.equal(body);
}); });
it('Updating page with not existing id', async () => { it('Updating page with not existing id', async () => {
@ -214,7 +312,7 @@ describe('Pages REST: ', () => {
{ {
type: 'header', type: 'header',
data: { data: {
text: 'Page header' text: 'Page header to be deleted'
} }
} }
] ]
@ -238,13 +336,18 @@ describe('Pages REST: ', () => {
const {success, result} = res.body; const {success, result} = res.body;
expect(success).to.be.true; expect(success).to.be.true;
if (result) {
expect(result._id).to.be.undefined; expect(result._id).to.be.undefined;
expect(result.title).to.equal(body.blocks[0].data.text); expect(result.title).to.equal(body.blocks[0].data.text);
expect(result.uri).to.equal(transformToUri(body.blocks[0].data.text));
expect(result.body).to.deep.equal(body); expect(result.body).to.deep.equal(body);
const deletedPage = await model.get(_id); const deletedPage = await model.get(_id);
expect(deletedPage._id).to.be.undefined; expect(deletedPage._id).to.be.undefined;
} else {
expect(result).to.be.null;
}
}); });
it('Removing page with not existing id', async () => { it('Removing page with not existing id', async () => {
@ -259,4 +362,164 @@ describe('Pages REST: ', () => {
expect(success).to.be.false; expect(success).to.be.false;
expect(error).to.equal('Page with given id does not exist'); expect(error).to.equal('Page with given id does not exist');
}); });
async function createPageTree() {
/**
* Creating page tree
*
* 0
* / \
* 1 2
* / \ \
* 3 5 6
* / / \
* 4 7 8
*/
const body = {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
};
let parent, res, result;
/** Page 1 */
parent = 0;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page1 = result;
/** Page 2 */
parent = 0;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page2 = result;
/** Page 3 */
parent = page1._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page3 = result;
/** Page 4 */
parent = page3._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page4 = result;
/** Page 5 */
parent = page1._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page5 = result;
/** Page 6 */
parent = page2._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page6 = result;
/** Page 7 */
parent = page6._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page7 = result;
/** Page 8 */
parent = page6._id;
res = await agent
.put('/api/page')
.send({body, parent});
result = res.body.result;
const page8 = result;
return [
0,
page1._id,
page2._id,
page3._id,
page4._id,
page5._id,
page6._id,
page7._id,
page8._id
];
}
it('Removing a page and its children', async () => {
let pages = await createPageTree();
/**
* Deleting from tree page1
* Also pages 3, 5 and 4 must be deleted
*/
await agent
.delete(`/api/page/${pages[1]}`);
const page3 = await Page.get(pages[3]);
expect(page3.data._id).to.be.undefined;
const page4 = await Page.get(pages[4]);
expect(page4.data._id).to.be.undefined;
const page5 = await Page.get(pages[5]);
expect(page5.data._id).to.be.undefined;
/** Okay, pages above is deleted */
const page2 = await Page.get(pages[2]);
expect(page2.data._id).not.to.be.undefined;
/** First check pages 6, 7 and 8 before deleting */
let page6 = await Page.get(pages[6]);
expect(page6.data._id).not.to.be.undefined;
let page7 = await Page.get(pages[7]);
expect(page7.data._id).not.to.be.undefined;
let page8 = await Page.get(pages[8]);
expect(page8.data._id).not.to.be.undefined;
/**
* Delete page6
* also pages 7 and 8 must be deleted
*/
await agent
.delete(`/api/page/${pages[6]}`);
page6 = await Page.get(pages[6]);
expect(page6.data._id).to.be.undefined;
page7 = await Page.get(pages[7]);
expect(page7.data._id).to.be.undefined;
page8 = await Page.get(pages[8]);
expect(page8.data._id).to.be.undefined;
});
}); });

952
yarn.lock

File diff suppressed because it is too large Load diff