1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-19 05:09:41 +02:00

Order child pages (#21)

* save ordering pages

* move to another collection

* requested changes. Code improvements

* add margin

* unit tests

* fix

* requested changes

* recursive method: create menu tree

* update comments

* fix bug

* requested changes

* move const

* fix error message on catch

* add migration

* rewrite to splices

* move methods

* testing put above method

* linter fix
This commit is contained in:
Murod Khaydarov 2019-01-19 17:09:11 +03:00 committed by GitHub
parent dbfc594e66
commit d61818761e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1069 additions and 2563 deletions

View file

@ -17,6 +17,7 @@
"codex.editor.header": "^2.0.5", "codex.editor.header": "^2.0.5",
"cookie-parser": "~1.4.3", "cookie-parser": "~1.4.3",
"debug": "~4.1.0", "debug": "~4.1.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",
"module-dispatcher": "^1.0.2", "module-dispatcher": "^1.0.2",
@ -48,6 +49,8 @@
"eslint": "^5.3.0", "eslint": "^5.3.0",
"eslint-config-codex": "github:codex-team/eslint-config", "eslint-config-codex": "github:codex-team/eslint-config",
"eslint-plugin-chai-friendly": "^0.4.1", "eslint-plugin-chai-friendly": "^0.4.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.1",
"highlight.js": "^9.13.1", "highlight.js": "^9.13.1",
"husky": "^1.1.2", "husky": "^1.1.2",
"mini-css-extract-plugin": "^0.4.3", "mini-css-extract-plugin": "^0.4.3",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -138,7 +138,6 @@ class Pages {
} }
page.data = data; page.data = data;
return page.save(); return page.save();
} }

View file

@ -0,0 +1,95 @@
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;
}
/**
* 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 PagesOrder.get(parentPageId);
const result = [];
children.order.forEach(pageId => {
pages.forEach(page => {
if (page._id === pageId && (pageId !== currentPageId || !ignoreSelf)) {
result.push(page);
}
});
});
return result;
}
/**
* @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(currentPageId, parentPageId, putAbovePageId) {
const pageOrder = await Model.get(parentPageId);
pageOrder.putAbove(currentPageId, putAbovePageId);
await pageOrder.save();
}
}
module.exports = PagesOrder;

View file

@ -27,7 +27,8 @@ export default class Writing {
this.nodes = { this.nodes = {
editorWrapper: null, editorWrapper: null,
saveButton: null, saveButton: null,
parentIdSelector: null parentIdSelector: null,
putAboveIdSelector: null,
}; };
} }
@ -61,6 +62,7 @@ export default class Writing {
this.saveButtonClicked(); this.saveButtonClicked();
}); });
this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]'); this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]');
this.nodes.putAboveIdSelector = moduleEl.querySelector('[name="above"]');
}; };
/** /**
@ -89,8 +91,15 @@ export default class Writing {
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,
body: editorData body: editorData
}; };
} }

View file

@ -16,5 +16,9 @@
&__left { &__left {
margin: auto 0; margin: auto 0;
color: var(--color-text-second); color: var(--color-text-second);
& span {
margin-right: 10px;
}
} }
} }

View file

@ -6,7 +6,6 @@ const {pages: db} = require('../utils/database/index');
* @property {string} title - page title * @property {string} title - page title
* @property {*} body - page body * @property {*} body - page body
* @property {string} parent - id of parent page * @property {string} parent - id of parent page
*
*/ */
/** /**

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

@ -0,0 +1,156 @@
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
*/
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);
}
/**
* @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 ordered list
*
* @return {string[]}
*/
get order() {
return this._order;
}
/**
* Save or update page data in the database
*/
async save() {
if (!this._id) {
const insertedRow = await db.insert(this.data);
this._id = insertedRow._id;
} else {
await db.update({_id: this._id}, this.data);
}
return this;
}
/**
* Remove page data from the database
*/
async destroy() {
await db.remove({_id: this._id});
delete this._id;
return this;
}
}
module.exports = PageOrder;

View file

