Skip to content

Commit f143a5c

Browse files
Merge pull request #230 from netgrif/NAE-1935
[NAE-1935] Improved breadcrumbs from menu items
2 parents 9ed6954 + 8d4118f commit f143a5c

File tree

7 files changed

+176
-17
lines changed

7 files changed

+176
-17
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Breadcrumbs
2+
Breadcrumbs on filtered case views are automatically resolved using URI node path and filter name. However,
3+
if we want a nice breadcrumb with readable title and diacritics, it is important to define each menu item
4+
separately defining uri, identifier and title using Petriflow actions.
5+
6+
The next action, where we define each level separately and define a title for them, will give a nice breadcrumb:
7+
```xml
8+
<caseEvents>
9+
<event type="create">
10+
<id>import_menu_on_create</id>
11+
<actions phase="post">
12+
<action id="menu_item">
13+
createOrUpdateMenuItem("/", "level_1", "Level 1 title", "feedback")
14+
createOrUpdateMenuItem("/level_1", "level_2", "Level 2 title", "feedback")
15+
createOrUpdateMenuItem("/level_1/level_2", "level_3", "Level 3 title", "feedback")
16+
def body1 = new com.netgrif.application.engine.workflow.domain.menu.MenuItemBody("/level_1/level_2/level_3", "nae_1935_1", "NAE-1935 Nice title", "text_snippet")
17+
createOrUpdateMenuItemAndFilter(body1, "processIdentifier:nae_1935", "Case", "private", ["nae_1935"])
18+
</action>
19+
</actions>
20+
</event>
21+
</caseEvents>
22+
```
23+
Result:
24+
25+
![img_1.png](img_1.png)
26+
27+
However, if we define a multilevel menu in one action:
28+
```xml
29+
<caseEvents>
30+
<event type="create">
31+
<id>import_menu_on_create</id>
32+
<actions phase="post">
33+
<action id="menu_item">
34+
def body5 = new com.netgrif.application.engine.workflow.domain.menu.MenuItemBody("/level_1/level_2/level_3/custom/without_name_and_diacritics", "with_diacritics", "Nice title with diacritics.", "text_snippet")
35+
createOrUpdateMenuItemAndFilter(body5, "processIdentifier:nae_1935", "Case", "private", ["nae_1935"])
36+
</action>
37+
</actions>
38+
</event>
39+
</caseEvents>
40+
```
41+
The menu without a defined title will be sanitized and look like as folows:
42+
![img.png](img.png)

docs/views/breadcrumbs/img.png

20.1 KB
Loading

docs/views/breadcrumbs/img_1.png

19.3 KB
Loading
Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,128 @@
1-
import {Component, Input} from '@angular/core';
1+
import {AfterViewInit, Component, Input, OnDestroy} from '@angular/core';
22
import {UriService} from '../service/uri.service';
3+
import {CaseResourceService} from "../../resources/engine-endpoint/case-resource.service";
4+
import {CaseSearchRequestBody} from "../../filter/models/case-search-request-body";
5+
import {HttpParams} from "@angular/common/http";
6+
import {PaginationParams} from "../../utility/pagination/pagination-params";
7+
import {SimpleFilter} from "../../filter/models/simple-filter";
8+
import {take} from "rxjs/operators";
9+
import {BehaviorSubject, Subscription} from "rxjs";
10+
import {ActivatedRoute, Router} from "@angular/router";
11+
import {
12+
DynamicNavigationRouteProviderService
13+
} from "../../routing/dynamic-navigation-route-provider/dynamic-navigation-route-provider.service";
14+
import {Case} from "../../resources/interface/case";
15+
import {I18nFieldValue} from "../../data-fields/i18n-field/models/i18n-field-value";
16+
import {TranslateService} from "@ngx-translate/core";
317

