Skip to content

Commit

Permalink
feat: CMS navigation page component
Browse files Browse the repository at this point in the history
* page tree handling for CMS navigation page component

Co-authored-by: Marcel Eisentraut <meisentraut@intershop.de>
  • Loading branch information
shauke and Eisie96 committed Mar 11, 2024
1 parent c9ca883 commit c4ff692
Show file tree
Hide file tree
Showing 9 changed files with 477 additions and 4 deletions.
8 changes: 7 additions & 1 deletion src/app/core/facades/cms.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { delay, switchMap, tap } from 'rxjs/operators';
import { CallParameters } from 'ish-core/models/call-parameters/call-parameters.model';
import { CategoryHelper } from 'ish-core/models/category/category.helper';
import { getContentInclude, loadContentInclude } from 'ish-core/store/content/includes';
import { getContentPageTree, loadContentPageTree } from 'ish-core/store/content/page-tree';
import { getCompleteContentPageTree, getContentPageTree, loadContentPageTree } from 'ish-core/store/content/page-tree';
import { getContentPagelet } from 'ish-core/store/content/pagelets';
import {
getContentPageLoading,
Expand Down Expand Up @@ -48,6 +48,12 @@ export class CMSFacade {
return this.store.pipe(select(getContentPageTree(rootId)));
}

completeContentPageTree$(rootId: string, depth: number) {
// fetch only the depth that is actually needed, depth=0 returns already the next child level
this.store.dispatch(loadContentPageTree({ rootId, depth: depth > 0 ? depth - 1 : 0 }));
return this.store.pipe(select(getCompleteContentPageTree(rootId, depth)));
}

/**
*
* @param rootId is taken into consideration as first element of breadcrumb for content page
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ export interface ContentPageTreeView extends ContentPageTreeElement {
pathElements: ContentPageTreeElement[];
}

export function createCompleteContentPageTreeView(
tree: ContentPageTree,
contentPageId: string,
depth: number
): ContentPageTreeView {
if (!tree || !contentPageId || !tree.nodes[contentPageId]) {
return;
}
return unflattenTree(getCompleteContentPageTreeElements(tree, contentPageId, depth), contentPageId);
}

/**
* @param tree
* @param elementId element of page tree. It will be decided, if element is part of displayed navigation tree.
Expand Down Expand Up @@ -95,7 +106,30 @@ export function createContentPageTreeView(
if (!tree || !rootId || !tree.nodes[rootId] || !isContentPagePartOfPageTreeElement(tree, contentPageId, rootId)) {
return;
}
return unflattenTree(getContentPageTreeElements(tree, rootId, rootId, contentPageId), rootId);
}

const contentPageTree = getContentPageTreeElements(tree, rootId, rootId, contentPageId);
return unflattenTree(contentPageTree, rootId);
function getCompleteContentPageTreeElements(
tree: ContentPageTree,
contentPageId: string,
depth: number,
currentDepth = 0
): ContentPageTreeView[] {
let treeElements: ContentPageTreeView[] = [];

if (tree.edges[contentPageId] && (currentDepth < depth || Number.isNaN(depth))) {
treeElements = tree.edges[contentPageId]
.map(child => getCompleteContentPageTreeElements(tree, child, depth, currentDepth + 1))
.flat();
}

const parent = tree.nodes[contentPageId].path[tree.nodes[contentPageId].path.length - 2];
treeElements.push({
...tree.nodes[contentPageId],
parent,
children: [],
pathElements: tree.nodes[contentPageId].path.map(p => tree.nodes[p]),
});

return treeElements;
}
57 changes: 57 additions & 0 deletions src/app/core/models/content-page-tree/content-page-tree.helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isEqual, pick } from 'lodash-es';

import { ContentPageTree, ContentPageTreeElement } from './content-page-tree.model';

export class ContentPageTreeHelper {
Expand Down Expand Up @@ -68,6 +70,40 @@ export class ContentPageTreeHelper {
return ContentPageTreeHelper.merge(tree, singleContentPageTree);
}

/**
* Extract a sub tree.
*/
static subTree(tree: ContentPageTree, uniqueId: string): ContentPageTree {
if (!uniqueId) {
return tree;
}

const subTreeElements = Object.keys(tree.nodes)
.map(id => tree.nodes[id])
.filter(elements => elements.path.find(path => path === uniqueId))
.map(el => el.contentPageId);

const select = (e: string) => subTreeElements.find(el => el === e);
return {
rootIds: tree.rootIds.filter(select),
edges: pick(tree.edges, ...Object.keys(tree.edges).filter(select)),
nodes: pick(tree.nodes, ...Object.keys(tree.nodes).filter(select)),
};
}

/**
* Perform check for equality. Order of items is ignored.
*/
static equals(tree1: ContentPageTree, tree2: ContentPageTree): boolean {
return (
tree1 &&
tree2 &&
ContentPageTreeHelper.rootIdsEqual(tree1.rootIds, tree2.rootIds) &&
ContentPageTreeHelper.edgesEqual(tree1.edges, tree2.edges) &&
ContentPageTreeHelper.contentEqual(tree1.nodes, tree2.nodes)
);
}

private static removeDuplicates<T>(input: T[]): T[] {
return input.filter((value, index, array) => array.indexOf(value) === index);
}
Expand Down Expand Up @@ -119,4 +155,25 @@ export class ContentPageTreeHelper {
return ContentPageTreeHelper.removeDuplicates([...current, ...incoming]);
}
}

