1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-09 23:45:25 +02:00
This commit is contained in:
gohabereg 2018-10-07 19:11:25 +03:00
commit 927b18387c
28 changed files with 15926 additions and 973 deletions

12071
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,15 +7,19 @@
"start:dev": "cross-env NODE_ENV=development nodemon ./bin/www",
"test": "cross-env NODE_ENV=testing mocha --recursive ./test",
"lint": "eslint --fix --cache ./src/**/*.js",
"build": "webpack ./src/frontend/js/app.js --o='./public/dist/bundle.js' --output-library=Docs -d --watch",
"build": "webpack ./src/frontend/js/app.js --o='./public/dist/[name].bundle.js' --output-library=Docs --output-public-path=/dist/ -p --watch",
"precommit": "yarn lint && yarn test --exit"
},
"dependencies": {
"@babel/polyfill": "^7.0.0",
"body-parser": "latest",
"codex.editor": "^2.0.11",
"codex.editor.header": "^2.0.5",
"cookie-parser": "~1.4.3",
"debug": "~2.6.9",
"express": "~4.16.0",
"http-errors": "~1.6.2",
"module-dispatcher": "^1.0.2",
"morgan": "~1.9.0",
"multer": "^1.3.1",
"nedb": "^1.8.0",
@ -26,10 +30,11 @@
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"autoprefixer": "^9.1.3",
"babel": "^6.23.0",
"babel-loader": "^8.0.2",
"babel-preset-env": "^1.7.0",
"chai": "^4.1.2",
"chai-http": "^4.0.0",
"cross-env": "^5.2.0",
@ -38,8 +43,8 @@
"eslint": "^5.3.0",
"eslint-config-codex": "github:codex-team/eslint-config",
"eslint-plugin-chai-friendly": "^0.4.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"husky": "^0.14.3",
"mini-css-extract-plugin": "^0.4.3",
"mocha": "^5.2.0",
"mocha-sinon": "^2.1.0",
"nyc": "^12.0.2",

View file

@ -1 +0,0 @@
/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */html{-webkit-text-size-adjust:100%;line-height:1.15}body{margin:0}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}:root{--color-line-gray:#e8e8eb;--color-link-active:#388ae5;--color-text-main:#1d202b;--color-text-second:#7b7e89;--layout-padding-horisontal:40px;--layout-width-aside:250px;--layout-width-main-col:650px}.docs{display:-webkit-box;display:-ms-flexbox;display:flex;padding:0 40px;padding:0 var(--layout-padding-horisontal)}.docs__aside{width:250px;width:var(--layout-width-aside)}.docs__content{-webkit-box-flex:2;-ms-flex-positive:2;flex-grow:2}.docs__content-inner{margin:0 auto;max-width:650px;max-width:var(--layout-width-main-col)}.docs__aside,.docs__content{padding:40px 0}.docs-header{border-bottom:1px solid #e8e8eb;border-bottom:1px solid var(--color-line-gray);display:-webkit-box;display:-ms-flexbox;display:flex;font-size:15.8px;line-height:50px;padding:0 40px;padding:0 var(--layout-padding-horisontal)}.docs-header a{color:inherit;display:inline-block;text-decoration:none}.docs-header a:hover{color:#388ae5;color:var(--color-link-active)}.docs-header__logo{font-weight:700}.docs-header__menu{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0 0 0 auto}.docs-header__menu li{list-style:none;margin-left:20px}.docs-aside{color:#7b7e89;color:var(--color-text-second);font-size:15px}.docs-aside a{text-decoration:none}.docs-aside__section{margin-bottom:30px}.docs-aside__section-title{color:#388ae5;color:var(--color-link-active);margin-bottom:15px}.docs-aside__section-list{list-style:none;padding-left:0}.docs-aside__section-list a{color:inherit;display:inline-block;padding:8px 0}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#1d202b;color:var(--color-text-main);font-family:system-ui,-apple-system,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,Helvetica,Arial,Verdana}

View file

@ -1,9 +0,0 @@
var Docs=function(n){var e={};function c(t){if(e[t])return e[t].exports;var r=e[t]={i:t,l:!1,exports:{}};return n[t].call(r.exports,r,r.exports,c),r.l=!0,r.exports}return c.m=n,c.c=e,c.d=function(n,e,t){c.o(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:t})},c.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},c.t=function(n,e){if(1&e&&(n=c(n)),8&e)return n;if(4&e&&"object"==typeof n&&n&&n.__esModule)return n;var t=Object.create(null);if(c.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:n}),2&e&&"string"!=typeof n)for(var r in n)c.d(t,r,function(e){return n[e]}.bind(null,r));return t},c.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return c.d(e,"a",e),e},c.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},c.p="",c(c.s="./src/frontend/js/app.js")}({"./src/frontend/js/app.js":
/*!********************************!*\
!*** ./src/frontend/js/app.js ***!
\********************************/
/*! no static exports found */function(module,exports,__webpack_require__){"use strict";eval('\n\nvar _main = __webpack_require__(/*! ../styles/main.pcss */ "./src/frontend/styles/main.pcss");\n\nvar _main2 = _interopRequireDefault(_main);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n\nmodule.exports =\n/**\n * @constructor\n */\nfunction Docs() {\n _classCallCheck(this, Docs);\n\n console.log(\'CodeX Docs initialized\');\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvZnJvbnRlbmQvanMvYXBwLmpzLmpzIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vRG9jcy8uL3NyYy9mcm9udGVuZC9qcy9hcHAuanM/YmM1NSJdLCJzb3VyY2VzQ29udGVudCI6WyIvLyBObyBpbnNwZWN0aW9uIGZvciB1bnVzZWQgdmFyIGBjc3NgIGJlY2F1c2UgaXQncyB1c2VkIGZvciBjc3MgYnVuZGxlXG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tdW51c2VkLXZhcnNcbmltcG9ydCBjc3MgZnJvbSAnLi4vc3R5bGVzL21haW4ucGNzcyc7XG5cbm1vZHVsZS5leHBvcnRzID0gY2xhc3MgRG9jcyB7XG4gIC8qKlxuICAgKiBAY29uc3RydWN0b3JcbiAgICovXG4gIGNvbnN0cnVjdG9yKCkge1xuICAgIGNvbnNvbGUubG9nKCdDb2RlWCBEb2NzIGluaXRpYWxpemVkJyk7XG4gIH1cbn07XG4iXSwibWFwcGluZ3MiOiI7O0FBRUE7QUFDQTs7Ozs7OztBQUNBO0FBQ0E7OztBQUdBO0FBQUE7QUFDQTtBQUFBO0FBQ0EiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./src/frontend/js/app.js\n')},"./src/frontend/styles/main.pcss":
/*!***************************************!*\
!*** ./src/frontend/styles/main.pcss ***!
\***************************************/
/*! no static exports found */function(module,exports){eval("// removed by extract-text-webpack-plugin//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvZnJvbnRlbmQvc3R5bGVzL21haW4ucGNzcy5qcyIsInNvdXJjZXMiOlsid2VicGFjazovL0RvY3MvLi9zcmMvZnJvbnRlbmQvc3R5bGVzL21haW4ucGNzcz83N2VjIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIHJlbW92ZWQgYnkgZXh0cmFjdC10ZXh0LXdlYnBhY2stcGx1Z2luIl0sIm1hcHBpbmdzIjoiQUFBQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./src/frontend/styles/main.pcss\n")}});

