Merged PR 1244: #3022 ISA File Caching

#3022 ISA File Caching
This commit is contained in:
Nino Righi
2022-05-19 08:29:30 +00:00
committed by Lorenz Hilpert
parent 50cc17a44b
commit 39147d7afa
18 changed files with 224 additions and 26 deletions

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable, InjectionToken } from '@angular/core';
import { SignalrHub, SignalRHubOptions } from '@core/signalr';
import { merge, of } from 'rxjs';
import { filter, shareReplay, tap } from 'rxjs/operators';
import { filter, map, publishReplay, shareReplay, tap } from 'rxjs/operators';
import { EnvelopeDTO, MessageBoardItemDTO } from './defs';
export const NOTIFICATIONS_HUB_OPTIONS = new InjectionToken<SignalRHubOptions>('hub.notifications.options');
@@ -33,4 +33,21 @@ export class NotificationsHub extends SignalrHub {
}
return undefined;
}
updateNotification() {
this.notifications$ = this.notifications$.pipe(
map((data) => {
const notifications = data;
if (!!notifications?.data && !notifications?.data?.find((notification) => notification?.category === 'ISA-Update')) {
notifications.data.push({
category: 'ISA-Update',
type: 'update',
headline: 'Update Benachrichtigung',
text: 'Es steht eine aktuellere Version der ISA bereit. Bitte aktualisieren Sie die Anwendung.',
});
}
return notifications;
})
);
}
}

View File

