Skip to content

Commit 1e04e81

Browse files
authored
feat: make page header configurable to persist active tab (#1147)
* feat: make page header configurable to persist active tab * style: prettier * test: add mockProviders * feat: add subscription lifecycle * feat: fix provider * feat: add distinctUntilChanged
1 parent aca64c2 commit 1e04e81

File tree

3 files changed

+79
-10
lines changed

3 files changed

+79
-10
lines changed

projects/components/src/header/page/page-header.component.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { NavigationService, PreferenceService, SubscriptionLifecycle } from '@hypertrace/common';
12
import { createHostFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
23
import { of } from 'rxjs';
34
import { BreadcrumbsService } from '../../breadcrumbs/breadcrumbs.service';
@@ -10,6 +11,9 @@ describe('Page Header Component', () => {
1011
component: PageHeaderComponent,
1112
shallow: true,
1213
providers: [
14+
mockProvider(NavigationService),
15+
mockProvider(PreferenceService),
16+
mockProvider(SubscriptionLifecycle),
1317
mockProvider(BreadcrumbsService, {
1418
breadcrumbs$: of([
1519
{

projects/components/src/header/page/page-header.component.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2-
import { Breadcrumb } from '@hypertrace/common';
3-
import { Observable } from 'rxjs';
4-
import { map } from 'rxjs/operators';
1+
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
2+
import {
3+
Breadcrumb,
4+
isNonEmptyString,
5+
NavigationService,
6+
PreferenceService,
7+
SubscriptionLifecycle
8+
} from '@hypertrace/common';
9+
import { Observable, of } from 'rxjs';
10+
import { first, map } from 'rxjs/operators';
511
import { BreadcrumbsService } from '../../breadcrumbs/breadcrumbs.service';
612
import { IconSize } from '../../icon/icon-size';
713
import { NavigableTab } from '../../tabs/navigable/navigable-tab';
@@ -10,6 +16,7 @@ import { NavigableTab } from '../../tabs/navigable/navigable-tab';
1016
selector: 'ht-page-header',
1117
styleUrls: ['./page-header.component.scss'],
1218
changeDetection: ChangeDetectionStrategy.OnPush,
19+
providers: [SubscriptionLifecycle],
1320
template: `
1421
<div
1522
*ngIf="this.breadcrumbs$ | async as breadcrumbs"
@@ -34,7 +41,7 @@ import { NavigableTab } from '../../tabs/navigable/navigable-tab';
3441
3542
<ng-content></ng-content>
3643
37-
<ht-navigable-tab-group *ngIf="this.tabs?.length" class="tabs">
44+
<ht-navigable-tab-group *ngIf="this.tabs?.length" class="tabs" (tabChange)="this.onTabChange($event)">
3845
<ht-navigable-tab
3946
*ngFor="let tab of this.tabs"
4047
[path]="tab.path"
@@ -47,7 +54,10 @@ import { NavigableTab } from '../../tabs/navigable/navigable-tab';
4754
</div>
4855
`
4956
})
50-
export class PageHeaderComponent {
57+
export class PageHeaderComponent implements OnInit {
58+
@Input()
59+
public persistenceId?: string;
60+
5161
@Input()
5262
public tabs?: NavigableTab[] = [];
5363

@@ -62,5 +72,47 @@ export class PageHeaderComponent {
6272
map(breadcrumbs => (breadcrumbs.length > 0 ? breadcrumbs[breadcrumbs.length - 1] : {}))
6373
);
6474

65-
public constructor(protected readonly breadcrumbsService: BreadcrumbsService) {}
75+
public constructor(
76+
protected readonly navigationService: NavigationService,
77+
protected readonly preferenceService: PreferenceService,
78+
protected readonly subscriptionLifecycle: SubscriptionLifecycle,
79+
protected readonly breadcrumbsService: BreadcrumbsService
80+
) {}
81+
82+
public ngOnInit(): void {
83+
this.subscriptionLifecycle.add(
84+
this.getPreferences().subscribe(preferences => this.navigateIfPersistedActiveTab(preferences))
85+
);
86+
}
87+
88+
private navigateIfPersistedActiveTab(preferences: PageHeaderPreferences): void {
89+
if (isNonEmptyString(this.persistenceId) && isNonEmptyString(preferences.selectedTabPath)) {
90+
this.navigationService.navigateWithinApp(
91+
preferences.selectedTabPath,
92+
this.navigationService.getCurrentActivatedRoute().parent!
93+
);
94+
}
95+
}
96+
97+
public onTabChange(path?: string): void {
98+
this.setPreferences(path);
99+
}
100+
101+
private getPreferences(): Observable<PageHeaderPreferences> {
102+
return isNonEmptyString(this.persistenceId)
103+
? this.preferenceService.get<PageHeaderPreferences>(this.persistenceId, {}).pipe(first())
104+
: of({});
105+
}
106+
107+
private setPreferences(selectedTabPath?: string): void {
108+
if (isNonEmptyString(this.persistenceId)) {
109+
this.preferenceService.set(this.persistenceId, {
110+
selectedTabPath: selectedTabPath
111+
});
112+
}
113+
}
114+
}
115+
116+
interface PageHeaderPreferences {
117+
selectedTabPath?: string;
66118
}

projects/components/src/tabs/navigable/navigable-tab-group.component.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
import { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, QueryList } from '@angular/core';
1+
import {
2+
AfterContentInit,
3+
ChangeDetectionStrategy,
4+
Component,
5+
ContentChildren,
6+
EventEmitter,
7+
Output,
8+
QueryList
9+
} from '@angular/core';
210
import { ActivatedRoute } from '@angular/router';
311
import { Color, FeatureState, NavigationParams, NavigationParamsType, NavigationService } from '@hypertrace/common';
412
import { merge, Observable } from 'rxjs';
5-
import { map, startWith } from 'rxjs/operators';
13+
import { distinctUntilChanged, map, startWith, tap } from 'rxjs/operators';
614
import { NavigableTabComponent } from './navigable-tab.component';
715

816
@Component({
@@ -47,6 +55,9 @@ export class NavigableTabGroupComponent implements AfterContentInit {
4755
@ContentChildren(NavigableTabComponent)
4856
public tabs!: QueryList<NavigableTabComponent>;
4957

58+
@Output()
59+
public readonly tabChange: EventEmitter<string | undefined> = new EventEmitter<string | undefined>();
60+
5061
public activeTab$?: Observable<NavigableTabComponent | undefined>;
5162

5263
public constructor(
@@ -57,7 +68,9 @@ export class NavigableTabGroupComponent implements AfterContentInit {
5768
public ngAfterContentInit(): void {
5869
this.activeTab$ = merge(this.navigationService.navigation$, this.tabs.changes).pipe(
5970
startWith(undefined),
60-
map(() => this.findActiveTab())
71+
map(() => this.findActiveTab()),
72+
distinctUntilChanged(),
73+
tap(activeTab => this.tabChange.emit(activeTab?.path))
6174
);
6275
}
6376

0 commit comments

Comments
 (0)