Skip to content

Commit 66ffd4f

Browse files
committed
PR title: Fix deep page nesting in sidebar, prev/next order, and parent dropdown indent
1 parent 9993be5 commit 66ffd4f

13 files changed

Lines changed: 111 additions & 53 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "codex.docs",
33
"license": "Apache-2.0",
4-
"version": "2.2.4",
4+
"version": "2.3.0",
55
"type": "module",
66
"bin": {
77
"codex.docs": "dist/backend/app.js"

src/backend/build-static.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ export default async function buildStatic(): Promise<void> {
105105
const parentIdOfRootPages = '0' as EntityId;
106106
const previousPage = await PagesFlatArray.getPageBefore(pageId);
107107
const nextPage = await PagesFlatArray.getPageAfter(pageId);
108-
const menu = createMenuTree(parentIdOfRootPages, allPages, pagesOrder, 2);
109-
108+
const menu = createMenuTree(parentIdOfRootPages, allPages, pagesOrder);
110109
const result = await renderTemplate('./views/pages/page.twig', {
111110
page,
112111
pageParent,

src/backend/controllers/pages.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,42 @@ class Pages {
8686
return pagesMap;
8787
}
8888

89+
/**
90+
* Depth in parent chain: 0 for root pages, +1 per ancestor below root (for select indent).
91+
*/
92+
private static computePageDepth(page: Page, pagesMap: Map<string, Page>): number {
93+
let depth = 0;
94+
let cur: Page | undefined = page;
95+
96+
while (cur?._parent && !isEqualIds(cur._parent, '0' as EntityId)) {
97+
depth++;
98+
cur = pagesMap.get(cur._parent.toString());
99+
}
100+
101+
return depth;
102+
}
103+
104+
/**
105+
* Ordered pages for the parent `<select>` with visual nesting (indent = depth).
106+
*
107+
* @param excludePageId - when editing, exclude this page and its descendants (same as groupByParent)
108+
*/
109+
public static async getParentSelectOptions(
110+
excludePageId?: EntityId
111+
): Promise<Array<{ page: Page; depth: number; indent: string }>> {
112+
const pages = excludePageId
113+
? await this.groupByParent(excludePageId)
114+
: await this.groupByParent();
115+
const pagesMap = await this.getPagesMap();
116+
const indentUnit = '\u00a0\u00a0';
117+
118+
return pages.map((page) => {
119+
const depth = Pages.computePageDepth(page, pagesMap);
120+
121+
return { page, depth, indent: indentUnit.repeat(depth) };
122+
});
123+
}
124+
89125
/**
90126
* Group all pages by their parents
91127
* If the pageId is passed, it excludes passed page from result pages

src/backend/models/pagesFlatArray.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ class PagesFlatArray {
5151
/**
5252
* Returns pages flat array
5353
*
54-
* @param nestingLimit - number of flat array nesting, set null to dismiss the restriction, default nesting 2
54+
* @param nestingLimit - max `level` to keep (level 0 = root pages, 1 = their children, …).
55+
* Pass **null** to return the full tree order (needed for prev/next links past depth 2).
5556
* @returns {Promise<Array<PagesFlatArrayData>>}
5657
*/
5758
public static async get(nestingLimit: number | null = 2): Promise<Array<PagesFlatArrayData>> {
@@ -108,7 +109,8 @@ class PagesFlatArray {
108109
* @returns {Promise<PagesFlatArrayData | undefined>}
109110
*/
110111
public static async getPageBefore(pageId: EntityId): Promise<PagesFlatArrayData | undefined> {
111-
const arr = await this.get();
112+
/** `null` = no level cap; default (2) would drop pages at level ≥2 from the chain */
113+
const arr = await this.get(null);
112114

113115
const pageIndex = arr.findIndex((item) => isEqualIds(item.id, pageId));
114116

@@ -128,7 +130,7 @@ class PagesFlatArray {
128130
* @returns {Promise<PagesFlatArrayData | undefined>}
129131
*/
130132
public static async getPageAfter(pageId: EntityId): Promise<PagesFlatArrayData | undefined> {
131-
const arr = await this.get();
133+
const arr = await this.get(null);
132134

133135
const pageIndex = arr.findIndex( (item) => isEqualIds(item.id, pageId));
134136

src/backend/routes/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ router.use('/', pagesMiddleware, home);
1414
router.use('/', pagesMiddleware, pages);
1515
router.use('/', pagesMiddleware, auth);
1616
router.use('/api', verifyToken, allowEdit, api);
17-
router.use('/', aliases);
17+
router.use('/', pagesMiddleware, aliases);
1818

1919
export default router;

src/backend/routes/middlewares/pages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default asyncMiddleware(async (req: Request, res: Response, next: NextFun
2525
const pages = await Pages.getAllPages();
2626
const pagesOrder = await PagesOrder.getAll();
2727

28-
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
28+
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder);
2929
} catch (error) {
3030
console.log('Can not load menu:', error);
3131
}

src/backend/routes/pages.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ const router = express.Router();
1313
*/
1414
router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => {
1515
try {
16-
const pagesAvailableGrouped = await Pages.groupByParent();
17-
18-
console.log(pagesAvailableGrouped);
16+
const parentSelectOptions = await Pages.getParentSelectOptions();
1917

2018
res.render('pages/form', {
21-
pagesAvailableGrouped,
19+
parentSelectOptions,
2220
page: null,
2321
});
2422
} catch (error) {
@@ -36,7 +34,7 @@ router.get('/page/edit/:id', verifyToken, allowEdit, async (req: Request, res: R
3634
try {
3735
const page = await Pages.get(pageId);
3836
const pagesAvailable = await Pages.getAllExceptChildren(pageId);
39-
const pagesAvailableGrouped = await Pages.groupByParent(pageId);
37+
const parentSelectOptions = await Pages.getParentSelectOptions(pageId);
4038

4139
if (!page._parent) {
4240
throw new Error('Parent not found');
@@ -47,7 +45,7 @@ router.get('/page/edit/:id', verifyToken, allowEdit, async (req: Request, res: R
4745
res.render('pages/form', {
4846
page,
4947
parentsChildrenOrdered,
50-
pagesAvailableGrouped,
48+
parentSelectOptions,
5149
});
5250
} catch (error) {
5351
res.status(404);

src/backend/utils/menu.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,26 @@ import Page from '../models/page.js';
33
import PageOrder from '../models/pageOrder.js';
44
import { isEqualIds } from '../database/index.js';
55

6+
/** Max sidebar nesting depth (root sections count as depth 1). */
7+
const MENU_MAX_DEPTH = 64;
8+
69
/**
7-
* Process one-level pages list to parent-children list
10+
* Build parent→children menu tree for the sidebar.
811
*
912
* @param parentPageId - parent page id
1013
* @param pages - list of all available pages
1114
* @param pagesOrder - list of pages order
12-
* @param level - max level recursion
13-
* @param currentLevel - current level of element
15+
* @param maxDepth - stop recursing deeper than this (default: MENU_MAX_DEPTH)
16+
* currentDepth - current depth from the tree root (1 = direct children of parentPageId)
1417
*/
15-
export function createMenuTree(parentPageId: EntityId, pages: Page[], pagesOrder: PageOrder[], level = 1, currentLevel = 1): Page[] {
16-
const childrenOrder = pagesOrder.find(order => isEqualIds(order.data.page, parentPageId));
18+
export function createMenuTree(
19+
parentPageId: EntityId,
20+
pages: Page[],
21+
pagesOrder: PageOrder[],
22+
maxDepth: number = MENU_MAX_DEPTH,
23+
currentDepth: number = 1
24+
): Page[] {
25+
const childrenOrder = pagesOrder.find((order) => isEqualIds(order.data.page, parentPageId));
1726

1827
/**
1928
* branch is a page children in tree
@@ -24,26 +33,26 @@ export function createMenuTree(parentPageId: EntityId, pages: Page[], pagesOrder
2433

2534
if (childrenOrder) {
2635
ordered = childrenOrder.order.map((pageId: EntityId) => {
27-
return pages.find(page => isEqualIds(page._id, pageId));
36+
return pages.find((page) => isEqualIds(page._id, pageId));
2837
});
2938
}
3039

31-
const unordered = pages.filter(page => isEqualIds(page._parent, parentPageId));
40+
const unordered = pages.filter((page) => isEqualIds(page._parent, parentPageId));
3241
const branch = Array.from(new Set([...ordered, ...unordered]));
3342

34-
/**
35-
* stop recursion when we got the passed max level
36-
*/
37-
if (currentLevel === level + 1) {
38-
return [];
39-
}
43+
const canRecurse = currentDepth < maxDepth;
4044

4145
/**
4246
* Each parents children can have subbranches
4347
*/
44-
return branch.filter(page => page && page._id).map(page => {
45-
return Object.assign({
46-
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1),
47-
}, page.data);
48-
});
48+
return branch
49+
.filter((page) => page && page._id)
50+
.map((page) => {
51+
const subtree = canRecurse
52+
? createMenuTree(page._id!, pages, pagesOrder, maxDepth, currentDepth + 1)
53+
: [];
54+
55+
/** `children` must win over anything stored on the page document */
56+
return { ...page.data, children: subtree };
57+
});
4958
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{% if nodes is not empty %}
2+
<ul class="docs-sidebar__section-list {{ ulClass }}">
3+
{% for node in nodes %}
4+
<li>
5+
<a
6+
class="docs-sidebar__section-list-item-wrapper"
7+
href="{{ node.uri ? '/' ~ node.uri : '/page/' ~ node._id }}">
8+
<div class="docs-sidebar__section-list-item {{ page is defined and toString(page._id) == toString(node._id) ? 'docs-sidebar__section-list-item--active' : '' }}">
9+
<span>{{ node.title | striptags }}</span>
10+
</div>
11+
</a>
12+
{% if node.children is defined and node.children is not empty %}
13+
{% include 'components/sidebar-branch.twig' with { nodes: node.children, ulClass: 'docs-sidebar__section-list--nested' } %}
14+
{% endif %}
15+
</li>
16+
{% endfor %}
17+
</ul>
18+
{% endif %}

src/backend/views/components/sidebar.twig

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,7 @@
2626
</div>
2727
</a>
2828
{% if firstLevelPage.children is not empty %}
29-
<ul class="docs-sidebar__section-list">
30-
{% for child in firstLevelPage.children %}
31-
<li>
32-
<a
33-
class="docs-sidebar__section-list-item-wrapper"
34-
href="{{ child.uri ? '/' ~ child.uri : '/page/' ~ child._id }}">
35-
<div class="docs-sidebar__section-list-item {{page is defined and toString(page._id) == toString(child._id) ? 'docs-sidebar__section-list-item--active' : ''}}">
36-
<span>{{ child.title | striptags }}</span>
37-
</div>
38-
</a>
39-
</li>
40-
{% endfor %}
41-
</ul>
29+
{% include 'components/sidebar-branch.twig' with { nodes: firstLevelPage.children, ulClass: '' } %}
4230
{% endif %}
4331
</section>
4432
{% endfor %}

0 commit comments

Comments
 (0)