@@ -6,17 +6,22 @@ import { ApplicationService } from '@core/application';
import { of } from 'rxjs';
import { Renderer2 } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ServiceWorkerModule, SwUpdate } from '@angular/service-worker';
import { NotificationsHub } from '@hub/notifications';
import { discardPeriodicTasks, fakeAsync, flush, tick } from '@angular/core/testing';
describe('AppComponent', () => {
let spectator: Spectator<AppComponent>;
let config: SpyObject<Config>;
let renderer: SpyObject<Renderer2>;
let applicationServiceMock: SpyObject<ApplicationService>;
let notificationsHubMock: SpyObject<NotificationsHub>;
let swUpdateMock: SpyObject<SwUpdate>;
const createComponent = createComponentFactory({
component: AppComponent,
imports: [CommonModule, RouterTestingModule],
providers: [],
mocks: [Config],
mocks: [Config, SwUpdate],
});
beforeEach(() => {
@@ -25,6 +30,10 @@ describe('AppComponent', () => {
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(undefined));
renderer = jasmine.createSpyObj('Renderer2', ['addClass', 'removeClass']);
notificationsHubMock = createSpyObject(NotificationsHub);
notificationsHubMock.notifications$ = of({});
swUpdateMock = createSpyObject(SwUpdate);
spectator = createComponent({
providers: [
{ provide: ApplicationService, useValue: applicationServiceMock },
@@ -32,6 +41,8 @@ describe('AppComponent', () => {
provide: Renderer2,
useValue: renderer,
},
{ provide: NotificationsHub, useValue: notificationsHubMock },
{ provide: SwUpdate, useValue: swUpdateMock },
],
});
config = spectator.inject(Config);
@@ -77,13 +88,45 @@ describe('AppComponent', () => {
});
});
// describe('sectionChangeHandler', () => {
// fit('should add class customer and remove class branch from body when section is customer', () => {
// spectator.component.sectionChangeHandler('customer');
// console.log(renderer);
// console.log('expect');
// expect(renderer.removeClass).toHaveBeenCalled();
// expect(renderer.addClass).toHaveBeenCalled();
// });
// });
describe('updateClient()', () => {
it('should call checkForUpdate() if SwUpdate.isEnabled is True', () => {
spyOn(spectator.component, 'checkForUpdate');
spyOn(spectator.component, 'initialCheckForUpdate');
(swUpdateMock as any).isEnabled = true;
spectator.component.updateClient();
expect(spectator.component.initialCheckForUpdate).toHaveBeenCalled();
expect(spectator.component.checkForUpdate).toHaveBeenCalled();
});
it('should not call checkForUpdate() if SwUpdate.isEnabled is False', () => {
spyOn(spectator.component, 'checkForUpdate');
spyOn(spectator.component, 'initialCheckForUpdate');
(swUpdateMock as any).isEnabled = false;
spectator.component.updateClient();
expect(spectator.component.initialCheckForUpdate).not.toHaveBeenCalled();
expect(spectator.component.checkForUpdate).not.toHaveBeenCalled();
});
});
describe('checkForUpdate', () => {
it('should call swUpdate.checkForUpdate() and notifications.updateNotification() every second', fakeAsync(() => {
swUpdateMock.checkForUpdate.and.returnValue(Promise.resolve());
spectator.component.checkForUpdates = 1000;
spectator.component.checkForUpdate();
spectator.detectChanges();
tick(1100);
expect(notificationsHubMock.updateNotification).toHaveBeenCalled();
discardPeriodicTasks();
}));
});
describe('initialCheckForUpdate', () => {
it('should call swUpdate.checkForUpdate()', () => {
swUpdateMock.checkForUpdate.and.returnValue(new Promise(undefined));
spectator.component.initialCheckForUpdate();
expect(swUpdateMock.checkForUpdate).toHaveBeenCalled();
});
});
});

View File

@@ -1,9 +1,12 @@
import { DOCUMENT } from '@angular/common';
import { Component, Inject, OnInit, Renderer2 } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { SwUpdate } from '@angular/service-worker';
import { ApplicationService } from '@core/application';
import { Config } from '@core/config';
import { NotificationsHub } from '@hub/notifications';
import packageInfo from 'package';
import { interval } from 'rxjs';
@Component({
selector: 'app-root',
@@ -11,13 +14,28 @@ import packageInfo from 'package';
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
_checkForUpdates: number = this._config.get('checkForUpdates');
get checkForUpdates(): number {
return this._checkForUpdates;
}
// For Unit Testing
set checkForUpdates(time: number) {
this._checkForUpdates = time;
}
constructor(
private readonly _config: Config,
private readonly _title: Title,
private readonly _appService: ApplicationService,
@Inject(DOCUMENT) private readonly _document: Document,
private readonly _renderer: Renderer2
) {}
private readonly _renderer: Renderer2,
private readonly _swUpdate: SwUpdate,
private readonly _notifications: NotificationsHub
) {
this.updateClient();
}
ngOnInit() {
this.setTitle();
@@ -43,4 +61,26 @@ export class AppComponent implements OnInit {
this._renderer.addClass(this._document.body, 'branch');
}
}
updateClient() {
this.checkForUpdate();
if (!this._swUpdate.isEnabled) {
return;
}
this.initialCheckForUpdate();
this.checkForUpdate();
}
checkForUpdate() {
interval(this._checkForUpdates).subscribe(() => {
this._swUpdate.checkForUpdate().then(() => {
this._notifications.updateNotification();
});
});
}
initialCheckForUpdate() {
this._swUpdate.checkForUpdate().then(() => location.reload());
}
}

View File

@@ -343,7 +343,7 @@ describe('ShellComponent', () => {
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
await spectator.component.closeProcess(1);
expect(router.navigate).not.toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalledWith(['/kunde', 'dashboard']);
});
it('should activate the next process when it was not the last process', async () => {

View File

@@ -97,7 +97,7 @@ export class ShellComponent {
}
async activateProcess(activatedProcessId: number) {
const latestCrumb = await this._breadcrumbService.getLastActivatedBreadcrumbByKey$(activatedProcessId).pipe(first()).toPromise();
const latestCrumb = await this._breadcrumbService?.getLastActivatedBreadcrumbByKey$(activatedProcessId)?.pipe(first()).toPromise();
if (latestCrumb) {
await this._router.navigate([latestCrumb.path], { queryParams: latestCrumb.params });

View File

@@ -1,5 +1,5 @@
{
"title": "ISA - Local",
"title": "ISA - Feature",
"@cdn/product-image": {
"url": "https://produktbilder.paragon-data.net"
},
@@ -58,5 +58,6 @@
"taskCalendar": 3000,
"remission": 4000
}
}
},
"checkForUpdates": 3600000
}

View File

@@ -17,5 +17,6 @@
"skipNegotiation": true
}
}
}
},
"checkForUpdates": 3600000
}

View File

@@ -58,5 +58,6 @@
"taskCalendar": 3000,
"remission": 4000
}
}
}
},
"checkForUpdates": 3600000
}

View File

@@ -17,5 +17,6 @@
"skipNegotiation": true
}
}
}
},
"checkForUpdates": 3600000
}

View File

@@ -17,5 +17,6 @@
"skipNegotiation": true
}
}
}
},
"checkForUpdates": 3600000
}

View File

@@ -58,5 +58,6 @@
"taskCalendar": 3000,
"remission": 4000
}
}
},
"checkForUpdates": 3600000
}

View File

@@ -0,0 +1,25 @@
<div class="header">
<div class="notification-icon">
<span class="notification-counter">{{ notifications.length }}</span>
<ui-icon icon="notification" size="26px"></ui-icon>
</div>
<h2>ISA-Update</h2>
</div>
<hr />
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
<div class="notification-headline">
<h1>{{ notification.headline }}</h1>
</div>
<div class="notification-text">{{ notification.text }}</div>
<hr />
</ng-container>
</div>
<div class="actions">
<button class="cta-primary" (click)="reload()">
Aktualisieren
</button>
</div>