2144
public/dist/editor.bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

10
public/dist/main.bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

1
public/dist/main.css vendored Normal file
View file

@ -0,0 +1 @@
/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */html{-webkit-text-size-adjust:100%;line-height:1.15}body{margin:0}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}:root{--color-line-gray:#e8e8eb;--color-link-active:#388ae5;--color-text-main:#1d202b;--color-text-second:#7b7e89;--layout-padding-horisontal:40px;--layout-padding-vertical:40px;--layout-width-aside:250px;--layout-width-main-col:650px}.docs{display:-webkit-box;display:-ms-flexbox;display:flex;padding:0 40px;padding:0 var(--layout-padding-horisontal)}.docs__aside{width:250px;width:var(--layout-width-aside)}.docs__content{-webkit-box-flex:2;-ms-flex-positive:2;flex-grow:2;word-wrap:break-word}.docs__content-inner{margin:0 auto;max-width:650px;max-width:var(--layout-width-main-col)}.docs__aside,.docs__content{padding:40px 0;padding:var(--layout-padding-vertical) 0}.docs-header{border-bottom:1px solid #e8e8eb;border-bottom:1px solid var(--color-line-gray);display:-webkit-box;display:-ms-flexbox;display:flex;font-size:15.8px;line-height:50px;padding:0 40px;padding:0 var(--layout-padding-horisontal)}.docs-header a{display:inline-block;text-decoration:none}.docs-header__logo{color:inherit;font-weight:700}.docs-header__menu{display:-webkit-box;display:-ms-flexbox;display:flex;margin:0 0 0 auto}.docs-header__menu li{list-style:none;margin-left:20px}.docs-header__menu a:not(.docs-header__button){color:inherit}.docs-header__menu a:not(.docs-header__button):hover{color:#388ae5;color:var(--color-link-active)}.docs-header__button{background:#388ae5;background:var(--color-link-active);border-radius:3px;color:#fff;display:inline-block;font-size:14px;line-height:1em;padding:9px 15px}.docs-header__button svg{margin:0 .3em 0 -.05em}.docs-header__button:hover{background:#387ecc}.docs-header__button{margin:auto 30px auto auto}.docs-aside{color:#7b7e89;color:var(--color-text-second);font-size:15px}.docs-aside a{text-decoration:none}.docs-aside__section{margin-bottom:30px}.docs-aside__section-title{color:#388ae5;color:var(--color-link-active);margin-bottom:15px}.docs-aside__section-list{list-style:none;padding-left:0}.docs-aside__section-list a{color:inherit;display:inline-block;padding:8px 0}.writing-header{background:#fff;-webkit-box-shadow:0 3px 10px #fff;box-shadow:0 3px 10px #fff;display:-webkit-box;display:-ms-flexbox;display:flex;margin-top:-40px;margin-top:calc(-1 * var(--layout-padding-vertical));padding:15px 0;position:-webkit-sticky;position:sticky;top:0;z-index:2}.writing-header__save{background:#388ae5;background:var(--color-link-active);border-radius:3px;color:#fff;display:inline-block;font-size:14px;line-height:1em;padding:9px 15px}.writing-header__save svg{margin:0 .3em 0 -.05em}.writing-header__save:hover{background:#387ecc}.writing-header__save{margin-left:auto}.writing-header__left{color:#7b7e89;color:var(--color-text-second);margin:auto 0}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#1d202b;color:var(--color-text-main);font-family:system-ui,-apple-system,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,Helvetica,Arial,Verdana}svg{fill:currentColor}

View file

@ -15,6 +15,7 @@ app.locals.config = config;
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'twig');
require('./utils/twig');
app.use(logger('dev'));
app.use(express.json());