@ -2,6 +2,7 @@ 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
@ -55,6 +56,9 @@ router.put('/page', multer.any(), async (req, res) => {
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,
result: page result: page
@ -76,9 +80,18 @@ 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} = req.body;
const page = await Pages.update(id, {title, body, parent}); let page = await Pages.get(id);
if (page._parent !== parent) {
await PagesOrder.move(page._parent, parent, id);
} else {
if (putAbovePageId && putAbovePageId !== '0') {
await PagesOrder.update(page._id, page._parent, putAbovePageId);
}
}
page = await Pages.update(id, {title, body, parent});
res.json({ res.json({
success: true, success: true,
result: page result: page

View file

@ -1,17 +1,47 @@
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 {Page[]} pages - list of all available pages * @param {string[]} pages - list of all available pages
* @param {number} level
* @param {number} currentLevel
*
* @return {Page[]} * @return {Page[]}
*/ */
function createMenuTree(pages) { async function createMenuTree(pages, level = 1, currentLevel = 1) {
return pages.filter(page => page._parent === '0').map(page => { return await Promise.all(pages.map(async pageId => {
const parent = await Pages.get(pageId);
/**
* By default we accept that deepestChildren is empty Array
* @type {Array}
*/
let deepestChildren = [];
/**
* Here we try to check parent's children order
* If we got something, pluck to found Page deeper and get its children order
*/
try {
/**
* Go deeper until we didn't get the deepest level
* On each 'currentLevel' create new Menu Tree with ordered Page ids
*/
if (currentLevel !== level) {
const children = await PagesOrder.get(pageId);
deepestChildren = await createMenuTree(children.order, level, currentLevel + 1)
}
} catch (e) {}
/**
* Assign parent's children with found Menu Tree
*/
return Object.assign({ return Object.assign({
children: pages.filter(child => child._parent === page._id).reverse() children: deepestChildren
}, page.data); }, parent.data);
}); }));
} }
/** /**
@ -21,13 +51,16 @@ function createMenuTree(pages) {
* @param next * @param next
*/ */
module.exports = asyncMiddleware(async function (req, res, next) { module.exports = asyncMiddleware(async function (req, res, next) {
/**
* Pages without parent
* @type {string}
*/
const parentIdOfRootPages = '0';
try { try {
const menu = await Pages.getAll(); const rootPages = await PagesOrder.get(parentIdOfRootPages);
res.locals.menu = await createMenuTree(rootPages.order, 2);
res.locals.menu = createMenuTree(menu);
} catch (error) { } catch (error) {
console.log('Can not load menu:', error); console.log('Can not load menu:', error);
} }
next(); next();
}); });

View file

@ -1,6 +1,7 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const Pages = require('../controllers/pages'); const Pages = require('../controllers/pages');
const PagesOrder = require('../controllers/pagesOrder');
/** /**
* Create new page form * Create new page form
@ -21,12 +22,14 @@ router.get('/page/edit/:id', 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.getAllExceptChildrens(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);

View file

@ -1,4 +1,5 @@
const pages = require('./pages'); const pages = require('./pages');
const pagesOrder = require('./pagesOrder');
/** /**
* @class Database * @class Database
@ -142,5 +143,6 @@ class Database {
module.exports = { module.exports = {
class: Database, class: Database,
pages: new Database(pages) pages: new Database(pages),
pagesOrder: new Database(pagesOrder)
}; };

View file

@ -0,0 +1,34 @@
const Datastore = require('nedb');
const config = require('../../../config');
const db = new Datastore({filename: `./${config.database}/pagesOrder.db`, autoload: true});
/**
* Current DataStore preparation
* Add initial row for RootPage
*/
(async function() {
const parentIdOfRootPages = '0';
const cbk = (resolve, reject) => (err, doc) => {
if (err) {
reject(err);
}
resolve(doc);
};
const order = await new Promise((resolve, reject) => {
db.findOne({page: parentIdOfRootPages}, cbk(resolve, reject));
});
if (!order) {
const initialData = {
page: '0',
order: []
};
await db.insert(initialData);
}
}());
module.exports = db;

View file

@ -14,21 +14,35 @@
</module-settings> </module-settings>
<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>
{% 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"> <span class="writing-header__save" name="js-submit">
Save Save

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

@ -0,0 +1,143 @@
const {expect} = require('chai');
const fs = require('fs');
const path = require('path');
const config = require('../../config');
const PageOrder = require('../../src/models/pageOrder');
const {pagesOrder} = require('../../src/utils/database');
describe('PageOrder model', () => {
after(() => {
const pathToDB = path.resolve(__dirname, '../../', config.database, './pages.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();
});
});

View file

@ -1,5 +1,6 @@
const {app} = require('../../bin/www'); const {app} = require('../../bin/www');
const model = require('../../src/models/page'); const model = require('../../src/models/page');
const PageOrder = require('../../src/models/pageOrder');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -34,12 +35,12 @@ describe('Pages REST: ', () => {
text: 'Page header' text: 'Page header'
} }
} }
] ],
}; };
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;
@ -58,7 +59,11 @@ describe('Pages REST: ', () => {
expect(createdPage.title).to.equal(body.blocks[0].data.text); expect(createdPage.title).to.equal(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 +111,14 @@ 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.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 () => {
@ -175,6 +182,7 @@ describe('Pages REST: ', () => {
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);
@ -182,7 +190,8 @@ describe('Pages REST: ', () => {
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('Updating page with not existing id', async () => { it('Updating page with not existing id', async () => {

881
yarn.lock

File diff suppressed because it is too large Load diff