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

Added page navigation (#209)

* Added navigation on page

* Removed useless log, added docs to navigator component

* Fixed duplicated variables, some changes in navigation functions, changed pages.twig and navigator.twig

* Added flatArray model, changed navigation functions

* Replaced page footer style to page.pcss

* Fixed generating flat array, when pages remove

* Removed useless generating

* Renamed flatArray model to pagesFlatArray, updated descriptions, renamed generate to regenerate, removed hardcoded key name in cache

* Changed styles naming and added margin for navigation

* Added ability to change nesting in flat array, fixed BEM

* Updated nesting parameter, fixed BEM

* Changed navigator component by passing objects, removed navigator wrapper

* Style navigator renamed to navigator__item

* Update src/backend/models/pagesFlatArray.ts

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>

* Renamed navigator__item to navigator_item

* Deleted wrappers from navigator buttons, removed page__footer

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
slaveeks 2022-08-02 17:38:02 +03:00 committed by GitHub
parent 213f9d89a3
commit 70f89f28da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 352 additions and 18 deletions

View file

@ -33,6 +33,7 @@
"morgan": "^1.10.0",
"multer": "^1.4.2",
"nedb": "^1.8.0",
"node-cache": "^5.1.2",
"node-fetch": "^2.6.1",
"open-graph-scraper": "^4.9.0",
"twig": "^1.15.4",

View file

@ -2,7 +2,8 @@ import Page, { PageData } from '../models/page';
import Alias from '../models/alias';
import PagesOrder from './pagesOrder';
import PageOrder from '../models/pageOrder';
import HttpException from "../exceptions/httpException";
import HttpException from '../exceptions/httpException';
import PagesFlatArray from '../models/pagesFlatArray';
type PageDataFields = keyof PageData;
@ -195,6 +196,7 @@ class Pages {
pagesAvailable[index] = null;
pagesAvailable = Pages.removeChildren(pagesAvailable, item._id);
});
PagesFlatArray.regenerate();
return pagesAvailable;
}
@ -221,6 +223,7 @@ class Pages {
alias.save();
}
await PagesFlatArray.regenerate();
return insertedPage;
} catch (e) {
@ -264,6 +267,7 @@ class Pages {
Alias.markAsDeprecated(previousUri);
}
}
await PagesFlatArray.regenerate();
return updatedPage;
}
@ -286,8 +290,10 @@ class Pages {
await alias.destroy();
}
const removedPage = page.destroy();
await PagesFlatArray.regenerate();
return page.destroy();
return removedPage;
}
/**

View file

@ -1,5 +1,6 @@
import PageOrder from '../models/pageOrder';
import Page from '../models/page';
import PagesFlatArray from '../models/pagesFlatArray';
/**
* @class PagesOrder
@ -62,6 +63,7 @@ class PagesOrder {
order.push(childId);
await order.save();
await PagesFlatArray.regenerate();
}
/**
@ -76,11 +78,13 @@ class PagesOrder {
oldParentOrder.remove(targetPageId);
await oldParentOrder.save();
await PagesFlatArray.regenerate();
const newParentOrder = await PageOrder.get(newParentId);
newParentOrder.push(targetPageId);
await newParentOrder.save();
await PagesFlatArray.regenerate();
}
/**
@ -125,6 +129,7 @@ class PagesOrder {
pageOrder.order = Array.from(new Set([...pageOrder.order, ...unordered]));
pageOrder.putAbove(currentPageId, putAbovePageId);
await pageOrder.save();
await PagesFlatArray.regenerate();
}
/**
@ -138,7 +143,8 @@ class PagesOrder {
throw new Error('Page with given id does not contain order');
}
return order.destroy();
await order.destroy();
await PagesFlatArray.regenerate();
}
}

View file

@ -182,7 +182,7 @@ class PageOrder {
*
* @param {string} pageId - identity of page
*/
public getPageBefore(pageId: string): string | null {
public getSubPageBefore(pageId: string): string | null {
if (this.order === undefined) {
return null;
}
@ -204,7 +204,7 @@ class PageOrder {
*
* @param pageId - identity of page
*/
public getPageAfter(pageId: string): string | null {
public getSubPageAfter(pageId: string): string | null {
if (this.order === undefined) {
return null;
}

View file

@ -0,0 +1,182 @@
import Page from './page';
import PageOrder from './pageOrder';
import NodeCache from 'node-cache';
// Create cache for flat array
const cache = new NodeCache({ stdTTL: 120 });
const cacheKey = 'pagesFlatArray';
/**
* Element for pagesFlatArray
*/
export interface PagesFlatArrayData {
/**
* Page id
*/
id: string;
/**
* Page parent id
*/
parentId?: string;
/**
* id of parent with parent id '0'
*/
rootId: string;
/**
* Page level in sidebar view
*/
level: number;
/**
* Page title
*/
title: string;
/**
* Page uri
*/
uri?: string;
}
/**
* @class PagesFlatArray model - flat array of pages, which are ordered like in sidebar
*/
class PagesFlatArray {
/**
* Returns pages flat array
*
* @param nestingLimit - number of flat array nesting, set null to dismiss the restriction, default nesting 2
* @returns {Promise<Array<PagesFlatArrayData>>}
*/
public static async get(nestingLimit: number | null = 2): Promise<Array<PagesFlatArrayData>> {
// Get flat array from cache
let arr = cache.get(cacheKey) as Array<PagesFlatArrayData>;
// Check is flat array consists in cache
if (!arr) {
arr = await this.regenerate();
}
if (!nestingLimit) {
return arr;
}
return arr.filter( (item) => item.level < nestingLimit );
}
/**
* Generates new flat array, saves it to cache, returns it
* Calls, when there is no pages flat array data in cache or when page or pageOrder data updates
*
* @returns {Promise<Array<PagesFlatArrayData>>}
*/
public static async regenerate(): Promise<Array<PagesFlatArrayData>> {
const pages = await Page.getAll();
const pagesOrders = await PageOrder.getAll();
let arr = new Array<PagesFlatArrayData>();
// Get root order
const rootOrder = pagesOrders.find( order => order.page == '0' );
// Check is root order is not empty
if (!rootOrder) {
return [];
}
for (const pageId of rootOrder.order) {
arr = arr.concat(this.getChildrenFlatArray(pageId, 0, pages,
pagesOrders));
}
// Save generated flat array to cache
cache.set(cacheKey, arr);
return arr;
}
/**
* Returns previous page
*
* @param pageId - page id
* @returns {Promise<PagesFlatArrayData | undefined>}
*/
public static async getPageBefore(pageId: string): Promise<PagesFlatArrayData | undefined> {
const arr = await this.get();
const pageIndex = arr.findIndex( (item) => item.id == pageId);
// Check if index is not the first
if (pageIndex && pageIndex > 0) {
// Return previous element from array
return arr[pageIndex - 1];
} else {
return;
}
}
/**
* Returns next page
*
* @param pageId - page id
* @returns {Promise<PagesFlatArrayData | undefined>}
*/
public static async getPageAfter(pageId: string): Promise<PagesFlatArrayData | undefined> {
const arr = await this.get();
const pageIndex = arr.findIndex( (item) => item.id == pageId );
// Check if index is not the last
if (pageIndex < arr.length -1) {
// Return next element from array
return arr[pageIndex + 1];
} else {
return;
}
}
/**
* Returns child pages array
*
* @param pageId - parent page id
* @param level - page level in sidebar
* @param pages - all pages
* @param orders - all page orders
* @returns {Promise<Array<PagesFlatArrayData>>}
*/
private static getChildrenFlatArray(pageId: string, level: number,
pages: Array<Page>, orders: Array<PageOrder>): Array<PagesFlatArrayData> {
let arr: Array<PagesFlatArrayData> = new Array<PagesFlatArrayData>();
const page = pages.find( item => item._id == pageId );
// Add element to child array
if (page) {
arr.push( {
id: page._id!,
level: level,
parentId: page._parent,
rootId: '0',
title: page.title!,
uri: page.uri,
} );
}
const order = orders.find(item => item.page == pageId);
if (order) {
for (const childPageId of order.order) {
arr = arr.concat(this.getChildrenFlatArray(childPageId, level + 1,
pages, orders));
}
}
return arr;
}
}
export default PagesFlatArray;

View file

@ -3,6 +3,7 @@ import Aliases from '../controllers/aliases';
import Pages from '../controllers/pages';
import Alias from '../models/alias';
import verifyToken from './middlewares/token';
import PagesFlatArray from '../models/pagesFlatArray';
const router = express.Router();
@ -32,9 +33,14 @@ router.get('*', verifyToken, async (req: Request, res: Response) => {
const pageParent = await page.getParent();
const previousPage = await PagesFlatArray.getPageBefore(alias.id);
const nextPage = await PagesFlatArray.getPageAfter(alias.id);
res.render('pages/page', {
page,
pageParent,
previousPage,
nextPage,
config: req.app.locals.config,
});
}

View file

@ -158,8 +158,8 @@ router.delete('/page/:id', async (req: Request, res: Response) => {
}
const parentPageOrder = await PagesOrder.get(page._parent);
const pageBeforeId = parentPageOrder.getPageBefore(page._id);
const pageAfterId = parentPageOrder.getPageAfter(page._id);
const pageBeforeId = parentPageOrder.getSubPageBefore(page._id);
const pageAfterId = parentPageOrder.getSubPageAfter(page._id);
let pageToRedirect;

View file

@ -3,6 +3,7 @@ import Pages from '../controllers/pages';
import PagesOrder from '../controllers/pagesOrder';
import verifyToken from './middlewares/token';
import allowEdit from './middlewares/locals';
import PagesFlatArray from '../models/pagesFlatArray';
const router = express.Router();
@ -62,10 +63,15 @@ router.get('/page/:id', verifyToken, async (req: Request, res: Response, next: N
const pageParent = await page.parent;
const previousPage = await PagesFlatArray.getPageBefore(pageId);
const nextPage = await PagesFlatArray.getPageAfter(pageId);
res.render('pages/page', {
page,
pageParent,
config: req.app.locals.config,
previousPage,
nextPage,
});
} catch (error) {
res.status(404);

View file

@ -0,0 +1,50 @@
{#
Reusable navigaor component.
Available props:
- previousPage
- nextPage
- class: additional class for the navigator
Usage example:
{% include 'components/navigator.twig' with {previousPage: previousPage, nextPage: nextPage} %}
#}
{% set mainClass = 'navigator__item' %}
{% set tag = 'div' %}
{% set tag = 'a' %}
<div class="navigator">
{% if previousPage %}
<{{ tag }}
{{ name is not empty ? 'name="' ~ name ~ '"': '' }}
class="{{ mainClass }} {{ mainClass }}--previous {{ class ?? '' }}"
href="/{{ previousPage.uri }}"
>
<div class="{{ mainClass }}-direction">
previous
</div>
<div class="{{ mainClass }}-label">
{{ previousPage.title }}
</div>
</{{ tag }}>
{% endif %}
{% if nextPage %}
<{{ tag }}
{{ name is not empty ? 'name="' ~ name ~ '"': '' }}
class="{{ mainClass }} {{ mainClass }}--next {{ class ?? '' }}"
href="/{{ nextPage.uri }}"
>
<div class="{{ mainClass }}-direction">
next
</div>
<div class="{{ mainClass }}-label">
{{ nextPage.title }}
</div>
</{{ tag }}>
{% endif %}
</div>

View file

@ -39,8 +39,7 @@
{% endif %}
{% endfor %}
</section>
<footer>
</footer>
{% include 'components/navigator.twig' with {previousPage: previousPage, nextPage: nextPage} %}
</article>
{% endblock %}

View file

@ -0,0 +1,42 @@
.navigator {
display: flex;
}
.navigator__item {
margin-top: 35px;
cursor: pointer;
-webkit-user-select: none;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: var(--color-link-hover);
border-radius: 10px;
padding: 12px 16px 12px 16px;
color: black;
width: max-content;
font-weight: 500;
font-size: 14px;
&--previous {
align-items: flex-start;
margin-left: 0;
}
&--next {
align-items: flex-end;
margin-left: auto;
}
&-direction {
text-transform: capitalize;
color: var(--color-direction-navigation);
font-size: 12px;
font-weight: 400;
}
&-label {
width: 100%;
}
}

View file

@ -496,3 +496,4 @@
}
}
}

View file

@ -10,6 +10,7 @@
@import './components/auth.pcss';
@import './components/button.pcss';
@import './components/sidebar.pcss';
@import './components/navigator.pcss';
@import './components/table-of-content.pcss';
body {

View file

@ -1,6 +1,7 @@
:root {
--color-text-main: #313649;
--color-text-second: #5d6068;
--color-direction-navigation: #717682;
--color-line-gray: #E8E8EB;
--color-link-active: #2071cc;
--color-link-hover: #F3F6F8;

View file

@ -10,7 +10,8 @@
"@babel/code-frame@7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz"
integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
dependencies:
"@babel/highlight" "^7.10.4"
@ -2016,6 +2017,11 @@ clone-response@^1.0.2:
dependencies:
mimic-response "^1.0.0"
clone@2.x:
version "2.1.2"
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz"
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
@ -3240,6 +3246,18 @@ growl@1.10.5:
version "1.10.5"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
handlebars@^4.1.0:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
source-map "^0.6.1"
wordwrap "^1.0.0"
optionalDependencies:
uglify-js "^3.1.4"
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
@ -3286,10 +3304,6 @@ hosted-git-info@^2.1.4:
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
htmlparser2@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
@ -3905,7 +3919,7 @@ make-dir@^1.3.0:
dependencies:
pify "^3.0.0"
make-dir@^2.0.0, make-dir@^2.1.0:
make-dir@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
dependencies:
@ -4132,9 +4146,10 @@ negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
neo-async@^2.6.2:
neo-async@^2.6.0, neo-async@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
nice-try@^1.0.4:
version "1.0.5"
@ -4150,6 +4165,13 @@ nise@^5.1.0:
just-extend "^4.0.2"
path-to-regexp "^1.7.0"
node-cache@^5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz"
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
dependencies:
clone "2.x"
node-fetch@^2.6.1:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
@ -5574,9 +5596,10 @@ supports-color@^5.3.0, supports-color@^5.4.0:
dependencies:
has-flag "^3.0.0"
supports-color@^6.1.0:
supports-color@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
dependencies:
has-flag "^3.0.0"
@ -5805,6 +5828,11 @@ typescript@^4.3.5:
version "4.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c"
uglify-js@^3.1.4:
version "3.16.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.3.tgz#94c7a63337ee31227a18d03b8a3041c210fd1f1d"
integrity sha512-uVbFqx9vvLhQg0iBaau9Z75AxWJ8tqM9AV890dIZCLApF4rTcyHwmAvLeEdYRs+BzYWu8Iw81F79ah0EfTXbaw==
uid-safe@2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
@ -6015,6 +6043,11 @@ word-wrap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"