View file

@ -12,7 +12,7 @@ class Pages {
* @returns {['title', 'body']}
*/
static get REQUIRED_FIELDS() {
return ['title', 'body'];
return [ 'body' ];
}
/**
@ -48,23 +48,47 @@ class Pages {
* @returns {Promise<Page>}
*/
static async insert(data) {
if (!Pages.validate(data)) {
throw new Error('Invalid request format');
try {
Pages.validate(data);
const page = new Model(data);
return page.save();
} catch (validationError) {
throw new Error(validationError);
}
const page = new Model(data);
return page.save();
}
/**
* Check PageData object for required fields
*
* @param {PageData} data
* @returns {boolean}
* @throws {Error} - validation error
*/
static validate(data) {
return Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined');
const allRequiredFields = Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined');
if (!allRequiredFields) {
throw new Error('Some of required fields is missed');
}
const hasBlocks = data.body && data.body.blocks && Array.isArray(data.body.blocks) && data.body.blocks.length > 0;
if (!hasBlocks) {
throw new Error('Page body is invalid');
}
const hasHeaderAsFirstBlock = data.body.blocks[0].type === 'header';
if (!hasHeaderAsFirstBlock) {
throw new Error('First page Block must be a Header');
}
const headerIsNotEmpty = data.body.blocks[0].data.text.replace('<br>', '').trim() !== '';
if (!headerIsNotEmpty) {
throw new Error('Please, fill page Header');
}
}
/**

View file

@ -2,11 +2,43 @@
// eslint-disable-next-line no-unused-vars
import css from '../styles/main.pcss';
module.exports = class Docs {
/**
* Module Dispatcher
* @see {@link https://github.com/codex-team/moduleDispatcher}
* @author CodeX
*/
import ModuleDispatcher from 'module-dispatcher';
/**
* Import modules
*/
import Writing from './modules/writing';
/**
* Main app class
*/
class Docs {
/**
* @constructor
*/
constructor() {
console.log('CodeX Docs initialized');
this.writing = new Writing();
document.addEventListener('DOMContentLoaded', (event) => {
this.docReady();
});
}
};
/**
* Document is ready
*/
docReady() {
this.moduleDispatcher = new ModuleDispatcher({
Library: window.Docs
});
}
}
export default new Docs();

View file

@ -0,0 +1,42 @@
import CodeXEditor from 'codex.editor';
import Header from 'codex.editor.header';
/**
* Class for working with Editor.js
*/
export default class Editor {
/**
* Creates Editor instance
*/
constructor() {
this.editor = new CodeXEditor({
tools: {
header: {
class: Header,
config: {
placeholder: 'Enter a title'
}
}
},
data: {
blocks: [
{
type: 'header',
data: {
text: '',
level: 2
}
}
]
}
});
}
/**
* Return Editor data
* @return {Promise.<{}>}
*/
save() {
return this.editor.saver.save();
}
}

View file

@ -0,0 +1,115 @@
/**
* Module for pages create/edit
*/
/**
* @typedef {object} editorData
* @property {{type, data}[]} blocks - array of Blocks
* @property {string} version - used Editor version
* @property {number} time - saving time
*/
export default class Writing {
/**
* Creates base properties
*/
constructor(){
this.editor = null;
this.nodes = {
editorWrapper: null,
saveButton: null,
parentIdSelector: null,
}
}
/**
* Called by ModuleDispatcher to initialize module from DOM
* @param {object} settings - module settings
* @param {HTMLElement} moduleEl - module element
*/
init(settings, moduleEl) {
/**
* Create Editor
*/
this.nodes.editorWrapper = document.createElement('div');
this.nodes.editorWrapper.id = 'codex-editor';
moduleEl.appendChild(this.nodes.editorWrapper);
this.loadEditor().then((editor) => {
this.editor = editor;
});
/**
* Activate form elements
*/
this.nodes.saveButton = moduleEl.querySelector('[name="js-submit"]');
this.nodes.saveButton.addEventListener('click', () => {
this.saveButtonClicked();
});
this.nodes.parentIdSelector = moduleEl.querySelector('[name="parent"]');
};
/**
* Loads class for working with Editor
* @return {Promise<Editor>}
*/
async loadEditor(){
const {default: Editor} = await import(/* webpackChunkName: "editor" */ './../classes/editor');
return new Editor();
}
/**
* Returns all writing form data
* @throws {Error} - validation error
* @return {Promise.<{parent: string, body: {editorData}}>}
*/
async getData(){
const editorData = await this.editor.save();
const firstBlock = editorData.blocks.length ? editorData.blocks[0] : null;
const title = firstBlock && firstBlock.type === 'header' ? firstBlock.data.text : null;
if (!title) {
throw new Error('Entry should start with Header');
}
return {
parent: this.nodes.parentIdSelector.value,
body: editorData
};
}
/**
* Handler for clicks on the Save button
*/
async saveButtonClicked(){
try {
const writingData = await this.getData();
try {
let response = await fetch('/page', {
method: 'PUT',
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(writingData),
});
response = await response.json();
if (response.success){
document.location = '/page/' + response.result._id;
} else {
alert(response.error);
console.log('Validation failed:', response.error);
}
} catch (sendingError) {
console.log('Saving request failed:', sendingError);
}
} catch (savingError){
alert(savingError);
console.log('Saving error: ', savingError);
}
}
}

View file

@ -7,16 +7,12 @@
a {
text-decoration: none;
color: inherit;
display: inline-block;
&:hover {
color: var(--color-link-active);
}
}
&__logo {
font-weight: bold;
color: inherit;
}
&__menu {
@ -27,5 +23,18 @@
list-style: none;
margin-left: 20px;
}
a:not(.docs-header__button) {
color: inherit;
&:hover {
color: var(--color-link-active);
}
}
}
&__button {
@apply --button;
margin: auto 30px auto auto;
}
}