private static rootIdsEqual(t1: string[], t2: string[]) {
return t1.length === t2.length && t1.every(e => t2.includes(e));
}

private static edgesEqual(t1: { [id: string]: string[] }, t2: { [id: string]: string[] }) {
return isEqual(t1, t2);
}

private static contentEqual(
t1: { [id: string]: ContentPageTreeElement },
t2: { [id: string]: ContentPageTreeElement }
) {
const keys1 = Object.keys(t1);
const keys2 = Object.keys(t2);
return (
keys1.length === keys2.length &&
keys1.every(id => keys2.includes(id)) &&
keys1.every(id => isEqual(t1[id], t2[id]))
);
}
}
20 changes: 19 additions & 1 deletion src/app/core/store/content/page-tree/page-tree.selectors.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createSelector, createSelectorFactory, resultMemoize } from '@ngrx/store';
import { createSelector, createSelectorFactory, defaultMemoize, resultMemoize } from '@ngrx/store';
import { isEqual } from 'lodash-es';

import {
ContentPageTreeView,
createCompleteContentPageTreeView,
createContentPageTreeView,
} from 'ish-core/models/content-page-tree-view/content-page-tree-view.model';
import { ContentPageTreeHelper } from 'ish-core/models/content-page-tree/content-page-tree.helper';
import { ContentPageTree } from 'ish-core/models/content-page-tree/content-page-tree.model';
import { getContentState } from 'ish-core/store/content/content-store';
import { selectRouteParam } from 'ish-core/store/core/router';
Expand All @@ -25,3 +27,19 @@ export const getContentPageTree = (rootId: string) =>
selectRouteParam('contentPageId'),
(pagetree: ContentPageTree, contentPageId: string) => createContentPageTreeView(pagetree, rootId, contentPageId)
);