418
@Component({
519
selector: 'ncc-breadcrumbs-component',
620
template: ''
721
})
8-
export abstract class AbstractBreadcrumbsComponent {
22+
export abstract class AbstractBreadcrumbsComponent implements OnDestroy, AfterViewInit {
923

1024
@Input() showHome: boolean = true;
25+
@Input() showFilter: boolean = true;
26+
@Input() redirectOnClick: boolean = true;
1127
@Input() lengthOfPath: number = 30;
1228
@Input() partsAfterDots: number = 2;
29+
filterName: string;
30+
breadcrumbsParts: Array<string>;
1331
private static DOTS: string = '...';
1432
private static DELIMETER: string = '/';
33+
private static NODE_PATH: string = 'nodePath';
34+
private static ITEM_SETTINGS: string = 'item_settings';
1535
private _showPaths: boolean = false;
36+
private nicePath: BehaviorSubject<Array<string>>;
37+
private redirectUrls: Map<string, Array<string>>;
38+
private nicePathSubscription: Subscription;
1639

17-
protected constructor(protected _uriService: UriService) {
40+
protected constructor(protected _uriService: UriService,
41+
protected _caseResourceService: CaseResourceService,
42+
protected _activatedRoute: ActivatedRoute,
43+
protected _router: Router,
44+
protected _dynamicRoutingService: DynamicNavigationRouteProviderService,
45+
protected _translateService: TranslateService) {
46+
this.nicePath = new BehaviorSubject<Array<string>>(undefined);
47+
this.redirectUrls = new Map<string, Array<string>>();
48+
this.initNicePath();
1849
}
1950

20-
public getPath(): Array<string> {
21-
const tmp = this._uriService.splitNodePath(this._uriService.activeNode);
22-
if (tmp?.length > this.partsAfterDots + 1 && this._uriService.activeNode?.uriPath.length > this.lengthOfPath && !this._showPaths) {
23-
const newPath = [tmp[0], AbstractBreadcrumbsComponent.DOTS];
24-
for (let i = tmp.length - this.partsAfterDots; i < tmp.length; i++) {
25-
newPath.push(tmp[i]);
51+
ngAfterViewInit() {
52+
this.resolveBreadcrumbs();
53+
}
54+
55+
ngOnDestroy(): void {
56+
if (!!this.nicePathSubscription) {
57+
this.nicePathSubscription.unsubscribe();
58+
}
59+
}
60+
61+
public resolveBreadcrumbs() {
62+
const filterId = this._activatedRoute.snapshot.params.filterCaseId
63+
if (!!filterId) {
64+
const splitPath = this._uriService.splitNodePath(this._uriService.activeNode);
65+
const fullPath = this.createFullPath(splitPath);
66+
const fullPathQueries = fullPath.map(p => '(processIdentifier:preference_item AND dataSet.nodePath.textValue.keyword:\"' + p + '\")')
67+
fullPathQueries.push('(taskMongoIds:\"' + filterId + '\")')
68+
69+
const searchBody: CaseSearchRequestBody = {
70+
query: fullPathQueries.join(" OR ")
71+
};
72+
let httpParams = new HttpParams()
73+
.set(PaginationParams.PAGE_SIZE, 25)
74+
.set(PaginationParams.PAGE_NUMBER, 0);
75+
76+
this._caseResourceService.searchCases(SimpleFilter.fromCaseQuery(searchBody), httpParams).pipe(take(1)).subscribe(result => {
77+
const cases = result.content;
78+
const filterCaseIndex = cases.findIndex(c => c.tasks.some(t => t.task === filterId) && !fullPath.includes(this.immediateValue(c, AbstractBreadcrumbsComponent.NODE_PATH)));
79+
if (filterCaseIndex >= 0) {
80+
const filterCase = cases.splice(cases.findIndex(c => c.tasks.some(t => t.task === filterId) && !fullPath.includes(this.immediateValue(c, AbstractBreadcrumbsComponent.NODE_PATH))), 1)[0];
81+
this.filterName = this.getTranslation(this.immediateValue(filterCase, 'menu_name'));
82+
}
83+
cases.sort((a, b) => fullPath.indexOf(this.immediateValue(a, AbstractBreadcrumbsComponent.NODE_PATH)) - fullPath.indexOf(this.immediateValue(b, AbstractBreadcrumbsComponent.NODE_PATH)));
84+
if (this.redirectOnClick) {
85+
cases.forEach(c => this.redirectUrls.set(this.immediateValue(c, AbstractBreadcrumbsComponent.NODE_PATH), [this._dynamicRoutingService.route, c.tasks.find(t => t.transition === AbstractBreadcrumbsComponent.ITEM_SETTINGS).task]))
86+
}
87+
this.nicePath.next(["", ...cases.map(c => this.getTranslation(this.immediateValue(c, 'menu_name')))]);
88+
});
89+
}
90+
}
91+
92+
public initNicePath() {
93+
this.nicePathSubscription = this.nicePath.subscribe(np => {
94+
if (!!np) {
95+
const path = np;
96+
if (path?.length > this.partsAfterDots + 1 && this._uriService.activeNode?.uriPath.length > this.lengthOfPath && !this._showPaths) {
97+
const newPath = [path[0], AbstractBreadcrumbsComponent.DOTS];
98+
for (let i = path.length - this.partsAfterDots; i < path.length; i++) {
99+
newPath.push(path[i]);
100+
}
101+
this.breadcrumbsParts = newPath;
102+
return;
103+
}
104+
this.breadcrumbsParts = path === undefined ? [] : path;
26105
}
27-
return newPath;
106+
});
107+
}
108+
109+
public redirect() {
110+
if (!this.redirectOnClick) {
111+
return;
28112
}
29-
return tmp === undefined ? [] : tmp;
113+
this._router.navigate(this.redirectUrls.get(this._uriService.activeNode.uriPath)).then(r => {})
30114
}
31115

32116
public reset(): void {
117+
this.filterName = undefined;
33118
this._uriService.reset();
119+
this.nicePath.next([""])
34120
}
35121

36122
public changePath(path: string, count: number) {
37123
if (path === AbstractBreadcrumbsComponent.DOTS && count === 1) {
38124
this._showPaths = true;
125+
this.nicePath.next(this.nicePath.value);
39126
return;
40127
}
41128
let fullPath: string = '';
@@ -48,6 +135,9 @@ export abstract class AbstractBreadcrumbsComponent {
48135
}
49136
this._uriService.getNodeByPath(fullPath).subscribe(node => {
50137
this._uriService.activeNode = node;
138+
this.filterName = undefined;
139+
this.nicePath.next(this.nicePath.value.slice(0, control + 1))
140+
this.redirect();
51141
})
52142
}
53143

@@ -57,4 +147,21 @@ export abstract class AbstractBreadcrumbsComponent {
57147
}
58148
return count;
59149
}
150+
151+
private createFullPath(splitPath: Array<string>): Array<string> {
152+
let tmp = '';
153+
return splitPath.filter(s => s !== "").map((value) => {
154+
tmp += AbstractBreadcrumbsComponent.DELIMETER + value;
155+
return tmp.replace("//", AbstractBreadcrumbsComponent.DELIMETER)
156+
});
157+
}
158+
159+
private immediateValue(aCase: Case, fieldId: string): any {
160+
return aCase.immediateData.find(s => s.stringId === fieldId)?.value
161+
}
162+
163+
private getTranslation(value: I18nFieldValue): string {
164+
const locale = this._translateService.currentLang.split('-')[0];
165+
return locale in value.translations ? value.translations[locale] : value.defaultValue;
166+
}
60167
}

projects/netgrif-components-core/src/lib/navigation/service/uri.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {GroupNavigationConstants} from "../model/group-navigation-constants";
2424
})
2525
export class UriService implements OnDestroy {
2626

27-
private static ROOT: string = 'root';
27+
public static ROOT: string = 'root';
2828
private _rootNode: UriNodeResource;
2929
private readonly _rootLoading$: LoadingEmitter;
3030
private readonly _parentLoading$: LoadingEmitter;

projects/netgrif-components/src/lib/navigation/breadcrumbs/breadcrumbs.component.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
<div *ngIf="showHome">
33
<span class="breadcrumbs-span" (click)="reset()">{{'toolbar.menu.home' | translate}}</span>
44
</div>
5-
<div *ngFor="let path of getPath();let i = index;let last = last;">
5+
<div *ngFor="let path of breadcrumbsParts;let i = index;let last = last;">
66
<span class="breadcrumbs-span" (click)="changePath(path, i)">{{path | titlecase}}</span>
7-
<span class="delimeter" *ngIf="!last">/</span>
7+
<span class="delimeter" *ngIf="!last || (showFilter && !!filterName)">/</span>
8+
</div>
9+
<div *ngIf="showFilter && !!filterName">
10+
<span class="breadcrumbs-span">{{filterName}}</span>
811
</div>
912
</div>
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {Component} from '@angular/core';
22
import {
33
UriService,
4-
AbstractBreadcrumbsComponent
4+
AbstractBreadcrumbsComponent, CaseResourceService, DynamicNavigationRouteProviderService
55
} from '@netgrif/components-core';
6+
import {ActivatedRoute, Router} from "@angular/router";
7+
import {TranslateService} from "@ngx-translate/core";
68

79
@Component({
810
selector: 'nc-breadcrumbs',
@@ -11,7 +13,12 @@ import {
1113
})
1214
export class BreadcrumbsComponent extends AbstractBreadcrumbsComponent {
1315

14-
constructor(protected _uriService: UriService) {
15-
super(_uriService);
16+
constructor(protected _uriService: UriService,
17+
_caseResourceService: CaseResourceService,
18+
_activatedRoute: ActivatedRoute,
19+
_router: Router,
20+
_dynamicRoutingService: DynamicNavigationRouteProviderService,
21+
_translateService: TranslateService) {
22+
super(_uriService, _caseResourceService, _activatedRoute, _router, _dynamicRoutingService, _translateService);
1623
}
1724
}

0 commit comments

Comments
 (0)