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:
commit
df10d46198
62 changed files with 2797 additions and 2709 deletions
|
@ -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
2
.gitignore
vendored
|
@ -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
11
nodemon.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
"src/frontend",
|
||||||
|
"public/dist"
|
||||||
|
],
|
||||||
|
"events": {
|
||||||
|
"restart": "echo \"App restarted due to: '$FILENAME'\""
|
||||||
|
},
|
||||||
|
"ext": "js,twig"
|
||||||
|
}
|
20
package.json
20
package.json
|
@ -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",
|
||||||
|
|
2
public/dist/code-styling.bundle.js
vendored
2
public/dist/code-styling.bundle.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/code-styling.css
vendored
2
public/dist/code-styling.css
vendored
|
@ -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}
|
||||||
|
|
2210
public/dist/editor.bundle.js
vendored
2210
public/dist/editor.bundle.js
vendored
File diff suppressed because one or more lines are too long
6
public/dist/main.bundle.js
vendored
6
public/dist/main.bundle.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/main.css
vendored
2
public/dist/main.css
vendored
File diff suppressed because one or more lines are too long
|
@ -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')));
|
||||||
|
|
||||||
|
|
26
src/controllers/aliases.js
Normal file
26
src/controllers/aliases.js
Normal 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;
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
126
src/controllers/pagesOrder.js
Normal file
126
src/controllers/pagesOrder.js
Normal 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;
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
src/frontend/styles/components/landing.pcss
Normal file
30
src/frontend/styles/components/landing.pcss
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
60
src/frontend/svg/loader.svg
Normal file
60
src/frontend/svg/loader.svg
Normal 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 |
1
src/frontend/svg/menu.svg
Normal file
1
src/frontend/svg/menu.svg
Normal 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
142
src/models/alias.js
Normal 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;
|
|
@ -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(/ /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
212
src/models/pageOrder.js
Normal 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;
|
|
@ -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
35
src/routes/aliases.js
Normal 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;
|
|
@ -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({
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
12
src/utils/crypto.js
Normal 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');
|
||||||
|
};
|
6
src/utils/database/aliases.js
Normal file
6
src/utils/database/aliases.js
Normal 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;
|
|
@ -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)
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
5
src/utils/database/pagesOrder.js
Normal file
5
src/utils/database/pagesOrder.js
Normal 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;
|
|
@ -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;
|
||||||
|
|
|
@ -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
78
src/utils/translation.js
Normal 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);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
{% extends 'layout.twig' %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
{{title}}
|
|
||||||
<p>Welcome to {{title}}</p>
|
|
||||||
{% endblock %}
|
|
|
@ -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" %}
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
<pre class="block-code">{{ code }}</pre>
|
<pre class="block-code">{{ code|escape }}</pre>
|
||||||
|
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -7,32 +7,55 @@
|
||||||
}
|
}
|
||||||
</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 %}
|
||||||
{% set currentPageId = page._id %}
|
{% set currentPageId = page._id %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<select name="parent">
|
<select name="parent">
|
||||||
<option value="0">Root</option>
|
<option value="0">Root</option>
|
||||||
{% for _page in pagesAvailable %}
|
{% for _page in pagesAvailable %}
|
||||||
{% if _page._id != currentPageId %}
|
{% if _page._id != currentPageId %}
|
||||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||||
{{ _page.title }}
|
{{ _page.title }}
|
||||||
</option>
|
</option>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% 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>
|
||||||
|
{% endif %}
|
||||||
</span>
|
</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 %}
|
||||||
|
|
17
src/views/pages/index.twig
Normal file
17
src/views/pages/index.twig
Normal 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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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
139
test/models/alias.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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(/ /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
143
test/models/pageOrder.js
Normal 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
59
test/rest/aliases.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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(/ /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;
|
||||||
expect(result._id).to.be.undefined;
|
|
||||||
expect(result.title).to.equal(body.blocks[0].data.text);
|
|
||||||
expect(result.body).to.deep.equal(body);
|
|
||||||
|
|
||||||
const deletedPage = await model.get(_id);
|
if (result) {
|
||||||
|
expect(result._id).to.be.undefined;
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue