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 030/153] 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 e4ce91fa73fa2d5bb90be43198d2b6baef8e2a53 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 28 Dec 2022 11:57:48 +0100 Subject: [PATCH 031/153] 97298: #3281 Self-register - type request query param --- .../forgot-email.component.html | 4 ++-- .../register-email-form.component.ts | 12 +++++++----- .../register-email/register-email.component.html | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/app/forgot-password/forgot-password-email/forgot-email.component.html b/src/app/forgot-password/forgot-password-email/forgot-email.component.html index 263f142c2e..8be86f17ae 100644 --- a/src/app/forgot-password/forgot-password-email/forgot-email.component.html +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.html @@ -1,3 +1,3 @@ - \ No newline at end of file + [MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="'forgot'"> + diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 9e7b783544..f2a338537d 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -29,6 +29,12 @@ export class RegisterEmailFormComponent implements OnInit { @Input() MESSAGE_PREFIX: string; + /** + * Type of register request to be done, register new email or forgot password (same endpoint) + */ + @Input() + typeRequest: string = null; + validMailDomains: string[]; constructor( @@ -64,12 +70,8 @@ export class RegisterEmailFormComponent implements OnInit { * Register an email address */ register() { - const typeMap = new Map([ - ['register-page.registration', 'register'], - ['forgot-email.form', 'forgot'] - ]); if (!this.form.invalid) { - this.epersonRegistrationService.registerEmail(this.email.value, typeMap.get(this.MESSAGE_PREFIX)).subscribe((response: RemoteData) => { + this.epersonRegistrationService.registerEmail(this.email.value, this.typeRequest).subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value})); diff --git a/src/app/register-page/register-email/register-email.component.html b/src/app/register-page/register-email/register-email.component.html index a60dc4c31e..80b6885272 100644 --- a/src/app/register-page/register-email/register-email.component.html +++ b/src/app/register-page/register-email/register-email.component.html @@ -1,3 +1,3 @@ + [MESSAGE_PREFIX]="'register-page.registration'" [typeRequest]="'register'"> From 31b17731f25403f79b04b2d2966f340efbda8198 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 28 Dec 2022 12:28:27 +0100 Subject: [PATCH 032/153] 97298: #3281 Self-register - test fixes --- .../data/eperson-registration.service.spec.ts | 4 +++- .../core/data/eperson-registration.service.ts | 6 +++++- .../register-email-form.component.spec.ts | 19 ++++++++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index 2407249615..32688d050b 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -9,6 +9,7 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; describe('EpersonRegistrationService', () => { let testScheduler; @@ -79,8 +80,9 @@ describe('EpersonRegistrationService', () => { it('should send an email registration', () => { const expected = service.registerEmail('test@mail.org'); + const options: HttpOptions = Object.create({}); - expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration)); + expect(requestService.send).toHaveBeenCalledWith(new PostRequest('request-id', 'rest-url/registrations', registration, options)); expect(expected).toBeObservable(cold('(a|)', { a: rd })); }); }); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 24c2263fd1..6a8d9c94f7 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -13,6 +13,7 @@ import { RegistrationResponseParsingService } from './registration-response-pars import { RemoteData } from './remote-data'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import {HttpParams} from '@angular/common/http'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; @Injectable( { @@ -63,7 +64,10 @@ export class EpersonRegistrationService { const href$ = this.getRegistrationEndpoint(); - const options = type ? {params: new HttpParams({fromString:'type=' + type})} : {}; + const options: HttpOptions = Object.create({}); + if (hasValue(type)) { + options.params = type ? new HttpParams({ fromString: 'type=' + type }) : new HttpParams(); + } href$.pipe( find((href: string) => hasValue(href)), diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index a415ef4808..585e69405a 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -14,8 +14,10 @@ import { RouterStub } from '../shared/testing/router.stub'; import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub'; import { RegisterEmailFormComponent } from './register-email-form.component'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; -describe('RegisterEmailComponent', () => { +describe('RegisterEmailFormComponent', () => { let comp: RegisterEmailFormComponent; let fixture: ComponentFixture; @@ -23,6 +25,7 @@ describe('RegisterEmailComponent', () => { let router; let epersonRegistrationService: EpersonRegistrationService; let notificationsService; + let configurationDataService: ConfigurationDataService; beforeEach(waitForAsync(() => { @@ -33,6 +36,15 @@ describe('RegisterEmailComponent', () => { registerEmail: createSuccessfulRemoteDataObject$({}) }); + configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'authentication-password.domain.valid', + values: [ + 'example.com, @gmail.com' + ] + })) + }); + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), ReactiveFormsModule], declarations: [RegisterEmailFormComponent], @@ -41,6 +53,7 @@ describe('RegisterEmailComponent', () => { {provide: EpersonRegistrationService, useValue: epersonRegistrationService}, {provide: FormBuilder, useValue: new FormBuilder()}, {provide: NotificationsService, useValue: notificationsService}, + {provide: ConfigurationDataService, useValue: configurationDataService}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); @@ -75,7 +88,7 @@ describe('RegisterEmailComponent', () => { comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null); expect(notificationsService.success).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith(['/home']); }); @@ -85,7 +98,7 @@ describe('RegisterEmailComponent', () => { comp.form.patchValue({email: 'valid@email.org'}); comp.register(); - expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org'); + expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith('valid@email.org', null); expect(notificationsService.error).toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled(); }); From 928431284f2a49b0a466c3ddc55a3cdbc88f1690 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 30 Dec 2022 10:12:15 +0100 Subject: [PATCH 033/153] [CST-7217] Improve fix --- .../search-facet-option/search-facet-option.component.html | 6 +++--- .../search-facet-option/search-facet-option.component.scss | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html index bf165274d5..967f0ff0d8 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html @@ -2,9 +2,9 @@ [tabIndex]="-1" [routerLink]="[searchLink]" [queryParams]="addQueryParams" queryParamsHandling="merge"> -