View File

@@ -0,0 +1,41 @@
import { createComponentFactory, Spectator } from '@ngneat/spectator';
import { CommonModule } from '@angular/common';
import { UiIconModule } from '@ui/icon';
import { ModalNotificationsUpdateGroupComponent } from './notifications-update-group.component';
describe('ModalNotificationsUpdateGroupComponent', () => {
let spectator: Spectator<ModalNotificationsUpdateGroupComponent>;
const createComponent = createComponentFactory({
component: ModalNotificationsUpdateGroupComponent,
imports: [CommonModule, UiIconModule],
});
beforeEach(() => {
spectator = createComponent({ props: { notifications: [] } });
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('notifications input', () => {
it('should display the right notification-counter value based on the length of the input array', () => {
spectator.setInput({ notifications: [{}, {}, {}] });
expect(spectator.query('.notification-counter')).toHaveText('3');
});
it('should not display notification-counter if input array has length 0', () => {
spectator.setInput({ notifications: [] });
expect(spectator.query('.notification-counter')).toHaveText('');
});
it('should render notification-headline and notification-text based on the input array', () => {
const notifications = [{}, {}];
spectator.setInput({ notifications });
spectator.detectComponentChanges();
expect(spectator.queryAll('.notification-headline')).toHaveLength(notifications.length);
expect(spectator.queryAll('.notification-text')).toHaveLength(notifications.length);
});
});
});

View File

@@ -0,0 +1,18 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
@Component({
selector: 'modal-notifications-update-group',
templateUrl: 'notifications-update-group.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ModalNotificationsUpdateGroupComponent {
@Input()
notifications: MessageBoardItemDTO[];
constructor() {}
reload() {
location.reload();
}
}

View File

@@ -12,6 +12,10 @@
</ng-container>
<ng-container [ngSwitch]="activeCard$ | async">
<modal-notifications-update-group
*ngSwitchCase="'ISA-Update'"
[notifications]="activeNotifications$ | async"
></modal-notifications-update-group>
<modal-notifications-reservation-group
*ngSwitchCase="'Reservierungsanfragen'"
[notifications]="activeNotifications$ | async"

View File

@@ -12,7 +12,8 @@ modal-notifications {
modal-notifications-remission-group,
modal-notifications-reservation-group,
modal-notifications-task-calendar-group {
modal-notifications-task-calendar-group,
modal-notifications-update-group {
@apply flex flex-col relative pb-2;
.header {
@@ -57,7 +58,8 @@ modal-notifications {
}
}
modal-notifications-list-item {
modal-notifications-list-item,
modal-notifications-update-group {
@apply flex flex-col relative py-1 px-4;
.notification-headline {

View File

@@ -7,6 +7,7 @@ import { ModalNotificationsListItemComponent } from './notifications-list-item/n
import { ModalNotificationsRemissionGroupComponent } from './notifications-remission-group/notifications-remission-group.component';
import { ModalNotificationsReservationGroupComponent } from './notifications-reservation-group/notifications-reservation-group.component';
import { ModalNotificationsTaskCalendarGroupComponent } from './notifications-task-calendar-group/notifications-task-calendar-group.component';
import { ModalNotificationsUpdateGroupComponent } from './notifications-update-group/notifications-update-group.component';
import { ModalNotificationsComponent } from './notifications.component';
@NgModule({
@@ -16,6 +17,7 @@ import { ModalNotificationsComponent } from './notifications.component';
ModalNotificationsReservationGroupComponent,
ModalNotificationsRemissionGroupComponent,
ModalNotificationsTaskCalendarGroupComponent,
ModalNotificationsUpdateGroupComponent,
ModalNotificationsListItemComponent,
],
exports: [
@@ -23,6 +25,7 @@ import { ModalNotificationsComponent } from './notifications.component';
ModalNotificationsReservationGroupComponent,
ModalNotificationsRemissionGroupComponent,
ModalNotificationsTaskCalendarGroupComponent,
ModalNotificationsUpdateGroupComponent,
ModalNotificationsListItemComponent,
],
})

View File

@@ -115,7 +115,6 @@ jobs:
demands:
- Agent.OS -equals Linux
- docker
# condition: eq(variables['Build.SourceBranch'], 'refs/heads/develop')
condition: and(ne(variables['Build.SourceBranch'], 'refs/heads/integration'), ne(variables['Build.SourceBranch'], 'refs/heads/master'), not(startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')), not(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')))
steps:
- task: npmAuthenticate@0