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

Page showing, page edit, move API to /api/ (#10)

This commit is contained in:
Peter Savchenko 2018-10-15 22:06:01 +03:00 committed by GitHub
parent ff09f695d0
commit 730eff7995
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 316 additions and 133 deletions

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

@ -7,8 +7,9 @@ import Header from 'codex.editor.header';
export default class Editor { export default class Editor {
/** /**
* Creates Editor instance * Creates Editor instance
* @property {object} initialData - data to start with
*/ */
constructor() { constructor({initialData}) {
this.editor = new CodeXEditor({ this.editor = new CodeXEditor({
tools: { tools: {
header: { header: {
@ -18,7 +19,7 @@ export default class Editor {
} }
} }
}, },
data: { data: initialData || {
blocks: [ blocks: [
{ {
type: 'header', type: 'header',

View file

@ -7,6 +7,12 @@
* @property {string} version - used Editor version * @property {string} version - used Editor version
* @property {number} time - saving time * @property {number} time - saving time
*/ */
/**
* @typedef {object} writingSettings
* @property {{_id, _parent, title, body: editorData}} [page] - page data for editing
*/
/** /**
* @class Writing * @class Writing
* @classdesc Class for create/edit pages * @classdesc Class for create/edit pages
@ -17,6 +23,7 @@ export default class Writing {
*/ */
constructor() { constructor() {
this.editor = null; this.editor = null;
this.page = null; // stores Page on editing
this.nodes = { this.nodes = {
editorWrapper: null, editorWrapper: null,
saveButton: null, saveButton: null,
@ -26,10 +33,10 @@ export default class Writing {
/** /**
* Called by ModuleDispatcher to initialize module from DOM * Called by ModuleDispatcher to initialize module from DOM
* @param {object} settings - module settings * @param {writingSettings} settings - module settings
* @param {HTMLElement} moduleEl - module element * @param {HTMLElement} moduleEl - module element
*/ */
init(settings, moduleEl) { init(settings = {}, moduleEl) {
/** /**
* Create Editor * Create Editor
*/ */
@ -38,6 +45,10 @@ export default class Writing {
moduleEl.appendChild(this.nodes.editorWrapper); moduleEl.appendChild(this.nodes.editorWrapper);
if (settings.page){
this.page = settings.page;
}
this.loadEditor().then((editor) => { this.loadEditor().then((editor) => {
this.editor = editor; this.editor = editor;
}); });
@ -59,7 +70,9 @@ export default class Writing {
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(); return new Editor({
initialData: this.page ? this.page.body : null
});
} }
/** /**
@ -88,10 +101,11 @@ export default class Writing {
async saveButtonClicked() { async saveButtonClicked() {
try { try {
const writingData = await this.getData(); const writingData = await this.getData();
const endpoint = this.page ? '/api/page/' + this.page._id : '/api/page';
try { try {
let response = await fetch('/page', { let response = await fetch(endpoint, {
method: 'PUT', method: this.page ? 'POST' : 'PUT',
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8' 'Content-Type': 'application/json; charset=utf-8'
}, },

View file

@ -0,0 +1,57 @@
.page {
font-size: 16px;
line-height: 1.6;
&__header {
display: flex;
color: var(--color-text-second);
&-nav {
color: inherit;
text-decoration: none;
&:hover {
color: var(--color-link-active);
}
&:not(:last-of-type) {
&::after {
content: '»';
margin: 0 0.7em 0 0.45em;
}
}
}
&-time {
margin-left: auto;
}
&-button {
@apply --button;
padding: 5px 10px;
font-size: 13px;
margin-left: 10px;
}
}
&__title {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.04px;
margin-bottom: -0.2em;
}
}
.block-header {
margin: 1.5em 0 0.5em;
&--2 {
font-size: 22px;
font-weight: 500;
}
&--3 {
font-size: 18px;
font-weight: 500;
}
}

View file

@ -4,6 +4,7 @@
@import url('components/header.pcss'); @import url('components/header.pcss');
@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');
body { body {
font-family: system-ui, Helvetica, Arial, Verdana; font-family: system-ui, Helvetica, Arial, Verdana;

View file

@ -9,7 +9,7 @@
*/ */
--layout-padding-horisontal: 40px; --layout-padding-horisontal: 40px;
--layout-padding-vertical: 40px; --layout-padding-vertical: 40px;
--layout-width-aside: 250px; --layout-width-aside: 200px;
--layout-width-main-col: 650px; --layout-width-main-col: 650px;
--button { --button {
@ -20,6 +20,7 @@
padding: 9px 15px; padding: 9px 15px;
font-size: 14px; font-size: 14px;
line-height: 1em; line-height: 1em;
text-decoration: none;
svg { svg {
margin: 0 0.3em 0 -0.05em; margin: 0 0.3em 0 -0.05em;

View file

@ -1,4 +1,4 @@
const {pages} = require('../utils/database/index'); const {pages: db} = require('../utils/database/index');
/** /**
* @typedef {Object} PageData * @typedef {Object} PageData
@ -25,7 +25,7 @@ class Page {
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
static async get(_id) { static async get(_id) {
const data = await pages.findOne({_id}); const data = await db.findOne({_id});
return new Page(data); return new Page(data);
} }
@ -37,7 +37,7 @@ class Page {
* @returns {Promise<Page[]>} * @returns {Promise<Page[]>}
*/ */
static async getAll(query = {}) { static async getAll(query = {}) {
const docs = await pages.find(query); const docs = await db.find(query);
return Promise.all(docs.map(doc => new Page(doc))); return Promise.all(docs.map(doc => new Page(doc)));
} }
@ -52,8 +52,6 @@ class Page {
data = {}; data = {};
} }
this.db = pages;
if (data._id) { if (data._id) {
this._id = data._id; this._id = data._id;
} }
@ -113,7 +111,7 @@ class Page {
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
get parent() { get parent() {
return this.db.findOne({_id: this._parent}) return db.findOne({_id: this._parent})
.then(data => new Page(data)); .then(data => new Page(data));
} }
@ -123,7 +121,7 @@ class Page {
* @returns {Promise<Page[]>} * @returns {Promise<Page[]>}
*/ */
get children() { get children() {
return this.db.find({parent: this._id}) return db.find({parent: this._id})
.then(data => data.map(page => new Page(page))); .then(data => data.map(page => new Page(page)));
} }
@ -134,11 +132,11 @@ class Page {
*/ */
async save() { async save() {
if (!this._id) { if (!this._id) {
const insertedRow = await this.db.insert(this.data); const insertedRow = await db.insert(this.data);
this._id = insertedRow._id; this._id = insertedRow._id;
} else { } else {
await this.db.update({_id: this._id}, this.data); await db.update({_id: this._id}, this.data);
} }
return this; return this;
@ -150,7 +148,7 @@ class Page {
* @returns {Promise<Page>} * @returns {Promise<Page>}
*/ */
async destroy() { async destroy() {
await this.db.remove({_id: this._id}); await db.remove({_id: this._id});
delete this._id; delete this._id;

8
src/routes/api/index.js Normal file
View file

@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const pagesAPI = require('./pages');
router.use('/', pagesAPI);
module.exports = router;

115
src/routes/api/pages.js Normal file
View file

@ -0,0 +1,115 @@
const express = require('express');
const router = express.Router();
const multer = require('multer')();
const Pages = require('../../controllers/pages');
/**
* GET /page/:id
*
* Return PageData of page with given id
*/
router.get('/page/:id', async (req, res) => {
try {
const page = await Pages.get(req.params.id);
res.json({
success: true,
result: page.data
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
/**
* GET /pages
*
* Return PageData for all pages
*/
router.get('/pages', async (req, res) => {
try {
const pages = await Pages.getAll();
res.json({
success: true,
result: pages
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
/**
* PUT /page
*
* Create new page in the database
*/
router.put('/page', multer.any(), async (req, res) => {
try {
const {title, body, parent} = req.body;
const page = await Pages.insert({title, body, parent});
res.json({
success: true,
result: page
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
/**
* POST /page/:id
*
* Update page data in the database
*/
router.post('/page/:id', multer.any(), async (req, res) => {
const {id} = req.params;
try {
const {title, body, parent} = req.body;
const page = await Pages.update(id, {title, body, parent});
res.json({
success: true,
result: page
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
/**
* DELETE /page/:id
*
* Remove page from the database
*/
router.delete('/page/:id', async (req, res) => {
try {
const page = await Pages.remove(req.params.id);
res.json({
success: true,
result: page
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
module.exports = router;

View file

@ -3,8 +3,10 @@ const router = express.Router();
const home = require('./home'); const home = require('./home');
const pages = require('./pages'); const pages = require('./pages');
const api = require('./api');
router.use('/', home); router.use('/', home);
router.use('/', pages); router.use('/', pages);
router.use('/api', api);
module.exports = router; module.exports = router;

View file

@ -1,6 +1,5 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const multer = require('multer')();
const Pages = require('../controllers/pages'); const Pages = require('../controllers/pages');
/** /**
@ -10,116 +9,46 @@ router.get('/page/new', async (req, res) => {
let pagesAvailable = await Pages.getAll(); let pagesAvailable = await Pages.getAll();
res.render('pages/form', { res.render('pages/form', {
pagesAvailable pagesAvailable,
page: null
}); });
}); });
/** /**
* GET /page/:id * Edit page form
*
* Return PageData of page with given id
*/ */
router.get('/page/:id', async (req, res) => { router.get('/page/edit/:id', async (req, res, next) => {
try { const pageId = req.params.id;
const page = await Pages.get(req.params.id);
res.json({ try {
success: true, let page = await Pages.get(pageId);
result: page.data let pagesAvailable = await Pages.getAll();
});
} catch (err) { res.render('pages/form', {
res.status(400).json({ pagesAvailable,
success: false, page
error: err.message
}); });
} catch (error) {
res.status(404);
next(error);
} }
}); });
/** /**
* GET /pages * View page
*
* Return PageData for all pages
*/ */
router.get('/pages', async (req, res) => { router.get('/page/:id', async (req, res, next) => {
try { const pageId = req.params.id;
const pages = await Pages.getAll();
res.json({
success: true,
result: pages
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
/**
* PUT /page
*
* Create new page in the database
*/
router.put('/page', multer.any(), async (req, res) => {
try {
const {title, body, parent} = req.body;
const page = await Pages.insert({title, body, parent});
res.json({
success: true,
result: page
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
/**
* POST /page/:id
*
* Update page data in the database
*/
router.post('/page/:id', multer.any(), async (req, res) => {
const {id} = req.params;
try { try {
const {title, body, parent} = req.body; let page = await Pages.get(pageId);
const page = await Pages.update(id, {title, body, parent});
res.json({ res.render('pages/page', {
success: true, page
result: page
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
/**
* DELETE /page/:id
*
* Remove page from the database
*/
router.delete('/page/:id', async (req, res) => {
try {
const page = await Pages.remove(req.params.id);
res.json({
success: true,
result: page
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
}); });
} catch (error) {
res.status(404);
next(error);
} }
}); });

View file

@ -0,0 +1,3 @@
<h{{ level }} class="block-header block-header--{{ level }}">
{{ text }}
</h{{ level }}>

View file

@ -0,0 +1,3 @@
<p class="block-paragraph">
{{ text }}
</p>

View file

@ -7,15 +7,28 @@
} }
</style> </style>
<section data-module="writing"> <section data-module="writing">
<module-settings hidden>
{
"page": {{ page | json_encode }}
}
</module-settings>
<header class="writing-header"> <header class="writing-header">
<span class="writing-header__left"> <span class="writing-header__left">
New Page at the New Page at the
<select name="parent"> {% set currentPageId = 0 %}
<option value="0">Root</option> {% if page is not empty %}
{% for page in pagesAvailable %} {% set currentPageId = page._id %}
<option value="{{ page._id }}">{{ page.title }}</option> {% endif %}
{% endfor %} <select name="parent">
</select> <option value="0">Root</option>
{% for _page in pagesAvailable %}
{% if _page._id != currentPageId %}
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
{{ _page.title }}
</option>
{% endif %}
{% endfor %}
</select>
</span> </span>
<span class="writing-header__save" name="js-submit"> <span class="writing-header__save" name="js-submit">
Save Save

38
src/views/pages/page.twig Normal file
View file

@ -0,0 +1,38 @@
{% extends 'layout.twig' %}
{% block body %}
<article class="page">
<header class="page__header">
<a href="/" class="page__header-nav">
Documentation
</a>
{% if page._parent %}
<a href="/page/{{ page._parent }}" class="page__header-nav">
Parent {{ page._parent }}
</a>
{% endif %}
<time class="page__header-time">
Last edit {{ (page.body.time / 1000) | date("M d Y") }}
<a href="/page/edit/{{ page._id }}" class="page__header-button">
Edit
</a>
</time>
</header>
<h1 class="page__title">
{{ page.title }}
</h1>
<section class="page__content">
{% for block in page.body.blocks %}
{# Skip first header, because it is already showed as a Title #}
{% if not (loop.first and block.type == 'header') %}
{% if block.type in ['paragraph', 'header'] %}
{% include './blocks/' ~ block.type ~ '.twig' with block.data %}
{% endif %}
{% endif %}
{% endfor %}
</section>
<footer>
</footer>
</article>
{% endblock %}

View file

@ -38,7 +38,7 @@ describe('Pages REST: ', () => {
}; };
const res = await agent const res = await agent
.put('/page') .put('/api/page')
.send({body}); .send({body});
expect(res).to.have.status(200); expect(res).to.have.status(200);
@ -63,7 +63,7 @@ describe('Pages REST: ', () => {
it('Page data validation on create', async () => { it('Page data validation on create', async () => {
const res = await agent const res = await agent
.put('/page') .put('/api/page')
.send({someField: 'Some text'}); .send({someField: 'Some text'});
expect(res).to.have.status(400); expect(res).to.have.status(400);
@ -88,7 +88,7 @@ describe('Pages REST: ', () => {
}; };
const put = await agent const put = await agent
.put('/page') .put('/api/page')
.send({body}); .send({body});
expect(put).to.have.status(200); expect(put).to.have.status(200);
@ -96,7 +96,7 @@ describe('Pages REST: ', () => {
const {result: {_id}} = put.body; const {result: {_id}} = put.body;
const get = await agent.get(`/page/${_id}`); const get = await agent.get(`/api/page/${_id}`);
expect(get).to.have.status(200); expect(get).to.have.status(200);
expect(get).to.be.json; expect(get).to.be.json;
@ -115,7 +115,7 @@ describe('Pages REST: ', () => {
}); });
it('Finding page with not existing id', async () => { it('Finding page with not existing id', async () => {
const res = await agent.get('/page/not-existing-id'); const res = await agent.get('/api/page/not-existing-id');
expect(res).to.have.status(400); expect(res).to.have.status(400);
expect(res).to.be.json; expect(res).to.be.json;
@ -139,7 +139,7 @@ describe('Pages REST: ', () => {
}; };
let res = await agent let res = await agent
.put('/page') .put('/api/page')
.send({body}); .send({body});
expect(res).to.have.status(200); expect(res).to.have.status(200);
@ -159,7 +159,7 @@ describe('Pages REST: ', () => {
}; };
res = await agent res = await agent
.post(`/page/${_id}`) .post(`/api/page/${_id}`)
.send({body: updatedBody}); .send({body: updatedBody});
expect(res).to.have.status(200); expect(res).to.have.status(200);
@ -187,7 +187,7 @@ describe('Pages REST: ', () => {
it('Updating page with not existing id', async () => { it('Updating page with not existing id', async () => {
const res = await agent const res = await agent
.post('/page/not-existing-id') .post('/api/page/not-existing-id')
.send({body: { .send({body: {
blocks: [ blocks: [
{ {
@ -221,7 +221,7 @@ describe('Pages REST: ', () => {
}; };
let res = await agent let res = await agent
.put('/page') .put('/api/page')
.send({body}); .send({body});
expect(res).to.have.status(200); expect(res).to.have.status(200);
@ -230,7 +230,7 @@ describe('Pages REST: ', () => {
const {result: {_id}} = res.body; const {result: {_id}} = res.body;
res = await agent res = await agent
.delete(`/page/${_id}`); .delete(`/api/page/${_id}`);
expect(res).to.have.status(200); expect(res).to.have.status(200);
expect(res).to.be.json; expect(res).to.be.json;
@ -249,7 +249,7 @@ describe('Pages REST: ', () => {
it('Removing page with not existing id', async () => { it('Removing page with not existing id', async () => {
const res = await agent const res = await agent
.delete('/page/not-existing-id'); .delete('/api/page/not-existing-id');
expect(res).to.have.status(400); expect(res).to.have.status(400);
expect(res).to.be.json; expect(res).to.be.json;