mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-08-09 15:35:25 +02:00
Merge branch 'main' into feature/table-of-content
This commit is contained in:
commit
a23c243c56
15 changed files with 242 additions and 15 deletions
|
@ -1,5 +1,8 @@
|
||||||
import Page, { PageData } from '../models/page';
|
import Page, { PageData } from '../models/page';
|
||||||
import Alias from '../models/alias';
|
import Alias from '../models/alias';
|
||||||
|
import PagesOrder from './pagesOrder';
|
||||||
|
import PageOrder from '../models/pageOrder';
|
||||||
|
import HttpException from "../exceptions/httpException";
|
||||||
|
|
||||||
type PageDataFields = keyof PageData;
|
type PageDataFields = keyof PageData;
|
||||||
|
|
||||||
|
@ -62,6 +65,121 @@ class Pages {
|
||||||
return nullFilteredPages;
|
return nullFilteredPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group all pages by their parents
|
||||||
|
* If the pageId is passed, it excludes passed page from result pages
|
||||||
|
*
|
||||||
|
* @param {string} pageId - pageId to exclude from result pages
|
||||||
|
* @returns {Page[]}
|
||||||
|
*/
|
||||||
|
public static async groupByParent(pageId = ''): Promise<Page[]> {
|
||||||
|
const result: Page[] = [];
|
||||||
|
const orderGroupedByParent: Record<string, string[]> = {};
|
||||||
|
const rootPageOrder = await PagesOrder.getRootPageOrder();
|
||||||
|
const childPageOrder = await PagesOrder.getChildPageOrder();
|
||||||
|
const orphanPageOrder: PageOrder[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is no root and child page order, then it returns an empty array
|
||||||
|
*/
|
||||||
|
if (!rootPageOrder || (!rootPageOrder && childPageOrder.length <= 0)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = (await this.getAll()).reduce((map, _page) => {
|
||||||
|
map.set(_page._id, _page);
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, new Map);
|
||||||
|
const idsOfRootPages = rootPageOrder.order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It groups root pages and 1 level pages by its parent
|
||||||
|
*/
|
||||||
|
idsOfRootPages.reduce((prev, curr, idx) => {
|
||||||
|
const childPages:PageOrder[] = [];
|
||||||
|
|
||||||
|
childPageOrder.forEach((pageOrder, _idx) => {
|
||||||
|
if (pageOrder.page === curr) {
|
||||||
|
childPages.push(pageOrder);
|
||||||
|
childPageOrder.splice(_idx, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasChildPage = childPages.length > 0;
|
||||||
|
|
||||||
|
prev[curr] = [];
|
||||||
|
prev[curr].push(curr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It attaches 1 level page id to its parent page id
|
||||||
|
*/
|
||||||
|
if (hasChildPage) {
|
||||||
|
prev[curr].push(...childPages[0].order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If non-attached childPages which is not 1 level page still remains,
|
||||||
|
* It is stored as an orphan page so that it can be processed in the next statements
|
||||||
|
*/
|
||||||
|
if (idx === idsOfRootPages.length - 1 && childPageOrder.length > 0) {
|
||||||
|
orphanPageOrder.push(...childPageOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}, orderGroupedByParent);
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It groups remained ungrouped pages by its parent
|
||||||
|
*/
|
||||||
|
while (orphanPageOrder.length > 0) {
|
||||||
|
if (count >= 1000) {
|
||||||
|
throw new HttpException(500, `Page cannot be processed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
orphanPageOrder.forEach((orphanOrder, idx) => {
|
||||||
|
// It loops each of grouped orders formatted as [root page id(1): corresponding child pages id(2)]
|
||||||
|
Object.entries(orderGroupedByParent).forEach(([parentPageId, value]) => {
|
||||||
|
// If (2) contains orphanOrder's parent id(page)
|
||||||
|
if (orphanOrder.page && orphanOrder.order && value.includes(orphanOrder.page)) {
|
||||||
|
// Append orphanOrder's id(order) into its parent id
|
||||||
|
orderGroupedByParent[parentPageId].splice(value.indexOf(orphanOrder.page) + 1, 0, ...orphanOrder.order);
|
||||||
|
// Finally, remove orphanOrder from orphanPageOrder
|
||||||
|
orphanPageOrder.splice(idx, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It converts grouped pages(object) to array
|
||||||
|
*/
|
||||||
|
Object.values(orderGroupedByParent).flatMap(arr => [ ...arr ])
|
||||||
|
.forEach(arr => {
|
||||||
|
result.push(pages.get(arr));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the pageId passed, it excludes itself from result pages
|
||||||
|
* Otherwise just returns result itself
|
||||||
|
*/
|
||||||
|
if (pageId) {
|
||||||
|
return this.removeChildren(result, pageId).reduce((prev, curr) => {
|
||||||
|
if (curr instanceof Page) {
|
||||||
|
prev.push(curr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}, Array<Page>());
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set all children elements to null
|
* Set all children elements to null
|
||||||
*
|
*
|
||||||
|
@ -69,7 +187,7 @@ class Pages {
|
||||||
* @param {string} parent - id of parent page
|
* @param {string} parent - id of parent page
|
||||||
* @returns {Array<?Page>}
|
* @returns {Array<?Page>}
|
||||||
*/
|
*/
|
||||||
public static removeChildren(pagesAvailable: Array<Page|null>, parent: string | undefined): Array<Page | null> {
|
public static removeChildren(pagesAvailable: Array<Page | null>, parent: string | undefined): Array<Page | null> {
|
||||||
pagesAvailable.forEach(async (item, index) => {
|
pagesAvailable.forEach(async (item, index) => {
|
||||||
if (item === null || item._parent !== parent) {
|
if (item === null || item._parent !== parent) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -33,6 +33,24 @@ class PagesOrder {
|
||||||
return PageOrder.getAll();
|
return PageOrder.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only root page's order
|
||||||
|
*
|
||||||
|
* @returns {Promise<PageOrder[]>}
|
||||||
|
*/
|
||||||
|
public static async getRootPageOrder(): Promise<PageOrder> {
|
||||||
|
return PageOrder.getRootPageOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only child page's order
|
||||||
|
*
|
||||||
|
* @returns {Promise<PageOrder[]>}
|
||||||
|
*/
|
||||||
|
public static async getChildPageOrder(): Promise<PageOrder[]> {
|
||||||
|
return PageOrder.getChildPageOrder();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pushes the child page to the parent's order list
|
* Pushes the child page to the parent's order list
|
||||||
*
|
*
|
||||||
|
|
|
@ -75,6 +75,28 @@ class PageOrder {
|
||||||
return Promise.all(docs.map(doc => new PageOrder(doc)));
|
return Promise.all(docs.map(doc => new PageOrder(doc)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only root page's order
|
||||||
|
*
|
||||||
|
* @returns {Promise<PageOrder[]>}
|
||||||
|
*/
|
||||||
|
public static async getRootPageOrder(): Promise<PageOrder> {
|
||||||
|
const docs = await db.findOne({ 'page': '0' });
|
||||||
|
|
||||||
|
return new PageOrder(docs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only child page's order
|
||||||
|
*
|
||||||
|
* @returns {Promise<PageOrder[]>}
|
||||||
|
*/
|
||||||
|
public static async getChildPageOrder(): Promise<PageOrder[]> {
|
||||||
|
const docs = await this.getAll({ 'page': { $ne: '0' } });
|
||||||
|
|
||||||
|
return Promise.all(docs.map(doc => new PageOrder(doc)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* constructor data setter
|
* constructor data setter
|
||||||
*
|
*
|
||||||
|
|
|
@ -11,10 +11,10 @@ const router = express.Router();
|
||||||
*/
|
*/
|
||||||
router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => {
|
router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const pagesAvailable = await Pages.getAll();
|
const pagesAvailableGrouped = await Pages.groupByParent();
|
||||||
|
|
||||||
res.render('pages/form', {
|
res.render('pages/form', {
|
||||||
pagesAvailable,
|
pagesAvailableGrouped,
|
||||||
page: null,
|
page: null,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -32,6 +32,7 @@ router.get('/page/edit/:id', verifyToken, allowEdit, async (req: Request, res: R
|
||||||
try {
|
try {
|
||||||
const page = await Pages.get(pageId);
|
const page = await Pages.get(pageId);
|
||||||
const pagesAvailable = await Pages.getAllExceptChildren(pageId);
|
const pagesAvailable = await Pages.getAllExceptChildren(pageId);
|
||||||
|
const pagesAvailableGrouped = await Pages.groupByParent(pageId);
|
||||||
|
|
||||||
if (!page._parent) {
|
if (!page._parent) {
|
||||||
throw new Error('Parent not found');
|
throw new Error('Parent not found');
|
||||||
|
@ -42,7 +43,7 @@ router.get('/page/edit/:id', verifyToken, allowEdit, async (req: Request, res: R
|
||||||
res.render('pages/form', {
|
res.render('pages/form', {
|
||||||
page,
|
page,
|
||||||
parentsChildrenOrdered,
|
parentsChildrenOrdered,
|
||||||
pagesAvailable,
|
pagesAvailableGrouped,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(404);
|
res.status(404);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{{ svg('menu') }} Table of contents
|
{{ svg('menu') }} Table of contents
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="docs-sidebar__content docs-sidebar__content--hidden">
|
<aside class="docs-sidebar__content docs-sidebar__content--invisible">
|
||||||
{% for firstLevelPage in menu %}
|
{% for firstLevelPage in menu %}
|
||||||
<section class="docs-sidebar__section" data-id="{{firstLevelPage._id}}">
|
<section class="docs-sidebar__section" data-id="{{firstLevelPage._id}}">
|
||||||
<a class="docs-sidebar__section-title-wrapper"
|
<a class="docs-sidebar__section-title-wrapper"
|
||||||
|
|
|
@ -21,9 +21,14 @@
|
||||||
{% include "components/sidebar.twig" %}
|
{% include "components/sidebar.twig" %}
|
||||||
|
|
||||||
<div class="docs__content">
|
<div class="docs__content">
|
||||||
|
|
||||||
<div class="docs__content-inner">
|
<div class="docs__content-inner">
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<aside class="docs__aside-right">
|
||||||
|
<div style="width: 100%; height: 100px; background: grey"/>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include "components/sidebar-right.twig" %}
|
{% include "components/sidebar-right.twig" %}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{% 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 pagesAvailableGrouped %}
|
||||||
{% 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' : ''}}>
|
||||||
{% if _page._parent != "0" %}
|
{% if _page._parent != "0" %}
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default class Sidebar {
|
||||||
sidebarToggler: 'docs-sidebar__toggler',
|
sidebarToggler: 'docs-sidebar__toggler',
|
||||||
sidebarContent: 'docs-sidebar__content',
|
sidebarContent: 'docs-sidebar__content',
|
||||||
sidebarContentHidden: 'docs-sidebar__content--hidden',
|
sidebarContentHidden: 'docs-sidebar__content--hidden',
|
||||||
|
sidebarContentInvisible: 'docs-sidebar__content--invisible',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,6 +174,6 @@ export default class Sidebar {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
ready() {
|
ready() {
|
||||||
this.nodes.sidebarContent.classList.remove(Sidebar.CSS.sidebarContentHidden);
|
this.nodes.sidebarContent.classList.remove(Sidebar.CSS.sidebarContentInvisible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition-property: background-color;
|
transition-property: background-color;
|
||||||
transition-duration: 0.1s;
|
transition-duration: 0.1s;
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
@apply --squircle;
|
@apply --squircle;
|
||||||
|
|
||||||
|
|
|
@ -35,9 +35,10 @@
|
||||||
&__menu-link {
|
&__menu-link {
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 8px;
|
|
||||||
transition: background-color .13s;
|
transition: background-color .13s;
|
||||||
|
|
||||||
|
@apply --squircle;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-link-hover);
|
background-color: var(--color-link-hover);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,10 @@
|
||||||
&--hidden {
|
&--hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,7 +62,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__section:not(:first-child) {
|
&__section:not(:first-child) {
|
||||||
margin-top: 20px;
|
margin-top: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__section-title {
|
&__section-title {
|
||||||
|
|
|
@ -53,10 +53,6 @@
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
letter-spacing: 0.005em;
|
letter-spacing: 0.005em;
|
||||||
|
|
||||||
@media (--desktop) {
|
|
||||||
margin: 0 -100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ce-code__textarea {
|
.ce-code__textarea {
|
||||||
color: #41314e;
|
color: #41314e;
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
|
|
|
@ -18,6 +18,11 @@
|
||||||
&-inner {
|
&-inner {
|
||||||
max-width: var(--layout-width-main-col);
|
max-width: var(--layout-width-main-col);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (--desktop) {
|
||||||
|
margin-right: var(--layout-padding-horizontal);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (--desktop) {
|
@media (--desktop) {
|
||||||
|
@ -26,10 +31,34 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
|
--max-space-between-cols: 160px;
|
||||||
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
|
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: calc(var(--layout-width-main-col) + var(--max-space-between-cols) + var(--layout-sidebar-width));
|
||||||
|
margin-left: max(0px, calc(50vw - var(--layout-sidebar-width) - var(--layout-width-main-col) / 2));
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
@media (--mobile) {
|
@media (--mobile) {
|
||||||
padding: 20px var(--layout-padding-horizontal);
|
padding: 20px var(--layout-padding-horizontal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__aside-right {
|
||||||
|
width: var(--layout-sidebar-width);
|
||||||
|
min-width: 160px;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--layout-height-header) + var(--layout-padding-vertical));
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
|
@media (--desktop) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-sidebar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
*/
|
*/
|
||||||
--layout-padding-horizontal: 22px;
|
--layout-padding-horizontal: 22px;
|
||||||
--layout-padding-vertical: 30px;
|
--layout-padding-vertical: 30px;
|
||||||
--layout-sidebar-width: 344px;
|
--layout-sidebar-width: 290px;
|
||||||
--layout-width-main-col: 650px;
|
--layout-width-main-col: 650px;
|
||||||
--layout-height-header: 56px;
|
--layout-height-header: 56px;
|
||||||
|
|
||||||
|
@ -35,6 +35,10 @@
|
||||||
--layout-height-header: 88px;
|
--layout-height-header: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (--wide-desktop) {
|
||||||
|
--layout-sidebar-width: 344px;
|
||||||
|
}
|
||||||
|
|
||||||
--font-mono: Menlo,Monaco,Consolas,Courier New,monospace;
|
--font-mono: Menlo,Monaco,Consolas,Courier New,monospace;
|
||||||
|
|
||||||
--font-serif {
|
--font-serif {
|
||||||
|
@ -74,6 +78,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
--squircle {
|
--squircle {
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
@supports(-webkit-mask-box-image: url('')){
|
@supports(-webkit-mask-box-image: url('')){
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
-webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 10.3872C0 1.83334 1.83334 0 10.3872 0H13.6128C22.1667 0 24 1.83334 24 10.3872V13.6128C24 22.1667 22.1667 24 13.6128 24H10.3872C1.83334 24 0 22.1667 0 13.6128V10.3872Z' fill='black'/%3E%3C/svg%3E%0A") 48% 41% 37.9% 53.3%;;
|
-webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 10.3872C0 1.83334 1.83334 0 10.3872 0H13.6128C22.1667 0 24 1.83334 24 10.3872V13.6128C24 22.1667 22.1667 24 13.6128 24H10.3872C1.83334 24 0 22.1667 0 13.6128V10.3872Z' fill='black'/%3E%3C/svg%3E%0A") 48% 41% 37.9% 53.3%;;
|
||||||
|
|
|
@ -146,4 +146,31 @@ describe('PageOrder model', () => {
|
||||||
|
|
||||||
await pageOrder.destroy();
|
await pageOrder.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Testing get parents and children order methods', async () => {
|
||||||
|
const parentTestData = {
|
||||||
|
page: '0',
|
||||||
|
order: ['1', '2', '3', '4', '5'],
|
||||||
|
};
|
||||||
|
const childTestData = {
|
||||||
|
page: 'child',
|
||||||
|
order: ['a', 'b', 'c', 'd', 'e'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const parentOrder = new PageOrder(parentTestData);
|
||||||
|
const childOrder = new PageOrder(childTestData);
|
||||||
|
const insertedParentOrder = await parentOrder.save();
|
||||||
|
const insertedChildOrder = await childOrder.save();
|
||||||
|
const fetchedParentOrder = await PageOrder.getRootPageOrder();
|
||||||
|
const fetchedChildOrder = await PageOrder.getChildPageOrder();
|
||||||
|
|
||||||
|
expect(fetchedParentOrder.page).to.deep.equals(parentTestData.page);
|
||||||
|
expect(fetchedParentOrder.order).to.deep.equal(parentTestData.order);
|
||||||
|
expect(fetchedChildOrder).to.be.an('array').that.is.length(1);
|
||||||
|
expect(fetchedChildOrder[0].page).to.deep.equals(childTestData.page);
|
||||||
|
expect(fetchedChildOrder[0].order).to.deep.equals(childTestData.order);
|
||||||
|
|
||||||
|
await insertedParentOrder.destroy();
|
||||||
|
await insertedChildOrder.destroy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue