diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index d3c3a9927d..540f57032b 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -61,7 +61,7 @@ + [ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}"> {{epersonDto.eperson.id}} {{ dsoNameService.getName(epersonDto.eperson) }} {{epersonDto.eperson.email}} diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.ts b/src/app/access-control/epeople-registry/epeople-registry.component.ts index 4596eec98e..12312cd4ac 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -46,6 +46,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { */ ePeopleDto$: BehaviorSubject> = new BehaviorSubject>({} as any); + activeEPerson$: Observable; + /** * An observable for the pageInfo, needed to pass to the pagination component */ @@ -111,6 +113,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { initialisePage() { this.searching$.next(true); this.search({scope: this.currentSearchScope, query: this.currentSearchQuery}); + this.activeEPerson$ = this.epersonService.getActiveEPerson(); this.subs.push(this.ePeople$.pipe( switchMap((epeople: PaginatedList) => { if (epeople.pageInfo.totalElements > 0) { @@ -178,23 +181,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { ); } - /** - * Checks whether the given EPerson is active (being edited) - * @param eperson - */ - isActive(eperson: EPerson): Observable { - return this.getActiveEPerson().pipe( - map((activeEPerson) => eperson === activeEPerson) - ); - } - - /** - * Gets the active eperson (being edited) - */ - getActiveEPerson(): Observable { - return this.epersonService.getActiveEPerson(); - } - /** * Deletes EPerson, show notification on success/failure & updates EPeople list */ diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html index 6c75b52077..d129e94ff9 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -2,7 +2,7 @@
-
+

{{messagePrefix + '.create' | translate}}

@@ -44,7 +44,7 @@ -
+

{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}