/**
* Get the complete content page tree (all branches) for the given root to the given depth.
*
* @param rootId The Id of the root content page of the tree
* @returns The complete content page tree
*/
export const getCompleteContentPageTree = (rootId: string, depth: number) =>
createSelectorFactory<object, ContentPageTreeView>(projector =>
defaultMemoize(projector, ContentPageTreeHelper.equals, isEqual)
)(getPageTree, (tree: ContentPageTree): ContentPageTreeView => {
if (!rootId) {
return;
}
return createCompleteContentPageTreeView(ContentPageTreeHelper.subTree(tree, rootId), rootId, depth);
});
9 changes: 9 additions & 0 deletions src/app/shared/cms/cms.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CMSFreestyleComponent } from './components/cms-freestyle/cms-freestyle.
import { CMSImageEnhancedComponent } from './components/cms-image-enhanced/cms-image-enhanced.component';
import { CMSImageComponent } from './components/cms-image/cms-image.component';
import { CMSNavigationLinkComponent } from './components/cms-navigation-link/cms-navigation-link.component';
import { CMSNavigationPageComponent } from './components/cms-navigation-page/cms-navigation-page.component';
import { CMSProductListCategoryComponent } from './components/cms-product-list-category/cms-product-list-category.component';
import { CMSProductListFilterComponent } from './components/cms-product-list-filter/cms-product-list-filter.component';
import { CMSProductListManualComponent } from './components/cms-product-list-manual/cms-product-list-manual.component';
Expand Down Expand Up @@ -139,6 +140,14 @@ import { CMS_COMPONENT } from './configurations/injection-keys';
},
multi: true,
},
{
provide: CMS_COMPONENT,
useValue: {
definitionQualifiedName: 'app_sf_base_cm:component.navigation.page.pagelet2-Component',
class: CMSNavigationPageComponent,
},
multi: true,
},
],
})
export class CMSModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->
<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->
<ng-container *ngIf="pageTree$ | async as page">
<li
#subMenu
[class]="'dropdown ' + pagelet.stringParam('CSSClass', '')"
[ngClass]="{ open: isOpened(page.contentPageId) }"
(mouseenter)="subMenuShow(subMenu)"
(mouseleave)="subMenuHide(subMenu)"
(click)="subMenuHide(subMenu)"
>
<a
[routerLink]="page | ishContentPageRoute"
[ngStyle]="{ width: !showSubMenu(page.children.length) ? '100%' : '' }"
>
<ng-container *ngIf="pagelet.hasParam('DisplayName'); else noDisplayName">
{{ pagelet.stringParam('DisplayName') }}
</ng-container>
<ng-template #noDisplayName>
{{ page.name }}
</ng-template>
</a>

<ng-container *ngIf="showSubMenu(page.children.length)">
<a class="dropdown-toggle" (click)="toggleOpen(page.contentPageId)">
<fa-icon *ngIf="isOpened(page.contentPageId); else closed" [icon]="['fas', 'minus']" />
<ng-template #closed><fa-icon [icon]="['fas', 'plus']" /></ng-template>
</a>

<ng-container [ngTemplateOutlet]="treeNodeTemplate" [ngTemplateOutletContext]="{ treeNode: page, depth: 1 }" />

<!-- the recursively used template to render the tree nodes -->
<ng-template #treeNodeTemplate let-treeNode="treeNode" let-depth="depth">
<ul class="category-level{{ depth }}" [ngClass]="{ 'dropdown-menu': depth === 1 }">
<li
*ngFor="let node of treeNode.children"
class="main-navigation-level{{ depth }}-item"
[ngClass]="{ open: isOpened(node.contentPageId) }"
>
<a [routerLink]="node | ishContentPageRoute" [ngStyle]="{ width: !node.children.length ? '100%' : '' }">
{{ node.name }}
</a>
<ng-container *ngIf="node.children.length">
<a class="dropdown-toggle" (click)="toggleOpen(node.contentPageId)">
<fa-icon *ngIf="isOpened(node.contentPageId); else closed" [icon]="['fas', 'minus']" />
<ng-template #closed><fa-icon [icon]="['fas', 'plus']" /></ng-template>
</a>
<ng-container
[ngTemplateOutlet]="treeNodeTemplate"
[ngTemplateOutletContext]="{ treeNode: node, depth: depth + 1 }"
/>
</ng-container>
</li>

<li *ngIf="pagelet.hasParam('SubNavigationHTML') && depth === 1" class="sub-navigation-content">
<div [ishServerHtml]="pagelet.stringParam('SubNavigationHTML')"></div>
</li>
</ul>
</ng-template>
</ng-container>
</li>
</ng-container>
Loading

0 comments on commit c4ff692

Please sign in to comment.