View file

@ -0,0 +1,20 @@
.writing-header {
display: flex;
padding: 15px 0;
margin-top: calc(-1 * var(--layout-padding-vertical));
position: sticky;
top: 0;
background: #fff;
z-index: 2;
box-shadow: 0 3px 10px #fff;
&__save {
@apply --button;
margin-left: auto;
}
&__left {
margin: auto 0;
color: var(--color-text-second);
}
}

View file

@ -8,6 +8,7 @@
&__content {
flex-grow: 2;
word-wrap: break-word;
&-inner {
max-width: var(--layout-width-main-col);
@ -17,6 +18,6 @@
&__aside,
&__content {
padding: 40px 0;
padding: var(--layout-padding-vertical) 0;
}
}

View file

@ -3,6 +3,7 @@
@import url('layout.pcss');
@import url('components/header.pcss');
@import url('components/aside.pcss');
@import url('components/writing.pcss');
body {
font-family: system-ui, Helvetica, Arial, Verdana;
@ -10,3 +11,7 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
svg {
fill: currentColor;
}

View file

@ -8,6 +8,25 @@
* Site layout sizes
*/
--layout-padding-horisontal: 40px;
--layout-padding-vertical: 40px;
--layout-width-aside: 250px;
--layout-width-main-col: 650px;
--button {
display: inline-block;
background: var(--color-link-active);
color: #fff;
border-radius: 3px;
padding: 9px 15px;
font-size: 14px;
line-height: 1em;
svg {
margin: 0 0.3em 0 -0.05em;
}
&:hover {
background: color-mod(var(--color-link-active) blackness(+10%));
}
}
}

View file

@ -0,0 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" xmlns="http://www.w3.org/2000/svg">
<path d="M6.44 4.64h3.7a.9.9 0 1 1 0 1.8h-3.7v3.7a.9.9 0 1 1-1.8 0v-3.7H.9a.9.9 0 0 1 0-1.8h3.74V.9a.9.9 0 0 1 1.8 0v3.74z"/>
</svg>

After

Width:  |  Height:  |  Size: 219 B

View file

@ -67,10 +67,10 @@ class Page {
* @param {PageData} pageData
*/
set data(pageData) {
const {title, body, parent} = pageData;
const {body, parent} = pageData;
this.title = title || this.title;
this.body = body || this.body;
this.title = this.extractTitleFromBody();
this._parent = parent || this._parent;
}
@ -88,6 +88,16 @@ class Page {
};
}
/**
* Extract first header from editor data
* @return {string}
*/
extractTitleFromBody() {
const headerBlock = this.body ? this.body.blocks.find(block => block.type === 'header') : '';
return headerBlock ? headerBlock.data.text : '';
}
/**
* Link given page as parent
*

View file

@ -3,6 +3,17 @@ const router = express.Router();
const multer = require('multer')();
const Pages = require('../controllers/pages');
/**
* Create new page form
*/
router.get('/page/new', async (req, res) => {
let pagesAvailable = await Pages.getAll();
res.render('pages/form', {
pagesAvailable
});
});
/**
* GET /page/:id
*

20
src/utils/twig.js Normal file
View file

@ -0,0 +1,20 @@
/**
* Twig extensions
*/
const twig = require('twig');
const fs = require('fs');
module.exports = (function () {
'use strict';
/**
* Function for include svg on page
*
* @example svg('path/from/root/dir')
* @param filename - name of icon
* @returns {String} - svg code
*/
twig.extendFunction('svg', function (filename) {
return fs.readFileSync(`${__dirname}/../frontend/svg/${filename}.svg`, 'utf-8');
});
}());

View file

@ -3,6 +3,12 @@
{{ config.title }}
</a>
<ul class="docs-header__menu">
<li>
<a class="docs-header__button" href="/page/new">
{{ svg('plus') }}
Add Page
</a>
</li>
{% for option in config.menu %}
<li>
<a href="{{option.uri}}">

View file

@ -2,7 +2,7 @@
<html>
<head>
<title>{{ title }}</title>
<link rel="stylesheet" href="/dist/bundle.css" />
<link rel="stylesheet" href="/dist/main.css" />
</head>
<body>
{% include "components/header.twig" %}
@ -16,9 +16,6 @@
</div>
</div>
</div>
<script src="/dist/bundle.js"></script>
<script>
new Docs();
</script>
<script src="/dist/main.bundle.js"></script>
</body>
</html>

25
src/views/pages/form.twig Normal file
View file

@ -0,0 +1,25 @@
{% extends 'layout.twig' %}
{% block body %}
<style>
.docs-header__button {
visibility: hidden;
}
</style>
<section data-module="writing">
<header class="writing-header">
<span class="writing-header__left">
New Page at the
<select name="parent">
<option value="0">Root</option>
{% for page in pagesAvailable %}
<option value="{{ page._id }}">{{ page.title }}</option>
{% endfor %}
</select>
</span>
<span class="writing-header__save" name="js-submit">
Save
</span>
</header>
</section>
{% endblock %}

View file

@ -22,7 +22,7 @@ describe('Page model', () => {
let {data} = page;
expect(data._id).to.be.undefined;
expect(data.title).to.be.undefined;
expect(data.title).to.be.empty;
expect(data.body).to.be.undefined;
expect(data.parent).to.be.undefined;
@ -31,11 +31,23 @@ describe('Page model', () => {
data = page.data;
expect(data._id).to.be.undefined;
expect(data.title).to.be.undefined;
expect(data.title).to.be.empty;
expect(data.body).to.be.undefined;
expect(data.parent).to.be.undefined;
const initialData = {_id: 'page_id', title: 'Test page', body: 'Test page body'};
const initialData = {
_id: 'page_id',
body: {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
}
};
page = new Page(initialData);
@ -44,19 +56,27 @@ describe('Page model', () => {
data = page.data;
expect(data._id).to.equal(initialData._id);
expect(data.title).to.equal(initialData.title);
expect(data.body).to.equal(initialData.body);
expect(data.title).to.equal(initialData.body.blocks[0].data.text);
expect(data.body).to.deep.equal(initialData.body);
expect(data.parent).to.be.undefined;
expect(json._id).to.equal(initialData._id);
expect(json.title).to.equal(initialData.title);
expect(json.body).to.equal(initialData.body);
expect(json.title).to.equal(initialData.body.blocks[0].data.text);
expect(json.body).to.deep.equal(initialData.body);
expect(json.parent).to.be.undefined;
const update = {
_id: 12345,
title: 'Test page',
body: 'Test page body'
body: {
blocks: [
{
type: 'header',
data: {
text: 'Updated page header'
}
}
]
}
};
page.data = update;
@ -64,19 +84,30 @@ describe('Page model', () => {
data = page.data;
expect(data._id).to.equal(initialData._id);
expect(data.title).to.equal(update.title);
expect(data.title).to.equal(update.body.blocks[0].data.text);
expect(data.body).to.equal(update.body);
expect(data.parent).to.be.undefined;
});
it('Saving, updating and deleting model in the database', async () => {
const initialData = {title: 'Test page', body: 'Test page body'};
const initialData = {
body: {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
}
};
const page = new Page(initialData);
let savedPage = await page.save();
expect(savedPage._id).not.be.undefined;
expect(savedPage.title).to.equal(initialData.title);
expect(savedPage.title).to.equal(initialData.body.blocks[0].data.text);
expect(savedPage.body).to.equal(initialData.body);
expect(page._id).not.be.undefined;
@ -84,9 +115,20 @@ describe('Page model', () => {
expect(insertedPage._id).to.equal(page._id);
expect(insertedPage.title).to.equal(page.title);
expect(insertedPage.body).to.equal(page.body);
expect(insertedPage.body).to.deep.equal(page.body);
const updateData = {title: 'Updated test page', body: 'Updated test page body'};
const updateData = {
body: {
blocks: [
{
type: 'header',
data: {
text: 'Updated page header'
}
}
]
}
};
page.data = updateData;
await page.save();
@ -96,8 +138,8 @@ describe('Page model', () => {
const updatedPage = await pages.findOne({_id: page._id});
expect(updatedPage._id).to.equal(savedPage._id);
expect(updatedPage.title).to.equal(updateData.title);
expect(updatedPage.body).to.equal(updateData.body);
expect(updatedPage.title).to.equal(updateData.body.blocks[0].data.text);
expect(updatedPage.body).to.deep.equal(updateData.body);
await page.destroy();
@ -109,7 +151,18 @@ describe('Page model', () => {
});
it('Static get method', async () => {
const initialData = {title: 'Test page', body: 'Test page body'};
const initialData = {
body: {
blocks: [
{
type: 'header',
data: {
text: 'Test Page header'
}
}
]
}
};
const page = new Page(initialData);
const savedPage = await page.save();
@ -119,16 +172,38 @@ describe('Page model', () => {
const {data} = foundPage;
expect(data._id).to.equal(savedPage._id);
expect(data.title).to.equal(initialData.title);
expect(data.body).to.equal(initialData.body);
expect(data.title).to.equal(initialData.body.blocks[0].data.text);
expect(data.body).to.deep.equal(initialData.body);
await page.destroy();
});
it('Static getAll method', async () => {
const pagesToSave = [
new Page({title: 'Page 1', body: 'Page 1 body'}),
new Page({title: 'Page 2', body: 'Page 2 body'})
new Page({
body: {
blocks: [
{
type: 'header',
data: {
text: 'Page 1 header'
}
}
]
}
}),
new Page({
body: {
blocks: [
{
type: 'header',
data: {
text: 'Page 2 header'
}
}
]
}
})
];
const savedPages = await Promise.all(pagesToSave.map(page => page.save()));
@ -137,16 +212,42 @@ describe('Page model', () => {
expect(foundPages.length).to.equal(2);
foundPages.forEach((page, i) => {
expect(page.title).to.equal(pagesToSave[i].title);
expect(page.body).to.equal(pagesToSave[i].body);
expect(page.title).to.equal(pagesToSave[i].body.blocks[0].data.text);
expect(page.body).to.deep.equal(pagesToSave[i].body);
});
});
it('Parent pages', async () => {
const parent = new Page({title: 'Parent page', body: 'Parent page body'});
const parent = new Page(
{
body: {
blocks: [
{
type: 'header',
data: {
text: 'Parent page header'
}
}
]
}
}
);
const {_id: parentId} = await parent.save();
const child = new Page({title: 'Child page', body: 'Child page body'});
const child = new Page(
{
body: {
blocks: [
{
type: 'header',
data: {
text: 'Child page header'
}
}
]
}
}
);
child.parent = parent;
@ -155,8 +256,8 @@ describe('Page model', () => {
const testedParent = await child.parent;
expect(testedParent._id).to.equal(parentId);
expect(testedParent.title).to.equal(parent.title);
expect(testedParent.body).to.equal(parent.body);
expect(testedParent.title).to.equal(parent.body.blocks[0].data.text);
expect(testedParent.body).to.deep.equal(parent.body);
const children = await parent.children;
@ -165,12 +266,31 @@ describe('Page model', () => {
const testedChild = children.pop();
expect(testedChild._id).to.equal(childId);
expect(testedChild.title).to.equal(child.title);
expect(testedChild.body).to.equal(child.body);
expect(testedChild.title).to.equal(child.body.blocks[0].data.text);
expect(testedChild.body).to.deep.equal(child.body);
expect(testedChild._parent).to.equal(child._parent);
expect(testedChild._parent).to.equal(parent._id);
parent.destroy();
child.destroy();
});
it('Extracting title from page body', async () => {
const pageData = {
body: {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
}
};
const page = new Page(pageData);
expect(page.title).to.equal(pageData.body.blocks[0].data.text);
});
});

View file

@ -26,12 +26,20 @@ describe('Pages REST: ', () => {
});
it('Creating page', async () => {
const title = 'Test page';
const body = 'Test page body';
const body = {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
};
const res = await agent
.put('/page')
.send({title, body});
.send({body});
expect(res).to.have.status(200);
expect(res).to.be.json;
@ -40,15 +48,15 @@ describe('Pages REST: ', () => {
expect(success).to.be.true;
expect(result._id).to.be.a('string');
expect(result.title).to.equal(title);
expect(result.body).to.equal(body);
expect(result.title).to.equal(body.blocks[0].data.text);
expect(result.body).to.deep.equal(body);
const createdPage = await model.get(result._id);
expect(createdPage).not.be.null;
expect(createdPage._id).to.equal(result._id);
expect(createdPage.title).to.equal(title);
expect(createdPage.body).to.equal(body);
expect(createdPage.title).to.equal(body.blocks[0].data.text);
expect(createdPage.body).to.deep.equal(body);
createdPage.destroy();
});
@ -64,16 +72,24 @@ describe('Pages REST: ', () => {
const {success, error} = res.body;
expect(success).to.be.false;
expect(error).to.equal('Invalid request format');
expect(error).to.equal('Error: Some of required fields is missed');
});
it('Finding page', async () => {
const title = 'Test page';
const body = 'Test page body';
const body = {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
};
const put = await agent
.put('/page')
.send({title, body});
.send({body});
expect(put).to.have.status(200);
expect(put).to.be.json;
@ -92,8 +108,8 @@ describe('Pages REST: ', () => {
const foundPage = await model.get(_id);
expect(foundPage._id).to.equal(_id);
expect(foundPage.title).to.equal(title);
expect(foundPage.body).to.equal(body);
expect(foundPage.title).to.equal(body.blocks[0].data.text);
expect(foundPage.body).to.deep.equal(body);
foundPage.destroy();
});
@ -111,24 +127,40 @@ describe('Pages REST: ', () => {
});
it('Updating page', async () => {
const title = 'Test page';
const body = 'Test page body';
const body = {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
};
let res = await agent
.put('/page')
.send({title, body});
.send({body});
expect(res).to.have.status(200);
expect(res).to.be.json;
const {result: {_id}} = res.body;
const updatedTitle = 'Updated test page';
const updatedBody = 'Updated test page body';
const updatedBody = {
blocks: [
{
type: 'header',
data: {
text: 'Updated page header'
}
}
]
};
res = await agent
.post(`/page/${_id}`)
.send({title: updatedTitle, body: updatedBody});
.send({body: updatedBody});
expect(res).to.have.status(200);
expect(res).to.be.json;
@ -137,26 +169,35 @@ describe('Pages REST: ', () => {
expect(success).to.be.true;
expect(result._id).to.equal(_id);
expect(result.title).not.equal(title);
expect(result.title).to.equal(updatedTitle);
expect(result.title).not.equal(body.blocks[0].data.text);
expect(result.title).to.equal(updatedBody.blocks[0].data.text);
expect(result.body).not.equal(body);
expect(result.body).to.equal(updatedBody);
expect(result.body).to.deep.equal(updatedBody);
const updatedPage = await model.get(_id);
expect(updatedPage._id).to.equal(_id);
expect(updatedPage.title).not.equal(title);
expect(updatedPage.title).to.equal(updatedTitle);
expect(updatedPage.title).not.equal(body.blocks[0].data.text);
expect(updatedPage.title).to.equal(updatedBody.blocks[0].data.text);
expect(updatedPage.body).not.equal(body);
expect(updatedPage.body).to.equal(updatedBody);
expect(updatedPage.body).to.deep.equal(updatedBody);
updatedPage.destroy();
});
it('Updating page with not existing id', async () => {
const res = await agent
. post('/page/not-existing-id')
.send({title: 'Updated title', body: 'Updated body'});
.post('/page/not-existing-id')
.send({body: {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
}});
expect(res).to.have.status(400);
expect(res).to.be.json;
@ -168,12 +209,20 @@ describe('Pages REST: ', () => {
});
it('Removing page', async () => {
const title = 'Test page';
const body = 'Test page body';
const body = {
blocks: [
{
type: 'header',
data: {
text: 'Page header'
}
}
]
};
let res = await agent
.put('/page')
.send({title, body});
.send({body});
expect(res).to.have.status(200);
expect(res).to.be.json;
@ -190,8 +239,8 @@ describe('Pages REST: ', () => {
expect(success).to.be.true;
expect(result._id).to.be.undefined;
expect(result.title).to.equal(title);
expect(result.body).to.equal(body);
expect(result.title).to.equal(body.blocks[0].data.text);
expect(result.body).to.deep.equal(body);
const deletedPage = await model.get(_id);

View file

@ -1,4 +1,4 @@
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
/**
* Options for the Babel
@ -8,25 +8,37 @@ const babelLoader = {
options: {
cacheDirectory: '.cache/babel-loader',
presets: [
'env'
[
'@babel/preset-env',
{
'useBuiltIns': 'usage'
}
]
],
plugins: [
/**
* Dont need to use «.default» after «export default Class Ui {}»
* @see {@link https://github.com/59naga/babel-plugin-add-module-exports}
*/
// 'add-module-exports',
'@babel/plugin-syntax-dynamic-import'
]
}
};
module.exports = (env) => {
return {
output: {
libraryExport: 'default' // uses to export .default field of app.js exported class instance
},
module: {
rules: [
{
test: /\.p?css$/,
use: ExtractTextPlugin.extract([
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
// you can specify a publicPath here
// by default it use publicPath in webpackOptions.output
publicPath: '../'
}
},
{
loader: 'css-loader',
options: {
@ -41,9 +53,10 @@ module.exports = (env) => {
}
}
}
])
]
}, {
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: [
babelLoader
]
@ -51,10 +64,14 @@ module.exports = (env) => {
]
},
plugins: [
new ExtractTextPlugin('bundle.css')
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
filename: '[name].css'
})
],
optimization: {
minimize: true
minimize: true,
splitChunks: false
}
};
};

1934
yarn.lock

File diff suppressed because it is too large Load diff