@@ -75,7 +75,9 @@ {{ dsoNameService.getName(group) }} - {{ dsoNameService.getName(undefined) }} + + {{ dsoNameService.getName((group.object | async)?.payload) }} + diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index b9aeeb0af2..f8d526fe39 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -1,11 +1,11 @@ import { Observable, of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; @@ -19,7 +19,6 @@ import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock' import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { AuthService } from '../../../core/auth/auth.service'; import { AuthServiceStub } from '../../../shared/testing/auth-service.stub'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -35,6 +34,7 @@ import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model import { ActivatedRoute, Router } from '@angular/router'; import { RouterStub } from '../../../shared/testing/router.stub'; import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; +import { RouterTestingModule } from '@angular/router/testing'; describe('EPersonFormComponent', () => { let component: EPersonFormComponent; @@ -59,9 +59,6 @@ describe('EPersonFormComponent', () => { ePersonDataServiceStub = { activeEPerson: null, allEpeople: mockEPeople, - getEPeople(): Observable>> { - return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople)); - }, getActiveEPerson(): Observable { return observableOf(this.activeEPerson); }, @@ -195,12 +192,8 @@ describe('EPersonFormComponent', () => { router = new RouterStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), + RouterTestingModule, + TranslateModule.forRoot(), ], declarations: [EPersonFormComponent], providers: [ @@ -217,7 +210,7 @@ describe('EPersonFormComponent', () => { { provide: Router, useValue: router }, EPeopleRegistryComponent ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -236,37 +229,13 @@ describe('EPersonFormComponent', () => { }); describe('check form validation', () => { - let firstName; - let lastName; - let email; - let canLogIn; - let requireCertificate; + let canLogIn: boolean; + let requireCertificate: boolean; - let expected; beforeEach(() => { - firstName = 'testName'; - lastName = 'testLastName'; - email = 'testEmail@test.com'; canLogIn = false; requireCertificate = false; - expected = Object.assign(new EPerson(), { - metadata: { - 'eperson.firstname': [ - { - value: firstName - } - ], - 'eperson.lastname': [ - { - value: lastName - }, - ], - }, - email: email, - canLogIn: canLogIn, - requireCertificate: requireCertificate, - }); spyOn(component.submitForm, 'emit'); component.canLogIn.value = canLogIn; component.requireCertificate.value = requireCertificate; @@ -340,15 +309,13 @@ describe('EPersonFormComponent', () => { expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy(); }); }); - - - }); + describe('when submitting the form', () => { let firstName; let lastName; let email; - let canLogIn; + let canLogIn: boolean; let requireCertificate; let expected; @@ -377,6 +344,7 @@ describe('EPersonFormComponent', () => { requireCertificate: requireCertificate, }); spyOn(component.submitForm, 'emit'); + component.ngOnInit(); component.firstName.value = firstName; component.lastName.value = lastName; component.email.value = email; @@ -416,9 +384,17 @@ describe('EPersonFormComponent', () => { email: email, canLogIn: canLogIn, requireCertificate: requireCertificate, - _links: undefined + _links: { + groups: { + href: '', + }, + self: { + href: '', + }, + }, }); spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId)); + component.ngOnInit(); component.onSubmit(); fixture.detectChanges(); }); @@ -466,22 +442,19 @@ describe('EPersonFormComponent', () => { }); describe('delete', () => { - - let ePersonId; let eperson: EPerson; let modalService; beforeEach(() => { spyOn(authService, 'impersonate').and.callThrough(); - ePersonId = 'testEPersonId'; eperson = EPersonMock; component.epersonInitial = eperson; component.canDelete$ = observableOf(true); spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson)); modalService = (component as any).modalService; spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) })); + component.ngOnInit(); fixture.detectChanges(); - }); it('the delete button should be visible if the ePerson can be deleted', () => { diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index d7d5a0b49c..97a0a13727 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -139,6 +139,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ canImpersonate$: Observable; + /** + * The current {@link EPerson} + */ + activeEPerson$: Observable; + /** * List of subscriptions */ @@ -199,7 +204,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { protected route: ActivatedRoute, protected router: Router, ) { - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + } + + ngOnInit() { + this.activeEPerson$ = this.epersonService.getActiveEPerson(); + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { this.epersonInitial = eperson; if (hasValue(eperson)) { this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); @@ -207,9 +216,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.submitLabel = 'form.submit'; } })); - } - - ngOnInit() { this.initialisePage(); } @@ -220,123 +226,112 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData) => { this.epersonService.editEPerson(ePersonRD.payload); })); - observableCombineLatest([ - this.translateService.get(`${this.messagePrefix}.firstName`), - this.translateService.get(`${this.messagePrefix}.lastName`), - this.translateService.get(`${this.messagePrefix}.email`), - this.translateService.get(`${this.messagePrefix}.canLogIn`), - this.translateService.get(`${this.messagePrefix}.requireCertificate`), - this.translateService.get(`${this.messagePrefix}.emailHint`), - ]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => { - this.firstName = new DynamicInputModel({ - id: 'firstName', - label: firstName, - name: 'firstName', - validators: { - required: null, - }, - required: true, - }); - this.lastName = new DynamicInputModel({ - id: 'lastName', - label: lastName, - name: 'lastName', - validators: { - required: null, - }, - required: true, - }); - this.email = new DynamicInputModel({ - id: 'email', - label: email, - name: 'email', - validators: { - required: null, - pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$', - }, - required: true, - errorMessages: { - emailTaken: 'error.validation.emailTaken', - pattern: 'error.validation.NotValidEmail' - }, - hint: emailHint - }); - this.canLogIn = new DynamicCheckboxModel( - { - id: 'canLogIn', - label: canLogIn, - name: 'canLogIn', - value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true) - }); - this.requireCertificate = new DynamicCheckboxModel( - { - id: 'requireCertificate', - label: requireCertificate, - name: 'requireCertificate', - value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false) - }); - this.formModel = [ - this.firstName, - this.lastName, - this.email, - this.canLogIn, - this.requireCertificate, - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { - if (eperson != null) { - this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, { - currentPage: 1, - elementsPerPage: this.config.pageSize - }); - } - this.formGroup.patchValue({ - firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', - lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '', - email: eperson != null ? eperson.email : '', - canLogIn: eperson != null ? eperson.canLogIn : true, - requireCertificate: eperson != null ? eperson.requireCertificate : false - }); - - if (eperson === null && !!this.formGroup.controls.email) { - this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService)); - this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => { - this.changeDetectorRef.detectChanges(); - }); - } - })); - - const activeEPerson$ = this.epersonService.getActiveEPerson(); - - this.groups = activeEPerson$.pipe( - switchMap((eperson) => { - return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { - currentPage: 1, - elementsPerPage: this.config.pageSize - })]); - }), - switchMap(([eperson, findListOptions]) => { - if (eperson != null) { - return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object')); - } - return observableOf(undefined); - }) - ); - - this.canImpersonate$ = activeEPerson$.pipe( - switchMap((eperson) => { - if (hasValue(eperson)) { - return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self); - } else { - return observableOf(false); - } - }) - ); - this.canDelete$ = activeEPerson$.pipe( - switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)) - ); - this.canReset$ = observableOf(true); + this.firstName = new DynamicInputModel({ + id: 'firstName', + label: this.translateService.instant(`${this.messagePrefix}.firstName`), + name: 'firstName', + validators: { + required: null, + }, + required: true, }); + this.lastName = new DynamicInputModel({ + id: 'lastName', + label: this.translateService.instant(`${this.messagePrefix}.lastName`), + name: 'lastName', + validators: { + required: null, + }, + required: true, + }); + this.email = new DynamicInputModel({ + id: 'email', + label: this.translateService.instant(`${this.messagePrefix}.email`), + name: 'email', + validators: { + required: null, + pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$', + }, + required: true, + errorMessages: { + emailTaken: 'error.validation.emailTaken', + pattern: 'error.validation.NotValidEmail' + }, + hint: this.translateService.instant(`${this.messagePrefix}.emailHint`), + }); + this.canLogIn = new DynamicCheckboxModel( + { + id: 'canLogIn', + label: this.translateService.instant(`${this.messagePrefix}.canLogIn`), + name: 'canLogIn', + value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true) + }); + this.requireCertificate = new DynamicCheckboxModel( + { + id: 'requireCertificate', + label: this.translateService.instant(`${this.messagePrefix}.requireCertificate`), + name: 'requireCertificate', + value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false) + }); + this.formModel = [ + this.firstName, + this.lastName, + this.email, + this.canLogIn, + this.requireCertificate, + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { + if (eperson != null) { + this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, { + currentPage: 1, + elementsPerPage: this.config.pageSize + }); + } + this.formGroup.patchValue({ + firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', + lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '', + email: eperson != null ? eperson.email : '', + canLogIn: eperson != null ? eperson.canLogIn : true, + requireCertificate: eperson != null ? eperson.requireCertificate : false + }); + + if (eperson === null && !!this.formGroup.controls.email) { + this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService)); + this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => { + this.changeDetectorRef.detectChanges(); + }); + } + })); + + this.groups = this.activeEPerson$.pipe( + switchMap((eperson) => { + return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { + currentPage: 1, + elementsPerPage: this.config.pageSize + })]); + }), + switchMap(([eperson, findListOptions]) => { + if (eperson != null) { + return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object')); + } + return observableOf(undefined); + }) + ); + + this.canImpersonate$ = this.activeEPerson$.pipe( + switchMap((eperson) => { + if (hasValue(eperson)) { + return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self); + } else { + return observableOf(false); + } + }) + ); + this.canDelete$ = this.activeEPerson$.pipe( + switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)) + ); + this.canReset$ = observableOf(true); } /** @@ -355,7 +350,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Emit the updated/created eperson using the EventEmitter submitForm */ onSubmit() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe( + this.activeEPerson$.pipe(take(1)).subscribe( (ePerson: EPerson) => { const values = { metadata: { @@ -474,7 +469,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * It'll either show a success or error message depending on whether the delete was successful or not. */ delete(): void { - this.epersonService.getActiveEPerson().pipe( + this.activeEPerson$.pipe( take(1), switchMap((eperson: EPerson) => { const modalRef = this.modalService.open(ConfirmationModalComponent); @@ -578,7 +573,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Update the list of groups by fetching it from the rest api or cache */ private updateGroups(options) { - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options); })); } diff --git a/src/app/access-control/group-registry/group-form/group-form.component.html b/src/app/access-control/group-registry/group-form/group-form.component.html index 491762a437..13e4387664 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.html +++ b/src/app/access-control/group-registry/group-form/group-form.component.html @@ -2,7 +2,7 @@
-
+

{{messagePrefix + '.head.create' | translate}}

@@ -23,11 +23,15 @@
- - - + + + + + + + {{messagePrefix + '.return' | translate}}
-
+
-
- -
- - - - + +
+ +
+ +
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index e281377c24..81ecdae7c0 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf } from 'rxjs'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; @@ -30,8 +30,6 @@ import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; import { GroupFormComponent } from './group-form.component'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; -import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; import { RouterMock } from '../../../shared/mocks/router.mock'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { Operation } from 'fast-json-patch'; @@ -39,23 +37,24 @@ import { ValidateGroupExists } from './validators/group-exists.validator'; import { NoContent } from '../../../core/shared/NoContent.model'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; describe('GroupFormComponent', () => { let component: GroupFormComponent; let fixture: ComponentFixture; - let translateService: TranslateService; let builderService: FormBuilderService; let ePersonDataServiceStub: any; let groupsDataServiceStub: any; let dsoDataServiceStub: any; let authorizationService: AuthorizationDataService; let notificationService: NotificationsServiceStub; - let router; + let router: RouterMock; + let route: ActivatedRouteStub; - let groups; - let groupName; - let groupDescription; - let expected; + let groups: Group[]; + let groupName: string; + let groupDescription: string; + let expected: Group; beforeEach(waitForAsync(() => { groups = [GroupMock, GroupMock2]; @@ -70,6 +69,15 @@ describe('GroupFormComponent', () => { } ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + } + }, }); ePersonDataServiceStub = {}; groupsDataServiceStub = { @@ -106,7 +114,14 @@ describe('GroupFormComponent', () => { create(group: Group): Observable> { this.allGroups = [...this.allGroups, group]; this.createdGroup = Object.assign({}, group, { - _links: { self: { href: 'group-selflink' } } + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); return createSuccessfulRemoteDataObject$(this.createdGroup); }, @@ -188,17 +203,13 @@ describe('GroupFormComponent', () => { return typeof value === 'object' && value !== null; } }); - translateService = getMockTranslateService(); router = new RouterMock(); + route = new ActivatedRouteStub(); notificationService = new NotificationsServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), + TranslateModule.forRoot(), ], declarations: [GroupFormComponent], providers: [ @@ -216,14 +227,11 @@ describe('GroupFormComponent', () => { { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, - { - provide: ActivatedRoute, - useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) } - }, + { provide: ActivatedRoute, useValue: route }, { provide: Router, useValue: router }, { provide: AuthorizationDataService, useValue: authorizationService }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -236,8 +244,8 @@ describe('GroupFormComponent', () => { describe('when submitting the form', () => { beforeEach(() => { spyOn(component.submitForm, 'emit'); - component.groupName.value = groupName; - component.groupDescription.value = groupDescription; + component.groupName.setValue(groupName); + component.groupDescription.setValue(groupDescription); }); describe('without active Group', () => { beforeEach(() => { @@ -245,14 +253,22 @@ describe('GroupFormComponent', () => { fixture.detectChanges(); }); - it('should emit a new group using the correct values', (async () => { - await fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected); - }); + it('should emit a new group using the correct values', (() => { + expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({ + name: groupName, + metadata: { + 'dc.description': [ + { + value: groupDescription, + }, + ], + }, + })); })); }); + describe('with active Group', () => { - let expected2; + let expected2: Group; beforeEach(() => { expected2 = Object.assign(new Group(), { name: 'newGroupName', @@ -263,15 +279,24 @@ describe('GroupFormComponent', () => { } ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected)); spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2)); - component.groupName.value = 'newGroupName'; - component.onSubmit(); - fixture.detectChanges(); + component.ngOnInit(); }); it('should edit with name and description operations', () => { + component.groupName.setValue('newGroupName'); + component.onSubmit(); const operations = [{ op: 'add', path: '/metadata/dc.description', @@ -285,9 +310,8 @@ describe('GroupFormComponent', () => { }); it('should edit with description operations', () => { - component.groupName.value = null; + component.groupName.setValue(null); component.onSubmit(); - fixture.detectChanges(); const operations = [{ op: 'add', path: '/metadata/dc.description', @@ -297,9 +321,9 @@ describe('GroupFormComponent', () => { }); it('should edit with name operations', () => { - component.groupDescription.value = null; + component.groupName.setValue('newGroupName'); + component.groupDescription.setValue(null); component.onSubmit(); - fixture.detectChanges(); const operations = [{ op: 'replace', path: '/name', @@ -308,12 +332,13 @@ describe('GroupFormComponent', () => { expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); }); - it('should emit the existing group using the correct new values', (async () => { - await fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); - }); - })); + it('should emit the existing group using the correct new values', () => { + component.onSubmit(); + expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); + }); + it('should emit success notification', () => { + component.onSubmit(); expect(notificationService.success).toHaveBeenCalled(); }); }); @@ -328,11 +353,8 @@ describe('GroupFormComponent', () => { describe('check form validation', () => { - let groupCommunity; - beforeEach(() => { groupName = 'testName'; - groupCommunity = 'testgroupCommunity'; groupDescription = 'testgroupDescription'; expected = Object.assign(new Group(), { @@ -344,8 +366,17 @@ describe('GroupFormComponent', () => { } ], }, + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(component.submitForm, 'emit'); + spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected)); fixture.detectChanges(); component.initialisePage(); @@ -395,21 +426,20 @@ describe('GroupFormComponent', () => { }); describe('delete', () => { - let deleteButton; - - beforeEach(() => { - component.initialisePage(); + let deleteButton: HTMLButtonElement; + beforeEach(async () => { + spyOn(groupsDataServiceStub, 'delete').and.callThrough(); + component.activeGroup$ = observableOf({ + id: 'active-group', + permanent: false, + } as Group); component.canEdit$ = observableOf(true); - component.groupBeingEdited = { - permanent: false - } as Group; + + component.initialisePage(); fixture.detectChanges(); deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement; - - spyOn(groupsDataServiceStub, 'delete').and.callThrough(); - spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' })); }); describe('if confirmed via modal', () => { diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 925a8bb859..19b604358c 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; +import { UntypedFormGroup, AbstractControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { @@ -12,10 +12,9 @@ import { TranslateService } from '@ngx-translate/core'; import { combineLatest as observableCombineLatest, Observable, - of as observableOf, Subscription, } from 'rxjs'; -import { catchError, map, switchMap, take, filter, debounceTime } from 'rxjs/operators'; +import { map, switchMap, take, debounceTime } from 'rxjs/operators'; import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths'; import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; @@ -24,17 +23,16 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; -import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { Group } from '../../../core/eperson/models/group.model'; import { Collection } from '../../../core/shared/collection.model'; import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { + getAllCompletedRemoteData, getRemoteDataPayload, getFirstSucceededRemoteData, getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { AlertType } from '../../../shared/alert/alert-type'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; @@ -68,9 +66,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { /** * Dynamic models for the inputs of form */ - groupName: DynamicInputModel; - groupCommunity: DynamicInputModel; - groupDescription: DynamicTextAreaModel; + groupName: AbstractControl; + groupCommunity: AbstractControl; + groupDescription: AbstractControl; /** * A list of all dynamic input models @@ -113,21 +111,30 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ subs: Subscription[] = []; - /** - * Group currently being edited - */ - groupBeingEdited: Group; - /** * Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group */ canEdit$: Observable; /** - * The AlertType enumeration - * @type {AlertType} + * The current {@link Group} */ - public AlertTypeEnum = AlertType; + activeGroup$: Observable; + + /** + * The current {@link Group}'s linked {@link Community}/{@link Collection} + */ + activeGroupLinkedDSO$: Observable; + + /** + * Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab + */ + linkedEditRolesRoute$: Observable; + + /** + * The AlertType enumeration + */ + public readonly AlertType = AlertType; /** * Subscription to email field value change @@ -137,126 +144,121 @@ export class GroupFormComponent implements OnInit, OnDestroy { constructor( public groupDataService: GroupDataService, - private ePersonDataService: EPersonDataService, - private dSpaceObjectDataService: DSpaceObjectDataService, - private formBuilderService: FormBuilderService, - private translateService: TranslateService, - private notificationsService: NotificationsService, - private route: ActivatedRoute, + protected dSpaceObjectDataService: DSpaceObjectDataService, + protected formBuilderService: FormBuilderService, + protected translateService: TranslateService, + protected notificationsService: NotificationsService, + protected route: ActivatedRoute, protected router: Router, - private authorizationService: AuthorizationDataService, - private modalService: NgbModal, + protected authorizationService: AuthorizationDataService, + protected modalService: NgbModal, public requestService: RequestService, protected changeDetectorRef: ChangeDetectorRef, public dsoNameService: DSONameService, ) { } - ngOnInit() { + ngOnInit(): void { + if (this.route.snapshot.params.groupId !== 'newGroup') { + this.setActiveGroup(this.route.snapshot.params.groupId); + } + this.activeGroup$ = this.groupDataService.getActiveGroup(); + this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO(); + this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute(); + this.canEdit$ = this.activeGroupLinkedDSO$.pipe( + switchMap((dso: DSpaceObject) => { + if (hasValue(dso)) { + return [false]; + } else { + return this.activeGroup$.pipe( + hasValueOperator(), + switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)), + ); + } + }), + ); this.initialisePage(); } initialisePage() { - this.subs.push(this.route.params.subscribe((params) => { - if (params.groupId !== 'newGroup') { - this.setActiveGroup(params.groupId); - } - })); - this.canEdit$ = this.groupDataService.getActiveGroup().pipe( - hasValueOperator(), - switchMap((group: Group) => { - return observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined), - this.hasLinkedDSO(group), - ]).pipe( - map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO), - ); - }), - ); - observableCombineLatest([ - this.translateService.get(`${this.messagePrefix}.groupName`), - this.translateService.get(`${this.messagePrefix}.groupCommunity`), - this.translateService.get(`${this.messagePrefix}.groupDescription`) - ]).subscribe(([groupName, groupCommunity, groupDescription]) => { - this.groupName = new DynamicInputModel({ - id: 'groupName', - label: groupName, - name: 'groupName', - validators: { - required: null, - }, - required: true, - }); - this.groupCommunity = new DynamicInputModel({ - id: 'groupCommunity', - label: groupCommunity, - name: 'groupCommunity', - required: false, - readOnly: true, - }); - this.groupDescription = new DynamicTextAreaModel({ - id: 'groupDescription', - label: groupDescription, - name: 'groupDescription', - required: false, - spellCheck: environment.form.spellCheck, - }); - this.formModel = [ - this.groupName, - this.groupDescription, - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - - if (!!this.formGroup.controls.groupName) { - this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService)); - this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => { - this.changeDetectorRef.detectChanges(); - }); - } - - this.subs.push( - observableCombineLatest([ - this.groupDataService.getActiveGroup(), - this.canEdit$, - this.groupDataService.getActiveGroup() - .pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload()))) - ]).subscribe(([activeGroup, canEdit, linkedObject]) => { - - if (activeGroup != null) { - - // Disable group name exists validator - this.formGroup.controls.groupName.clearAsyncValidators(); - - this.groupBeingEdited = activeGroup; - - if (linkedObject?.name) { - if (!this.formGroup.controls.groupCommunity) { - this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity); - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupCommunity: linkedObject?.name ?? '', - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); - } - } else { - this.formModel = [ - this.groupName, - this.groupDescription, - ]; - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); - } - setTimeout(() => { - if (!canEdit || activeGroup.permanent) { - this.formGroup.disable(); - } - }, 200); - } - }) - ); + const groupNameModel = new DynamicInputModel({ + id: 'groupName', + label: this.translateService.instant(`${this.messagePrefix}.groupName`), + name: 'groupName', + validators: { + required: null, + }, + required: true, }); + const groupCommunityModel = new DynamicInputModel({ + id: 'groupCommunity', + label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`), + name: 'groupCommunity', + required: false, + readOnly: true, + }); + const groupDescriptionModel = new DynamicTextAreaModel({ + id: 'groupDescription', + label: this.translateService.instant(`${this.messagePrefix}.groupDescription`), + name: 'groupDescription', + required: false, + spellCheck: environment.form.spellCheck, + }); + this.formModel = [ + groupNameModel, + groupDescriptionModel, + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.groupName = this.formGroup.get('groupName'); + this.groupDescription = this.formGroup.get('groupDescription'); + + if (hasValue(this.groupName)) { + this.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService)); + this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => { + this.changeDetectorRef.detectChanges(); + }); + } + + this.subs.push( + observableCombineLatest([ + this.activeGroup$, + this.canEdit$, + this.activeGroupLinkedDSO$, + ]).subscribe(([activeGroup, canEdit, linkedObject]) => { + + if (activeGroup != null) { + + // Disable group name exists validator + this.formGroup.controls.groupName.clearAsyncValidators(); + + if (isNotEmpty(linkedObject?.name)) { + if (!this.formGroup.controls.groupCommunity) { + this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); + this.groupDescription = this.formGroup.get('groupCommunity'); + } + this.formGroup.patchValue({ + groupName: activeGroup.name, + groupCommunity: linkedObject?.name ?? '', + groupDescription: activeGroup.firstMetadataValue('dc.description'), + }); + } else { + this.formModel = [ + groupNameModel, + groupDescriptionModel, + ]; + this.formGroup.patchValue({ + groupName: activeGroup.name, + groupDescription: activeGroup.firstMetadataValue('dc.description'), + }); + } + if (!canEdit || activeGroup.permanent) { + this.formGroup.disable(); + } else { + this.formGroup.enable(); + } + } + }) + ); } /** @@ -275,25 +277,22 @@ export class GroupFormComponent implements OnInit, OnDestroy { * Emit the updated/created eperson using the EventEmitter submitForm */ onSubmit() { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe( - (group: Group) => { - const values = { + this.activeGroup$.pipe(take(1)).subscribe((group: Group) => { + if (group === null) { + this.createNewGroup({ name: this.groupName.value, metadata: { 'dc.description': [ { - value: this.groupDescription.value - } - ] + value: this.groupDescription.value, + }, + ], }, - }; - if (group === null) { - this.createNewGroup(values); - } else { - this.editGroup(group); - } + }); + } else { + this.editGroup(group); } - ); + }); } /** @@ -399,7 +398,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { * @param groupSelfLink SelfLink of group to set as active */ setActiveGroupWithLink(groupSelfLink: string) { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup === null) { this.groupDataService.cancelEditGroup(); this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object')) @@ -418,7 +417,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { * It'll either show a success or error message depending on whether the delete was successful or not. */ delete() { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => { + this.activeGroup$.pipe(take(1)).subscribe((group: Group) => { const modalRef = this.modalService.open(ConfirmationModalComponent); modalRef.componentInstance.dso = group; modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header'; @@ -462,52 +461,38 @@ export class GroupFormComponent implements OnInit, OnDestroy { } /** - * Check if group has a linked object (community or collection linked to a workflow group) - * @param group + * Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a + * workflow group) */ - hasLinkedDSO(group: Group): Observable { - if (hasValue(group) && hasValue(group._links.object.href)) { - return this.getLinkedDSO(group).pipe( - map((rd: RemoteData) => { - return hasValue(rd) && hasValue(rd.payload); - }), - catchError(() => observableOf(false)), - ); - } + getActiveGroupLinkedDSO(): Observable { + return this.activeGroup$.pipe( + hasValueOperator(), + switchMap((group: Group) => { + if (group.object === undefined) { + return this.dSpaceObjectDataService.findByHref(group._links.object.href); + } + return group.object; + }), + getAllCompletedRemoteData(), + getRemoteDataPayload(), + ); } /** - * Get group's linked object if it has one (community or collection linked to a workflow group) - * @param group + * Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked + * to a workflow group) if it has one */ - getLinkedDSO(group: Group): Observable> { - if (hasValue(group) && hasValue(group._links.object.href)) { - if (group.object === undefined) { - return this.dSpaceObjectDataService.findByHref(group._links.object.href); - } - return group.object; - } - } - - /** - * Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one - * @param group - */ - getLinkedEditRolesRoute(group: Group): Observable { - if (hasValue(group) && hasValue(group._links.object.href)) { - return this.getLinkedDSO(group).pipe( - map((rd: RemoteData) => { - if (hasValue(rd) && hasValue(rd.payload)) { - const dso = rd.payload; - switch ((dso as any).type) { - case Community.type.value: - return getCommunityEditRolesRoute(rd.payload.id); - case Collection.type.value: - return getCollectionEditRolesRoute(rd.payload.id); - } - } - }) - ); - } + getLinkedEditRolesRoute(): Observable { + return this.activeGroupLinkedDSO$.pipe( + hasValueOperator(), + map((dso: DSpaceObject) => { + switch ((dso as any).type) { + case Community.type.value: + return getCommunityEditRolesRoute(dso.id); + case Collection.type.value: + return getCollectionEditRolesRoute(dso.id); + } + }), + ); } } diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html index 320b191d54..afb34dad49 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html @@ -9,9 +9,9 @@
@@ -26,12 +26,12 @@ - +
-
diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index 0cfdf3bd29..8041e21206 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -16,8 +16,7 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications- import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level'; import { XSRFService } from '../../../core/xsrf/xsrf.service'; -import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; +import { hot } from 'jasmine-marbles'; import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject, @@ -32,7 +31,6 @@ describe('BitstreamFormatsComponent', () => { let comp: BitstreamFormatsComponent; let fixture: ComponentFixture; let bitstreamFormatService; - let scheduler: TestScheduler; let notificationsServiceStub; let paginationService; @@ -87,8 +85,6 @@ describe('BitstreamFormatsComponent', () => { const initAsync = () => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -180,17 +176,17 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(initAsync)); beforeEach(initBeforeEach); it('should return an observable of true if the provided bistream is in the list returned by the service', () => { - const result = comp.isSelected(bitstreamFormat1); - - expect(result).toBeObservable(cold('b', { b: true })); + comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => { + expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id); + }); }); it('should return an observable of false if the provided bistream is not in the list returned by the service', () => { const format = new BitstreamFormat(); format.uuid = 'new'; - const result = comp.isSelected(format); - - expect(result).toBeObservable(cold('b', { b: false })); + comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => { + expect(selectedBitstreamFormatIDs).not.toContain(format.id); + }); }); }); @@ -216,8 +212,6 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(() => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -265,8 +259,6 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(() => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index 162bf2bdb2..263c1aede2 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable} from 'rxjs'; +import { Observable} from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; @@ -7,7 +7,6 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { NoContent } from '../../../core/shared/NoContent.model'; import { PaginationService } from '../../../core/pagination/pagination.service'; @@ -26,7 +25,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { /** * A paginated list of bitstream formats to be shown on the page */ - bitstreamFormats: Observable>>; + bitstreamFormats$: Observable>>; + + /** + * The currently selected {@link BitstreamFormat} IDs + */ + selectedBitstreamFormatIDs$: Observable; /** * The current pagination configuration for the page @@ -39,7 +43,6 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { }); constructor(private notificationsService: NotificationsService, - private router: Router, private translateService: TranslateService, private bitstreamFormatService: BitstreamFormatDataService, private paginationService: PaginationService, @@ -94,14 +97,11 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { } /** - * Checks whether a given bitstream format is selected in the list (checkbox) - * @param bitstreamFormat + * Returns the list of all the bitstream formats that are selected in the list (checkbox) */ - isSelected(bitstreamFormat: BitstreamFormat): Observable { + selectedBitstreamFormatIDs(): Observable { return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe( - map((bitstreamFormats: BitstreamFormat[]) => { - return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null; - }) + map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)), ); } @@ -125,27 +125,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { const prefix = 'admin.registries.bitstream-formats.delete'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(`${prefix}.${suffix}.head`), - this.translateService.get(`${prefix}.${suffix}.amount`, {amount: amount}) - ); - messages.subscribe(([head, content]) => { + const head: string = this.translateService.instant(`${prefix}.${suffix}.head`); + const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount }); - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } ngOnInit(): void { - - this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe( + this.bitstreamFormats$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe( switchMap((findListOptions: FindListOptions) => { return this.bitstreamFormatService.findAll(findListOptions); }) ); + this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs(); } diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html index d3be5df08e..4808132893 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html @@ -27,14 +27,15 @@ + [ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}"> {{schema.id}} diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts index 944288a7a5..d69d14aacf 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts @@ -1,7 +1,6 @@ import { MetadataRegistryComponent } from './metadata-registry.component'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; @@ -15,18 +14,21 @@ import { HostWindowService } from '../../../shared/host-window.service'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { RestResponse } from '../../../core/cache/response.models'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; describe('MetadataRegistryComponent', () => { let comp: MetadataRegistryComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - let paginationService; - const mockSchemasList = [ + + let paginationService: PaginationServiceStub; + let registryService: RegistryServiceStub; + + const mockSchemasList: MetadataSchema[] = [ { id: 1, _links: { @@ -47,32 +49,18 @@ describe('MetadataRegistryComponent', () => { prefix: 'mock', namespace: 'http://dspace.org/mockschema' } - ]; - const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getMetadataSchemas: () => mockSchemas, - getActiveMetadataSchema: () => observableOf(undefined), - getSelectedMetadataSchemas: () => observableOf([]), - editMetadataSchema: (schema) => { - }, - cancelEditMetadataSchema: () => { - }, - deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')), - deselectAllMetadataSchema: () => { - }, - clearMetadataSchemaRequests: () => observableOf(undefined) - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - - paginationService = new PaginationServiceStub(); + ] as MetadataSchema[]; beforeEach(waitForAsync(() => { + paginationService = new PaginationServiceStub(); + registryService = new RegistryServiceStub(); + spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList))); + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [MetadataRegistryComponent, PaginationComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, + { provide: RegistryService, useValue: registryService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: PaginationService, useValue: paginationService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() } @@ -123,7 +111,7 @@ describe('MetadataRegistryComponent', () => { })); it('should cancel editing the selected schema when clicked again', waitForAsync(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema)); + comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema); spyOn(registryService, 'cancelEditMetadataSchema'); row.click(); fixture.detectChanges(); @@ -138,7 +126,7 @@ describe('MetadataRegistryComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataSchema').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[])); + comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id)); comp.deleteSchemas(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts index 857034604e..dbeb2b4f52 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts @@ -1,13 +1,11 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { RegistryService } from '../../../core/registry/registry.service'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs'; +import { BehaviorSubject, Observable, zip, Subscription } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { filter, map, switchMap, take } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; @@ -24,13 +22,23 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; * A component used for managing all existing metadata schemas within the repository. * The admin can create, edit or delete metadata schemas here. */ -export class MetadataRegistryComponent { +export class MetadataRegistryComponent implements OnDestroy, OnInit { /** * A list of all the current metadata schemas within the repository */ metadataSchemas: Observable>>; + /** + * The {@link MetadataSchema}that is being edited + */ + activeMetadataSchema$: Observable; + + /** + * The selected {@link MetadataSchema} IDs + */ + selectedMetadataSchemaIDs$: Observable; + /** * Pagination config used to display the list of metadata schemas */ @@ -40,15 +48,25 @@ export class MetadataRegistryComponent { }); /** - * Whether or not the list of MetadataSchemas needs an update + * Whether the list of MetadataSchemas needs an update */ needsUpdate$: BehaviorSubject = new BehaviorSubject(true); - constructor(private registryService: RegistryService, - private notificationsService: NotificationsService, - private router: Router, - private paginationService: PaginationService, - private translateService: TranslateService) { + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected notificationsService: NotificationsService, + protected paginationService: PaginationService, + protected translateService: TranslateService, + ) { + } + + ngOnInit(): void { + this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema(); + this.selectedMetadataSchemaIDs$ = this.registryService.getSelectedMetadataSchemas().pipe( + map((schemas: MetadataSchema[]) => schemas.map((schema: MetadataSchema) => schema.id)), + ); this.updateSchemas(); } @@ -77,30 +95,13 @@ export class MetadataRegistryComponent { * @param schema */ editSchema(schema: MetadataSchema) { - this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => { + this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => { if (schema === activeSchema) { this.registryService.cancelEditMetadataSchema(); } else { this.registryService.editMetadataSchema(schema); } - }); - } - - /** - * Checks whether the given metadata schema is active (being edited) - * @param schema - */ - isActive(schema: MetadataSchema): Observable { - return this.getActiveSchema().pipe( - map((activeSchema) => schema === activeSchema) - ); - } - - /** - * Gets the active metadata schema (being edited) - */ - getActiveSchema(): Observable { - return this.registryService.getActiveMetadataSchema(); + })); } /** @@ -114,42 +115,25 @@ export class MetadataRegistryComponent { this.registryService.deselectMetadataSchema(schema); } - /** - * Checks whether a given metadata schema is selected in the list (checkbox) - * @param schema - */ - isSelected(schema: MetadataSchema): Observable { - return this.registryService.getSelectedMetadataSchemas().pipe( - map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null) - ); - } - /** * Delete all the selected metadata schemas */ deleteSchemas() { - this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe( - (schemas) => { - const tasks$ = []; - for (const schema of schemas) { - if (hasValue(schema.id)) { - tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData())); - } - } - zip(...tasks$).subscribe((responses: RemoteData[]) => { - const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - if (successResponses.length > 0) { - this.showNotification(true, successResponses.length); - } - if (failedResponses.length > 0) { - this.showNotification(false, failedResponses.length); - } - this.registryService.deselectAllMetadataSchema(); - this.registryService.cancelEditMetadataSchema(); - }); + this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe( + take(1), + switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))), + ).subscribe((responses: RemoteData[]) => { + const successResponses: RemoteData[] = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses: RemoteData[] = responses.filter((response: RemoteData) => response.hasFailed); + if (successResponses.length > 0) { + this.showNotification(true, successResponses.length); } - ); + if (failedResponses.length > 0) { + this.showNotification(false, failedResponses.length); + } + this.registryService.deselectAllMetadataSchema(); + this.registryService.cancelEditMetadataSchema(); + })); } /** @@ -160,20 +144,20 @@ export class MetadataRegistryComponent { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.deleted.${suffix}`, {amount: amount}) - ); - messages.subscribe(([head, content]) => { - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + + const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`); + const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, {amount: amount}); + + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); + this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html index 85d1e90692..15cc81d9ca 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html @@ -1,4 +1,4 @@ -
+

{{messagePrefix + '.create' | translate}}

diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts index b758767ddb..442a06dc67 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { MetadataSchemaFormComponent } from './metadata-schema-form.component'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; @@ -10,41 +10,26 @@ import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { of as observableOf } from 'rxjs'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; +import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; describe('MetadataSchemaFormComponent', () => { let component: MetadataSchemaFormComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getActiveMetadataSchema: () => observableOf(undefined), - createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema), - cancelEditMetadataSchema: () => { - }, - clearMetadataSchemaRequests: () => observableOf(undefined) - }; - const formBuilderServiceStub = { - createFormGroup: () => { - return { - patchValue: () => { - }, - reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { - }, - }; - } - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + let registryService: RegistryServiceStub; beforeEach(waitForAsync(() => { + registryService = new RegistryServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [MetadataSchemaFormComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: FormBuilderService, useValue: formBuilderServiceStub } + { provide: RegistryService, useValue: registryService }, + { provide: FormBuilderService, useValue: getMockFormBuilderService() } ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -75,7 +60,7 @@ describe('MetadataSchemaFormComponent', () => { describe('without an active schema', () => { beforeEach(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined)); + component.activeMetadataSchema$ = observableOf(undefined); component.onSubmit(); fixture.detectChanges(); }); @@ -94,7 +79,7 @@ describe('MetadataSchemaFormComponent', () => { } as MetadataSchema); beforeEach(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId)); + component.activeMetadataSchema$ = observableOf(expectedWithId); component.onSubmit(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts index 24bf430661..c8ffd77861 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts @@ -8,10 +8,10 @@ import { import { UntypedFormGroup } from '@angular/forms'; import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { switchMap, take, tap } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, combineLatest } from 'rxjs'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; +import { Subscription, Observable } from 'rxjs'; @Component({ selector: 'ds-metadata-schema-form', @@ -73,64 +73,71 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ @Output() submitForm: EventEmitter = new EventEmitter(); - constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) { + /** + * The {@link MetadataSchema} that is currently being edited + */ + activeMetadataSchema$: Observable; + + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected formBuilderService: FormBuilderService, + protected translateService: TranslateService, + ) { } ngOnInit() { - combineLatest([ - this.translateService.get(`${this.messagePrefix}.name`), - this.translateService.get(`${this.messagePrefix}.namespace`) - ]).subscribe(([name, namespace]) => { - this.name = new DynamicInputModel({ - id: 'name', - label: name, - name: 'name', - validators: { - required: null, - pattern: '^[^. ,]*$', - maxLength: 32, - }, - required: true, - errorMessages: { - pattern: 'error.validation.metadata.name.invalid-pattern', - maxLength: 'error.validation.metadata.name.max-length', - }, - }); - this.namespace = new DynamicInputModel({ - id: 'namespace', - label: namespace, - name: 'namespace', - validators: { - required: null, - maxLength: 256, - }, - required: true, - errorMessages: { - maxLength: 'error.validation.metadata.namespace.max-length', - }, - }); - this.formModel = [ - new DynamicFormGroupModel( - { - id: 'metadatadataschemagroup', - group:[this.namespace, this.name] - }) - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => { - if (schema == null) { - this.clearFields(); - } else { - this.formGroup.patchValue({ - metadatadataschemagroup: { - name: schema.prefix, - namespace: schema.namespace, - }, - }); - this.name.disabled = true; - } + this.name = new DynamicInputModel({ + id: 'name', + label: this.translateService.instant(`${this.messagePrefix}.name`), + name: 'name', + validators: { + required: null, + pattern: '^[^. ,]*$', + maxLength: 32, + }, + required: true, + errorMessages: { + pattern: 'error.validation.metadata.name.invalid-pattern', + maxLength: 'error.validation.metadata.name.max-length', + }, }); - }); + this.namespace = new DynamicInputModel({ + id: 'namespace', + label: this.translateService.instant(`${this.messagePrefix}.namespace`), + name: 'namespace', + validators: { + required: null, + maxLength: 256, + }, + required: true, + errorMessages: { + maxLength: 'error.validation.metadata.namespace.max-length', + }, + }); + this.formModel = [ + new DynamicFormGroupModel( + { + id: 'metadatadataschemagroup', + group:[this.namespace, this.name] + }) + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema(); + this.subscriptions.push(this.activeMetadataSchema$.subscribe((schema: MetadataSchema) => { + if (schema == null) { + this.clearFields(); + } else { + this.formGroup.patchValue({ + metadatadataschemagroup: { + name: schema.prefix, + namespace: schema.namespace, + }, + }); + this.name.disabled = true; + } + })); } /** @@ -147,48 +154,29 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { * Emit the updated/created schema using the EventEmitter submitForm */ onSubmit(): void { - this.registryService - .getActiveMetadataSchema() - .pipe( - take(1), - switchMap((schema: MetadataSchema) => { - const metadataValues = { - prefix: this.name.value, - namespace: this.namespace.value, - }; - - let createOrUpdate$: Observable; - - if (schema == null) { - createOrUpdate$ = - this.registryService.createOrUpdateMetadataSchema( - Object.assign(new MetadataSchema(), metadataValues) - ); - } else { - const updatedSchema = Object.assign( - new MetadataSchema(), - schema, - { - namespace: metadataValues.namespace, - } - ); - createOrUpdate$ = - this.registryService.createOrUpdateMetadataSchema( - updatedSchema - ); - } - - return createOrUpdate$; - }), - tap(() => { - this.registryService.clearMetadataSchemaRequests().subscribe(); - }) - ) - .subscribe((updatedOrCreatedSchema: MetadataSchema) => { - this.submitForm.emit(updatedOrCreatedSchema); - this.clearFields(); - this.registryService.cancelEditMetadataSchema(); - }); + this.activeMetadataSchema$.pipe( + take(1), + switchMap((schema: MetadataSchema) => { + const metadataValues = { + prefix: this.name.value, + namespace: this.namespace.value, + }; + if (schema == null) { + return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues)); + } else { + return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, { + namespace: metadataValues.namespace, + })); + } + }), + switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe( + map(() => updatedOrCreatedSchema), + )), + ).subscribe((updatedOrCreatedSchema: MetadataSchema) => { + this.submitForm.emit(updatedOrCreatedSchema); + this.clearFields(); + this.registryService.cancelEditMetadataSchema(); + }); } /** @@ -204,5 +192,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.onCancel(); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts index ad7b54945d..7ae672f9e1 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts @@ -9,14 +9,17 @@ import { TranslateModule } from '@ngx-translate/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub'; describe('MetadataFieldFormComponent', () => { let component: MetadataFieldFormComponent; let fixture: ComponentFixture; - let registryService: RegistryService; + + let registryService: RegistryServiceStub; const metadataSchema = Object.assign(new MetadataSchema(), { id: 1, @@ -24,38 +27,17 @@ describe('MetadataFieldFormComponent', () => { prefix: 'fake' }); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getActiveMetadataField: () => observableOf(undefined), - createMetadataField: (field: MetadataField) => observableOf(field), - updateMetadataField: (field: MetadataField) => observableOf(field), - cancelEditMetadataField: () => { - }, - cancelEditMetadataSchema: () => { - }, - clearMetadataFieldRequests: () => observableOf(undefined) - }; - const formBuilderServiceStub = { - createFormGroup: () => { - return { - patchValue: () => { - }, - reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { - }, - }; - } - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - beforeEach(waitForAsync(() => { + registryService = new RegistryServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [MetadataFieldFormComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: FormBuilderService, useValue: formBuilderServiceStub } + { provide: RegistryService, useValue: registryService }, + { provide: FormBuilderService, useValue: getMockFormBuilderService() } ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); })); diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html index 1ba8da1e25..254919e232 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html @@ -31,8 +31,8 @@ - + [ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}"> + { let comp: MetadataSchemaComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - const mockSchemasList = [ + + let registryService: RegistryServiceStub; + let activatedRoute: ActivatedRouteStub; + let paginationService: PaginationServiceStub; + + const mockSchemasList: MetadataSchema[] = [ { id: 1, _links: { @@ -51,8 +54,8 @@ describe('MetadataSchemaComponent', () => { prefix: 'mock', namespace: 'http://dspace.org/mockschema' } - ]; - const mockFieldsList = [ + ] as MetadataSchema[]; + const mockFieldsList: MetadataField[] = [ { id: 1, _links: { @@ -101,47 +104,29 @@ describe('MetadataSchemaComponent', () => { scopeNote: null, schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]) } - ]; - const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getMetadataSchemas: () => mockSchemas, - getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))), - getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]), - getActiveMetadataField: () => observableOf(undefined), - getSelectedMetadataFields: () => observableOf([]), - editMetadataField: (schema) => { - }, - cancelEditMetadataField: () => { - }, - deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')), - deselectAllMetadataField: () => { - }, - clearMetadataFieldRequests: () => observableOf(undefined) - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + ] as MetadataField[]; const schemaNameParam = 'mock'; - const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { - params: observableOf({ - schemaName: schemaNameParam - }) - }); - - const paginationService = new PaginationServiceStub(); beforeEach(waitForAsync(() => { + activatedRoute = new ActivatedRouteStub({ + schemaName: schemaNameParam, + }); + paginationService = new PaginationServiceStub(); + registryService = new RegistryServiceStub(); + spyOn(registryService, 'getMetadataFieldsBySchema').and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4)))); + spyOn(registryService, 'getMetadataSchemaByPrefix').and.callFake((schemaName) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0])); + TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: RegistryService, useValue: registryService }, + { provide: ActivatedRoute, useValue: activatedRoute }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: Router, useValue: new RouterStub() }, { provide: PaginationService, useValue: paginationService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -190,7 +175,7 @@ describe('MetadataSchemaComponent', () => { })); it('should cancel editing the selected field when clicked again', waitForAsync(() => { - spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField)); + comp.activeField$ = observableOf(mockFieldsList[2] as MetadataField); spyOn(registryService, 'cancelEditMetadataField'); row.click(); fixture.detectChanges(); @@ -205,7 +190,7 @@ describe('MetadataSchemaComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataField').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[])); + comp.selectedMetadataFieldIDs$ = observableOf(selectedFields.map((metadataField: MetadataField) => metadataField.id)); comp.deleteFields(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts index 4dec129ead..597e669d1c 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -3,17 +3,16 @@ import { RegistryService } from '../../../core/registry/registry.service'; import { ActivatedRoute } from '@angular/router'; import { BehaviorSubject, - combineLatest as observableCombineLatest, combineLatest, Observable, of as observableOf, - zip + zip, + Subscription, } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { map, switchMap, take } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; @@ -32,7 +31,7 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; * A component used for managing all existing metadata fields within the current metadata schema. * The admin can create, edit or delete metadata fields here. */ -export class MetadataSchemaComponent implements OnInit, OnDestroy { +export class MetadataSchemaComponent implements OnDestroy, OnInit { /** * The metadata schema */ @@ -57,26 +56,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { */ needsUpdate$: BehaviorSubject = new BehaviorSubject(true); - constructor(private registryService: RegistryService, - private route: ActivatedRoute, - private notificationsService: NotificationsService, - private paginationService: PaginationService, - private translateService: TranslateService) { + /** + * The current {@link MetadataField} that is being edited + */ + activeField$: Observable; + /** + * The selected {@link MetadataField} IDs + */ + selectedMetadataFieldIDs$: Observable; + + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected paginationService: PaginationService, + protected translateService: TranslateService, + ) { } ngOnInit(): void { - this.route.params.subscribe((params) => { - this.initialize(params); - }); - } - - /** - * Initialize the component using the params within the url (schemaName) - * @param params - */ - initialize(params) { - this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload()); + this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload()); + this.activeField$ = this.registryService.getActiveMetadataField(); + this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe( + map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)), + ); this.updateFields(); } @@ -109,30 +115,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { * @param field */ editField(field: MetadataField) { - this.getActiveField().pipe(take(1)).subscribe((activeField) => { + this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => { if (field === activeField) { this.registryService.cancelEditMetadataField(); } else { this.registryService.editMetadataField(field); } - }); - } - - /** - * Checks whether the given metadata field is active (being edited) - * @param field - */ - isActive(field: MetadataField): Observable { - return this.getActiveField().pipe( - map((activeField) => field === activeField) - ); - } - - /** - * Gets the active metadata field (being edited) - */ - getActiveField(): Observable { - return this.registryService.getActiveMetadataField(); + })); } /** @@ -146,42 +135,25 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { this.registryService.deselectMetadataField(field); } - /** - * Checks whether a given metadata field is selected in the list (checkbox) - * @param field - */ - isSelected(field: MetadataField): Observable { - return this.registryService.getSelectedMetadataFields().pipe( - map((fields) => fields.find((selectedField) => selectedField === field) != null) - ); - } - /** * Delete all the selected metadata fields */ deleteFields() { - this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe( - (fields) => { - const tasks$ = []; - for (const field of fields) { - if (hasValue(field.id)) { - tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData())); - } - } - zip(...tasks$).subscribe((responses: RemoteData[]) => { - const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - if (successResponses.length > 0) { - this.showNotification(true, successResponses.length); - } - if (failedResponses.length > 0) { - this.showNotification(false, failedResponses.length); - } - this.registryService.deselectAllMetadataField(); - this.registryService.cancelEditMetadataField(); - }); + this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe( + take(1), + switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))), + ).subscribe((responses: RemoteData[]) => { + const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); + if (successResponses.length > 0) { + this.showNotification(true, successResponses.length); } - ); + if (failedResponses.length > 0) { + this.showNotification(false, failedResponses.length); + } + this.registryService.deselectAllMetadataField(); + this.registryService.cancelEditMetadataField(); + })); } /** @@ -192,21 +164,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest([ - this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }) - ]); - messages.subscribe(([head, content]) => { - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`); + const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount }); + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); this.registryService.deselectAllMetadataField(); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html index bd00c9411f..c2f5299d4b 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html @@ -8,7 +8,7 @@ - +