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/125] 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/125] 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/125] 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/125] [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"> -
-
- +
+ + {{ '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 039/125] 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 66c6872b0051c86867afade4de7d0f8daa97eb0f Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 25 Jan 2023 15:38:48 +0100 Subject: [PATCH 040/125] 97061: type request param name change to avoid confusion with rest object type & non valid email domain error code changed --- src/app/core/data/eperson-registration.service.ts | 3 ++- src/app/register-email-form/register-email-form.component.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 6a8d9c94f7..67f5116953 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -66,7 +66,8 @@ export class EpersonRegistrationService { const options: HttpOptions = Object.create({}); if (hasValue(type)) { - options.params = type ? new HttpParams({ fromString: 'type=' + type }) : new HttpParams(); + options.params = type ? + new HttpParams({ fromString: 'accountRequestType=' + type }) : new HttpParams(); } href$.pipe( 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 f2a338537d..dc742d6760 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -76,7 +76,7 @@ export class RegisterEmailFormComponent implements OnInit { this.notificationService.success(this.translateService.get(`${this.MESSAGE_PREFIX}.success.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.success.content`, {email: this.email.value})); this.router.navigate(['/home']); - } else if (response.statusCode === 400) { + } else if (response.statusCode === 422) { this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), this.translateService.get(`${this.MESSAGE_PREFIX}.error.maildomain`, { domains: this.validMailDomains.join(', ')})); } else { this.notificationService.error(this.translateService.get(`${this.MESSAGE_PREFIX}.error.head`), From 0d7a030960116e6635f5dacd9c007c992dbd9d9d Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Wed, 25 Jan 2023 17:46:21 +0100 Subject: [PATCH 041/125] 98863: Fixed typo and added domain name validation on input field --- .../register-email-form.component.ts | 23 ++++++++++++++----- src/assets/i18n/en.json5 | 2 +- 2 files changed, 18 insertions(+), 7 deletions(-) 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 dc742d6760..9269a0cb2d 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -3,11 +3,12 @@ import {EpersonRegistrationService} from '../core/data/eperson-registration.serv import {NotificationsService} from '../shared/notifications/notifications.service'; import {TranslateService} from '@ngx-translate/core'; import {Router} from '@angular/router'; -import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup, Validators, ValidatorFn } from '@angular/forms'; import {Registration} from '../core/shared/registration.model'; import {RemoteData} from '../core/data/remote-data'; import {ConfigurationDataService} from '../core/data/configuration-data.service'; import {getAllCompletedRemoteData} from '../core/shared/operators'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; @Component({ selector: 'ds-register-email-form', @@ -48,22 +49,32 @@ export class RegisterEmailFormComponent implements OnInit { } ngOnInit(): void { + const validators: ValidatorFn[] = [ + Validators.required, + Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$') + ]; this.form = this.formBuilder.group({ email: new FormControl('', { - validators: [Validators.required, - Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$') - ], + validators: validators, }) }); this.validMailDomains = []; this.configurationService.findByPropertyName('authentication-password.domain.valid') .pipe(getAllCompletedRemoteData()) - .subscribe((remoteData) => { + .subscribe((remoteData: RemoteData) => { + if (remoteData.payload) { for (const remoteValue of remoteData.payload.values) { this.validMailDomains.push(remoteValue); + if (this.validMailDomains.length !== 0 && this.MESSAGE_PREFIX === 'register-page.registration') { + this.form.get('email').setValidators([ + ...validators, + Validators.pattern(this.validMailDomains.map((domain: string) => '(^.*@' + domain.replace(new RegExp('\\.', 'g'), '\\.') + '$)').join('|')), + ]); + this.form.updateValueAndValidity(); + } } } - ); + }); } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 8737bed9b3..511d923f7d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3027,7 +3027,7 @@ "register-page.registration.error.content": "An error occured when registering the following email address: {{ email }}", - "register-page.registration.error.maildomain": "this email address is not on the list of domains who can register. Allowed domains are {{ domains }}", + "register-page.registration.error.maildomain": "This email address is not on the list of domains who can register. Allowed domains are {{ domains }}", "register-page.registration.info.maildomain": "Accounts can be registered for mail addresses of the domains", From 31d86eeb8cac289085d61477ce31f753e9111f91 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 26 Jan 2023 14:24:52 +0100 Subject: [PATCH 042/125] 98863: Domain validator fix + error message --- .../forgot-email.component.html | 2 +- .../forgot-email.component.ts | 3 ++- .../register-email-form.component.html | 7 +++++-- .../register-email-form.component.ts | 17 +++++++++++++++-- .../register-email.component.html | 2 +- .../register-email/register-email.component.ts | 3 ++- src/assets/i18n/en.json5 | 6 ++++-- 7 files changed, 30 insertions(+), 10 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 8be86f17ae..995108cdbc 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 @@ + [MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest"> diff --git a/src/app/forgot-password/forgot-password-email/forgot-email.component.ts b/src/app/forgot-password/forgot-password-email/forgot-email.component.ts index af482bdb67..66a61ed7ee 100644 --- a/src/app/forgot-password/forgot-password-email/forgot-email.component.ts +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { TYPE_REQUEST_FORGOT } from '../../register-email-form/register-email-form.component'; @Component({ selector: 'ds-forgot-email', @@ -9,5 +10,5 @@ import { Component } from '@angular/core'; * Component responsible the forgot password email step */ export class ForgotEmailComponent { - + typeRequest = TYPE_REQUEST_FORGOT; } diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index a1eb0cef98..8cc0d293cf 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -18,8 +18,11 @@ {{ MESSAGE_PREFIX + '.email.error.required' | translate }} - - {{ MESSAGE_PREFIX + '.email.error.pattern' | translate }} + + {{ MESSAGE_PREFIX + '.email.error.not-email-form' | translate }} + + + {{ MESSAGE_PREFIX + '.email.error.not-valid-domain' | translate: { domains: validMailDomains } }}
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 9269a0cb2d..83053bd345 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -10,6 +10,9 @@ import {ConfigurationDataService} from '../core/data/configuration-data.service' import {getAllCompletedRemoteData} from '../core/shared/operators'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; +export const TYPE_REQUEST_FORGOT = 'forgot'; +export const TYPE_REQUEST_REGISTER = 'register'; + @Component({ selector: 'ds-register-email-form', templateUrl: './register-email-form.component.html' @@ -37,6 +40,15 @@ export class RegisterEmailFormComponent implements OnInit { typeRequest: string = null; validMailDomains: string[]; + TYPE_REQUEST_REGISTER = TYPE_REQUEST_REGISTER; + + captchaVersion(): Observable { + return this.googleRecaptchaService.captchaVersion(); + } + + captchaMode(): Observable { + return this.googleRecaptchaService.captchaMode(); + } constructor( private epersonRegistrationService: EpersonRegistrationService, @@ -51,6 +63,7 @@ export class RegisterEmailFormComponent implements OnInit { ngOnInit(): void { const validators: ValidatorFn[] = [ Validators.required, + Validators.email, Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$') ]; this.form = this.formBuilder.group({ @@ -65,10 +78,10 @@ export class RegisterEmailFormComponent implements OnInit { if (remoteData.payload) { for (const remoteValue of remoteData.payload.values) { this.validMailDomains.push(remoteValue); - if (this.validMailDomains.length !== 0 && this.MESSAGE_PREFIX === 'register-page.registration') { + if (this.validMailDomains.length !== 0 && this.typeRequest === TYPE_REQUEST_REGISTER) { this.form.get('email').setValidators([ ...validators, - Validators.pattern(this.validMailDomains.map((domain: string) => '(^.*@' + domain.replace(new RegExp('\\.', 'g'), '\\.') + '$)').join('|')), + Validators.pattern(this.validMailDomains.map((domain: string) => '(^.*' + domain.replace(new RegExp('\\.', 'g'), '\\.') + '$)').join('|')), ]); this.form.updateValueAndValidity(); } 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 80b6885272..1829bb2914 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]="typeRequest"> diff --git a/src/app/register-page/register-email/register-email.component.ts b/src/app/register-page/register-email/register-email.component.ts index 7b7b0f631b..228e8c56a0 100644 --- a/src/app/register-page/register-email/register-email.component.ts +++ b/src/app/register-page/register-email/register-email.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { TYPE_REQUEST_REGISTER } from '../../register-email-form/register-email-form.component'; @Component({ selector: 'ds-register-email', @@ -9,5 +10,5 @@ import { Component } from '@angular/core'; * Component responsible the email registration step when registering as a new user */ export class RegisterEmailComponent { - + typeRequest = TYPE_REQUEST_REGISTER; } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 511d923f7d..7a011ecff5 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1406,7 +1406,7 @@ "forgot-email.form.email.error.required": "Please fill in an email address", - "forgot-email.form.email.error.pattern": "Please fill in a valid email address", + "forgot-email.form.email.error.not-email-form": "Please fill in a valid email address", "forgot-email.form.email.hint": "This address will be verified and used as your login name.", @@ -3013,7 +3013,9 @@ "register-page.registration.email.error.required": "Please fill in an email address", - "register-page.registration.email.error.pattern": "Please fill in a valid email address", + "register-page.registration.email.error.not-email-form": "Please fill in a valid email address. ", + + "register-page.registration.email.error.not-valid-domain": "Use email with allowed domains: {{ domains }}", "register-page.registration.email.hint": "This address will be verified and used as your login name.", From b4273fa734983b9b8c82748fefd62a5fd5793dfe Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 26 Jan 2023 15:41:45 +0100 Subject: [PATCH 043/125] 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 81d21e25c226a18528ee1d1ba6e07f235add3ce5 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 26 Jan 2023 16:13:21 +0100 Subject: [PATCH 044/125] 98863: IT fix --- .../register-email-form/register-email-form.component.ts | 8 -------- 1 file changed, 8 deletions(-) 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 83053bd345..bb10aaed38 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -42,14 +42,6 @@ export class RegisterEmailFormComponent implements OnInit { validMailDomains: string[]; TYPE_REQUEST_REGISTER = TYPE_REQUEST_REGISTER; - captchaVersion(): Observable { - return this.googleRecaptchaService.captchaVersion(); - } - - captchaMode(): Observable { - return this.googleRecaptchaService.captchaMode(); - } - constructor( private epersonRegistrationService: EpersonRegistrationService, private notificationService: NotificationsService, From 14dfd6d9b855ff1acaf0138da2fe17d0ac1183bc Mon Sep 17 00:00:00 2001 From: cris Date: Mon, 30 Jan 2023 21:44:38 +0000 Subject: [PATCH 045/125] aria-selected added to item edit tab --- src/app/item-page/edit-item-page/edit-item-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/edit-item-page.component.html b/src/app/item-page/edit-item-page/edit-item-page.component.html index 9458df2249..6465026532 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.component.html +++ b/src/app/item-page/edit-item-page/edit-item-page.component.html @@ -4,7 +4,7 @@

{{'item.edit.head' | translate}}

diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index f01f5c1f7a..a719e621dc 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -3,14 +3,23 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { ItemOperation } from '../item-operation/itemOperation.model'; -import { distinctUntilChanged, first, map, mergeMap, toArray } from 'rxjs/operators'; -import { BehaviorSubject, Observable, from as observableFrom } from 'rxjs'; +import {distinctUntilChanged, first, map, mergeMap, startWith, switchMap, toArray} from 'rxjs/operators'; +import {BehaviorSubject, Observable, from as observableFrom, Subscription, combineLatest, of} from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { hasValue } from '../../../shared/empty.util'; -import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { + getAllSucceededRemoteDataPayload, + getFirstCompletedRemoteData, getFirstSucceededRemoteData, + getFirstSucceededRemoteDataPayload +} from '../../../core/shared/operators'; +import {IdentifierDataService} from '../../../core/data/identifier-data.service'; +import {IdentifierData} from '../../../shared/object-list/identifier-data/identifier-data.model'; +import {Identifier} from '../../../shared/object-list/identifier-data/identifier.model'; +import {ConfigurationProperty} from '../../../core/shared/configuration-property.model'; +import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; @Component({ selector: 'ds-item-status', @@ -51,15 +60,32 @@ export class ItemStatusComponent implements OnInit { */ actionsKeys; + /** + * Identifiers (handles, DOIs) + */ + identifiers$: Observable; + + /** + * Configuration and state variables regarding DOIs + */ + + public subs: Subscription[] = []; + /** * Route to the item's page */ itemPageRoute$: Observable; constructor(private route: ActivatedRoute, - private authorizationService: AuthorizationDataService) { + private authorizationService: AuthorizationDataService, + private identifierDataService: IdentifierDataService, + private configurationService: ConfigurationDataService, + ) { } + /** + * Initialise component + */ ngOnInit(): void { this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)); this.itemRD$.pipe( @@ -72,6 +98,35 @@ export class ItemStatusComponent implements OnInit { lastModified: item.lastModified }); this.statusDataKeys = Object.keys(this.statusData); + + // Observable for item identifiers (retrieved from embedded link) + this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe( + map((identifierRD) => { + if (identifierRD.statusCode !== 401 && hasValue(identifierRD.payload)) { + return identifierRD.payload.identifiers; + } else { + return null; + } + }), + ); + + // Observable for configuration determining whether the Register DOI feature is enabled + let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.register').pipe( + map((enabled: RemoteData) => { + let show: boolean = false; + if (enabled.hasSucceeded) { + if (enabled.payload !== undefined && enabled.payload !== null) { + if (enabled.payload.values !== undefined) { + enabled.payload.values.forEach((value) => { + show = true; + }); + } + } + } + return show; + }) + ); + /* The key is used to build messages i18n example: 'item.edit.tabs.status.buttons..label' @@ -92,27 +147,66 @@ export class ItemStatusComponent implements OnInit { } operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true)); operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true)); - this.operations$.next(operations); - observableFrom(operations).pipe( - mergeMap((operation) => { - if (hasValue(operation.featureID)) { - return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( - distinctUntilChanged(), - map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) - ); - } else { - return [operation]; + // Observable that reads identifiers and their status and, and config properties, and decides + // if we're allowed to show a Register DOI feature + let showRegister$: Observable = combineLatest([this.identifiers$, registerConfigEnabled$]).pipe( + distinctUntilChanged(), + map(([identifiers, enabled]) => { + let no_doi: boolean = true; + let pending: boolean = false; + if (identifiers !== undefined && identifiers !== null) { + identifiers.forEach((identifier: Identifier) => { + if (hasValue(identifier) && identifier.identifierType == 'doi') { + // The item has some kind of DOI + no_doi = false; + if (identifier.identifierStatus == '10' || identifier.identifierStatus == '11' + || identifier.identifierStatus == null) { + // The item's DOI is pending, minted or null. + // It isn't registered, reserved, queued for registration or reservation or update, deleted + // or queued for deletion. + pending = true; + } + } + }); } - }), - toArray() - ).subscribe((ops) => this.operations$.next(ops)); + // If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true + return ((pending || no_doi) && enabled); + }) + ) + + // Subscribe to changes from the showRegister check and rebuild operations list accordingly + this.subs.push(showRegister$.subscribe((show) => { + // Copy the static array first so we don't keep appending to it + let tmp_operations = [...operations]; + if (show) { + // Push the new Register DOI item operation + tmp_operations.push(new ItemOperation('registerDOI', this.getCurrentUrl(item) + '/registerdoi', FeatureID.CanRegisterDOI)); + } + // Check authorisations and merge into new operations list + observableFrom(tmp_operations).pipe( + mergeMap((operation) => { + if (hasValue(operation.featureID)) { + return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( + distinctUntilChanged(), + map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) + ); + } else { + return [operation]; + } + }), + toArray() + ).subscribe((ops) => this.operations$.next(ops)); + })); + }); + this.itemPageRoute$ = this.itemRD$.pipe( getAllSucceededRemoteDataPayload(), map((item) => getItemPageRoute(item)) ); + } /** @@ -127,4 +221,10 @@ export class ItemStatusComponent implements OnInit { return hasValue(operation) ? operation.operationKey : undefined; } + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + } diff --git a/src/app/shared/object-list/identifier-data/identifier-data.component.html b/src/app/shared/object-list/identifier-data/identifier-data.component.html new file mode 100644 index 0000000000..91470628c4 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier-data.component.html @@ -0,0 +1,5 @@ + +
+ {{ identifiers[0].value | translate }} +
+
diff --git a/src/app/shared/object-list/identifier-data/identifier-data.component.ts b/src/app/shared/object-list/identifier-data/identifier-data.component.ts new file mode 100644 index 0000000000..85989f34c6 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier-data.component.ts @@ -0,0 +1,56 @@ +import { Component, Input } from '@angular/core'; +import { catchError, map } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; +import { hasValue } from '../../empty.util'; +import { environment } from 'src/environments/environment'; +import { Item } from 'src/app/core/shared/item.model'; +import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service'; +import {IdentifierData} from './identifier-data.model'; +import {IdentifierDataService} from '../../../core/data/identifier-data.service'; + +@Component({ + selector: 'ds-identifier-data', + templateUrl: './identifier-data.component.html' +}) +/** + * Component rendering the access status of an item as a badge + */ +export class IdentifierDataComponent { + + @Input() item: Item; + identifiers$: Observable; + + /** + * Whether to show the access status badge or not + */ + showAccessStatus: boolean; + + /** + * Initialize instance variables + * + * @param {IdentifierDataService} identifierDataService + */ + constructor(private identifierDataService: IdentifierDataService) { } + + ngOnInit(): void { + if (this.item == null) { + // Do not show the badge if the feature is inactive or if the item is null. + return; + } + if (this.item.identifiers == null) { + // In case the access status has not been loaded, do it individually. + this.item.identifiers = this.identifierDataService.getIdentifierDataFor(this.item); + } + this.identifiers$ = this.item.identifiers.pipe( + map((identifierRD) => { + if (identifierRD.statusCode !== 401 && hasValue(identifierRD.payload)) { + return identifierRD.payload; + } else { + return null; + } + }), + // EMpty array if none + //map((identifiers: IdentifierData) => hasValue(identifiers.identifiers) ? identifiers.identifiers : []) + ); + } +} diff --git a/src/app/shared/object-list/identifier-data/identifier-data.model.ts b/src/app/shared/object-list/identifier-data/identifier-data.model.ts new file mode 100644 index 0000000000..e707f396e4 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier-data.model.ts @@ -0,0 +1,33 @@ +import { autoserialize, deserialize } from 'cerialize'; +import { typedObject } from 'src/app/core/cache/builders/build-decorators'; +import { CacheableObject } from 'src/app/core/cache/cacheable-object.model'; +import { HALLink } from 'src/app/core/shared/hal-link.model'; +import { ResourceType } from 'src/app/core/shared/resource-type'; +import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators'; +import { IDENTIFIERS } from './identifier-data.resource-type'; +import {Identifier} from './identifier.model'; + +@typedObject +export class IdentifierData implements CacheableObject { + static type = IDENTIFIERS; + /** + * The type for this IdentifierData + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The + */ + @autoserialize + identifiers: Identifier[]; + + /** + * The {@link HALLink}s for this IdentifierData + */ + @deserialize + _links: { + self: HALLink; + }; +} diff --git a/src/app/shared/object-list/identifier-data/identifier-data.resource-type.ts b/src/app/shared/object-list/identifier-data/identifier-data.resource-type.ts new file mode 100644 index 0000000000..823a43eff9 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier-data.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from 'src/app/core/shared/resource-type'; + +/** + * The resource type for Identifiers + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const IDENTIFIERS = new ResourceType('identifiers'); diff --git a/src/app/shared/object-list/identifier-data/identifier.model.ts b/src/app/shared/object-list/identifier-data/identifier.model.ts new file mode 100644 index 0000000000..87b162afe1 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier.model.ts @@ -0,0 +1,12 @@ +import {autoserialize} from 'cerialize'; + +export class Identifier { + @autoserialize + value: string; + @autoserialize + identifierType: string; + @autoserialize + identifierStatus: string; + @autoserialize + type: string; +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2790c664ec..47e9f57b6d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1918,6 +1918,46 @@ "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper", + "item.edit.identifiers.doi.status.1": "Queued for registration", + + "item.edit.identifiers.doi.status.2": "Queued for reservation", + + "item.edit.identifiers.doi.status.3": "Registered", + + "item.edit.identifiers.doi.status.4": "Reserved", + + "item.edit.identifiers.doi.status.5": "Reserved", + + "item.edit.identifiers.doi.status.6": "Registered", + + "item.edit.identifiers.doi.status.7": "Queued for registration", + + "item.edit.identifiers.doi.status.8": "Queued for deletion", + + "item.edit.identifiers.doi.status.9": "Deleted", + + "item.edit.identifiers.doi.status.10": "Pending approval", + + "item.edit.identifiers.doi.status.11": "Minted, not registered", + + "item.edit.tabs.status.buttons.registerDOI.label": "Register a new or pending identifier", + + "item.edit.tabs.status.buttons.registerDOI.button": "Register DOI...", + + "item.edit.registerdoi.header": "Register a new or pending DOI", + + "item.edit.registerdoi.description": "Review any pending identifiers and item metadata below and click Confirm to proceed with DOI registration, or Cancel to back out", + + "item.edit.registerdoi.confirm": "Confirm", + + "item.edit.registerdoi.cancel": "Cancel", + + "item.edit.registerdoi.success": "DOI registered successfully. Refresh Item Status page to see new DOI details.", + + "item.edit.registerdoi.error": "Error registering DOI", + + "item.edit.registerdoi.to-update": "The following DOI has already been minted and will be queued for registration online", + "item.edit.item-mapper.buttons.add": "Map item to selected collections", "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", From 87bbe50732c817939452d55359a6bf64de31f4d8 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 31 Aug 2022 15:21:38 +1200 Subject: [PATCH 093/125] [TLC-249] Linting --- .../edit-item-page/edit-item-page.routing.module.ts | 2 +- .../item-register-doi/item-registerdoi.component.ts | 8 +++++++- .../item-status/item-status.component.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts index ee4c563646..22fd528653 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts @@ -10,7 +10,7 @@ import { ItemStatusComponent } from './item-status/item-status.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; import { ItemMoveComponent } from './item-move/item-move.component'; -import { ItemRegisterDoiComponent } from './item-register-doi/item-registerdoi.component' +import { ItemRegisterDoiComponent } from './item-register-doi/item-registerdoi.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts index 060fef3b32..15af4ea633 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts @@ -27,7 +27,7 @@ export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent protected messageKey = 'registerdoi'; doiToUpdateMessage = 'item.edit.' + this.messageKey + '.to-update'; identifiers$: Observable; - processing: boolean = false; + processing = false; constructor(protected route: ActivatedRoute, protected router: Router, @@ -38,6 +38,9 @@ export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent super(route, router, notificationsService, itemDataService, translateService); } + /** + * Initialise component + */ ngOnInit(): void { this.itemRD$ = this.route.data.pipe( map((data) => data.dso), @@ -74,6 +77,9 @@ export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent this.registerDoi(); } + /** + * Request that a pending, minted or null DOI be queued for registration + */ registerDoi() { this.processing = true; this.itemDataService.registerDOI(this.item.id).pipe(getFirstCompletedRemoteData()).subscribe( diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index a719e621dc..df636d78dc 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -113,7 +113,7 @@ export class ItemStatusComponent implements OnInit { // Observable for configuration determining whether the Register DOI feature is enabled let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.register').pipe( map((enabled: RemoteData) => { - let show: boolean = false; + let show = false; if (enabled.hasSucceeded) { if (enabled.payload !== undefined && enabled.payload !== null) { if (enabled.payload.values !== undefined) { @@ -154,14 +154,14 @@ export class ItemStatusComponent implements OnInit { let showRegister$: Observable = combineLatest([this.identifiers$, registerConfigEnabled$]).pipe( distinctUntilChanged(), map(([identifiers, enabled]) => { - let no_doi: boolean = true; - let pending: boolean = false; + let no_doi = true; + let pending = false; if (identifiers !== undefined && identifiers !== null) { identifiers.forEach((identifier: Identifier) => { - if (hasValue(identifier) && identifier.identifierType == 'doi') { + if (hasValue(identifier) && identifier.identifierType === 'doi') { // The item has some kind of DOI no_doi = false; - if (identifier.identifierStatus == '10' || identifier.identifierStatus == '11' + if (identifier.identifierStatus === '10' || identifier.identifierStatus === '11' || identifier.identifierStatus == null) { // The item's DOI is pending, minted or null. // It isn't registered, reserved, queued for registration or reservation or update, deleted @@ -174,7 +174,7 @@ export class ItemStatusComponent implements OnInit { // If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true return ((pending || no_doi) && enabled); }) - ) + ); // Subscribe to changes from the showRegister check and rebuild operations list accordingly this.subs.push(showRegister$.subscribe((show) => { From e0c0d3c8e0dcbbee1b00694afd2a3f922cab7945 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 8 Sep 2022 10:08:45 +1200 Subject: [PATCH 094/125] [TLC-337] Unit tests for register DOI component --- .../item-registerdoi.component.spec.ts | 106 ++++++++++++++++++ .../item-registerdoi.component.ts | 3 +- .../item-status/item-status.component.spec.ts | 24 +++- 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts new file mode 100644 index 0000000000..eaf4766a86 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts @@ -0,0 +1,106 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { RouterStub } from '../../../shared/testing/router.stub'; +import { of as observableOf } from 'rxjs'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { ItemRegisterDoiComponent } from './item-registerdoi.component'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { IdentifierDataService } from '../../../core/data/identifier-data.service'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; + +let comp: ItemRegisterDoiComponent; +let fixture: ComponentFixture; + +let mockItem; +let itemPageUrl; +let routerStub; +let mockItemDataService: ItemDataService; +let mockIdentifierDataService: IdentifierDataService; +let routeStub; +let notificationsServiceStub; + +describe('ItemRegisterDoiComponent', () => { + beforeEach(waitForAsync(() => { + + mockItem = Object.assign(new Item(), { + id: 'fake-id', + handle: 'fake/handle', + lastModified: '2018', + isWithdrawn: true + }); + + itemPageUrl = `fake-url/${mockItem.id}`; + routerStub = Object.assign(new RouterStub(), { + url: `${itemPageUrl}/edit` + }); + + mockIdentifierDataService = jasmine.createSpyObj('mockIdentifierDataService', { + getIdentifierDataFor: createSuccessfulRemoteDataObject$({"identifiers": []}), + getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$("true") + }); + + mockItemDataService = jasmine.createSpyObj('mockItemDataService', { + registerDOI: createSuccessfulRemoteDataObject$(mockItem) + }); + + routeStub = { + data: observableOf({ + dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), { + id: 'fake-id' + })) + }) + }; + + notificationsServiceStub = new NotificationsServiceStub(); + + TestBed.configureTestingModule({ + imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], + declarations: [ItemRegisterDoiComponent], + providers: [ + { provide: ActivatedRoute, useValue: routeStub }, + { provide: Router, useValue: routerStub }, + { provide: ItemDataService, useValue: mockItemDataService }, + { provide: IdentifierDataService, useValue: mockIdentifierDataService}, + { provide: NotificationsService, useValue: notificationsServiceStub } + ], schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemRegisterDoiComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should render a page with messages based on the \'registerdoi\' messageKey', () => { + const header = fixture.debugElement.query(By.css('h2')).nativeElement; + expect(header.innerHTML).toContain('item.edit.registerdoi.header'); + const description = fixture.debugElement.query(By.css('p')).nativeElement; + expect(description.innerHTML).toContain('item.edit.registerdoi.description'); + const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement; + expect(confirmButton.innerHTML).toContain('item.edit.registerdoi.confirm'); + const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement; + expect(cancelButton.innerHTML).toContain('item.edit.registerdoi.cancel'); + }); + + describe('performAction', () => { + it('should call registerDOI function from the ItemDataService', () => { + spyOn(comp, 'processRestResponse'); + comp.performAction(); + expect(mockItemDataService.registerDOI).toHaveBeenCalledWith(comp.item.id); + expect(comp.processRestResponse).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts index 15af4ea633..875bcbac7e 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts @@ -85,7 +85,8 @@ export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent this.itemDataService.registerDOI(this.item.id).pipe(getFirstCompletedRemoteData()).subscribe( (response: RemoteData) => { this.processing = false; - this.router.navigateByUrl(getItemEditRoute(this.item)); + //this.router.navigateByUrl(getItemEditRoute(this.item)); + this.processRestResponse(response); } ); } diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts index 0829480670..1f27d97f29 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts @@ -11,8 +11,14 @@ import { Item } from '../../../core/shared/item.model'; import { By } from '@angular/platform-browser'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { of as observableOf } from 'rxjs'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { IdentifierDataService } from '../../../core/data/identifier-data.service'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; + +let mockIdentifierDataService: IdentifierDataService; +let mockConfigurationDataService: ConfigurationDataService; describe('ItemStatusComponent', () => { let comp: ItemStatusComponent; @@ -28,6 +34,20 @@ describe('ItemStatusComponent', () => { } }); + mockIdentifierDataService = jasmine.createSpyObj('mockIdentifierDataService', { + getIdentifierDataFor: createSuccessfulRemoteDataObject$({"identifiers": []}), + getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$("true") + }); + + mockConfigurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'identifiers.item-status.register', + values: [ + 'true' + ] + })) + }); + const itemPageUrl = `/items/${mockItem.uuid}`; const routeStub = { @@ -50,6 +70,8 @@ describe('ItemStatusComponent', () => { { provide: ActivatedRoute, useValue: routeStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: IdentifierDataService, useValue: mockIdentifierDataService }, + { provide: ConfigurationDataService, useValue: mockConfigurationDataService } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); })); From 8d70e1788b077fedd998e69398f07763c7e49759 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 16 Sep 2022 13:01:13 +1200 Subject: [PATCH 095/125] [TLC-249] Lint fixes --- .../item-register-doi/item-registerdoi.component.spec.ts | 4 ++-- .../edit-item-page/item-status/item-status.component.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts index eaf4766a86..d7941f3bb5 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts @@ -45,8 +45,8 @@ describe('ItemRegisterDoiComponent', () => { }); mockIdentifierDataService = jasmine.createSpyObj('mockIdentifierDataService', { - getIdentifierDataFor: createSuccessfulRemoteDataObject$({"identifiers": []}), - getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$("true") + getIdentifierDataFor: createSuccessfulRemoteDataObject$({'identifiers': []}), + getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$('true') }); mockItemDataService = jasmine.createSpyObj('mockItemDataService', { diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts index 1f27d97f29..ce8293ce79 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts @@ -35,8 +35,8 @@ describe('ItemStatusComponent', () => { }); mockIdentifierDataService = jasmine.createSpyObj('mockIdentifierDataService', { - getIdentifierDataFor: createSuccessfulRemoteDataObject$({"identifiers": []}), - getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$("true") + getIdentifierDataFor: createSuccessfulRemoteDataObject$({'identifiers': []}), + getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$('true') }); mockConfigurationDataService = jasmine.createSpyObj('configurationDataService', { From ed5ab710a3f10c20d85de858e985d224a0e67cf4 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 16 Sep 2022 13:19:07 +1200 Subject: [PATCH 096/125] [TLC-249] Update data service usage since rebasing --- src/app/core/data/identifier-data.service.ts | 21 ++++++++----------- .../item-registerdoi.component.spec.ts | 1 - 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts index 9847f0b2b5..874d486d35 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -2,31 +2,27 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { dataService } from '../cache/builders/build-decorators'; +import { dataService } from './base/data-service.decorator'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { DataService } from './data.service'; +import { BaseDataService } from './base/base-data.service'; import { RequestService } from './request.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { CoreState } from '../core-state.model'; -import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model'; -import { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type'; import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; import { Item } from '../shared/item.model'; -import {IDENTIFIERS} from '../../shared/object-list/identifier-data/identifier-data.resource-type'; -import {IdentifierData} from '../../shared/object-list/identifier-data/identifier-data.model'; -import {getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload} from '../shared/operators'; -import {map, startWith} from 'rxjs/operators'; +import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; +import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { map } from 'rxjs/operators'; import {ConfigurationProperty} from '../shared/configuration-property.model'; import {ConfigurationDataService} from './configuration-data.service'; @Injectable() @dataService(IDENTIFIERS) -export class IdentifierDataService extends DataService { - - protected linkPath = 'identifiers'; +export class IdentifierDataService extends BaseDataService { constructor( protected comparator: DefaultChangeAnalyzer, @@ -38,8 +34,9 @@ export class IdentifierDataService extends DataService { protected requestService: RequestService, protected store: Store, private configurationService: ConfigurationDataService, + protected linkPath = 'identifiers', ) { - super(); + super(linkPath, requestService, rdbService, objectCache, halService); } /** diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts index d7941f3bb5..9f0bb280c7 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts @@ -16,7 +16,6 @@ import { By } from '@angular/platform-browser'; import { ItemRegisterDoiComponent } from './item-registerdoi.component'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { IdentifierDataService } from '../../../core/data/identifier-data.service'; -import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; let comp: ItemRegisterDoiComponent; let fixture: ComponentFixture; From 7795f92234fda8dd3f015db918c0668b2ba6636f Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sun, 18 Sep 2022 11:17:49 +1200 Subject: [PATCH 097/125] [TLC-249] Update data service usage since rebasing --- src/app/core/data/identifier-data.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts index 874d486d35..d494b5171d 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -34,9 +34,8 @@ export class IdentifierDataService extends BaseDataService { protected requestService: RequestService, protected store: Store, private configurationService: ConfigurationDataService, - protected linkPath = 'identifiers', ) { - super(linkPath, requestService, rdbService, objectCache, halService); + super('identifiers', requestService, rdbService, objectCache, halService); } /** From 21c9ef4ea2f5762dfd622f8b1c6be146831f7203 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 31 Oct 2022 11:03:58 +1300 Subject: [PATCH 098/125] [TLC-249] Ensure identifier data model mapped to type --- src/app/core/core.module.ts | 4 +++- src/app/core/shared/item.model.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index ede23ba43b..9ad962caa7 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -170,6 +170,7 @@ import { OrcidHistory } from './orcid/model/orcid-history.model'; import { OrcidAuthService } from './orcid/orcid-auth.service'; import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service'; import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service'; +import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -356,7 +357,8 @@ export const models = ResearcherProfile, OrcidQueue, OrcidHistory, - AccessStatusObject + AccessStatusObject, + IdentifierData, ]; @NgModule({ diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index a9acf8c10f..3441896ed1 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -128,7 +128,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject * The identifier data for this Item * Will be undefined unless the identifiers {@link HALLink} has been resolved. */ - @link(IDENTIFIERS) + @link(IDENTIFIERS, false, 'identifiers') identifiers?: Observable>; /** From d0b5fc257a99af4e15ceb03625fee093e8223f21 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 1 Nov 2022 11:49:24 +1300 Subject: [PATCH 099/125] [TLC-249] Update spec test for new config form data service --- .../identifiers/section-identifiers.component.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts index 378ec911c7..b381bdf33c 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts @@ -22,7 +22,7 @@ import { FormBuilderService } from '../../../shared/form/builder/form-builder.se import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; import { getMockFormService } from '../../../shared/mocks/form-service.mock'; import { FormService } from '../../../shared/form/form.service'; -import { SubmissionFormsConfigService } from '../../../core/config/submission-forms-config.service'; +import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsType } from '../sections-type'; import {mockSectionsData, mockSubmissionCollectionId, mockSubmissionId} from '../../../shared/mocks/submission.mock'; @@ -41,7 +41,7 @@ import { Item } from '../../../core/shared/item.model'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; -function getMockSubmissionFormsConfigService(): SubmissionFormsConfigService { +function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService { return jasmine.createSpyObj('FormOperationsService', { getConfigAll: jasmine.createSpy('getConfigAll'), getConfigByHref: jasmine.createSpy('getConfigByHref'), @@ -146,7 +146,7 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { { provide: SectionFormOperationsService, useValue: getMockFormOperationsService() }, { provide: FormService, useValue: getMockFormService() }, { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, - { provide: SubmissionFormsConfigService, useValue: getMockSubmissionFormsConfigService() }, + { provide: SubmissionFormsConfigDataService, useValue: getMockSubmissionFormsConfigService() }, { provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: SectionsService, useClass: SectionsServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }, From 838cde48417c0a2cefe855e617cc9d8a5feaf787 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 19 Jan 2023 15:01:04 +1300 Subject: [PATCH 100/125] [TLC-249] Addressing review feedback Adding comments and tidying some comments, imports Expect text for status not integer Send a 'type' parameter with a DOI registration Rename item-status.register to registerDOI As per todonohue's feedback on 2022-01-18 --- src/app/core/data/identifier-data.service.ts | 7 ++++++- src/app/core/data/item-data.service.ts | 4 +++- .../item-status/item-status.component.spec.ts | 2 +- .../item-status/item-status.component.ts | 9 +++------ .../identifier-data.component.ts | 19 +++++------------- .../identifier-data/identifier.model.ts | 20 ++++++++++++++++++- 6 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts index d494b5171d..be4c5c8b59 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -20,6 +20,11 @@ import { map } from 'rxjs/operators'; import {ConfigurationProperty} from '../shared/configuration-property.model'; import {ConfigurationDataService} from './configuration-data.service'; +/** + * The service handling all REST requests to get item identifiers like handles and DOIs + * from the /identifiers endpoint, as well as the backend configuration that controls whether a 'Register DOI' + * button appears for admins in the item status page + */ @Injectable() @dataService(IDENTIFIERS) export class IdentifierDataService extends BaseDataService { @@ -50,7 +55,7 @@ export class IdentifierDataService extends BaseDataService { * Should we allow registration of new DOIs via the item status page? */ public getIdentifierRegistrationConfiguration(): Observable { - return this.configurationService.findByPropertyName('identifiers.item-status.register').pipe( + return this.configurationService.findByPropertyName('identifiers.item-status.registerDOI').pipe( getFirstCompletedRemoteData(), map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []) ); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 5306dd468b..79719e502c 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -256,7 +256,9 @@ export abstract class BaseItemDataService extends IdentifiableDataService let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'application/json'); options.headers = headers; - const request = new PostRequest(requestId, href, JSON.stringify({}), options); + // Pass identifier type as a simple parameter, no need for full JSON data + let hrefWithParams: string = this.buildHrefWithParams(href, [new RequestParam("type", "doi")]); + const request = new PostRequest(requestId, hrefWithParams, JSON.stringify({}), options); this.requestService.send(request); }); return this.rdbService.buildFromRequestUUID(requestId); diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts index ce8293ce79..1f447deb9e 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts @@ -41,7 +41,7 @@ describe('ItemStatusComponent', () => { mockConfigurationDataService = jasmine.createSpyObj('configurationDataService', { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { - name: 'identifiers.item-status.register', + name: 'identifiers.item-status.registerDOI', values: [ 'true' ] diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index df636d78dc..20f167fb80 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -3,7 +3,7 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { ItemOperation } from '../item-operation/itemOperation.model'; -import {distinctUntilChanged, first, map, mergeMap, startWith, switchMap, toArray} from 'rxjs/operators'; +import {distinctUntilChanged, first, map, mergeMap, toArray} from 'rxjs/operators'; import {BehaviorSubject, Observable, from as observableFrom, Subscription, combineLatest, of} from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; @@ -12,11 +12,8 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { hasValue } from '../../../shared/empty.util'; import { getAllSucceededRemoteDataPayload, - getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import {IdentifierDataService} from '../../../core/data/identifier-data.service'; -import {IdentifierData} from '../../../shared/object-list/identifier-data/identifier-data.model'; import {Identifier} from '../../../shared/object-list/identifier-data/identifier.model'; import {ConfigurationProperty} from '../../../core/shared/configuration-property.model'; import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; @@ -111,7 +108,7 @@ export class ItemStatusComponent implements OnInit { ); // Observable for configuration determining whether the Register DOI feature is enabled - let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.register').pipe( + let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.registerDOI').pipe( map((enabled: RemoteData) => { let show = false; if (enabled.hasSucceeded) { @@ -161,7 +158,7 @@ export class ItemStatusComponent implements OnInit { if (hasValue(identifier) && identifier.identifierType === 'doi') { // The item has some kind of DOI no_doi = false; - if (identifier.identifierStatus === '10' || identifier.identifierStatus === '11' + if (identifier.identifierStatus === 'PENDING' || identifier.identifierStatus === 'MINTED' || identifier.identifierStatus == null) { // The item's DOI is pending, minted or null. // It isn't registered, reserved, queued for registration or reservation or update, deleted diff --git a/src/app/shared/object-list/identifier-data/identifier-data.component.ts b/src/app/shared/object-list/identifier-data/identifier-data.component.ts index 85989f34c6..5b495bfa62 100644 --- a/src/app/shared/object-list/identifier-data/identifier-data.component.ts +++ b/src/app/shared/object-list/identifier-data/identifier-data.component.ts @@ -1,10 +1,8 @@ import { Component, Input } from '@angular/core'; -import { catchError, map } from 'rxjs/operators'; -import { Observable, of as observableOf } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import { hasValue } from '../../empty.util'; -import { environment } from 'src/environments/environment'; import { Item } from 'src/app/core/shared/item.model'; -import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service'; import {IdentifierData} from './identifier-data.model'; import {IdentifierDataService} from '../../../core/data/identifier-data.service'; @@ -13,18 +11,13 @@ import {IdentifierDataService} from '../../../core/data/identifier-data.service' templateUrl: './identifier-data.component.html' }) /** - * Component rendering the access status of an item as a badge + * Component rendering an identifier, eg. DOI or handle */ export class IdentifierDataComponent { @Input() item: Item; identifiers$: Observable; - /** - * Whether to show the access status badge or not - */ - showAccessStatus: boolean; - /** * Initialize instance variables * @@ -34,11 +27,11 @@ export class IdentifierDataComponent { ngOnInit(): void { if (this.item == null) { - // Do not show the badge if the feature is inactive or if the item is null. + // Do not show the identifier if the feature is inactive or if the item is null. return; } if (this.item.identifiers == null) { - // In case the access status has not been loaded, do it individually. + // In case the identifier has not been loaded, do it individually. this.item.identifiers = this.identifierDataService.getIdentifierDataFor(this.item); } this.identifiers$ = this.item.identifiers.pipe( @@ -49,8 +42,6 @@ export class IdentifierDataComponent { return null; } }), - // EMpty array if none - //map((identifiers: IdentifierData) => hasValue(identifiers.identifiers) ? identifiers.identifiers : []) ); } } diff --git a/src/app/shared/object-list/identifier-data/identifier.model.ts b/src/app/shared/object-list/identifier-data/identifier.model.ts index 87b162afe1..f528824b36 100644 --- a/src/app/shared/object-list/identifier-data/identifier.model.ts +++ b/src/app/shared/object-list/identifier-data/identifier.model.ts @@ -1,12 +1,30 @@ -import {autoserialize} from 'cerialize'; +import { autoserialize } from 'cerialize'; +/** + * Identifier model. Identifiers using this model are returned in lists from the /item/{id}/identifiers endpoint + * + * @author Kim Shepherd + */ export class Identifier { + /** + * The value of the identifier, eg. http://hdl.handle.net/123456789/123 or https://doi.org/test/doi/1234 + */ @autoserialize value: string; + /** + * The type of identiifer, eg. "doi", or "handle", or "other" + */ @autoserialize identifierType: string; + /** + * The status of the identifier. Some schemes, like DOI, will have a different status based on whether it is + * queued for remote registration, reservation, or update, or has been registered, simply minted locally, etc. + */ @autoserialize identifierStatus: string; + /** + * The type of resource, in this case Identifier + */ @autoserialize type: string; } From 52a37760270c522ef34f9e45985d78d19f663011 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 19 Jan 2023 16:02:15 +1300 Subject: [PATCH 101/125] [TLC-249] Addressing review feedback #2 Update WorkspaceitemSectionIdentifiersObject model to include a display types string array, so identifier types can be included or excluded from the step as needed. Update section template to work with the above new data As per todonohue's feedback on 2022-01-18 --- ...workspaceitem-section-identifiers.model.ts | 1 + .../identifier-data.component.ts | 4 +- .../section-identifiers.component.html | 40 +++++++++++-------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts b/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts index 7d22bf0b61..6a8956dff3 100644 --- a/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts @@ -5,4 +5,5 @@ export interface WorkspaceitemSectionIdentifiersObject { doi?: string handle?: string otherIdentifiers?: string[] + displayTypes?: string[] } diff --git a/src/app/shared/object-list/identifier-data/identifier-data.component.ts b/src/app/shared/object-list/identifier-data/identifier-data.component.ts index 5b495bfa62..cb6d1d97e5 100644 --- a/src/app/shared/object-list/identifier-data/identifier-data.component.ts +++ b/src/app/shared/object-list/identifier-data/identifier-data.component.ts @@ -3,8 +3,8 @@ import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { hasValue } from '../../empty.util'; import { Item } from 'src/app/core/shared/item.model'; -import {IdentifierData} from './identifier-data.model'; -import {IdentifierDataService} from '../../../core/data/identifier-data.service'; +import { IdentifierData } from './identifier-data.model'; +import { IdentifierDataService } from '../../../core/data/identifier-data.service'; @Component({ selector: 'ds-identifier-data', diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.html b/src/app/submission/sections/identifiers/section-identifiers.component.html index 1c78119931..39645704da 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.html +++ b/src/app/submission/sections/identifiers/section-identifiers.component.html @@ -9,26 +9,34 @@ Template for the identifiers submission section component {{'submission.sections.identifiers.info' | translate}}
    - -
  • {{'submission.sections.identifiers.no_handle' | translate}}
  • -
    - -
  • {{'submission.sections.identifiers.handle_label' | translate}}{{identifierData.handle}}
  • + + +
  • {{'submission.sections.identifiers.no_handle' | translate}}
  • +
    + +
  • {{'submission.sections.identifiers.handle_label' | translate}}{{identifierData.handle}}
  • +
    + - -
  • {{'submission.sections.identifiers.no_doi' | translate}}
  • -
    - -
  • {{'submission.sections.identifiers.doi_label' | translate}} - {{identifierData.doi}} -
  • + + +
  • {{'submission.sections.identifiers.no_doi' | translate}}
  • +
    + +
  • {{'submission.sections.identifiers.doi_label' | translate}} + {{identifierData.doi}} +
  • +
    + - -
  • {{'submission.sections.identifiers.otherIdentifiers_label' | translate}} - {{identifierData.otherIdentifiers.join(',')}} -
  • + + +
  • {{'submission.sections.identifiers.otherIdentifiers_label' | translate}} + {{identifierData.otherIdentifiers.join(',')}} +
  • +
From 39fb61ca856c75f9b6d32518d0b6d97346cfb40b Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 19 Jan 2023 16:15:31 +1300 Subject: [PATCH 102/125] [TLC-249] Lint fixes (imports, quotes) --- src/app/core/data/item-data.service.ts | 2 +- .../item-register-doi/item-registerdoi.component.ts | 4 ++-- .../item-status/item-status.component.ts | 10 +++++----- .../identifiers/section-identifiers.component.spec.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 79719e502c..96007f5598 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -257,7 +257,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService headers = headers.append('Content-Type', 'application/json'); options.headers = headers; // Pass identifier type as a simple parameter, no need for full JSON data - let hrefWithParams: string = this.buildHrefWithParams(href, [new RequestParam("type", "doi")]); + let hrefWithParams: string = this.buildHrefWithParams(href, [new RequestParam('type', 'doi')]); const request = new PostRequest(requestId, hrefWithParams, JSON.stringify({}), options); this.requestService.send(request); }); diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts index 875bcbac7e..5f20c2a3d9 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts @@ -11,9 +11,9 @@ import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../../ import { first, map } from 'rxjs/operators'; import { hasValue } from '../../../shared/empty.util'; import { Observable } from 'rxjs'; -import {getItemEditRoute, getItemPageRoute} from '../../item-page-routing-paths'; +import { getItemPageRoute } from '../../item-page-routing-paths'; import { IdentifierDataService } from '../../../core/data/identifier-data.service'; -import {Identifier} from '../../../shared/object-list/identifier-data/identifier.model'; +import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model'; @Component({ selector: 'ds-item-registerdoi', diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index 20f167fb80..0f3b44f26d 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -4,7 +4,7 @@ import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { ItemOperation } from '../item-operation/itemOperation.model'; import {distinctUntilChanged, first, map, mergeMap, toArray} from 'rxjs/operators'; -import {BehaviorSubject, Observable, from as observableFrom, Subscription, combineLatest, of} from 'rxjs'; +import { BehaviorSubject, Observable, from as observableFrom, Subscription, combineLatest } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -13,10 +13,10 @@ import { hasValue } from '../../../shared/empty.util'; import { getAllSucceededRemoteDataPayload, } from '../../../core/shared/operators'; -import {IdentifierDataService} from '../../../core/data/identifier-data.service'; -import {Identifier} from '../../../shared/object-list/identifier-data/identifier.model'; -import {ConfigurationProperty} from '../../../core/shared/configuration-property.model'; -import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; +import { IdentifierDataService } from '../../../core/data/identifier-data.service'; +import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; @Component({ selector: 'ds-item-status', diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts index b381bdf33c..6a001c4d5f 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts @@ -7,7 +7,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NgxPaginationModule } from 'ngx-pagination'; import { cold } from 'jasmine-marbles'; -import {Observable, of as observableOf} from 'rxjs'; +import { of as observableOf } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; @@ -25,7 +25,7 @@ import { FormService } from '../../../shared/form/form.service'; import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsType } from '../sections-type'; -import {mockSectionsData, mockSubmissionCollectionId, mockSubmissionId} from '../../../shared/mocks/submission.mock'; +import { mockSubmissionCollectionId, mockSubmissionId } from '../../../shared/mocks/submission.mock'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { SubmissionSectionIdentifiersComponent } from './section-identifiers.component'; import { CollectionDataService } from '../../../core/data/collection-data.service'; From d6f458863f40f6ac0c102f9a0b888e58cae857f2 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 25 Jan 2023 13:01:57 +1300 Subject: [PATCH 103/125] [TLC-249] Improve model of identifier data in workspace section --- ...workspaceitem-section-identifiers.model.ts | 6 +-- .../section-identifiers.component.html | 37 ++++--------------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts b/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts index 6a8956dff3..f6bfb1ae04 100644 --- a/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts +++ b/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts @@ -1,9 +1,9 @@ /* * Object model for the data returned by the REST API to present minted identifiers in a submission section */ +import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model'; + export interface WorkspaceitemSectionIdentifiersObject { - doi?: string - handle?: string - otherIdentifiers?: string[] + identifiers?: Identifier[] displayTypes?: string[] } diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.html b/src/app/submission/sections/identifiers/section-identifiers.component.html index 39645704da..dd0b5d2930 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.html +++ b/src/app/submission/sections/identifiers/section-identifiers.component.html @@ -3,41 +3,18 @@ Template for the identifiers submission section component @author Kim Shepherd --> - -
+ +
{{'submission.sections.identifiers.info' | translate}}
    - - - -
  • {{'submission.sections.identifiers.no_handle' | translate}}
  • -
    - -
  • {{'submission.sections.identifiers.handle_label' | translate}}{{identifierData.handle}}
  • -
    -
    - - - - -
  • {{'submission.sections.identifiers.no_doi' | translate}}
  • -
    - -
  • {{'submission.sections.identifiers.doi_label' | translate}} - {{identifierData.doi}} -
  • -
    -
    - - - - -
  • {{'submission.sections.identifiers.otherIdentifiers_label' | translate}} - {{identifierData.otherIdentifiers.join(',')}} -
  • + + +
  • {{'submission.sections.identifiers.' + identifier.identifierType + '_label' | translate}} + {{identifier.value}}
+ From 9471aa98977ac843a88ce0748d59759507d9e352 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 25 Jan 2023 14:23:41 +1300 Subject: [PATCH 104/125] [TLC-249] Update en.json5 with DOI status labels --- src/assets/i18n/en.json5 | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 47e9f57b6d..c811383c86 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1918,27 +1918,29 @@ "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper", - "item.edit.identifiers.doi.status.1": "Queued for registration", + "item.edit.identifiers.doi.status.UNKNOWN": "Unknown", - "item.edit.identifiers.doi.status.2": "Queued for reservation", + "item.edit.identifiers.doi.status.TO_BE_REGISTERED": "Queued for registration", - "item.edit.identifiers.doi.status.3": "Registered", + "item.edit.identifiers.doi.status.TO_BE_RESERVED": "Queued for reservation", - "item.edit.identifiers.doi.status.4": "Reserved", + "item.edit.identifiers.doi.status.IS_REGISTERED": "Registered", - "item.edit.identifiers.doi.status.5": "Reserved", + "item.edit.identifiers.doi.status.IS_RESERVED": "Reserved", - "item.edit.identifiers.doi.status.6": "Registered", + "item.edit.identifiers.doi.status.UPDATE_RESERVED": "Reserved (update queued)", - "item.edit.identifiers.doi.status.7": "Queued for registration", + "item.edit.identifiers.doi.status.UPDATE_REGISTERED": "Registered (update queued)", - "item.edit.identifiers.doi.status.8": "Queued for deletion", + "item.edit.identifiers.doi.status.UPDATE_BEFORE_REGISTRATION": "Queued for update and registration", - "item.edit.identifiers.doi.status.9": "Deleted", + "item.edit.identifiers.doi.status.TO_BE_DELETED": "Queued for deletion", - "item.edit.identifiers.doi.status.10": "Pending approval", + "item.edit.identifiers.doi.status.DELETED": "Deleted", - "item.edit.identifiers.doi.status.11": "Minted, not registered", + "item.edit.identifiers.doi.status.PENDING": "Pending (not registered)", + + "item.edit.identifiers.doi.status.MINTED": "Minted (not registered)", "item.edit.tabs.status.buttons.registerDOI.label": "Register a new or pending identifier", From b5c6e7f1b76a10827354116221f92d712e468782 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 25 Jan 2023 16:02:23 +1300 Subject: [PATCH 105/125] [TLC-249] Identifier mock data updated in unit test --- .../section-identifiers.component.spec.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts index 6a001c4d5f..e9663ae20c 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts @@ -72,9 +72,20 @@ const mockItem = Object.assign(new Item(), { // Mock identifier data to use with tests const identifierData: WorkspaceitemSectionIdentifiersObject = { - doi: 'https://doi.org/10.33515/dspace/1', - handle: '123456789/999', - otherIdentifiers: ['123-123-123', 'ANBX-159'] + identifiers: [{ + value: 'https://doi.org/10.33515/dspace-61', + identifierType: 'doi', + identifierStatus: 'TO_BE_REGISTERED', + type: 'identifier' + }, + { + value: '123456789/418', + identifierType: 'handle', + identifierStatus: null, + type: 'identifier' + } + ], + displayTypes: ["doi", "handle"] }; // Mock section object to use with tests From b779509bf0ed027d555739c0078f387b5ab7fbea Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 25 Jan 2023 16:14:00 +1300 Subject: [PATCH 106/125] [TLC-249] Identifier mock data updated in unit test --- .../sections/identifiers/section-identifiers.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts index e9663ae20c..041e2af23a 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts @@ -85,7 +85,7 @@ const identifierData: WorkspaceitemSectionIdentifiersObject = { type: 'identifier' } ], - displayTypes: ["doi", "handle"] + displayTypes: ['doi', 'handle'] }; // Mock section object to use with tests From ce84c3fe36080ca888cad48db895398612c2fc0d Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 1 Feb 2023 13:50:29 +1300 Subject: [PATCH 107/125] [TLC-380] Renaming, changes as per review feedback --- src/app/core/data/identifier-data.service.ts | 2 +- .../edit-item-page.routing.module.ts | 6 +++--- ...ard.ts => item-page-register-doi.guard.ts} | 0 ....html => item-register-doi-component.html} | 0 ...ts => item-register-doi.component.spec.ts} | 12 +++++------ ...nent.ts => item-register-doi.component.ts} | 4 ++-- .../item-status/item-status.component.spec.ts | 2 +- .../item-status/item-status.component.ts | 2 +- src/assets/i18n/en.json5 | 20 +++++++++---------- 9 files changed, 24 insertions(+), 24 deletions(-) rename src/app/item-page/edit-item-page/{item-page-registerdoi.guard.ts => item-page-register-doi.guard.ts} (100%) rename src/app/item-page/edit-item-page/item-register-doi/{item-registerdoi-component.html => item-register-doi-component.html} (100%) rename src/app/item-page/edit-item-page/item-register-doi/{item-registerdoi.component.spec.ts => item-register-doi.component.spec.ts} (93%) rename src/app/item-page/edit-item-page/item-register-doi/{item-registerdoi.component.ts => item-register-doi.component.ts} (97%) diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts index be4c5c8b59..6596f6493c 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -55,7 +55,7 @@ export class IdentifierDataService extends BaseDataService { * Should we allow registration of new DOIs via the item status page? */ public getIdentifierRegistrationConfiguration(): Observable { - return this.configurationService.findByPropertyName('identifiers.item-status.registerDOI').pipe( + return this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe( getFirstCompletedRemoteData(), map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []) ); diff --git a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts index 22fd528653..88172e2620 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts @@ -10,7 +10,7 @@ import { ItemStatusComponent } from './item-status/item-status.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; import { ItemMoveComponent } from './item-move/item-move.component'; -import { ItemRegisterDoiComponent } from './item-register-doi/item-registerdoi.component'; +import { ItemRegisterDoiComponent } from './item-register-doi/item-register-doi.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; @@ -40,7 +40,7 @@ import { ItemPageRelationshipsGuard } from './item-page-relationships.guard'; import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard'; import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard'; import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; -import { ItemPageRegisterDoiGuard } from './item-page-registerdoi.guard'; +import { ItemPageRegisterDoiGuard } from './item-page-register-doi.guard'; /** * Routing module that handles the routing for the Edit Item page administrator functionality @@ -149,7 +149,7 @@ import { ItemPageRegisterDoiGuard } from './item-page-registerdoi.guard'; path: ITEM_EDIT_REGISTER_DOI_PATH, component: ItemRegisterDoiComponent, canActivate: [ItemPageRegisterDoiGuard], - data: { title: 'item.edit.registerdoi.title' }, + data: { title: 'item.edit.register-doi.title' }, }, { path: ITEM_EDIT_AUTHORIZATIONS_PATH, diff --git a/src/app/item-page/edit-item-page/item-page-registerdoi.guard.ts b/src/app/item-page/edit-item-page/item-page-register-doi.guard.ts similarity index 100% rename from src/app/item-page/edit-item-page/item-page-registerdoi.guard.ts rename to src/app/item-page/edit-item-page/item-page-register-doi.guard.ts diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi-component.html b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi-component.html similarity index 100% rename from src/app/item-page/edit-item-page/item-register-doi/item-registerdoi-component.html rename to src/app/item-page/edit-item-page/item-register-doi/item-register-doi-component.html diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.spec.ts similarity index 93% rename from src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts rename to src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.spec.ts index 9f0bb280c7..ac17d7c751 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.spec.ts @@ -13,7 +13,7 @@ import { ItemDataService } from '../../../core/data/item-data.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; -import { ItemRegisterDoiComponent } from './item-registerdoi.component'; +import { ItemRegisterDoiComponent } from './item-register-doi.component'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { IdentifierDataService } from '../../../core/data/identifier-data.service'; @@ -83,15 +83,15 @@ describe('ItemRegisterDoiComponent', () => { fixture.detectChanges(); }); - it('should render a page with messages based on the \'registerdoi\' messageKey', () => { + it('should render a page with messages based on the \'register-doi\' messageKey', () => { const header = fixture.debugElement.query(By.css('h2')).nativeElement; - expect(header.innerHTML).toContain('item.edit.registerdoi.header'); + expect(header.innerHTML).toContain('item.edit.register-doi.header'); const description = fixture.debugElement.query(By.css('p')).nativeElement; - expect(description.innerHTML).toContain('item.edit.registerdoi.description'); + expect(description.innerHTML).toContain('item.edit.register-doi.description'); const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement; - expect(confirmButton.innerHTML).toContain('item.edit.registerdoi.confirm'); + expect(confirmButton.innerHTML).toContain('item.edit.register-doi.confirm'); const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement; - expect(cancelButton.innerHTML).toContain('item.edit.registerdoi.cancel'); + expect(cancelButton.innerHTML).toContain('item.edit.register-doi.cancel'); }); describe('performAction', () => { diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts similarity index 97% rename from src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts rename to src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts index 5f20c2a3d9..9345ba18cc 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts @@ -16,8 +16,8 @@ import { IdentifierDataService } from '../../../core/data/identifier-data.servic import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model'; @Component({ - selector: 'ds-item-registerdoi', - templateUrl: './item-registerdoi-component.html' + selector: 'ds-item-register-doi', + templateUrl: './item-register-doi-component.html' }) /** * Component responsible for rendering the Item Registe DOI page diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts index 1f447deb9e..a67de2f435 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts @@ -41,7 +41,7 @@ describe('ItemStatusComponent', () => { mockConfigurationDataService = jasmine.createSpyObj('configurationDataService', { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { - name: 'identifiers.item-status.registerDOI', + name: 'identifiers.item-status.register-doi', values: [ 'true' ] diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index 0f3b44f26d..94122f5658 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -108,7 +108,7 @@ export class ItemStatusComponent implements OnInit { ); // Observable for configuration determining whether the Register DOI feature is enabled - let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.registerDOI').pipe( + let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe( map((enabled: RemoteData) => { let show = false; if (enabled.hasSucceeded) { diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index c811383c86..18fa6e0b4b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1327,7 +1327,7 @@ "curation-task.task.vscan.label": "Virus Scan", - "curation-task.task.registerdoi.label": "Register DOI", + "curation-task.task.register-doi.label": "Register DOI", @@ -1942,23 +1942,23 @@ "item.edit.identifiers.doi.status.MINTED": "Minted (not registered)", - "item.edit.tabs.status.buttons.registerDOI.label": "Register a new or pending identifier", + "item.edit.tabs.status.buttons.register-doi.label": "Register a new or pending identifier", - "item.edit.tabs.status.buttons.registerDOI.button": "Register DOI...", + "item.edit.tabs.status.buttons.register-doi.button": "Register DOI...", - "item.edit.registerdoi.header": "Register a new or pending DOI", + "item.edit.register-doi.header": "Register a new or pending DOI", - "item.edit.registerdoi.description": "Review any pending identifiers and item metadata below and click Confirm to proceed with DOI registration, or Cancel to back out", + "item.edit.register-doi.description": "Review any pending identifiers and item metadata below and click Confirm to proceed with DOI registration, or Cancel to back out", - "item.edit.registerdoi.confirm": "Confirm", + "item.edit.register-doi.confirm": "Confirm", - "item.edit.registerdoi.cancel": "Cancel", + "item.edit.register-doi.cancel": "Cancel", - "item.edit.registerdoi.success": "DOI registered successfully. Refresh Item Status page to see new DOI details.", + "item.edit.register-doi.success": "DOI registered successfully. Refresh Item Status page to see new DOI details.", - "item.edit.registerdoi.error": "Error registering DOI", + "item.edit.register-doi.error": "Error registering DOI", - "item.edit.registerdoi.to-update": "The following DOI has already been minted and will be queued for registration online", + "item.edit.register-doi.to-update": "The following DOI has already been minted and will be queued for registration online", "item.edit.item-mapper.buttons.add": "Map item to selected collections", From e86f0d3d13fe8f69130abd86cbbd6e34e4eef5f1 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 1 Feb 2023 16:30:44 +1300 Subject: [PATCH 108/125] [TLC-380] WIP trying new routing / create flow --- src/app/core/data/identifier-data.service.ts | 44 ++++++++++++++++++- src/app/core/data/item-data.service.ts | 6 ++- .../item-register-doi.component.ts | 4 +- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts index 6596f6493c..53f811bc40 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -1,4 +1,4 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -16,9 +16,18 @@ import { Item } from '../shared/item.model'; import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; import { getFirstCompletedRemoteData } from '../shared/operators'; -import { map } from 'rxjs/operators'; +import { find, map, switchMap } from 'rxjs/operators'; import {ConfigurationProperty} from '../shared/configuration-property.model'; import {ConfigurationDataService} from './configuration-data.service'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { hasValue } from '../../shared/empty.util'; +import { PostRequest } from './request.models'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { ResponseParsingService } from './parsing.service'; +import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service'; +import { sendRequest } from '../shared/request.operators'; +import { RestRequest } from './rest-request.model'; +import { OrcidHistory } from '../orcid/model/orcid-history.model'; /** * The service handling all REST requests to get item identifiers like handles and DOIs @@ -60,4 +69,35 @@ export class IdentifierDataService extends BaseDataService { map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []) ); } + + public registerIdentifier(item: Item, type: string): Observable> { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + let params = new HttpParams(); + params = params.append('type', 'doi'); + options.params = params; + + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getEndpoint(); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + + const request = new PostRequest(requestId, href, item._links.self.href, options); + Object.assign(request, { + getResponseParser(): GenericConstructor { + return StatusCodeOnlyResponseParsingService; + } + }); + return request; + }) + ).subscribe((request) => { + this.requestService.send(request); + }); + + return this.rdbService.buildFromRequestUUID(requestId); + } } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 96007f5598..7082058612 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -233,7 +233,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService } /** - * Get the endpoint for an item's bundles + * Get the endpoint for an item's identifiers * @param itemId */ public getIdentifiersEndpoint(itemId: string): Observable { @@ -264,6 +264,10 @@ export abstract class BaseItemDataService extends IdentifiableDataService return this.rdbService.buildFromRequestUUID(requestId); } + + + + /** * Get the endpoint to move the item * @param itemId diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts index 9345ba18cc..f0ea46ba4a 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts @@ -82,13 +82,13 @@ export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent */ registerDoi() { this.processing = true; - this.itemDataService.registerDOI(this.item.id).pipe(getFirstCompletedRemoteData()).subscribe( + this.identifierDataService.registerIdentifier(this.item, 'doi').subscribe( (response: RemoteData) => { this.processing = false; //this.router.navigateByUrl(getItemEditRoute(this.item)); this.processRestResponse(response); } - ); + ) } } From 0a69560a8c7ce1526985f2005ad1e5d6c04c2c30 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 2 Feb 2023 09:35:49 +1300 Subject: [PATCH 109/125] [TLC-380] Refactor to use new endpoints, better naming --- src/app/core/data/identifier-data.service.ts | 48 ++++++------------- .../edit-item-page.routing-paths.ts | 2 +- .../item-register-doi.component.ts | 13 ++--- .../item-status/item-status.component.ts | 2 +- src/assets/i18n/en.json5 | 4 +- 5 files changed, 26 insertions(+), 43 deletions(-) diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts index 53f811bc40..03422dadfb 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -16,18 +16,13 @@ import { Item } from '../shared/item.model'; import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; import { getFirstCompletedRemoteData } from '../shared/operators'; -import { find, map, switchMap } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import {ConfigurationProperty} from '../shared/configuration-property.model'; import {ConfigurationDataService} from './configuration-data.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { hasValue } from '../../shared/empty.util'; import { PostRequest } from './request.models'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { ResponseParsingService } from './parsing.service'; -import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service'; import { sendRequest } from '../shared/request.operators'; import { RestRequest } from './rest-request.model'; -import { OrcidHistory } from '../orcid/model/orcid-history.model'; /** * The service handling all REST requests to get item identifiers like handles and DOIs @@ -71,33 +66,20 @@ export class IdentifierDataService extends BaseDataService { } public registerIdentifier(item: Item, type: string): Observable> { - const options: HttpOptions = Object.create({}); - let headers = new HttpHeaders(); - headers = headers.append('Content-Type', 'text/uri-list'); - options.headers = headers; - let params = new HttpParams(); - params = params.append('type', 'doi'); - options.params = params; - const requestId = this.requestService.generateRequestId(); - const hrefObs = this.getEndpoint(); - - hrefObs.pipe( - find((href: string) => hasValue(href)), - map((href: string) => { - - const request = new PostRequest(requestId, href, item._links.self.href, options); - Object.assign(request, { - getResponseParser(): GenericConstructor { - return StatusCodeOnlyResponseParsingService; - } - }); - return request; - }) - ).subscribe((request) => { - this.requestService.send(request); - }); - - return this.rdbService.buildFromRequestUUID(requestId); + return this.getEndpoint().pipe( + map((endpointURL: string) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + let params = new HttpParams(); + params = params.append('type', type); + options.params = params; + return new PostRequest(requestId, endpointURL, item._links.self.href, options); + }), + sendRequest(this.requestService), + switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable>) + ); } } diff --git a/src/app/item-page/edit-item-page/edit-item-page.routing-paths.ts b/src/app/item-page/edit-item-page/edit-item-page.routing-paths.ts index 2826d06bb4..6b0907dceb 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.routing-paths.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.routing-paths.ts @@ -5,4 +5,4 @@ export const ITEM_EDIT_PUBLIC_PATH = 'public'; export const ITEM_EDIT_DELETE_PATH = 'delete'; export const ITEM_EDIT_MOVE_PATH = 'move'; export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations'; -export const ITEM_EDIT_REGISTER_DOI_PATH = 'registerdoi'; +export const ITEM_EDIT_REGISTER_DOI_PATH = 'register-doi'; diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts index f0ea46ba4a..17ec16b1bd 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts @@ -7,7 +7,7 @@ import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { ItemDataService } from '../../../core/data/item-data.service'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { first, map } from 'rxjs/operators'; import { hasValue } from '../../../shared/empty.util'; import { Observable } from 'rxjs'; @@ -20,11 +20,11 @@ import { Identifier } from '../../../shared/object-list/identifier-data/identifi templateUrl: './item-register-doi-component.html' }) /** - * Component responsible for rendering the Item Registe DOI page + * Component responsible for rendering the Item Register DOI page */ export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent { - protected messageKey = 'registerdoi'; + protected messageKey = 'register-doi'; doiToUpdateMessage = 'item.edit.' + this.messageKey + '.to-update'; identifiers$: Observable; processing = false; @@ -84,9 +84,10 @@ export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent this.processing = true; this.identifierDataService.registerIdentifier(this.item, 'doi').subscribe( (response: RemoteData) => { - this.processing = false; - //this.router.navigateByUrl(getItemEditRoute(this.item)); - this.processRestResponse(response); + if (response.hasCompleted) { + this.processing = false; + this.processRestResponse(response); + } } ) } diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index 94122f5658..a7d8f80ea0 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -179,7 +179,7 @@ export class ItemStatusComponent implements OnInit { let tmp_operations = [...operations]; if (show) { // Push the new Register DOI item operation - tmp_operations.push(new ItemOperation('registerDOI', this.getCurrentUrl(item) + '/registerdoi', FeatureID.CanRegisterDOI)); + tmp_operations.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI)); } // Check authorisations and merge into new operations list observableFrom(tmp_operations).pipe( diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 18fa6e0b4b..0c62342b2d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1942,7 +1942,7 @@ "item.edit.identifiers.doi.status.MINTED": "Minted (not registered)", - "item.edit.tabs.status.buttons.register-doi.label": "Register a new or pending identifier", + "item.edit.tabs.status.buttons.register-doi.label": "Register a new or pending DOI", "item.edit.tabs.status.buttons.register-doi.button": "Register DOI...", @@ -1954,7 +1954,7 @@ "item.edit.register-doi.cancel": "Cancel", - "item.edit.register-doi.success": "DOI registered successfully. Refresh Item Status page to see new DOI details.", + "item.edit.register-doi.success": "DOI queued for registration successfully.", "item.edit.register-doi.error": "Error registering DOI", From 639fe69c5f52ee972bec75b162f4a0af9227ddb3 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 2 Feb 2023 09:35:55 +1300 Subject: [PATCH 110/125] [TLC-380] Refactor to use new endpoints, better naming --- src/app/core/data/item-data.service.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 7082058612..80da91acb3 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -242,32 +242,6 @@ export abstract class BaseItemDataService extends IdentifiableDataService ); } - /** - * Register a DOI for a given item - * @param itemId - */ - public registerDOI(itemId: string): Observable> { - const requestId = this.requestService.generateRequestId(); - const hrefObs = this.getIdentifiersEndpoint(itemId); - hrefObs.pipe( - take(1) - ).subscribe((href) => { - const options: HttpOptions = Object.create({}); - let headers = new HttpHeaders(); - headers = headers.append('Content-Type', 'application/json'); - options.headers = headers; - // Pass identifier type as a simple parameter, no need for full JSON data - let hrefWithParams: string = this.buildHrefWithParams(href, [new RequestParam('type', 'doi')]); - const request = new PostRequest(requestId, hrefWithParams, JSON.stringify({}), options); - this.requestService.send(request); - }); - return this.rdbService.buildFromRequestUUID(requestId); - } - - - - - /** * Get the endpoint to move the item * @param itemId From e65bb88e23dd4210b56f04b206e52766439186b8 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 6 Feb 2023 10:32:54 +1300 Subject: [PATCH 111/125] [TLC-249] Attempting art's sub fix (doesn't work) --- .../item-status/item-status.component.ts | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index a7d8f80ea0..24b352685e 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -3,7 +3,7 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { ItemOperation } from '../item-operation/itemOperation.model'; -import {distinctUntilChanged, first, map, mergeMap, toArray} from 'rxjs/operators'; +import { distinctUntilChanged, first, map, mergeMap, switchMap, take, toArray } from 'rxjs/operators'; import { BehaviorSubject, Observable, from as observableFrom, Subscription, combineLatest } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; @@ -174,27 +174,36 @@ export class ItemStatusComponent implements OnInit { ); // Subscribe to changes from the showRegister check and rebuild operations list accordingly - this.subs.push(showRegister$.subscribe((show) => { - // Copy the static array first so we don't keep appending to it - let tmp_operations = [...operations]; - if (show) { - // Push the new Register DOI item operation - tmp_operations.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI)); - } + this.subs.push(showRegister$.pipe( + switchMap((show: boolean) => { + console.dir('show? ' + show); + // Copy the static array first so we don't keep appending to it + let tmp_operations = [...operations]; + if (show) { + // Push the new Register DOI item operation + tmp_operations.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI)); + } + // emit the operations one at a time + return tmp_operations + }), // Check authorisations and merge into new operations list - observableFrom(tmp_operations).pipe( - mergeMap((operation) => { - if (hasValue(operation.featureID)) { - return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( - distinctUntilChanged(), - map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) - ); - } else { - return [operation]; - } - }), - toArray() - ).subscribe((ops) => this.operations$.next(ops)); + mergeMap((operation: ItemOperation) => { + console.dir("operation! " + (operation.featureID)); + if (hasValue(operation.featureID)) { + return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( + //distinctUntilChanged(), + map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) + ); + } else { + return [operation]; + } + }), + //take(operations.length + 1), + // wait until all observables have completed and emit them all as a single array + toArray(), + ).subscribe((ops: ItemOperation[]) => { + console.dir('next!'); + this.operations$.next(ops); })); }); From 76407866c02abf70e881dff8577512c879a3a52b Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 7 Feb 2023 14:58:30 +1300 Subject: [PATCH 112/125] [TLC-249] Update item status button logic to avoid nested subs --- .../item-status/item-status.component.ts | 103 ++++++++---------- 1 file changed, 47 insertions(+), 56 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index 24b352685e..c2db518415 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -3,20 +3,21 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { ItemOperation } from '../item-operation/itemOperation.model'; -import { distinctUntilChanged, first, map, mergeMap, switchMap, take, toArray } from 'rxjs/operators'; -import { BehaviorSubject, Observable, from as observableFrom, Subscription, combineLatest } from 'rxjs'; +import { distinctUntilChanged, first, map, mergeMap, switchMap, toArray } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { hasValue } from '../../../shared/empty.util'; import { - getAllSucceededRemoteDataPayload, + getAllSucceededRemoteDataPayload, getFirstSucceededRemoteData, getRemoteDataPayload, } from '../../../core/shared/operators'; import { IdentifierDataService } from '../../../core/data/identifier-data.service'; import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model'; import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { IdentifierData } from '../../../shared/object-list/identifier-data/identifier-data.model'; @Component({ selector: 'ds-item-status', @@ -52,11 +53,6 @@ export class ItemStatusComponent implements OnInit { */ operations$: BehaviorSubject = new BehaviorSubject([]); - /** - * The keys of the actions (to loop over) - */ - actionsKeys; - /** * Identifiers (handles, DOIs) */ @@ -109,27 +105,23 @@ export class ItemStatusComponent implements OnInit { // Observable for configuration determining whether the Register DOI feature is enabled let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe( - map((enabled: RemoteData) => { - let show = false; - if (enabled.hasSucceeded) { - if (enabled.payload !== undefined && enabled.payload !== null) { - if (enabled.payload.values !== undefined) { - enabled.payload.values.forEach((value) => { - show = true; - }); - } - } + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + map((enabled: ConfigurationProperty) => { + if (enabled !== undefined && enabled.values) { + return true; } - return show; + return false; }) ); /* + Construct a base list of operations. The key is used to build messages i18n example: 'item.edit.tabs.status.buttons..label' The value is supposed to be a href for the button */ - const operations = []; + const operations: ItemOperation[] = []; operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true)); operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true)); if (item.isWithdrawn) { @@ -146,11 +138,16 @@ export class ItemStatusComponent implements OnInit { operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true)); this.operations$.next(operations); - // Observable that reads identifiers and their status and, and config properties, and decides - // if we're allowed to show a Register DOI feature - let showRegister$: Observable = combineLatest([this.identifiers$, registerConfigEnabled$]).pipe( - distinctUntilChanged(), - map(([identifiers, enabled]) => { + /* + When the identifier data stream changes, determine whether the register DOI button should be shown or not. + This is based on whether the DOI is in the right state (minted or pending, not already queued for registration + or registered) and whether the configuration property identifiers.item-status.register-doi is true + */ + this.identifierDataService.getIdentifierDataFor(item).pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + mergeMap((data: IdentifierData) => { + let identifiers = data.identifiers; let no_doi = true; let pending = false; if (identifiers !== undefined && identifiers !== null) { @@ -169,43 +166,37 @@ export class ItemStatusComponent implements OnInit { }); } // If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true - return ((pending || no_doi) && enabled); - }) - ); - - // Subscribe to changes from the showRegister check and rebuild operations list accordingly - this.subs.push(showRegister$.pipe( - switchMap((show: boolean) => { - console.dir('show? ' + show); - // Copy the static array first so we don't keep appending to it - let tmp_operations = [...operations]; - if (show) { - // Push the new Register DOI item operation - tmp_operations.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI)); - } - // emit the operations one at a time - return tmp_operations + return registerConfigEnabled$.pipe( + map((enabled: boolean) => { + return enabled && (pending || no_doi); + } + )); }), - // Check authorisations and merge into new operations list - mergeMap((operation: ItemOperation) => { - console.dir("operation! " + (operation.featureID)); - if (hasValue(operation.featureID)) { - return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( - //distinctUntilChanged(), - map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) + // Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe + switchMap((showDoi: boolean) => { + let ops = [...operations]; + if (showDoi) { + ops.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI, true)); + } + return ops; + }), + // Merge map checks and transforms each operation in the array based on whether it is authorized or not (disabled) + mergeMap((op: ItemOperation) => { + if (hasValue(op.featureID)) { + return this.authorizationService.isAuthorized(op.featureID, item.self).pipe( + distinctUntilChanged(), + map((authorized) => new ItemOperation(op.operationKey, op.operationUrl, op.featureID, !authorized, authorized)) ); } else { - return [operation]; + return [op]; } }), - //take(operations.length + 1), - // wait until all observables have completed and emit them all as a single array + // Wait for all operations to be emitted and return as an array toArray(), - ).subscribe((ops: ItemOperation[]) => { - console.dir('next!'); - this.operations$.next(ops); - })); - + ).subscribe((data) => { + // Update the operations$ subject that draws the administrative buttons on the status page + this.operations$.next(data); + }); }); this.itemPageRoute$ = this.itemRD$.pipe( From 08096e682987e67cd8b14f81c9134a7b6698353d Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 7 Feb 2023 17:12:02 +1300 Subject: [PATCH 113/125] [TLC-249] Lint --- .../item-register-doi/item-register-doi.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts index 17ec16b1bd..54878c6026 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.ts @@ -89,7 +89,7 @@ export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent this.processRestResponse(response); } } - ) + ); } } From 83462324447c87630319c342bd936ff7f310fd5c Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 7 Feb 2023 17:22:59 +1300 Subject: [PATCH 114/125] [TLC-249] Fix import typo (from merge conflict) --- src/app/item-page/edit-item-page/edit-item-page.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index bbce184375..24d27b3340 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -36,7 +36,7 @@ import { ResourcePoliciesModule } from '../../shared/resource-policies/resource- import { ItemVersionsModule } from '../versions/item-versions.module'; import { IdentifierDataService } from '../../core/data/identifier-data.service'; import { IdentifierDataComponent } from '../../shared/object-list/identifier-data/identifier-data.component'; -import { ItemRegisterDoiComponent } from './item-register-doi/item-registerdoi.component'; +import { ItemRegisterDoiComponent } from './item-register-doi/item-register-doi.component'; import { DsoSharedModule } from '../../dso-shared/dso-shared.module'; From 129342435fc26eb1d20ac76b8d1a1ad77bbb73bf Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 7 Feb 2023 17:48:06 +1300 Subject: [PATCH 115/125] [TLC-249] Further test fixes --- .../item-register-doi/item-register-doi.component.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.spec.ts b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.spec.ts index ac17d7c751..af52114642 100644 --- a/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-register-doi/item-register-doi.component.spec.ts @@ -45,7 +45,8 @@ describe('ItemRegisterDoiComponent', () => { mockIdentifierDataService = jasmine.createSpyObj('mockIdentifierDataService', { getIdentifierDataFor: createSuccessfulRemoteDataObject$({'identifiers': []}), - getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$('true') + getIdentifierRegistrationConfiguration: createSuccessfulRemoteDataObject$('true'), + registerIdentifier: createSuccessfulRemoteDataObject$({'identifiers': []}), }); mockItemDataService = jasmine.createSpyObj('mockItemDataService', { @@ -98,7 +99,7 @@ describe('ItemRegisterDoiComponent', () => { it('should call registerDOI function from the ItemDataService', () => { spyOn(comp, 'processRestResponse'); comp.performAction(); - expect(mockItemDataService.registerDOI).toHaveBeenCalledWith(comp.item.id); + expect(mockIdentifierDataService.registerIdentifier).toHaveBeenCalledWith(comp.item, 'doi'); expect(comp.processRestResponse).toHaveBeenCalled(); }); }); From 06de559974c085e0098831054cc9e0f6b446502c Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 7 Feb 2023 14:49:22 +0100 Subject: [PATCH 116/125] Retrieve the XSRF token first, and set it as both the XSRF header and cookie --- src/app/core/auth/auth-request.service.ts | 8 ++- .../core/auth/browser-auth-request.service.ts | 9 ++-- .../core/auth/server-auth-request.service.ts | 54 +++++++++++++------ src/app/core/services/server-xhr.service.ts | 16 ++++++ src/app/core/xsrf/xsrf.interceptor.ts | 2 + src/modules/app/server-app.module.ts | 6 +++ 6 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 src/app/core/services/server-xhr.service.ts diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 5c0c3340c7..7c1f17dec2 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -100,14 +100,12 @@ export abstract class AuthRequestService { ); } /** - * Factory function to create the request object to send. This needs to be a POST client side and - * a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow - * only the server IP to send a GET to this endpoint. + * Factory function to create the request object to send. * * @param href The href to send the request to * @protected */ - protected abstract createShortLivedTokenRequest(href: string): GetRequest | PostRequest; + protected abstract createShortLivedTokenRequest(href: string): Observable; /** * Send a request to retrieve a short-lived token which provides download access of restricted files @@ -117,7 +115,7 @@ export abstract class AuthRequestService { filter((href: string) => isNotEmpty(href)), distinctUntilChanged(), map((href: string) => new URLCombiner(href, this.shortlivedtokensEndpoint).toString()), - map((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)), + switchMap((endpointURL: string) => this.createShortLivedTokenRequest(endpointURL)), tap((request: RestRequest) => this.requestService.send(request)), switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), getFirstCompletedRemoteData(), diff --git a/src/app/core/auth/browser-auth-request.service.ts b/src/app/core/auth/browser-auth-request.service.ts index 85d5f54340..485e2ef9c4 100644 --- a/src/app/core/auth/browser-auth-request.service.ts +++ b/src/app/core/auth/browser-auth-request.service.ts @@ -4,6 +4,7 @@ import { PostRequest } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Observable, of as observableOf } from 'rxjs'; /** * Client side version of the service to send authentication requests @@ -20,15 +21,13 @@ export class BrowserAuthRequestService extends AuthRequestService { } /** - * Factory function to create the request object to send. This needs to be a POST client side and - * a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow - * only the server IP to send a GET to this endpoint. + * Factory function to create the request object to send. * * @param href The href to send the request to * @protected */ - protected createShortLivedTokenRequest(href: string): PostRequest { - return new PostRequest(this.requestService.generateRequestId(), href); + protected createShortLivedTokenRequest(href: string): Observable { + return observableOf(new PostRequest(this.requestService.generateRequestId(), href)); } } diff --git a/src/app/core/auth/server-auth-request.service.ts b/src/app/core/auth/server-auth-request.service.ts index 9954e7c7ef..05131b7375 100644 --- a/src/app/core/auth/server-auth-request.service.ts +++ b/src/app/core/auth/server-auth-request.service.ts @@ -4,8 +4,19 @@ import { PostRequest } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HttpHeaders, HttpXsrfTokenExtractor } from '@angular/common/http'; -import { XSRF_REQUEST_HEADER } from '../xsrf/xsrf.interceptor'; +import { + HttpHeaders, + HttpXsrfTokenExtractor, + HttpClient, + HttpResponse +} from '@angular/common/http'; +import { + XSRF_REQUEST_HEADER, + XSRF_RESPONSE_HEADER, + DSPACE_XSRF_COOKIE +} from '../xsrf/xsrf.interceptor'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; /** * Server side version of the service to send authentication requests @@ -17,29 +28,40 @@ export class ServerAuthRequestService extends AuthRequestService { halService: HALEndpointService, requestService: RequestService, rdbService: RemoteDataBuildService, - protected tokenExtractor: HttpXsrfTokenExtractor, + protected httpClient: HttpClient, ) { super(halService, requestService, rdbService); } /** - * Factory function to create the request object to send. This needs to be a POST client side and - * a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow - * only the server IP to send a GET to this endpoint. + * Factory function to create the request object to send. * * @param href The href to send the request to * @protected */ - protected createShortLivedTokenRequest(href: string): PostRequest { - let options = new HttpHeaders(); - options = options.set('Content-Type', 'application/json; charset=utf-8'); - options = options.set(XSRF_REQUEST_HEADER, this.tokenExtractor.getToken()); - let requestOptions = { - headers: options, - }; - return Object.assign(new PostRequest(this.requestService.generateRequestId(), href, {}, requestOptions), { - responseMsToLive: 2 * 1000 // A short lived token is only valid for 2 seconds. - }); + protected createShortLivedTokenRequest(href: string): Observable { + // First do a call to the root endpoint in order to get an XSRF token + return this.httpClient.get(this.halService.getRootHref(), { observe: 'response' }).pipe( + // retrieve the XSRF token from the response header + map((response: HttpResponse) => response.headers.get(XSRF_RESPONSE_HEADER)), + // Use that token to create an HttpHeaders object + map((xsrfToken: string) => new HttpHeaders() + .set('Content-Type', 'application/json; charset=utf-8') + // set the token as the XSRF header + .set(XSRF_REQUEST_HEADER, xsrfToken) + // and as the DSPACE-XSRF-COOKIE + .set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)), + map((headers: HttpHeaders) => + // Create a new PostRequest using those headers and the given href + new PostRequest( + this.requestService.generateRequestId(), + href, + {}, + { + headers: headers, + } + )) + ) } } diff --git a/src/app/core/services/server-xhr.service.ts b/src/app/core/services/server-xhr.service.ts new file mode 100644 index 0000000000..835072bbd3 --- /dev/null +++ b/src/app/core/services/server-xhr.service.ts @@ -0,0 +1,16 @@ +import { XhrFactory } from '@angular/common'; +import { Injectable } from '@angular/core'; +import * as xhr2 from 'xhr2'; + +/** + * Overrides the default XhrFactoru server side, to allow us to set cookies in requests to the + * backend. This was added to be able to perform a working XSRF request from the node server, as it + * needs to set a cookie for the XSRF token + */ +@Injectable() +export class ServerXhrService implements XhrFactory { + build(): XMLHttpRequest { + xhr2.prototype._restrictedHeaders.cookie = false; + return new xhr2.XMLHttpRequest(); + } +} diff --git a/src/app/core/xsrf/xsrf.interceptor.ts b/src/app/core/xsrf/xsrf.interceptor.ts index d527924a28..cded432397 100644 --- a/src/app/core/xsrf/xsrf.interceptor.ts +++ b/src/app/core/xsrf/xsrf.interceptor.ts @@ -19,6 +19,8 @@ export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN'; export const XSRF_RESPONSE_HEADER = 'DSPACE-XSRF-TOKEN'; // Name of cookie where we store the XSRF token export const XSRF_COOKIE = 'XSRF-TOKEN'; +// Name of cookie the backend expects the XSRF token to be in +export const DSPACE_XSRF_COOKIE = 'DSPACE-XSRF-COOKIE'; /** * Custom Http Interceptor intercepting Http Requests & Responses to diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 81426e7fcc..7d162c5fd1 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -33,6 +33,8 @@ import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mo import { AuthRequestService } from '../../app/core/auth/auth-request.service'; import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service'; import { ServerInitService } from './server-init.service'; +import { XhrFactory } from '@angular/common'; +import { ServerXhrService } from '../../app/core/services/server-xhr.service'; export function createTranslateLoader(transferState: TransferState) { return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json'); @@ -104,6 +106,10 @@ export function createTranslateLoader(transferState: TransferState) { provide: HardRedirectService, useClass: ServerHardRedirectService, }, + { + provide: XhrFactory, + useClass: ServerXhrService, + }, ] }) export class ServerAppModule { From 88cb397dc9dde018741d2a372b72866370db8456 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Tue, 7 Feb 2023 15:01:01 +0100 Subject: [PATCH 117/125] Fix direct CSR By moving from environment.ts to config.yml we made it so the environment is _not_ up to date with the server configuration when main.js is first loaded. Because of this the app behaved as if CSR always happened _after_ SSR, effectively breaking direct CSR. Here the "criterion" for SSR/non-SSR HTML is changed from the related configuration property to the presence of Angular Universal transfer state. This means we can correctly determine when to bootstrap the app for direct CSR, and it's' now "safe" to just send index.html by itself. --- server.ts | 22 +++++----------------- src/index.csr.html | 18 ------------------ src/main.browser.ts | 19 +++++++++++++------ 3 files changed, 18 insertions(+), 41 deletions(-) delete mode 100644 src/index.csr.html diff --git a/server.ts b/server.ts index 81137ad56a..0fecf6bd52 100644 --- a/server.ts +++ b/server.ts @@ -31,7 +31,6 @@ import * as expressStaticGzip from 'express-static-gzip'; import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; -import { APP_BASE_HREF } from '@angular/common'; import { enableProdMode } from '@angular/core'; import { ngExpressEngine } from '@nguniversal/express-engine'; @@ -57,7 +56,7 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser'); // Set path fir IIIF viewer. const IIIF_VIEWER = join(process.cwd(), 'dist/iiif'); -const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index'; +const indexHtml = join(DIST_FOLDER, 'index.html'); const cookieParser = require('cookie-parser'); @@ -207,7 +206,6 @@ function ngApp(req, res) { baseUrl: environment.ui.nameSpace, originUrl: environment.ui.baseUrl, requestUrl: req.originalUrl, - providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }, (err, data) => { if (hasNoValue(err) && hasValue(data)) { res.locals.ssr = true; // mark response as SSR @@ -222,25 +220,15 @@ function ngApp(req, res) { if (hasValue(err)) { console.warn('Error details : ', err); } - res.render(indexHtml, { - req, - providers: [{ - provide: APP_BASE_HREF, - useValue: req.baseUrl - }] - }); + + res.sendFile(indexHtml); } }); } else { // If preboot is disabled, just serve the client console.log('Universal off, serving for direct CSR'); - res.render(indexHtml, { - req, - providers: [{ - provide: APP_BASE_HREF, - useValue: req.baseUrl - }] - }); + + res.sendFile(indexHtml); } } diff --git a/src/index.csr.html b/src/index.csr.html deleted file mode 100644 index b1ef4343b1..0000000000 --- a/src/index.csr.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - DSpace - - - - - - - - - - - diff --git a/src/main.browser.ts b/src/main.browser.ts index d5efe828c3..68debfb355 100644 --- a/src/main.browser.ts +++ b/src/main.browser.ts @@ -2,21 +2,27 @@ import 'zone.js'; import 'reflect-metadata'; import 'core-js/es/reflect'; -import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { load as loadWebFont } from 'webfontloader'; -import { hasValue } from './app/shared/empty.util'; - import { BrowserAppModule } from './modules/app/browser-app.module'; import { environment } from './environments/environment'; import { AppConfig } from './config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './config/config.util'; +import { enableProdMode } from '@angular/core'; const bootstrap = () => platformBrowserDynamic() .bootstrapModule(BrowserAppModule, {}); +/** + * We use this to determine have been serven SSR HTML or not. + * + * At this point, {@link environment} may not be in sync with the configuration. + * Therefore, we cannot depend on it to determine how to bootstrap the app. + */ +const hasTransferState = document.querySelector('script#dspace-angular-state') !== null; + const main = () => { // Load fonts async // https://github.com/typekit/webfontloader#configuration @@ -30,22 +36,23 @@ const main = () => { enableProdMode(); } - if (hasValue(environment.universal) && environment.universal.preboot) { + if (hasTransferState) { + // Configuration will be taken from transfer state during initialization return bootstrap(); } else { + // Configuration must be fetched explicitly return fetch('assets/config.json') .then((response) => response.json()) .then((appConfig: AppConfig) => { // extend environment with app config for browser when not prerendered extendEnvironmentWithAppConfig(environment, appConfig); - return bootstrap(); }); } }; // support async tag or hmr -if (document.readyState === 'complete' && hasValue(environment.universal) && !environment.universal.preboot) { +if (document.readyState === 'complete' && !hasTransferState) { main(); } else { document.addEventListener('DOMContentLoaded', main); From ce6324a569e6ecde23b49ab4eca9a4e5b1229248 Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Tue, 7 Feb 2023 11:50:42 -0500 Subject: [PATCH 118/125] Fix lint and test issues --- .../core/auth/auth-request.service.spec.ts | 5 +- .../auth/browser-auth-request.service.spec.ts | 14 ++++-- .../auth/server-auth-request.service.spec.ts | 50 ++++++++++++------- .../core/auth/server-auth-request.service.ts | 10 ++-- src/app/core/services/server-xhr.service.ts | 8 +-- 5 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/app/core/auth/auth-request.service.spec.ts b/src/app/core/auth/auth-request.service.spec.ts index 704922c5b5..063aad612f 100644 --- a/src/app/core/auth/auth-request.service.spec.ts +++ b/src/app/core/auth/auth-request.service.spec.ts @@ -11,6 +11,7 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import objectContaining = jasmine.objectContaining; import { AuthStatus } from './models/auth-status.model'; import { RestRequestMethod } from '../data/rest-request-method'; +import { Observable, of as observableOf } from 'rxjs'; describe(`AuthRequestService`, () => { let halService: HALEndpointService; @@ -34,8 +35,8 @@ describe(`AuthRequestService`, () => { super(hes, rs, rdbs); } - protected createShortLivedTokenRequest(href: string): PostRequest { - return new PostRequest(this.requestService.generateRequestId(), href); + protected createShortLivedTokenRequest(href: string): Observable { + return observableOf(new PostRequest(this.requestService.generateRequestId(), href)); } } diff --git a/src/app/core/auth/browser-auth-request.service.spec.ts b/src/app/core/auth/browser-auth-request.service.spec.ts index 18d27340af..2875553feb 100644 --- a/src/app/core/auth/browser-auth-request.service.spec.ts +++ b/src/app/core/auth/browser-auth-request.service.spec.ts @@ -1,6 +1,8 @@ import { AuthRequestService } from './auth-request.service'; import { RequestService } from '../data/request.service'; import { BrowserAuthRequestService } from './browser-auth-request.service'; +import { Observable } from 'rxjs'; +import { PostRequest } from '../data/request.models'; describe(`BrowserAuthRequestService`, () => { let href: string; @@ -17,13 +19,17 @@ describe(`BrowserAuthRequestService`, () => { describe(`createShortLivedTokenRequest`, () => { it(`should return a PostRequest`, () => { - const result = (service as any).createShortLivedTokenRequest(href); - expect(result.constructor.name).toBe('PostRequest'); + const obs = (service as any).createShortLivedTokenRequest(href) as Observable; + obs.subscribe((result: PostRequest) => { + expect(result.constructor.name).toBe('PostRequest'); + }); }); it(`should return a request with the given href`, () => { - const result = (service as any).createShortLivedTokenRequest(href); - expect(result.href).toBe(href) ; + const obs = (service as any).createShortLivedTokenRequest(href) as Observable; + obs.subscribe((result: PostRequest) => { + expect(result.href).toBe(href); + }); }); }); }); diff --git a/src/app/core/auth/server-auth-request.service.spec.ts b/src/app/core/auth/server-auth-request.service.spec.ts index 2612f9d5b4..181603eef7 100644 --- a/src/app/core/auth/server-auth-request.service.spec.ts +++ b/src/app/core/auth/server-auth-request.service.spec.ts @@ -1,44 +1,60 @@ import { AuthRequestService } from './auth-request.service'; import { RequestService } from '../data/request.service'; import { ServerAuthRequestService } from './server-auth-request.service'; -import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock'; +import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; +import { Observable, of as observableOf } from 'rxjs'; +import { XSRF_RESPONSE_HEADER } from '../xsrf/xsrf.interceptor'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { PostRequest } from '../data/request.models'; describe(`ServerAuthRequestService`, () => { let href: string; let requestService: RequestService; let service: AuthRequestService; - let xsrfExtractor: HttpXsrfTokenExtractorMock; - - const mockToken = 'mockToken'; + let httpClient: HttpClient; + let httpResponse: HttpResponse; + let halService: HALEndpointService; + const mockToken = 'mock-token'; beforeEach(() => { href = 'https://rest.api/auth/shortlivedtokens'; requestService = jasmine.createSpyObj('requestService', { 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2' }); - xsrfExtractor = new HttpXsrfTokenExtractorMock(mockToken); - service = new ServerAuthRequestService(null, requestService, null, xsrfExtractor); + httpResponse = { + body: { bar: false }, + headers: new HttpHeaders({ XSRF_RESPONSE_HEADER: mockToken }), + statusText: '200' + } as HttpResponse; + httpClient = jasmine.createSpyObj('httpClient', { + get: observableOf(httpResponse), + }); + halService = jasmine.createSpyObj('halService', { + 'getRootHref': '/api' + }); + service = new ServerAuthRequestService(halService, requestService, null, httpClient); }); describe(`createShortLivedTokenRequest`, () => { it(`should return a PostRequest`, () => { - const result = (service as any).createShortLivedTokenRequest(href); - expect(result.constructor.name).toBe('PostRequest'); + const obs = (service as any).createShortLivedTokenRequest(href) as Observable; + obs.subscribe((result: PostRequest) => { + expect(result.constructor.name).toBe('PostRequest'); + }); }); it(`should return a request with the given href`, () => { - const result = (service as any).createShortLivedTokenRequest(href); - expect(result.href).toBe(href) ; + const obs = (service as any).createShortLivedTokenRequest(href) as Observable; + obs.subscribe((result: PostRequest) => { + expect(result.href).toBe(href) ; + }); }); it(`should return a request with a xsrf header`, () => { - const result = (service as any).createShortLivedTokenRequest(href); - expect(result.options.headers.get('X-XSRF-TOKEN')).toBe(mockToken); - }); - - it(`should have a responseMsToLive of 2 seconds`, () => { - const result = (service as any).createShortLivedTokenRequest(href); - expect(result.responseMsToLive).toBe(2 * 1000) ; + const obs = (service as any).createShortLivedTokenRequest(href) as Observable; + obs.subscribe((result: PostRequest) => { + expect(result.options.headers.get(XSRF_RESPONSE_HEADER)).toBe(mockToken); + }); }); }); }); diff --git a/src/app/core/auth/server-auth-request.service.ts b/src/app/core/auth/server-auth-request.service.ts index 05131b7375..183e085589 100644 --- a/src/app/core/auth/server-auth-request.service.ts +++ b/src/app/core/auth/server-auth-request.service.ts @@ -6,7 +6,6 @@ import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { HttpHeaders, - HttpXsrfTokenExtractor, HttpClient, HttpResponse } from '@angular/common/http'; @@ -53,15 +52,16 @@ export class ServerAuthRequestService extends AuthRequestService { .set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)), map((headers: HttpHeaders) => // Create a new PostRequest using those headers and the given href - new PostRequest( + Object.assign(new PostRequest( this.requestService.generateRequestId(), href, {}, { headers: headers, - } - )) - ) + }, + ),{}) + ) + ); } } diff --git a/src/app/core/services/server-xhr.service.ts b/src/app/core/services/server-xhr.service.ts index 835072bbd3..69ae741402 100644 --- a/src/app/core/services/server-xhr.service.ts +++ b/src/app/core/services/server-xhr.service.ts @@ -1,16 +1,16 @@ import { XhrFactory } from '@angular/common'; import { Injectable } from '@angular/core'; -import * as xhr2 from 'xhr2'; +import { prototype, XMLHttpRequest } from 'xhr2'; /** - * Overrides the default XhrFactoru server side, to allow us to set cookies in requests to the + * Overrides the default XhrFactory server side, to allow us to set cookies in requests to the * backend. This was added to be able to perform a working XSRF request from the node server, as it * needs to set a cookie for the XSRF token */ @Injectable() export class ServerXhrService implements XhrFactory { build(): XMLHttpRequest { - xhr2.prototype._restrictedHeaders.cookie = false; - return new xhr2.XMLHttpRequest(); + prototype._restrictedHeaders.cookie = false; + return new XMLHttpRequest(); } } From c099bc468d752fb357ac6ad786701c12728ef6c4 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 7 Feb 2023 12:22:32 -0600 Subject: [PATCH 119/125] Add "debug" config and "allowStale" configs --- config/config.example.yml | 12 ++++++++++++ server.ts | 15 +++++++++------ src/config/cache-config.interface.ts | 6 ++++++ src/config/default-app-config.ts | 3 +++ src/environments/environment.test.ts | 3 +++ 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 52b06b1b08..500c2c476a 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -49,6 +49,8 @@ cache: # NOTE: To control the cache size, use the "max" setting. Keep in mind, individual cached pages are usually small (<100KB). # Enabling *both* caches will mean that a page may be cached twice, once in each cache (but may expire at different times via timeToLive). serverSide: + # Set to true to see all cache hits/misses/refreshes in your console logs. Useful for debugging SSR caching issues. + debug: false # When enabled (i.e. max > 0), known bots will be sent pages from a server side cache specific for bots. # (Keep in mind, bot detection cannot be guarranteed. It is possible some bots will bypass this cache.) botCache: @@ -62,6 +64,11 @@ cache: # NOTE: For the bot cache, this setting may impact how quickly search engine bots will index new content on your site. # For example, setting this to one week may mean that search engine bots may not find all new content for one week. timeToLive: 86400000 # 1 day + # When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page + # behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive). + # When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache). + # This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR. + allowStale: true # When enabled (i.e. max > 0), all anonymous users will be sent pages from a server side cache. # This allows anonymous users to interact more quickly with the site, but also means they may see slightly # outdated content (based on timeToLive) @@ -74,6 +81,11 @@ cache: # copy is automatically refreshed on the next request. # NOTE: For the anonymous cache, it is recommended to keep this value low to avoid anonymous users seeing outdated content. timeToLive: 10000 # 10 seconds + # When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page + # behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive). + # When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache). + # This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR. + allowStale: true # Authentication settings auth: diff --git a/server.ts b/server.ts index 8c9835cf16..478a4c0ec1 100644 --- a/server.ts +++ b/server.ts @@ -324,7 +324,7 @@ function initCache() { botCache = new LRU( { max: environment.cache.serverSide.botCache.max, ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day - allowStale: true // If object is found to be stale, return stale value before deleting + allowStale: environment.cache.serverSide.botCache.allowStale || true // if object is stale, return stale value before deleting }); } @@ -335,7 +335,7 @@ function initCache() { anonymousCache = new LRU( { max: environment.cache.serverSide.anonymousCache.max, ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds - allowStale: true // If object is found to be stale, return stale value before deleting + allowStale: environment.cache.serverSide.anonymousCache.allowStale || true // if object is stale, return stale value before deleting }); } } @@ -399,24 +399,25 @@ function cacheCheck(req, res, next) { * @returns cached copy (if found) or undefined (if not found) */ function checkCacheForRequest(cacheName: string, cache: LRU, req, res): any { - let debug = false; // Enable to see cache hits & re-rendering in logs - // Get the cache key for this request const key = getCacheKey(req); // Check if this page is in our cache let cachedCopy = cache.get(key); if (cachedCopy) { - if (debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } + if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } // Check if cached copy is expired (If expired, the key will now be gone from cache) + // NOTE: This will only occur when "allowStale=true", as it means the "get(key)" above returned a stale value. if (!cache.has(key)) { - if (debug) { console.log(`CACHE EXPIRED FOR ${key} in ${cacheName} cache. Re-rendering...`); } + if (environment.cache.serverSide.debug) { console.log(`CACHE EXPIRED FOR ${key} in ${cacheName} cache. Re-rendering...`); } // Update cached copy by rerendering server-side // NOTE: In this scenario the currently cached copy will be returned to the current user. // This re-render is peformed behind the scenes to update cached copy for next user. serverSideRender(req, res, false); } + } else { + if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); } } // return page from cache @@ -455,11 +456,13 @@ function saveToCache(req, page: any) { // (NOTE: has() will return false if page is expired in cache) if (botCacheEnabled() && !botCache.has(key)) { botCache.set(key, page); + if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); } } // If anonymous cache is enabled, save it to that cache if it doesn't exist or is expired if (anonymousCacheEnabled() && !anonymousCache.has(key)) { anonymousCache.set(key, page); + if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); } } } } diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index 1826bd0d30..9560fe46a5 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -11,12 +11,16 @@ export interface CacheConfig extends Config { // In-memory caches of server-side rendered (SSR) content. These caches can be used to limit the frequency // of re-generating SSR pages to improve performance. serverSide: { + // Debug server-side caching. Set to true to see cache hits/misses/refreshes in console logs. + debug: boolean, // Cache specific to known bots. Allows you to serve cached contents to bots only. botCache: { // Maximum number of pages (rendered via SSR) to cache. Setting max=0 disables the cache. max: number; // Amount of time after which cached pages are considered stale (in ms) timeToLive: number; + // true = return page from cache after timeToLive expires. false = return a fresh page after timeToLive expires + allowStale: boolean; }, // Cache specific to anonymous users. Allows you to serve cached content to non-authenticated users. anonymousCache: { @@ -24,6 +28,8 @@ export interface CacheConfig extends Config { max: number; // Amount of time after which cached pages are considered stale (in ms) timeToLive: number; + // true = return page from cache after timeToLive expires. false = return a fresh page after timeToLive expires + allowStale: boolean; } } } diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 9e5b535872..e7851d4b34 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -76,6 +76,7 @@ export class DefaultAppConfig implements AppConfig { }, // In-memory cache of server-side rendered content serverSide: { + debug: false, // Cache specific to known bots. Allows you to serve cached contents to bots only. // Defaults to caching 1,000 pages. Each page expires after 1 day botCache: { @@ -83,6 +84,7 @@ export class DefaultAppConfig implements AppConfig { max: 1000, // Amount of time after which cached pages are considered stale (in ms) timeToLive: 24 * 60 * 60 * 1000, // 1 day + allowStale: true, }, // Cache specific to anonymous users. Allows you to serve cached content to non-authenticated users. // Defaults to caching 0 pages. But, when enabled, each page expires after 10 seconds (to minimize anonymous users seeing out-of-date content) @@ -91,6 +93,7 @@ export class DefaultAppConfig implements AppConfig { max: 0, // disabled by default // Amount of time after which cached pages are considered stale (in ms) timeToLive: 10 * 1000, // 10 seconds + allowStale: true, } } }; diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 613024f798..0bb36da61f 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -58,13 +58,16 @@ export const environment: BuildConfig = { }, // In-memory cache of server-side rendered pages. Disabled in test environment (max=0) serverSide: { + debug: false, botCache: { max: 0, timeToLive: 24 * 60 * 60 * 1000, // 1 day + allowStale: true, }, anonymousCache: { max: 0, timeToLive: 10 * 1000, // 10 seconds + allowStale: true, } } }, From 9435f1c4a73e7a3e3ce1e26619ce322c8839ab1c Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Tue, 7 Feb 2023 13:59:57 -0500 Subject: [PATCH 120/125] Add done() to async tests --- .../auth/browser-auth-request.service.spec.ts | 6 +++-- .../auth/server-auth-request.service.spec.ts | 22 +++++++++++++------ .../core/auth/server-auth-request.service.ts | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/app/core/auth/browser-auth-request.service.spec.ts b/src/app/core/auth/browser-auth-request.service.spec.ts index 2875553feb..b41d981bcf 100644 --- a/src/app/core/auth/browser-auth-request.service.spec.ts +++ b/src/app/core/auth/browser-auth-request.service.spec.ts @@ -18,17 +18,19 @@ describe(`BrowserAuthRequestService`, () => { }); describe(`createShortLivedTokenRequest`, () => { - it(`should return a PostRequest`, () => { + it(`should return a PostRequest`, (done) => { const obs = (service as any).createShortLivedTokenRequest(href) as Observable; obs.subscribe((result: PostRequest) => { expect(result.constructor.name).toBe('PostRequest'); + done(); }); }); - it(`should return a request with the given href`, () => { + it(`should return a request with the given href`, (done) => { const obs = (service as any).createShortLivedTokenRequest(href) as Observable; obs.subscribe((result: PostRequest) => { expect(result.href).toBe(href); + done(); }); }); }); diff --git a/src/app/core/auth/server-auth-request.service.spec.ts b/src/app/core/auth/server-auth-request.service.spec.ts index 181603eef7..df6d78256b 100644 --- a/src/app/core/auth/server-auth-request.service.spec.ts +++ b/src/app/core/auth/server-auth-request.service.spec.ts @@ -3,9 +3,12 @@ import { RequestService } from '../data/request.service'; import { ServerAuthRequestService } from './server-auth-request.service'; import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; import { Observable, of as observableOf } from 'rxjs'; -import { XSRF_RESPONSE_HEADER } from '../xsrf/xsrf.interceptor'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { PostRequest } from '../data/request.models'; +import { + XSRF_REQUEST_HEADER, + XSRF_RESPONSE_HEADER +} from '../xsrf/xsrf.interceptor'; describe(`ServerAuthRequestService`, () => { let href: string; @@ -21,9 +24,11 @@ describe(`ServerAuthRequestService`, () => { requestService = jasmine.createSpyObj('requestService', { 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2' }); + let headers = new HttpHeaders(); + headers = headers.set(XSRF_RESPONSE_HEADER, mockToken); httpResponse = { body: { bar: false }, - headers: new HttpHeaders({ XSRF_RESPONSE_HEADER: mockToken }), + headers: headers, statusText: '200' } as HttpResponse; httpClient = jasmine.createSpyObj('httpClient', { @@ -36,24 +41,27 @@ describe(`ServerAuthRequestService`, () => { }); describe(`createShortLivedTokenRequest`, () => { - it(`should return a PostRequest`, () => { + it(`should return a PostRequest`, (done) => { const obs = (service as any).createShortLivedTokenRequest(href) as Observable; obs.subscribe((result: PostRequest) => { expect(result.constructor.name).toBe('PostRequest'); + done(); }); }); - it(`should return a request with the given href`, () => { + it(`should return a request with the given href`, (done) => { const obs = (service as any).createShortLivedTokenRequest(href) as Observable; obs.subscribe((result: PostRequest) => { - expect(result.href).toBe(href) ; + expect(result.href).toBe(href); + done(); }); }); - it(`should return a request with a xsrf header`, () => { + it(`should return a request with a xsrf header`, (done) => { const obs = (service as any).createShortLivedTokenRequest(href) as Observable; obs.subscribe((result: PostRequest) => { - expect(result.options.headers.get(XSRF_RESPONSE_HEADER)).toBe(mockToken); + expect(result.options.headers.get(XSRF_REQUEST_HEADER)).toBe(mockToken); + done(); }); }); }); diff --git a/src/app/core/auth/server-auth-request.service.ts b/src/app/core/auth/server-auth-request.service.ts index 183e085589..d6302081bc 100644 --- a/src/app/core/auth/server-auth-request.service.ts +++ b/src/app/core/auth/server-auth-request.service.ts @@ -52,14 +52,14 @@ export class ServerAuthRequestService extends AuthRequestService { .set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)), map((headers: HttpHeaders) => // Create a new PostRequest using those headers and the given href - Object.assign(new PostRequest( + new PostRequest( this.requestService.generateRequestId(), href, {}, { headers: headers, }, - ),{}) + ) ) ); } From 5d2f63ff65ca28948b3bd5359db63e8a3cc3d813 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Wed, 8 Feb 2023 10:17:36 +0100 Subject: [PATCH 121/125] 99053: Added test to check that the TYPE_REQUEST_FORGOT doesn't use the authentication-password.domain.valid --- .../register-email-form.component.spec.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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 4fea169e35..89af6ff05a 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 @@ -74,15 +74,6 @@ describe('RegisterEmailFormComponent', () => { const elem = fixture.debugElement.queryAll(By.css('input#email'))[0].nativeElement; expect(elem).toBeDefined(); }); - - it('should not retrieve the validDomains for TYPE_REQUEST_FORGOT', () => { - spyOn(configurationDataService, 'findByPropertyName'); - comp.typeRequest = TYPE_REQUEST_FORGOT; - - comp.ngOnInit(); - - expect(configurationDataService.findByPropertyName).not.toHaveBeenCalled(); - }); }); describe('email validation', () => { it('should be invalid when no email is present', () => { @@ -96,6 +87,18 @@ describe('RegisterEmailFormComponent', () => { comp.form.patchValue({email: 'valid@email.org'}); expect(comp.form.invalid).toBeFalse(); }); + it('should accept email with other domain names on TYPE_REQUEST_FORGOT form', () => { + spyOn(configurationDataService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'authentication-password.domain.valid', + values: ['marvel.com'], + }))); + comp.typeRequest = TYPE_REQUEST_FORGOT; + + comp.ngOnInit(); + + comp.form.patchValue({ email: 'valid@email.org' }); + expect(comp.form.invalid).toBeFalse(); + }); it('should not accept email with other domain names', () => { spyOn(configurationDataService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { name: 'authentication-password.domain.valid', From ad870829d2e962c6d259ab74e9c5ee1e8562af48 Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Wed, 8 Feb 2023 11:13:03 +0100 Subject: [PATCH 122/125] 97732 Remove unnecessary subscriptions --- .../context-help-toggle.component.ts | 13 +++---------- .../context-help-wrapper.component.ts | 19 ++++++------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/app/header/context-help-toggle/context-help-toggle.component.ts b/src/app/header/context-help-toggle/context-help-toggle.component.ts index 14f9ebeb63..6685df7106 100644 --- a/src/app/header/context-help-toggle/context-help-toggle.component.ts +++ b/src/app/header/context-help-toggle/context-help-toggle.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ContextHelpService } from '../../shared/context-help.service'; -import { Observable, Subscription } from 'rxjs'; +import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; /** @@ -12,22 +12,15 @@ import { map } from 'rxjs/operators'; templateUrl: './context-help-toggle.component.html', styleUrls: ['./context-help-toggle.component.scss'] }) -export class ContextHelpToggleComponent implements OnInit, OnDestroy { +export class ContextHelpToggleComponent implements OnInit { buttonVisible$: Observable; constructor( private contextHelpService: ContextHelpService, ) { } - private subs: Subscription[]; - ngOnInit(): void { this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0)); - this.subs = [this.buttonVisible$.subscribe()]; - } - - ngOnDestroy() { - this.subs.forEach(sub => sub.unsubscribe()); } onClick() { diff --git a/src/app/shared/context-help-wrapper/context-help-wrapper.component.ts b/src/app/shared/context-help-wrapper/context-help-wrapper.component.ts index c5bb40cf1b..e170d522b5 100644 --- a/src/app/shared/context-help-wrapper/context-help-wrapper.component.ts +++ b/src/app/shared/context-help-wrapper/context-help-wrapper.component.ts @@ -61,8 +61,7 @@ export class ContextHelpWrapperComponent implements OnInit, OnDestroy { parsedContent$: Observable; - private subs: {always: Subscription[], tooltipBound: Subscription[]} - = {always: [], tooltipBound: []}; + private subs: Subscription[] = []; constructor( private translateService: TranslateService, @@ -78,14 +77,13 @@ export class ContextHelpWrapperComponent implements OnInit, OnDestroy { dontParseLinks ? [text] : this.parseLinks(text)) ); this.shouldShowIcon$ = this.contextHelpService.shouldShowIcons$(); - this.subs.always = [this.parsedContent$.subscribe(), this.shouldShowIcon$.subscribe()]; } @ViewChild('tooltip', { static: false }) set setTooltip(tooltip: NgbTooltip) { this.tooltip = tooltip; - this.clearSubs('tooltipBound'); + this.clearSubs(); if (this.tooltip !== undefined) { - this.subs.tooltipBound = [ + this.subs = [ this.contextHelpService.getContextHelp$(this.id) .pipe(hasValueOperator()) .subscribe((ch: ContextHelp) => { @@ -159,13 +157,8 @@ export class ContextHelpWrapperComponent implements OnInit, OnDestroy { }); } - private clearSubs(filter: null | 'tooltipBound' = null) { - if (filter === null) { - [].concat(...Object.values(this.subs)).forEach(sub => sub.unsubscribe()); - this.subs = {always: [], tooltipBound: []}; - } else { - this.subs[filter].forEach(sub => sub.unsubscribe()); - this.subs[filter] = []; - } + private clearSubs() { + this.subs.forEach(sub => sub.unsubscribe()); + this.subs = []; } } From 0755e300e803247931b9b7fc2083edd94c746677 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 9 Feb 2023 11:41:14 +0100 Subject: [PATCH 123/125] [CST-7217] Add margins to better align badge --- .../search-facet-option/search-facet-option.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4e03767b9f..9a4bffadb8 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 @@ -5,7 +5,7 @@ From 9e11d69a8ade6fae07676d49f6a2cb4697705107 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 9 Feb 2023 08:48:50 -0600 Subject: [PATCH 124/125] Fix bug where allowStale couldn't be disabled --- server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.ts b/server.ts index 478a4c0ec1..ba0c8fd7b2 100644 --- a/server.ts +++ b/server.ts @@ -324,7 +324,7 @@ function initCache() { botCache = new LRU( { max: environment.cache.serverSide.botCache.max, ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day - allowStale: environment.cache.serverSide.botCache.allowStale || true // if object is stale, return stale value before deleting + allowStale: environment.cache.serverSide.botCache.allowStale ?? true // if object is stale, return stale value before deleting }); } @@ -335,7 +335,7 @@ function initCache() { anonymousCache = new LRU( { max: environment.cache.serverSide.anonymousCache.max, ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds - allowStale: environment.cache.serverSide.anonymousCache.allowStale || true // if object is stale, return stale value before deleting + allowStale: environment.cache.serverSide.anonymousCache.allowStale ?? true // if object is stale, return stale value before deleting }); } } From 8ddcd42d04f9a6afa0c84c2bc8576f9cd3588e51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Feb 2023 18:14:15 +0000 Subject: [PATCH 125/125] Bump http-cache-semantics from 4.1.0 to 4.1.1 Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/kornelski/http-cache-semantics/releases) - [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: http-cache-semantics dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3843ac4dab..2fd4fa3bc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6140,9 +6140,9 @@ htmlparser2@^6.0.0, htmlparser2@^6.1.0: entities "^2.0.0" http-cache-semantics@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-deceiver@^1.2.7: version "1.2.7"