diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html new file mode 100644 index 0000000000..3e360cc55e --- /dev/null +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html @@ -0,0 +1,27 @@ +
+
+
+ + + {{'system-wide-alert-banner.countdown.prefix' | translate }} + + + {{'system-wide-alert-banner.countdown.days' | translate: { + days: countDownDays|async + } }} + + + {{'system-wide-alert-banner.countdown.hours' | translate: { + hours: countDownHours| async + } }} + + + {{'system-wide-alert-banner.countdown.minutes' | translate: { + minutes: countDownMinutes|async + } }} + + + {{(systemWideAlert$ |async)?.message}} +
+
+
diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.scss b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts new file mode 100644 index 0000000000..d27e5379e9 --- /dev/null +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts @@ -0,0 +1,111 @@ +import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { SystemWideAlertBannerComponent } from './system-wide-alert-banner.component'; +import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; +import { SystemWideAlert } from '../system-wide-alert.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { utcToZonedTime } from 'date-fns-tz'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; + + +describe('SystemWideAlertBannerComponent', () => { + let comp: SystemWideAlertBannerComponent; + let fixture: ComponentFixture; + let systemWideAlertDataService: SystemWideAlertDataService; + + let systemWideAlert: SystemWideAlert; + let scheduler: TestScheduler; + + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + + const countDownDate = new Date(); + countDownDate.setDate(countDownDate.getDate() + 1); + countDownDate.setHours(countDownDate.getHours() + 1); + countDownDate.setMinutes(countDownDate.getMinutes() + 1); + + systemWideAlert = Object.assign(new SystemWideAlert(), { + alertId: 1, + message: 'Test alert message', + active: true, + countdownTo: utcToZonedTime(countDownDate, 'UTC').toISOString() + }); + + systemWideAlertDataService = jasmine.createSpyObj('systemWideAlertDataService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList([systemWideAlert])), + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [SystemWideAlertBannerComponent], + providers: [ + {provide: SystemWideAlertDataService, useValue: systemWideAlertDataService}, + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SystemWideAlertBannerComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('init', () => { + it('should init the comp', () => { + expect(comp).toBeTruthy(); + }); + it('should set the time countdown parts in their respective behaviour subjects', fakeAsync(() => { + spyOn(comp.countDownDays, 'next'); + spyOn(comp.countDownHours, 'next'); + spyOn(comp.countDownMinutes, 'next'); + comp.ngOnInit(); + tick(2000); + expect(comp.countDownDays.next).toHaveBeenCalled(); + expect(comp.countDownHours.next).toHaveBeenCalled(); + expect(comp.countDownMinutes.next).toHaveBeenCalled(); + discardPeriodicTasks(); + + })); + }); + + describe('banner', () => { + it('should display the alert message and the timer', () => { + comp.countDownDays.next(1); + comp.countDownHours.next(1); + comp.countDownMinutes.next(1); + fixture.detectChanges(); + + const banner = fixture.debugElement.queryAll(By.css('span')); + expect(banner.length).toEqual(6); + + expect(banner[0].nativeElement.innerHTML).toContain('system-wide-alert-banner.countdown.prefix'); + expect(banner[0].nativeElement.innerHTML).toContain('system-wide-alert-banner.countdown.days'); + expect(banner[0].nativeElement.innerHTML).toContain('system-wide-alert-banner.countdown.hours'); + expect(banner[0].nativeElement.innerHTML).toContain('system-wide-alert-banner.countdown.minutes'); + + expect(banner[5].nativeElement.innerHTML).toContain(systemWideAlert.message); + }); + + it('should display the alert message but no timer when no timer is present', () => { + comp.countDownDays.next(0); + comp.countDownHours.next(0); + comp.countDownMinutes.next(0); + fixture.detectChanges(); + + const banner = fixture.debugElement.queryAll(By.css('span')); + expect(banner.length).toEqual(2); + expect(banner[1].nativeElement.innerHTML).toContain(systemWideAlert.message); + }); + + it('should not display an alert when none is present', () => { + comp.systemWideAlert$.next(null); + fixture.detectChanges(); + + const banner = fixture.debugElement.queryAll(By.css('span')); + expect(banner.length).toEqual(0); + }); + }); +}); diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts new file mode 100644 index 0000000000..b405957c54 --- /dev/null +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts @@ -0,0 +1,113 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; +import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { filter, map, switchMap } from 'rxjs/operators'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { SystemWideAlert } from '../system-wide-alert.model'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { BehaviorSubject, EMPTY, interval, Subscription } from 'rxjs'; +import { zonedTimeToUtc } from 'date-fns-tz'; + +/** + * Component responsible for rendering a banner and the countdown for an active system-wide alert + */ +@Component({ + selector: 'ds-system-wide-alert-banner', + styleUrls: ['./system-wide-alert-banner.component.scss'], + templateUrl: './system-wide-alert-banner.component.html' +}) +export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { + + /** + * BehaviorSubject that keeps track of the currently configured system-wide alert + */ + systemWideAlert$ = new BehaviorSubject(undefined); + + /** + * BehaviorSubject that keeps track of the amount of minutes left to count down to + */ + countDownMinutes = new BehaviorSubject(0); + + /** + * BehaviorSubject that keeps track of the amount of hours left to count down to + */ + countDownHours = new BehaviorSubject(0); + + /** + * BehaviorSubject that keeps track of the amount of days left to count down to + */ + countDownDays = new BehaviorSubject(0); + + /** + * List of subscriptions + */ + subscriptions: Subscription[] = []; + + constructor( + protected systemWideAlertDataService: SystemWideAlertDataService + ) { + } + + ngOnInit() { + this.subscriptions.push(this.systemWideAlertDataService.findAll().pipe( + getAllSucceededRemoteDataPayload(), + map((payload: PaginatedList) => payload.page), + filter((page) => isNotEmpty(page)), + map((page) => page[0]) + ).subscribe((alert: SystemWideAlert) => { + this.systemWideAlert$.next(alert); + })); + + this.subscriptions.push(this.systemWideAlert$.pipe( + switchMap((alert: SystemWideAlert) => { + if (hasValue(alert) && hasValue(alert.countdownTo)) { + const date = zonedTimeToUtc(alert.countdownTo, 'UTC'); + const timeDifference = date.getTime() - new Date().getTime(); + if (timeDifference > 0) { + return interval(1000); + } + } + // Reset the countDown times to 0 and return EMPTY to prevent unnecessary countdown calculations + this.countDownDays.next(0); + this.countDownHours.next(0); + this.countDownMinutes.next(0); + return EMPTY; + }) + ).subscribe(() => { + this.setTimeDifference(this.systemWideAlert$.getValue().countdownTo); + })); + } + + /** + * Helper method to calculate the time difference between the countdown date from the system-wide alert and "now" + * @param countdownTo - The date to count down to + */ + private setTimeDifference(countdownTo: string) { + const date = zonedTimeToUtc(countdownTo, 'UTC'); + + const timeDifference = date.getTime() - new Date().getTime(); + this.allocateTimeUnits(timeDifference); + } + + /** + * Helper method to push how many days, hours and minutes are left in the countdown to their respective behaviour subject + * @param timeDifference - The time difference to calculate and push the time units for + */ + private allocateTimeUnits(timeDifference) { + const minutes = Math.floor((timeDifference) / (1000 * 60) % 60); + const hours = Math.floor((timeDifference) / (1000 * 60 * 60) % 24); + const days = Math.floor((timeDifference) / (1000 * 60 * 60 * 24)); + + this.countDownMinutes.next(minutes); + this.countDownHours.next(hours); + this.countDownDays.next(days); + } + + ngOnDestroy(): void { + this.subscriptions.forEach((sub: Subscription) => { + if (hasValue(sub)) { + sub.unsubscribe(); + } + }); + } +} diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html new file mode 100644 index 0000000000..2821a14bf8 --- /dev/null +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html @@ -0,0 +1,74 @@ +
+ +
+
+
+
+ +
+
+
+
+ + +
+ + {{ 'system-wide-alert.form.error.message' | translate }} + +
+
+
+
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ {{'system-wide-alert.form.label.countdownTo.hint' | translate}} +
+ +
+ + +
+ +
diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.scss b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.scss new file mode 100644 index 0000000000..ce3e065caf --- /dev/null +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.scss @@ -0,0 +1,3 @@ +.timepicker-margin { + margin-top: -38px; +} diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts new file mode 100644 index 0000000000..435aef3a7d --- /dev/null +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts @@ -0,0 +1,219 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; +import { SystemWideAlert } from '../system-wide-alert.model'; +import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { TranslateModule } from '@ngx-translate/core'; +import { SystemWideAlertFormComponent } from './system-wide-alert-form.component'; +import { RequestService } from '../../core/data/request.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { Router } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { SharedModule } from '../../shared/shared.module'; +import { UiSwitchModule } from 'ngx-ui-switch'; + +describe('SystemWideAlertFormComponent', () => { + let comp: SystemWideAlertFormComponent; + let fixture: ComponentFixture; + let systemWideAlertDataService: SystemWideAlertDataService; + + let systemWideAlert: SystemWideAlert; + let requestService: RequestService; + let notificationsService; + let router; + + + beforeEach(waitForAsync(() => { + + const countDownDate = new Date(); + countDownDate.setDate(countDownDate.getDate() + 1); + countDownDate.setHours(countDownDate.getHours() + 1); + countDownDate.setMinutes(countDownDate.getMinutes() + 1); + + systemWideAlert = Object.assign(new SystemWideAlert(), { + alertId: 1, + message: 'Test alert message', + active: true, + countdownTo: utcToZonedTime(countDownDate, 'UTC').toISOString() + }); + + systemWideAlertDataService = jasmine.createSpyObj('systemWideAlertDataService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList([systemWideAlert])), + put: createSuccessfulRemoteDataObject$(systemWideAlert), + create: createSuccessfulRemoteDataObject$(systemWideAlert) + }); + + requestService = jasmine.createSpyObj('requestService', ['setStaleByHrefSubstring']); + + notificationsService = new NotificationsServiceStub(); + router = new RouterStub(); + + TestBed.configureTestingModule({ + imports: [FormsModule, SharedModule, UiSwitchModule, TranslateModule.forRoot()], + declarations: [SystemWideAlertFormComponent], + providers: [ + {provide: SystemWideAlertDataService, useValue: systemWideAlertDataService}, + {provide: NotificationsService, useValue: notificationsService}, + {provide: Router, useValue: router}, + {provide: RequestService, useValue: requestService}, + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SystemWideAlertFormComponent); + comp = fixture.componentInstance; + + spyOn(comp, 'createForm').and.callThrough(); + spyOn(comp, 'initFormValues').and.callThrough(); + + fixture.detectChanges(); + }); + + describe('init', () => { + it('should init the comp', () => { + expect(comp).toBeTruthy(); + }); + it('should create the form and init the values based on an existing alert', () => { + expect(comp.createForm).toHaveBeenCalled(); + expect(comp.initFormValues).toHaveBeenCalledWith(systemWideAlert); + }); + }); + + describe('createForm', () => { + it('should create the form', () => { + const now = new Date(); + + comp.createForm(); + expect(comp.formMessage.value).toEqual(''); + expect(comp.formActive.value).toEqual(false); + expect(comp.time).toEqual({hour: now.getHours(), minute: now.getMinutes()}); + expect(comp.date).toEqual({year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate()}); + }); + }); + + describe('initFormValues', () => { + it('should fill in the form based on the provided system-wide alert', () => { + comp.initFormValues(systemWideAlert); + + const countDownTo = zonedTimeToUtc(systemWideAlert.countdownTo, 'UTC'); + + expect(comp.formMessage.value).toEqual(systemWideAlert.message); + expect(comp.formActive.value).toEqual(true); + expect(comp.time).toEqual({hour: countDownTo.getHours(), minute: countDownTo.getMinutes()}); + expect(comp.date).toEqual({ + year: countDownTo.getFullYear(), + month: countDownTo.getMonth() + 1, + day: countDownTo.getDate() + }); + }); + }); + describe('save', () => { + it('should update the exising alert with the form values and show a success notification on success', () => { + spyOn(comp, 'back'); + comp.currentAlert = systemWideAlert; + + comp.formMessage.patchValue('New message'); + comp.formActive.patchValue(true); + comp.time = {hour: 4, minute: 26}; + comp.date = {year: 2023, month: 1, day: 25}; + + const expectedAlert = new SystemWideAlert(); + expectedAlert.alertId = systemWideAlert.alertId; + expectedAlert.message = 'New message'; + expectedAlert.active = true; + const countDownTo = new Date(2023, 0, 25, 4, 26); + expectedAlert.countdownTo = utcToZonedTime(countDownTo, 'UTC').toUTCString(); + + comp.save(); + + expect(systemWideAlertDataService.put).toHaveBeenCalledWith(expectedAlert); + expect(notificationsService.success).toHaveBeenCalled(); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); + expect(comp.back).toHaveBeenCalled(); + }); + it('should update the exising alert with the form values and show a error notification on error', () => { + spyOn(comp, 'back'); + (systemWideAlertDataService.put as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); + comp.currentAlert = systemWideAlert; + + comp.formMessage.patchValue('New message'); + comp.formActive.patchValue(true); + comp.time = {hour: 4, minute: 26}; + comp.date = {year: 2023, month: 1, day: 25}; + + const expectedAlert = new SystemWideAlert(); + expectedAlert.alertId = systemWideAlert.alertId; + expectedAlert.message = 'New message'; + expectedAlert.active = true; + const countDownTo = new Date(2023, 0, 25, 4, 26); + expectedAlert.countdownTo = utcToZonedTime(countDownTo, 'UTC').toUTCString(); + + comp.save(); + + expect(systemWideAlertDataService.put).toHaveBeenCalledWith(expectedAlert); + expect(notificationsService.error).toHaveBeenCalled(); + expect(requestService.setStaleByHrefSubstring).not.toHaveBeenCalledWith('systemwidealerts'); + expect(comp.back).not.toHaveBeenCalled(); + }); + it('should create a new alert with the form values and show a success notification on success', () => { + spyOn(comp, 'back'); + comp.currentAlert = undefined; + + comp.formMessage.patchValue('New message'); + comp.formActive.patchValue(true); + comp.time = {hour: 4, minute: 26}; + comp.date = {year: 2023, month: 1, day: 25}; + + const expectedAlert = new SystemWideAlert(); + expectedAlert.message = 'New message'; + expectedAlert.active = true; + const countDownTo = new Date(2023, 0, 25, 4, 26); + expectedAlert.countdownTo = utcToZonedTime(countDownTo, 'UTC').toUTCString(); + + comp.save(); + + expect(systemWideAlertDataService.create).toHaveBeenCalledWith(expectedAlert); + expect(notificationsService.success).toHaveBeenCalled(); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); + expect(comp.back).toHaveBeenCalled(); + + }); + it('should create a new alert with the form values and show a error notification on error', () => { + spyOn(comp, 'back'); + (systemWideAlertDataService.create as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); + + comp.currentAlert = undefined; + + comp.formMessage.patchValue('New message'); + comp.formActive.patchValue(true); + comp.time = {hour: 4, minute: 26}; + comp.date = {year: 2023, month: 1, day: 25}; + + const expectedAlert = new SystemWideAlert(); + expectedAlert.message = 'New message'; + expectedAlert.active = true; + const countDownTo = new Date(2023, 0, 25, 4, 26); + expectedAlert.countdownTo = utcToZonedTime(countDownTo, 'UTC').toUTCString(); + + comp.save(); + + expect(systemWideAlertDataService.create).toHaveBeenCalledWith(expectedAlert); + expect(notificationsService.error).toHaveBeenCalled(); + expect(requestService.setStaleByHrefSubstring).not.toHaveBeenCalledWith('systemwidealerts'); + expect(comp.back).not.toHaveBeenCalled(); + + }); + }); + describe('back', () => { + it('should navigate back to the home page', () => { + comp.back(); + expect(router.navigate).toHaveBeenCalledWith(['/home']); + }); + }); + + +}); diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts new file mode 100644 index 0000000000..59aa9166ce --- /dev/null +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts @@ -0,0 +1,163 @@ +import { Component, OnInit } from '@angular/core'; +import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { filter, map } from 'rxjs/operators'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { SystemWideAlert } from '../system-wide-alert.model'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { Observable } from 'rxjs'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'; +import { RemoteData } from '../../core/data/remote-data'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { Router } from '@angular/router'; +import { RequestService } from '../../core/data/request.service'; +import { TranslateService } from '@ngx-translate/core'; + + +/** + * Component responsible for rendering the form to update a system-wide alert + */ +@Component({ + selector: 'ds-system-wide-alert-form', + styleUrls: ['./system-wide-alert-form.component.scss'], + templateUrl: './system-wide-alert-form.component.html' +}) +export class SystemWideAlertFormComponent implements OnInit { + + /** + * Observable to track an existing system-wide alert + */ + systemWideAlert$: Observable; + + /** + * The currently configured system-wide alert + */ + currentAlert: SystemWideAlert; + + /** + * The form group representing the system-wide alert + */ + alertForm: FormGroup; + + /** + * Date object to store the countdown date part + */ + date: NgbDateStruct; + + /** + * Object to store the countdown time part + */ + time; + + + constructor( + protected systemWideAlertDataService: SystemWideAlertDataService, + protected notificationsService: NotificationsService, + protected router: Router, + protected requestService: RequestService, + protected translateService: TranslateService + ) { + } + + ngOnInit() { + this.systemWideAlert$ = this.systemWideAlertDataService.findAll().pipe( + getFirstSucceededRemoteDataPayload(), + map((payload: PaginatedList) => payload.page), + filter((page) => isNotEmpty(page)), + map((page) => page[0]) + ); + this.createForm(); + + this.systemWideAlert$.subscribe((alert) => { + this.currentAlert = alert; + this.initFormValues(alert); + }); + } + + /** + * Creates the form with empty values + */ + createForm() { + this.alertForm = new FormBuilder().group({ + formMessage: new FormControl('', { + validators: [Validators.required], + }), + formActive: new FormControl(false), + } + ); + this.setDateTime(new Date()); + } + + /** + * Sets the form values based on the values retrieve from the provided system-wide alert + * @param alert - System-wide alert to use to init the form + */ + initFormValues(alert: SystemWideAlert) { + this.formMessage.patchValue(alert.message); + this.formActive.patchValue(alert.active); + const countDownTo = zonedTimeToUtc(alert.countdownTo, 'UTC'); + if (countDownTo.getTime() - new Date().getTime() > 0) { + this.setDateTime(countDownTo); + } + + } + + private setDateTime(dateToSet) { + this.time = {hour: dateToSet.getHours(), minute: dateToSet.getMinutes()}; + this.date = {year: dateToSet.getFullYear(), month: dateToSet.getMonth() + 1, day: dateToSet.getDate()}; + + } + + get formMessage() { + return this.alertForm.get('formMessage'); + } + + get formActive() { + return this.alertForm.get('formActive'); + } + + /** + * Save the system-wide alert present in the form + * When no alert is present yet on the server, a new one will be created + * When one already exists, the existing one will be updated + */ + save() { + const alert = new SystemWideAlert(); + alert.message = this.formMessage.value; + alert.active = this.formActive.value; + const countDownTo = new Date(this.date.year, this.date.month - 1, this.date.day, this.time.hour, this.time.minute); + alert.countdownTo = utcToZonedTime(countDownTo, 'UTC').toUTCString(); + + if (hasValue(this.currentAlert)) { + const updatedAlert = Object.assign(new SystemWideAlert(), this.currentAlert, alert); + this.handleResponse(this.systemWideAlertDataService.put(updatedAlert), 'system-wide-alert.form.update'); + } else { + this.handleResponse(this.systemWideAlertDataService.create(alert), 'system-wide-alert.form.create'); + } + } + + private handleResponse(response$: Observable>, messagePrefix) { + response$.pipe( + getFirstCompletedRemoteData() + ).subscribe((response: RemoteData) => { + if (response.hasSucceeded) { + this.notificationsService.success(this.translateService.get(`${messagePrefix}.success`)); + this.requestService.setStaleByHrefSubstring('systemwidealerts'); + this.back(); + } else { + this.notificationsService.error(this.translateService.get(`${messagePrefix}.error`, response.errorMessage)); + } + }); + } + + /** + * Navigate back to the homepage + */ + back() { + this.router.navigate(['/home']); + } + + +} diff --git a/src/app/system-wide-alert/system-wide-alert-routing.module.ts b/src/app/system-wide-alert/system-wide-alert-routing.module.ts new file mode 100644 index 0000000000..beb1b32187 --- /dev/null +++ b/src/app/system-wide-alert/system-wide-alert-routing.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { + SiteAdministratorGuard +} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +import { SystemWideAlertFormComponent } from './alert-form/system-wide-alert-form.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + canActivate: [SiteAdministratorGuard], + component: SystemWideAlertFormComponent, + }, + + ]) + ] +}) +export class SystemWideAlertRoutingModule { + +} diff --git a/src/app/system-wide-alert/system-wide-alert.model.ts b/src/app/system-wide-alert/system-wide-alert.model.ts new file mode 100644 index 0000000000..158deb2603 --- /dev/null +++ b/src/app/system-wide-alert/system-wide-alert.model.ts @@ -0,0 +1,55 @@ +import { autoserialize, deserialize } from 'cerialize'; +import { typedObject } from '../core/cache/builders/build-decorators'; +import { CacheableObject } from '../core/cache/cacheable-object.model'; +import { HALLink } from '../core/shared/hal-link.model'; +import { ResourceType } from '../core/shared/resource-type'; +import { excludeFromEquals } from '../core/utilities/equals.decorators'; +import { SYSTEMWIDEALERT } from './system-wide-alert.resource-type'; + +/** + * Object representing a system-wide alert + */ +@typedObject +export class SystemWideAlert implements CacheableObject { + static type = SYSTEMWIDEALERT; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The identifier for this system-wide alert + */ + @autoserialize + alertId: string; + + /** + * The message for this system-wide alert + */ + @autoserialize + message: string; + + /** + * A string representation of the date to which this system-wide alert will count down when active + */ + @autoserialize + countdownTo: string; + + /** + * Whether the system-wide alert is active + */ + @autoserialize + active: boolean; + + + /** + * The {@link HALLink}s for this system-wide alert + */ + @deserialize + _links: { + self: HALLink, + }; +} diff --git a/src/app/system-wide-alert/system-wide-alert.module.ts b/src/app/system-wide-alert/system-wide-alert.module.ts new file mode 100644 index 0000000000..ce2a87f982 --- /dev/null +++ b/src/app/system-wide-alert/system-wide-alert.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SystemWideAlertBannerComponent } from './alert-banner/system-wide-alert-banner.component'; +import { SystemWideAlertFormComponent } from './alert-form/system-wide-alert-form.component'; +import { SharedModule } from '../shared/shared.module'; +import { SystemWideAlertDataService } from '../core/data/system-wide-alert-data.service'; +import { SystemWideAlertRoutingModule } from './system-wide-alert-routing.module'; +import { UiSwitchModule } from 'ngx-ui-switch'; + +@NgModule({ + imports: [ + FormsModule, + SharedModule, + UiSwitchModule, + SystemWideAlertRoutingModule, + ], + exports: [ + SystemWideAlertBannerComponent + ], + declarations: [ + SystemWideAlertBannerComponent, + SystemWideAlertFormComponent + ], + providers: [ + SystemWideAlertDataService + ] +}) +export class SystemWideAlertModule { + +} diff --git a/src/app/system-wide-alert/system-wide-alert.resource-type.ts b/src/app/system-wide-alert/system-wide-alert.resource-type.ts new file mode 100644 index 0000000000..f67f00719b --- /dev/null +++ b/src/app/system-wide-alert/system-wide-alert.resource-type.ts @@ -0,0 +1,10 @@ +/** + * The resource type for SystemWideAlert + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +import { ResourceType } from '../core/shared/resource-type'; + +export const SYSTEMWIDEALERT = new ResourceType('systemwidealert'); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 597f226cc7..dd52b7dd9d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4811,4 +4811,47 @@ "person.orcid.registry.auth": "ORCID Authorizations", "home.recent-submissions.head": "Recent Submissions", + + + "system-wide-alert-banner.countdown.prefix": "In", + + "system-wide-alert-banner.countdown.days": "{{days}} day(s),", + + "system-wide-alert-banner.countdown.hours": "{{hours}} hour(s) and", + + "system-wide-alert-banner.countdown.minutes": "{{minutes}} minute(s):", + + + + "menu.section.system-wide-alert": "System-wide Alerts", + + "system-wide-alert.form.header": "System-wide Alerts", + + "system-wide-alert.form.cancel": "Cancel", + + "system-wide-alert.form.save": "Save", + + "system-wide-alert.form.label.active": "ACTIVE", + + "system-wide-alert.form.label.inactive": "INACTIVE", + + "system-wide-alert.form.error.message": "The system wide alert must have a message", + + "system-wide-alert.form.label.message": "Alert message", + + "system-wide-alert.form.label.countdownTo": "Count down", + + "system-wide-alert.form.label.countdownTo.hint": "Hint: When a date in the future is set, the system wide alert banner will perform a countdown to the set date. Setting the date to the current time or in the past will disable the countdown.", + + "system-wide-alert.form.update.success": "The system-wide alert was successfully updated", + + "system-wide-alert.form.update.error": "Something went wrong when updating the system-wide alert", + + "system-wide-alert.form.create.success": "The system-wide alert was successfully created", + + "system-wide-alert.form.create.error": "Something went wrong when creating the system-wide alert", + + "admin.system-wide-alert.breadcrumbs": "System-wide Alerts", + + "admin.system-wide-alert.title": "System-wide Alerts", } diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index d2ac0ae787..3aacac9c73 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -114,6 +114,7 @@ import { ObjectListComponent } from './app/shared/object-list/object-list.compon import { BrowseByMetadataPageComponent } from './app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component'; import { BrowseByDatePageComponent } from './app/browse-by/browse-by-date-page/browse-by-date-page.component'; import { BrowseByTitlePageComponent } from './app/browse-by/browse-by-title-page/browse-by-title-page.component'; +import { SystemWideAlertModule } from '../../app/system-wide-alert/system-wide-alert.module'; const DECLARATIONS = [ FileSectionComponent, @@ -220,6 +221,7 @@ const DECLARATIONS = [ FormsModule, ResourcePoliciesModule, ComcolModule, + SystemWideAlertModule ], declarations: DECLARATIONS, exports: [ diff --git a/yarn.lock b/yarn.lock index 9542bfdbe1..2e2ce3140e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4759,6 +4759,16 @@ data-urls@^3.0.1: whatwg-mimetype "^3.0.0" whatwg-url "^10.0.0" +date-fns-tz@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a" + integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g== + +date-fns@^2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + date-format@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.4.tgz#b58036e29e74121fca3e1b3e0dc4a62c65faa233" From b6218494e548a4bdf11689ce39056df2007a2b1b Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 23 Dec 2022 12:15:35 +0100 Subject: [PATCH 02/10] 97425: Fix countdown timer intial display and remove rounded banner corner --- .../alert-banner/system-wide-alert-banner.component.html | 2 +- .../alert-banner/system-wide-alert-banner.component.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html index 3e360cc55e..5f741091f1 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html @@ -1,5 +1,5 @@
-
+
diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts index b405957c54..57a9604f90 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts @@ -64,6 +64,7 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { const date = zonedTimeToUtc(alert.countdownTo, 'UTC'); const timeDifference = date.getTime() - new Date().getTime(); if (timeDifference > 0) { + this.allocateTimeUnits(timeDifference); return interval(1000); } } From 827ab4d49ead977f23b4241bd7cb996269f4fe50 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 6 Jan 2023 10:07:23 +0100 Subject: [PATCH 03/10] Yarn lock change --- yarn.lock | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 68f37ebf57..e92c5bf954 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4333,20 +4333,15 @@ date-fns-tz@^1.3.7: resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a" integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g== -date-fns-tz@^1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a" - integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g== - date-fns@^2.29.3: version "2.29.3" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== -date-format@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.4.tgz#b58036e29e74121fca3e1b3e0dc4a62c65faa233" - integrity sha512-/jyf4rhB17ge328HJuJjAcmRtCsGd+NDeAtahRBTaK6vSPR6MO5HlrAit3Nn7dVjaa6sowW0WXt8yQtLyZQFRg== +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== dayjs@^1.10.4: version "1.11.7" From 3e8cb4f3d94f4e9a6cde2a224754a76081deb270 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Mon, 23 Jan 2023 16:48:47 +0100 Subject: [PATCH 04/10] 97425: Implement feedback --- .../system-wide-alert-form.component.html | 91 +++++++++++++------ .../system-wide-alert-form.component.scss | 1 + .../system-wide-alert-form.component.spec.ts | 61 +++++++++++++ .../system-wide-alert-form.component.ts | 69 +++++++++++++- src/assets/i18n/en.json5 | 10 +- 5 files changed, 198 insertions(+), 34 deletions(-) diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html index 2821a14bf8..062b06c294 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html @@ -17,50 +17,89 @@ [className]="(formMessage.invalid) && (formMessage.dirty || formMessage.touched) ? 'form-control is-invalid' :'form-control'" formControlName="formMessage"> -
+
{{ 'system-wide-alert.form.error.message' | translate }} -
+
-
- +
+ + {{ 'system-wide-alert.form.label.countdownTo.enable' | translate }}
-
-
-
- - +
+
+
+
+ + +
-
-
-
- +
+
+ +
-
-
-
- +
+
+ +
-
+
{{'system-wide-alert.form.label.countdownTo.hint' | translate}}
+ +
+
+
+ +
+
+
+ + + {{'system-wide-alert-banner.countdown.prefix' | translate }} + + + {{'system-wide-alert-banner.countdown.days' | translate: { + days: previewDays + } }} + + + {{'system-wide-alert-banner.countdown.hours' | translate: { + hours: previewHours + } }} + + + {{'system-wide-alert-banner.countdown.minutes' | translate: { + minutes: previewMinutes + } }} + + + {{formMessage.value}} +
+
+
-
+
From 715d3ae014a74f510be41a7eaedae1b9e986382f Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 24 Jan 2023 11:24:13 +0100 Subject: [PATCH 06/10] Fix issues with module changes --- .../alert-form/system-wide-alert-form.component.spec.ts | 4 ++-- src/app/system-wide-alert/system-wide-alert.module.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts index 608170a094..4fc79c1caa 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts @@ -12,8 +12,8 @@ import { RouterStub } from '../../shared/testing/router.stub'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { Router } from '@angular/router'; import { FormsModule } from '@angular/forms'; -import { SharedModule } from '../../shared/shared.module'; import { UiSwitchModule } from 'ngx-ui-switch'; +import { SystemWideAlertModule } from '../system-wide-alert.module'; describe('SystemWideAlertFormComponent', () => { let comp: SystemWideAlertFormComponent; @@ -52,7 +52,7 @@ describe('SystemWideAlertFormComponent', () => { router = new RouterStub(); TestBed.configureTestingModule({ - imports: [FormsModule, SharedModule, UiSwitchModule, TranslateModule.forRoot()], + imports: [FormsModule, SystemWideAlertModule, UiSwitchModule, TranslateModule.forRoot()], declarations: [SystemWideAlertFormComponent], providers: [ {provide: SystemWideAlertDataService, useValue: systemWideAlertDataService}, diff --git a/src/app/system-wide-alert/system-wide-alert.module.ts b/src/app/system-wide-alert/system-wide-alert.module.ts index ce2a87f982..ca200fa4f1 100644 --- a/src/app/system-wide-alert/system-wide-alert.module.ts +++ b/src/app/system-wide-alert/system-wide-alert.module.ts @@ -6,6 +6,7 @@ import { SharedModule } from '../shared/shared.module'; import { SystemWideAlertDataService } from '../core/data/system-wide-alert-data.service'; import { SystemWideAlertRoutingModule } from './system-wide-alert-routing.module'; import { UiSwitchModule } from 'ngx-ui-switch'; +import { NgbDatepickerModule, NgbTimepickerModule } from '@ng-bootstrap/ng-bootstrap'; @NgModule({ imports: [ @@ -13,6 +14,8 @@ import { UiSwitchModule } from 'ngx-ui-switch'; SharedModule, UiSwitchModule, SystemWideAlertRoutingModule, + NgbTimepickerModule, + NgbDatepickerModule, ], exports: [ SystemWideAlertBannerComponent From b4273fa734983b9b8c82748fefd62a5fd5793dfe Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 26 Jan 2023 15:41:45 +0100 Subject: [PATCH 07/10] 97425: Implement feedback --- .../system-wide-alert-banner.component.ts | 11 ++++-- .../system-wide-alert-form.component.html | 3 +- .../system-wide-alert-form.component.spec.ts | 36 ++++++++++++++++++- .../system-wide-alert-form.component.ts | 33 ++++++++++++++--- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts index 57a9604f90..a19d2a7e41 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; import { filter, map, switchMap } from 'rxjs/operators'; @@ -7,6 +7,7 @@ import { SystemWideAlert } from '../system-wide-alert.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { BehaviorSubject, EMPTY, interval, Subscription } from 'rxjs'; import { zonedTimeToUtc } from 'date-fns-tz'; +import { isPlatformBrowser } from '@angular/common'; /** * Component responsible for rendering a banner and the countdown for an active system-wide alert @@ -44,6 +45,7 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { subscriptions: Subscription[] = []; constructor( + @Inject(PLATFORM_ID) protected platformId: Object, protected systemWideAlertDataService: SystemWideAlertDataService ) { } @@ -65,7 +67,12 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { const timeDifference = date.getTime() - new Date().getTime(); if (timeDifference > 0) { this.allocateTimeUnits(timeDifference); - return interval(1000); + if (isPlatformBrowser(this.platformId)) { + return interval(1000); + } else { + return EMPTY; + } + } } // Reset the countDown times to 0 and return EMPTY to prevent unnecessary countdown calculations diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html index d6eeaf5046..169081e277 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html @@ -7,7 +7,7 @@ + (change)="setActive($event)">
@@ -44,6 +44,7 @@ placeholder="yyyy-mm-dd" name="dp" [(ngModel)]="date" + [minDate]="minDate" ngbDatepicker #d="ngbDatepicker" (ngModelChange)="updatePreviewTime()" diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts index 4fc79c1caa..505990b445 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts @@ -149,8 +149,19 @@ describe('SystemWideAlertFormComponent', () => { }); }); + describe('setActive', () => { + it('should set whether the alert is active and save the current alert', () => { + spyOn(comp, 'save'); + spyOn(comp.formActive, 'patchValue'); + comp.setActive(true); + + expect(comp.formActive.patchValue).toHaveBeenCalledWith(true); + expect(comp.save).toHaveBeenCalledWith(false); + }); + }); + describe('save', () => { - it('should update the exising alert with the form values and show a success notification on success', () => { + it('should update the exising alert with the form values and show a success notification on success and navigate back', () => { spyOn(comp, 'back'); comp.currentAlert = systemWideAlert; @@ -173,6 +184,29 @@ describe('SystemWideAlertFormComponent', () => { expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); expect(comp.back).toHaveBeenCalled(); }); + it('should update the exising alert with the form values and show a success notification on success and not navigate back when false is provided to the save method', () => { + spyOn(comp, 'back'); + comp.currentAlert = systemWideAlert; + + comp.formMessage.patchValue('New message'); + comp.formActive.patchValue(true); + comp.time = {hour: 4, minute: 26}; + comp.date = {year: 2023, month: 1, day: 25}; + + const expectedAlert = new SystemWideAlert(); + expectedAlert.alertId = systemWideAlert.alertId; + expectedAlert.message = 'New message'; + expectedAlert.active = true; + const countDownTo = new Date(2023, 0, 25, 4, 26); + expectedAlert.countdownTo = utcToZonedTime(countDownTo, 'UTC').toUTCString(); + + comp.save(false); + + expect(systemWideAlertDataService.put).toHaveBeenCalledWith(expectedAlert); + expect(notificationsService.success).toHaveBeenCalled(); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); + expect(comp.back).not.toHaveBeenCalled(); + }); it('should update the exising alert with the form values but add an empty countdown date when disabled and show a success notification on success', () => { spyOn(comp, 'back'); comp.currentAlert = systemWideAlert; diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts index 6e0d2030fd..db517ef8cd 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts @@ -46,6 +46,11 @@ export class SystemWideAlertFormComponent implements OnInit { */ date: NgbDateStruct; + /** + * The minimum date for the countdown timer + */ + minDate: NgbDateStruct; + /** * Object to store the countdown time part */ @@ -90,6 +95,10 @@ export class SystemWideAlertFormComponent implements OnInit { ); this.createForm(); + const currentDate = new Date(); + this.minDate = {year: currentDate.getFullYear(), month: currentDate.getMonth() + 1, day: currentDate.getDate()}; + + this.systemWideAlert$.subscribe((alert) => { this.currentAlert = alert; this.initFormValues(alert); @@ -125,6 +134,16 @@ export class SystemWideAlertFormComponent implements OnInit { } + /** + * Set whether the system-wide alert is active + * Will also save the info in the current system-wide alert + * @param active + */ + setActive(active: boolean) { + this.formActive.patchValue(active); + this.save(false); + } + /** * Set whether the countdown timer is enabled or disabled. This will also update the counter in the preview * @param enabled - Whether the countdown timer is enabled or disabled. @@ -180,8 +199,10 @@ export class SystemWideAlertFormComponent implements OnInit { * Save the system-wide alert present in the form * When no alert is present yet on the server, a new one will be created * When one already exists, the existing one will be updated + * + * @param navigateToHomePage - Whether the user should be navigated back on successful save or not */ - save() { + save(navigateToHomePage = true) { const alert = new SystemWideAlert(); alert.message = this.formMessage.value; alert.active = this.formActive.value; @@ -193,20 +214,22 @@ export class SystemWideAlertFormComponent implements OnInit { } if (hasValue(this.currentAlert)) { const updatedAlert = Object.assign(new SystemWideAlert(), this.currentAlert, alert); - this.handleResponse(this.systemWideAlertDataService.put(updatedAlert), 'system-wide-alert.form.update'); + this.handleResponse(this.systemWideAlertDataService.put(updatedAlert), 'system-wide-alert.form.update', navigateToHomePage); } else { - this.handleResponse(this.systemWideAlertDataService.create(alert), 'system-wide-alert.form.create'); + this.handleResponse(this.systemWideAlertDataService.create(alert), 'system-wide-alert.form.create', navigateToHomePage); } } - private handleResponse(response$: Observable>, messagePrefix) { + private handleResponse(response$: Observable>, messagePrefix, navigateToHomePage: boolean) { response$.pipe( getFirstCompletedRemoteData() ).subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationsService.success(this.translateService.get(`${messagePrefix}.success`)); this.requestService.setStaleByHrefSubstring('systemwidealerts'); - this.back(); + if (navigateToHomePage) { + this.back(); + } } else { this.notificationsService.error(this.translateService.get(`${messagePrefix}.error`, response.errorMessage)); } From 38063d617e465db958efa6b96a3c35d826b4bc57 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 31 Jan 2023 13:06:03 +0100 Subject: [PATCH 08/10] 97425: Implement feedback --- .../data/system-wide-alert-data.service.ts | 23 ++++++++++++++++++- ...system-wide-alert-banner.component.spec.ts | 5 +++- .../system-wide-alert-banner.component.ts | 17 ++++++++++---- src/assets/i18n/en.json5 | 2 ++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/app/core/data/system-wide-alert-data.service.ts b/src/app/core/data/system-wide-alert-data.service.ts index 029ab76693..51a88b907a 100644 --- a/src/app/core/data/system-wide-alert-data.service.ts +++ b/src/app/core/data/system-wide-alert-data.service.ts @@ -17,16 +17,18 @@ import { SYSTEMWIDEALERT } from '../../system-wide-alert/system-wide-alert.resou import { SystemWideAlert } from '../../system-wide-alert/system-wide-alert.model'; import { PutData, PutDataImpl } from './base/put-data'; import { RequestParam } from '../cache/models/request-param.model'; +import { SearchData, SearchDataImpl } from './base/search-data'; /** * Dataservice representing a system-wide alert */ @Injectable() @dataService(SYSTEMWIDEALERT) -export class SystemWideAlertDataService extends IdentifiableDataService implements FindAllData, CreateData, PutData { +export class SystemWideAlertDataService extends IdentifiableDataService implements FindAllData, CreateData, PutData, SearchData { private findAllData: FindAllDataImpl; private createData: CreateDataImpl; private putData: PutDataImpl; + private searchData: SearchData; constructor( protected requestService: RequestService, @@ -40,6 +42,7 @@ export class SystemWideAlertDataService extends IdentifiableDataService>} + * Return an observable that emits response from the server + */ + searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + } diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts index d27e5379e9..f767d0f196 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts @@ -9,6 +9,8 @@ import { TestScheduler } from 'rxjs/testing'; import { getTestScheduler } from 'jasmine-marbles'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; describe('SystemWideAlertBannerComponent', () => { @@ -35,7 +37,7 @@ describe('SystemWideAlertBannerComponent', () => { }); systemWideAlertDataService = jasmine.createSpyObj('systemWideAlertDataService', { - findAll: createSuccessfulRemoteDataObject$(createPaginatedList([systemWideAlert])), + searchBy: createSuccessfulRemoteDataObject$(createPaginatedList([systemWideAlert])), }); TestBed.configureTestingModule({ @@ -43,6 +45,7 @@ describe('SystemWideAlertBannerComponent', () => { declarations: [SystemWideAlertBannerComponent], providers: [ {provide: SystemWideAlertDataService, useValue: systemWideAlertDataService}, + {provide: NotificationsService, useValue: new NotificationsServiceStub()}, ] }).compileComponents(); })); diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts index a19d2a7e41..320e42db85 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts @@ -1,6 +1,6 @@ import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; -import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { getAllCompletedRemoteData } from '../../core/shared/operators'; import { filter, map, switchMap } from 'rxjs/operators'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { SystemWideAlert } from '../system-wide-alert.model'; @@ -8,6 +8,7 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { BehaviorSubject, EMPTY, interval, Subscription } from 'rxjs'; import { zonedTimeToUtc } from 'date-fns-tz'; import { isPlatformBrowser } from '@angular/common'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; /** * Component responsible for rendering a banner and the countdown for an active system-wide alert @@ -46,13 +47,21 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { constructor( @Inject(PLATFORM_ID) protected platformId: Object, - protected systemWideAlertDataService: SystemWideAlertDataService + protected systemWideAlertDataService: SystemWideAlertDataService, + protected notificationsService: NotificationsService, ) { } ngOnInit() { - this.subscriptions.push(this.systemWideAlertDataService.findAll().pipe( - getAllSucceededRemoteDataPayload(), + this.subscriptions.push(this.systemWideAlertDataService.searchBy('active').pipe( + getAllCompletedRemoteData(), + map((rd) => { + if (rd.hasSucceeded) { + return rd.payload; + } else { + this.notificationsService.error('system-wide-alert-banner.retrieval.error'); + } + }), map((payload: PaginatedList) => payload.page), filter((page) => isNotEmpty(page)), map((page) => page[0]) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 26844bb1d6..056fc593d8 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4844,6 +4844,8 @@ "listable-notification-object.default-message": "This object couldn't be retrieved", + "system-wide-alert-banner.retrieval.error": "Something went wrong retrieving the system-wide alert banner", + "system-wide-alert-banner.countdown.prefix": "In", "system-wide-alert-banner.countdown.days": "{{days}} day(s),", From 36e10f87b3e29c6f3a67abf3314b2d413292ee14 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 2 Feb 2023 16:22:59 +0100 Subject: [PATCH 09/10] 97425: Fix minor issues --- .../system-wide-alert-banner.component.ts | 13 ++++--------- .../alert-form/system-wide-alert-form.component.ts | 11 +++++++++-- src/assets/i18n/en.json5 | 2 ++ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts index 320e42db85..27bdb14f1d 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts @@ -1,6 +1,8 @@ import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; -import { getAllCompletedRemoteData } from '../../core/shared/operators'; +import { + getAllSucceededRemoteDataPayload +} from '../../core/shared/operators'; import { filter, map, switchMap } from 'rxjs/operators'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { SystemWideAlert } from '../system-wide-alert.model'; @@ -54,14 +56,7 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { ngOnInit() { this.subscriptions.push(this.systemWideAlertDataService.searchBy('active').pipe( - getAllCompletedRemoteData(), - map((rd) => { - if (rd.hasSucceeded) { - return rd.payload; - } else { - this.notificationsService.error('system-wide-alert-banner.retrieval.error'); - } - }), + getAllSucceededRemoteDataPayload(), map((payload: PaginatedList) => payload.page), filter((page) => isNotEmpty(page)), map((page) => page[0]) diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts index db517ef8cd..23c9fb75f9 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { filter, map } from 'rxjs/operators'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { SystemWideAlert } from '../system-wide-alert.model'; @@ -88,7 +88,14 @@ export class SystemWideAlertFormComponent implements OnInit { ngOnInit() { this.systemWideAlert$ = this.systemWideAlertDataService.findAll().pipe( - getFirstSucceededRemoteDataPayload(), + getFirstCompletedRemoteData(), + map((rd) => { + if (rd.hasSucceeded) { + return rd.payload; + } else { + this.notificationsService.error('system-wide-alert-form.retrieval.error'); + } + }), map((payload: PaginatedList) => payload.page), filter((page) => isNotEmpty(page)), map((page) => page[0]) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 056fc593d8..60bbb9e860 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4860,6 +4860,8 @@ "system-wide-alert.form.header": "System-wide Alert", + "system-wide-alert-form.retrieval.error": "Something went wrong retrieving the system-wide alert", + "system-wide-alert.form.cancel": "Cancel", "system-wide-alert.form.save": "Save", From bb7eef263196e6cb5d3c578d6687b39b3871bd53 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 2 Feb 2023 17:58:55 +0100 Subject: [PATCH 10/10] Fix typo --- .../alert-form/system-wide-alert-form.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.scss b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.scss index 6e881e4807..cdcfd8b0cd 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.scss +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.scss @@ -1,4 +1,4 @@ .timepicker-margin { - // Negative margin to offset the time picker arrowsand ensure the date and time are correctly aligned + // Negative margin to offset the time picker arrows and ensure the date and time are correctly aligned margin-top: -38px; }