Skip to content

Commit 9988439

Browse files
authored
Split header into reusable components + add space for extra icons (#9)
1 parent 62815ef commit 9988439

37 files changed

Lines changed: 1157 additions & 1003 deletions
Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,2 @@
1-
<appshell-platform-layout
2-
[headerVariant]="headerVariant"
3-
[applicationSymbol]="applicationSymbol"
4-
[applicationLogo]="applicationLogo"
5-
[applicationName]="applicationName"
6-
[applicationNameLink]="applicationNameLink"
7-
[appShellHelpLink]="appShellHelpLink"
8-
[appShellNotificationsLink]="appShellNotificationsLink"
9-
[appShellNotificationsCount]="appShellNotificationsCount"
10-
[headerLinks]="headerLinks"
11-
[headerProjectPicker]="headerProjectPicker"
12-
[headerSecondaryPicker]="headerSecondaryPicker"
13-
[sidenavSections]="sidenavSections"
14-
[sidenavLinks]="sidenavLinks"
15-
[loggedUser]="loggedUser"
16-
[isPlatformPickerOpened]="false"
17-
(userLoggedIn)="login()"
18-
(userLoggedOut)="logout()"
19-
(userProjectPick)="projectPickerChanged($event)"
20-
(userSecondaryPick)="secondaryPickerChanged($event)">
21-
</appshell-platform-layout>
1+
<router-outlet></router-outlet>
222
<appshell-toasts [toastsLimit]="toastLimitInScreen"></appshell-toasts>
Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +0,0 @@
1-
appshell-platform-layout {
2-
display: flex;
3-
flex-direction: column;
4-
min-height: 100vh;
5-
}
Lines changed: 9 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,27 @@
1-
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
22
import { AppComponent } from './app.component';
33
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
44
import { provideHttpClient } from '@angular/common/http';
5-
import { AzureService } from './services/azure.service';
6-
import { of, Subject } from 'rxjs';
7-
import { NatsMessage, NatsService } from './services/nats.service';
8-
import { AppShellToastsComponent, AppShellToastService, AppShellUser } from 'ngx-appshell';
5+
import { of } from 'rxjs';
6+
import { AppShellToastsComponent, AppShellToastService } from 'ngx-appshell';
7+
import { RouterModule } from '@angular/router';
98

109
describe('AppComponent', () => {
1110
let component: AppComponent;
1211
let fixture: ComponentFixture<AppComponent>;
13-
let mockAzureService: jasmine.SpyObj<AzureService>;
14-
let mockNatsService: jasmine.SpyObj<NatsService>;
1512
let mockToastService: jasmine.SpyObj<AppShellToastService>;
16-
let azureLoggedUser$: Subject<AppShellUser>;
17-
let natsLiveMessage$: Subject<NatsMessage | null>;
18-
let natsMessageCount$: Subject<number>;
1913

2014
beforeEach(async () => {
21-
azureLoggedUser$ = new Subject<AppShellUser>();
22-
natsLiveMessage$ = new Subject<NatsMessage | null>();
23-
natsMessageCount$ = new Subject<number>();
24-
mockAzureService = jasmine.createSpyObj('AzureService', ['initialize', 'login', 'logout'], { loggedUser$: azureLoggedUser$.asObservable() });
25-
mockNatsService = jasmine.createSpyObj('NatsService', ['initialize', 'initializeUser', 'readMessages', 'isValidMessage'], { liveMessage$: natsLiveMessage$.asObservable(), unreadMessagesCount$: natsMessageCount$.asObservable() });
2615
mockToastService = jasmine.createSpyObj('AppShellToastService', ['showToast'], { toasts$: of([]) });
2716

2817
await TestBed.configureTestingModule({
29-
imports: [AppComponent, BrowserAnimationsModule, AppShellToastsComponent],
18+
imports: [AppComponent, BrowserAnimationsModule, AppShellToastsComponent, RouterModule.forRoot([])],
3019
providers: [
31-
{ provide: AzureService, useValue: mockAzureService },
32-
{ provide: NatsService, useValue: mockNatsService },
3320
{ provide: AppShellToastService, useValue: mockToastService },
3421
provideHttpClient()
3522
]
3623
}).compileComponents();
37-
24+
3825
fixture = TestBed.createComponent(AppComponent);
3926
component = fixture.componentInstance;
4027
fixture.detectChanges();
@@ -45,76 +32,8 @@ describe('AppComponent', () => {
4532
expect(component).toBeTruthy();
4633
});
4734

48-
it('should initialize the service and subscribe to loggedUser$ on ngOnInit', () => {
49-
component.ngOnInit();
50-
expect(mockAzureService.initialize).toHaveBeenCalled();
51-
expect(component.loggedUser).toBeNull();
52-
});
53-
54-
it('should call login method of AzureService on login', () => {
55-
component.login();
56-
expect(mockAzureService.login).toHaveBeenCalled();
57-
});
58-
59-
it('should call logout method of AzureService on logout', () => {
60-
component.logout();
61-
expect(mockAzureService.logout).toHaveBeenCalled();
35+
it('should have a toastLimitInScreen value', () => {
36+
expect(component.toastLimitInScreen).toBeGreaterThan(0);
6237
});
63-
64-
it('should complete the _destroying$ subject on ngOnDestroy', () => {
65-
const completeSpy = spyOn(component['_destroying$'], 'complete');
66-
component.ngOnDestroy();
67-
expect(completeSpy).toHaveBeenCalled();
68-
});
69-
70-
it('should log the selected option when pickerChanged is called', () => {
71-
const consoleSpy = spyOn(console, 'log');
72-
const testOption = 'Test Option';
73-
74-
component.projectPickerChanged(testOption);
75-
76-
expect(consoleSpy).toHaveBeenCalledWith(testOption);
77-
});
78-
79-
it('should log the selected option when secondaryPickerChanged is called', () => {
80-
const consoleSpy = spyOn(console, 'log');
81-
const testOption = 'Test Option';
82-
83-
component.secondaryPickerChanged(testOption);
84-
85-
expect(consoleSpy).toHaveBeenCalledWith(testOption);
86-
});
87-
88-
it('should call initializeUser with a valid NATS user name', fakeAsync(() => {
89-
azureLoggedUser$.next({fullName: 'Fake', username: 'fake.user@fakemail.com'} as AppShellUser);
90-
natsMessageCount$.next(0);
91-
tick(5000);
92-
expect(mockNatsService.initializeUser).toHaveBeenCalledWith('fake_user');
93-
expect(mockToastService.showToast).not.toHaveBeenCalled();
94-
}));
95-
96-
it('should show a toast with the initial notifications count', fakeAsync(() => {
97-
azureLoggedUser$.next({fullName: 'Fake', username: 'fake.user@fakemail.com'} as AppShellUser);
98-
natsMessageCount$.next(3);
99-
tick(5000);
100-
expect(mockNatsService.initializeUser).toHaveBeenCalledWith('fake_user');
101-
expect(mockToastService.showToast).toHaveBeenCalled();
102-
}));
103-
104-
it('should manage properly the received message from nats service', fakeAsync(() => {
105-
mockToastService.showToast.calls.reset();
106-
natsLiveMessage$.next(null);
107-
expect(mockToastService.showToast).not.toHaveBeenCalled();
108-
natsLiveMessage$.next({data: null} as NatsMessage);
109-
expect(mockToastService.showToast).not.toHaveBeenCalled();
110-
mockNatsService.isValidMessage.and.returnValue(false);
111-
natsLiveMessage$.next({data: {}} as NatsMessage);
112-
expect(mockToastService.showToast).not.toHaveBeenCalled();
113-
mockNatsService.isValidMessage.and.throwError(new Error('Invalid message format'));
114-
natsLiveMessage$.next({data: {}} as NatsMessage);
115-
expect(mockToastService.showToast).not.toHaveBeenCalled();
116-
mockNatsService.isValidMessage.and.returnValue(true);
117-
natsLiveMessage$.next({data: {}} as NatsMessage);
118-
expect(mockToastService.showToast).toHaveBeenCalled();
119-
}));
12038
});
39+
Lines changed: 7 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,22 @@
1-
import { Component, OnDestroy, OnInit } from '@angular/core';
2-
import { CommonModule } from '@angular/common';
1+
import { Component, OnInit } from '@angular/core';
2+
import { RouterOutlet } from '@angular/router';
33
import { AppShellConfiguration as AppShellConfig } from './appshell.configuration';
4-
import { AppShellLinksGroup, AppShellLink, AppShellUser, AppShellPlatformLayoutComponent, AppShellPicker, AppShellToastService, AppShellToastsComponent, AppShellNotification } from 'ngx-appshell';
5-
import { Subject, Subscription } from 'rxjs';
6-
import { AzureService } from './services/azure.service';
7-
import { NatsService } from './services/nats.service';
4+
import { AppShellToastsComponent } from 'ngx-appshell';
85
import { MatIconRegistry } from '@angular/material/icon';
9-
import { AppConfigService } from './services/app-config.service';
106

117
@Component({
128
selector: 'app-root',
13-
imports: [CommonModule, AppShellPlatformLayoutComponent, AppShellToastsComponent],
9+
imports: [RouterOutlet, AppShellToastsComponent],
1410
templateUrl: './app.component.html',
1511
styleUrl: './app.component.scss'
1612
})
17-
export class AppComponent implements OnInit, OnDestroy {
13+
export class AppComponent implements OnInit {
1814

19-
headerVariant: string = AppShellConfig.headerVariant;
20-
applicationSymbol?: string = AppShellConfig.applicationSymbol;
21-
applicationLogo?: string = AppShellConfig.applicationLogo;
22-
applicationName: string = AppShellConfig.applicationName;
23-
applicationNameLink: string = AppShellConfig.applicationNameLink;
24-
appShellHelpLink: AppShellLink = AppShellConfig.appShellHelpLink;
25-
headerLinks: AppShellLink[] = AppShellConfig.headerLinks;
26-
sidenavSections: AppShellLinksGroup[] = AppShellConfig.sidenavSections;
27-
sidenavLinks: AppShellLinksGroup = AppShellConfig.sidenavLinks;
2815
toastLimitInScreen: number = AppShellConfig.toastLimitInScreen;
29-
headerProjectPicker: AppShellPicker;
30-
headerSecondaryPicker: AppShellPicker;
31-
loggedUser: AppShellUser|null = null;
32-
appShellNotificationsLink: AppShellLink = AppShellConfig.appShellNotificationsLink;
33-
appShellNotificationsCount: number = 0;
34-
35-
private readonly _destroying$ = new Subject<void>();
36-
private natsUrl: string | undefined;
37-
private unreadMessagesCountSubscription!: Subscription;
38-
private liveMessageSubscription!: Subscription;
3916

40-
constructor(
41-
private azureService: AzureService,
42-
private toastService: AppShellToastService,
43-
private natsService: NatsService,
44-
private readonly appConfigService :AppConfigService,
45-
private matIconReg: MatIconRegistry
46-
) {
47-
this.headerSecondaryPicker = {
48-
label: 'Catalog: ',
49-
options: ['Option 1', 'Option 2', 'Option 3'],
50-
selected: 'Option 2'
51-
};
52-
this.headerProjectPicker = {
53-
label: 'Project: ',
54-
options: ['Value 1', 'Project 2', 'Project 3',
55-
'Project 4', 'Project 5', 'Project 6',
56-
'Project 7', 'Project 8', 'Project 9',
57-
'Project 10', 'Project 11', 'Project 12',
58-
'Project 13', 'Project 14', 'Project 15'],
59-
selected: 'Project 2',
60-
noOptionsMessage: 'You don\'t have access to any projects in the Marketplace.<br/><br/>You can either <a href="#" target="_blank">create a project</a> or <a href="#" target="_blank">request access</a> to an existing project.',
61-
noFilteredOptionsMessage: 'No projects match the search term.'
62-
};
63-
this.natsUrl = this.appConfigService.getConfig()?.natsUrl;
64-
}
17+
constructor(private matIconReg: MatIconRegistry) {}
6518

66-
async ngOnInit(): Promise<void> {
19+
ngOnInit(): void {
6720
this.matIconReg.setDefaultFontSetClass('material-symbols-outlined');
68-
await this.natsService.initialize(this.natsUrl!);
69-
this.azureService.initialize();
70-
this.azureService.loggedUser$.subscribe((user) => {
71-
this.loggedUser = user;
72-
this.initUserNotifications(user);
73-
});
74-
this.initializeNatsListeners();
75-
}
76-
77-
login() {
78-
this.azureService.login();
79-
}
80-
81-
logout() {
82-
this.azureService.logout();
83-
}
84-
85-
ngOnDestroy(): void {
86-
this._destroying$.next(undefined);
87-
this._destroying$.complete();
88-
this.liveMessageSubscription.unsubscribe();
89-
this.unreadMessagesCountSubscription.unsubscribe();
90-
}
91-
92-
private initUserNotifications(user: AppShellUser|null) {
93-
if(user) {
94-
// We convert the username to a valid NATS user name based on their validations:
95-
// validBucketRe = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
96-
// validKeyRe = regexp.MustCompile(`^[-/_=\.a-zA-Z0-9]+$`)
97-
const natsUser = user.username.split('@')[0].replace(/[^a-zA-Z0-9_-]/g, '_')
98-
this.natsService.initializeUser(natsUser);
99-
100-
// On login, leave 4 seconds for the NATS connection to be established and show a toast with the unread messages count
101-
setTimeout(() => {
102-
if(this.appShellNotificationsCount > 0) {
103-
const notification = {
104-
id: new Date().getTime().toString() + `-logged`,
105-
title: `You have ${this.appShellNotificationsCount} unread notifications`,
106-
read: false,
107-
subject: 'only-toast'
108-
} as AppShellNotification;
109-
this.toastService.showToast(notification, 8000);
110-
}
111-
}, 4000);
112-
}
113-
}
114-
115-
private initializeNatsListeners() {
116-
this.unreadMessagesCountSubscription = this.natsService.unreadMessagesCount$.subscribe((count) => {
117-
this.appShellNotificationsCount = count;
118-
});
119-
this.liveMessageSubscription = this.natsService.liveMessage$.subscribe((message) => {
120-
if (!message || !message.data) {
121-
return;
122-
}
123-
try {
124-
if (this.natsService.isValidMessage(message.data)) {
125-
console.log('Received valid message:', message);
126-
const notification = {
127-
id: message.id,
128-
type: message.data.type,
129-
title: `You have 1 new notification`,
130-
date: new Date(message.data.date),
131-
read: message.read,
132-
subject: message.subject
133-
};
134-
// If you want to show the actual notification, you can show message.data instead of notification
135-
this.toastService.showToast(notification, 8000);
136-
} else {
137-
console.log('Invalid message format:', message);
138-
}
139-
} catch (error) {
140-
console.log('Invalid message format:', message);
141-
}
142-
});
143-
}
144-
145-
projectPickerChanged(option: string) {
146-
console.log(option);
147-
}
148-
149-
secondaryPickerChanged(option: string) {
150-
console.log(option);
15121
}
15222
}

projects/ngx-appshell-example/src/app/app.routes.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,24 @@ import { Routes } from '@angular/router';
22
import { ProductCatalogScreenComponent } from './screens/product-catalog-screen/product-catalog-screen.component';
33
import { ProductViewScreenComponent } from './screens/product-view-screen/product-view-screen.component';
44
import { NotificationsScreenComponent } from './screens/notifications-screen/notifications-screen.component';
5+
import { PlatformShellComponent } from './screens/platform-shell/platform-shell.component';
6+
import { BasicShellComponent } from './screens/basic-shell/basic-shell.component';
57

68
export const routes: Routes = [
7-
{ path: '', component: ProductCatalogScreenComponent },
8-
{ path: 'item/:id', component: ProductViewScreenComponent },
9-
{ path: 'notifications', component: NotificationsScreenComponent }
9+
{
10+
path: '',
11+
component: PlatformShellComponent,
12+
children: [
13+
{ path: '', component: ProductCatalogScreenComponent },
14+
{ path: 'item/:id', component: ProductViewScreenComponent },
15+
{ path: 'notifications', component: NotificationsScreenComponent },
16+
],
17+
},
18+
{
19+
path: 'basic-shell',
20+
component: BasicShellComponent,
21+
children: [
22+
{ path: '', component: ProductCatalogScreenComponent },
23+
],
24+
},
1025
];

projects/ngx-appshell-example/src/app/appshell.configuration.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ export class AppShellConfiguration {
1919
};
2020
public static headerLinks = [
2121
{label: 'About Us', anchor: '/'},
22-
{label: 'Feedback', anchor: '/product'},
22+
{label: 'Feedback', anchor: '/basic-shell'},
2323
{label: 'Contact', anchor: '/contact'}
2424
];
2525
public static sidenavSections = [
2626
{
2727
label: 'SECTION 1',
2828
links: [
29-
{label: 'Page 1', anchor: '/'},
30-
{label: 'Page 2', anchor: '/product'},
29+
{label: 'Platform Layout', anchor: '/'},
30+
{label: 'Basic Layout', anchor: '/basic-shell'},
3131
{label: 'Page 3', anchor: 'https://www.google.com'}
3232
]
3333
},
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<appshell-layout
2+
[headerVariant]="headerVariant"
3+
[applicationSymbol]="applicationSymbol"
4+
[applicationName]="applicationName"
5+
[applicationNameLink]="applicationNameLink"
6+
[appShellHelpLink]="appShellHelpLink"
7+
[appShellNotificationsLink]="appShellNotificationsLink"
8+
[appShellNotificationsCount]="appShellNotificationsCount"
9+
[headerLinks]="headerLinks"
10+
[headerPicker]="headerPicker"
11+
[sidenavSections]="sidenavSections"
12+
[sidenavLinks]="sidenavLinks"
13+
[loggedUser]="loggedUser"
14+
(userLoggedIn)="onUserLoggedIn()"
15+
(userLoggedOut)="onUserLoggedOut()"
16+
(userPick)="onUserPick($event)">
17+
<div id="extra-icons-ctn" ngProjectAs="extra-icons-header">
18+
<button class="extra-icon-btn" (click)="extraHeaderIconClick('1')"><appshell-icon [icon]="'counter_1'"></appshell-icon></button>
19+
<button class="extra-icon-btn" (click)="extraHeaderIconClick('2')"><appshell-icon [icon]="'counter_2'"></appshell-icon></button>
20+
</div>
21+
</appshell-layout>

0 commit comments

Comments
 (0)